Authentication etc.

This commit is contained in:
Markus Thielen 2023-03-04 12:49:17 +01:00
parent e587974a29
commit d96e86ba56
Signed by: markus
GPG Key ID: 3D4980D3EC9C8E26
8 changed files with 161 additions and 26 deletions

View File

@ -45,21 +45,16 @@ idp_url = "https://kcdev.basebox.health:8443"
# OpenID Connect scope; default is "openid profile email"
scope = "openid profile email"
# Optional base URL for OAuth2 URLs, e.g. "https://domain.tld/auth"
# If omitted, it will be derived from the fields in the [server] section.
# base_url = "http://127.0.0.1:8080"
# Fully qualified URL to the OAuth2 callback endpoint.
# After the user entered his/her credentials at the IdP's login form, the client will be redirected
# to this URL. When the client receives a request to this URL, it must send the request's query
# string to the broker's "openid_connect_path" set below.
redirect_url = "http://127.0.0.1:5173/oauth-callback"
# Will be appended to `base_url` to form the OAuth2 callback URL
redirect_path = "/oauth/callback"
# Set to true to get a user's additional claims from OAuth2
user_info_additional_claims_required = true
# On successful login (auth code flow complete), the browser can optionally
# be redirected to the application URL.
# If this is unset, the browser gets an empty 200 response on successful
# authorization code flow completion.
client_app_url = "http://127.0.0.1:5173/"
# OpenID Connect login completion request path.
# The client must pass the query string from the call to "redirect_url" to this URL and gets
# a basebox session token in return.
openid_connect_path = "/oauth/complete-login"
# Path to the browser login URL.
# This path is where the basebox broker returns a 302 response that redirects the browser to
@ -71,6 +66,9 @@ login_path = "/oauth/login"
# Simply POST to this URL with the session cookie or bearer token.
logout_path = "/oauth/logout"
# Set to true to get a user's additional claims from OAuth2
user_info_additional_claims_required = true
[business_logic_layer]
business_logic_layer_enabled = false

View File

@ -22,7 +22,7 @@ function logOut() {
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<a v-if="store.loggedIn" href="" @click="logOut">Log Out</a>
<a v-if="store.session" href="" @click="logOut">Log Out</a>
</nav>
</div>
</header>

View File

@ -0,0 +1,35 @@
<template>
<div id="error-message">
<h3>An error occurred...</h3>
<p>{{ errorMsg }}</p>
<p>
<a href="/" class="btn btn-primary">Start Over</a>
</p>
</div>
</template>
<script>
export default {
name: "OAuthError",
props: ["errorMsg"],
}
</script>
<style lang="scss" scoped>
#error-message {
margin: 5rem 0;
border: 1px solid #ff5050;
border-radius: .5rem;
padding: 2rem;
text-align: center;
p {
margin: 50px 0;
}
}
</style>

View File

@ -0,0 +1,25 @@
<!--
Dummy component that handles OAuth login callback requests.
-->
<script setup>
import { onMounted } from 'vue'
/**
* onMounted lifecycle hook
*/
onMounted(() => {
});
export default {
name: "OAuthCallback"
mounted: {
},
}
</script>

View File

@ -5,8 +5,11 @@
* https://basebox.tech
*/
import { store } from "../store";
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import OAUthError from "../components/OAUthError.vue";
import { objectToQueryString } from "../util/net";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -23,8 +26,68 @@ const router = createRouter({
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
},
{
path: '/oauth-error',
name: 'oauth-error',
component: OAUthError,
props: true,
},
]
})
router.beforeEach(async (to, from) => {
// Handle OAuth callback request.
// This request is coming in after the user entered her/his credentials at the IdP
// login form; the IdP (e.g. Keycloak) redirects the browser to the URL of this route.
if (to.path === "/oauth-callback") {
let queryString = objectToQueryString(to.query);
console.info(`Got /oauth-callback with query string '${queryString}`);
/* Pass the query parameters to the broker to complete login. We will get
* the session token and user data in return.
*
* The URL we post to is broker's base URL and the `openid_connect_path`
* setting from broker's config.
*/
const response = await fetch(`${store.baseboxHost}/oauth/complete-login?${queryString}`, {
method: "POST",
});
if (!response.ok) {
/* redirect to error component */
console.error("Failed to get session token: " + response.statusText);
return {
name: 'oauth-error',
query: {
code: response.status,
addlInfo: await response.text()
}
}
}
/* store session data; this is a JSON object of the following form:
{
"token":"47caa9df-ac89-45a8-8b22-9c0925d6aed0",
"username":"tester",
"first_name":"Fred",
"last_name":"Feuerstein",
"roles":[
"/user"
],
"claims":{
"groups":[
"/user"
]
}
}
*/
store.session = await response.json();
store.userName = store.session.first_name ? store.session.first_name : store.session.username;
/* redirect to home */
return { name: 'home' };
}
});
export default router

View File

@ -5,16 +5,17 @@
* https://basebox.tech
*/
import { reactive } from 'vue'
import { ref } from "vue";
export const store = reactive({
/** true if a user is currently logged in */
loggedIn: ref(false),
/** Username of the currently logged-in user */
userName: ref("stranger"),
userName: "stranger",
/** The host that runs basebox and waits for GraphQL requests */
baseboxHost: "http://127.0.0.1:8080",
/** basebox session data */
session: null,
})

View File

@ -4,8 +4,8 @@
* markus.thielen@basebox.health
*/
import store from '@/store'
import router from '@/router'
import {store} from "../store";
import router from "../router";
/**
* GqlError - custom error thrown by the gqlQuery function.
@ -107,8 +107,8 @@ export function gqlQuery(query)
};
/* if we're logged in, we add the authorization header */
if (store.getters.isLoggedIn) {
fetchOpt.headers["Authorization"] = store.getters.authData.token;
if (store.session) {
fetchOpt.headers["Authorization"] = `Bearer ${store.session.token}`;
}
console.info(fetchOpt);
@ -157,3 +157,16 @@ export function gqlQuery(query)
);
}
/**
* Encode object as a query string
* @param obj - a JavaScript object to encode
* @returns {string} query string, e.g. "parm1=778&read=all"
*/
export function objectToQueryString(obj) {
const str = [];
for (let p in obj)
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
return str.join("&");
}

View File

@ -14,7 +14,7 @@ function login() {
<template>
<main>
<!-- Force user to log in before he/she can see tasks. -->
<div v-if="!store.loggedIn" id="login-prompt">
<div v-if="!store.session" id="login-prompt">
<p>Your are currently not logged in.</p>
<button class="btn btn-primary" @click="login" type="button">Login</button>
</div>