From 3c30049dc2d0ab04767d76b361004b6efd122d30 Mon Sep 17 00:00:00 2001 From: Markus Thielen Date: Wed, 1 Nov 2023 03:36:55 +0100 Subject: [PATCH] BBIO-46 WIP --- bbconf/bb_todo-datamodel.sql | 2 +- bbconf/bb_todo-resolver.toml | 266 +++++++++++++++---------------- src/App.vue | 4 +- src/components/auth/Callback.vue | 12 ++ src/components/auth/Login.vue | 0 src/oidc/oidc-client.js | 80 ---------- src/router/index.js | 27 ++-- src/store.js | 29 ++-- src/util/oauth.js | 58 ------- src/util/oidc.js | 74 +++++++++ src/views/HomeView.vue | 19 ++- 11 files changed, 259 insertions(+), 312 deletions(-) create mode 100644 src/components/auth/Callback.vue create mode 100644 src/components/auth/Login.vue delete mode 100644 src/oidc/oidc-client.js delete mode 100644 src/util/oauth.js create mode 100644 src/util/oidc.js diff --git a/bbconf/bb_todo-datamodel.sql b/bbconf/bb_todo-datamodel.sql index c38a9e2..b2ddeb9 100644 --- a/bbconf/bb_todo-datamodel.sql +++ b/bbconf/bb_todo-datamodel.sql @@ -1,5 +1,5 @@ -- --- Generated by basebox compiler (bbc) version 0.1.0-beta.23 at 2023-10-31 15:13:34+01:00 +-- Generated by basebox compiler (bbc) version 0.1.0-beta.23 at 2023-11-01 03:18:33+01:00 -- CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/bbconf/bb_todo-resolver.toml b/bbconf/bb_todo-resolver.toml index ce0f71a..45335b6 100644 --- a/bbconf/bb_todo-resolver.toml +++ b/bbconf/bb_todo-resolver.toml @@ -1,5 +1,5 @@ # -# Generated by bbc (basebox compiler) version 0.1.0-beta.23 at 2023-10-31 15:13:34+01:00 +# Generated by bbc (basebox compiler) version 0.1.0-beta.23 at 2023-11-01 03:18:33+01:00 # [resolvers.updateTask] operation_name = "updateTask" @@ -36,6 +36,138 @@ column = "id" condition_str = "= '$id'" index = "" +[resolvers.getUser] +operation_name = "getUser" + +[resolvers.getUser.resolver.QueryBuilder] +command_type = "SQLSelect" + +[resolvers.getUser.resolver.QueryBuilder.command] +table = "User" +command_type = "SQLSelect" +columns = [] +modify_values = [] +nested_modify_tables = [] +aggregate_result = true + +[[resolvers.getUser.resolver.QueryBuilder.command.where_clauses]] +table = "User" +column = "username" +condition_str = "= '$username'" +index = "" + +[resolvers.deleteTask] +operation_name = "deleteTask" + +[resolvers.deleteTask.resolver.QueryBuilder] +command_type = "SQLDelete" + +[resolvers.deleteTask.resolver.QueryBuilder.command] +table = "Task" +command_type = "SQLSelect" +columns = [] +modify_values = [] +nested_modify_tables = [] +aggregate_result = true + +[[resolvers.deleteTask.resolver.QueryBuilder.command.where_clauses]] +table = "Task" +column = "id" +condition_str = "= '$id'" +index = "" + +[resolvers.createUser] +operation_name = "createUser" + +[resolvers.createUser.resolver.QueryBuilder] +command_type = "SQLInsert" + +[resolvers.createUser.resolver.QueryBuilder.command] +table = "User" +command_type = "SQLSelect" +columns = [] +nested_modify_tables = [] +where_clauses = [] +aggregate_result = true + +[[resolvers.createUser.resolver.QueryBuilder.command.modify_values]] +column = "username" +value = "'$username'" + +[[resolvers.createUser.resolver.QueryBuilder.command.modify_values]] +column = "name" +value = "'$name'" + +[resolvers.createList] +operation_name = "createList" + +[resolvers.createList.resolver.QueryBuilder] +command_type = "SQLInsert" + +[resolvers.createList.resolver.QueryBuilder.command] +table = "List" +command_type = "SQLSelect" +columns = [] +nested_modify_tables = [] +where_clauses = [] +aggregate_result = true + +[[resolvers.createList.resolver.QueryBuilder.command.modify_values]] +column = "title" +value = "'$title'" + +[[resolvers.createList.resolver.QueryBuilder.command.modify_values]] +column = "user_username" +value = "'$user.$username'" + +[resolvers.updateList] +operation_name = "updateList" + +[resolvers.updateList.resolver.QueryBuilder] +command_type = "SQLUpdate" + +[resolvers.updateList.resolver.QueryBuilder.command] +table = "List" +command_type = "SQLSelect" +columns = [] +nested_modify_tables = [] +aggregate_result = true + +[[resolvers.updateList.resolver.QueryBuilder.command.modify_values]] +column = "title" +value = "'$title'" + +[[resolvers.updateList.resolver.QueryBuilder.command.where_clauses]] +table = "List" +column = "id" +condition_str = "= '$id'" +index = "" + +[resolvers._bb_user_User] +operation_name = "_bb_user_User" + +[resolvers._bb_user_User.resolver.InternalQueryBuilder] +command_type = "SQLSelect" + +[resolvers._bb_user_User.resolver.InternalQueryBuilder.command] +table = "User" +command_type = "SQLSelect" +modify_values = [] +nested_modify_tables = [] +aggregate_result = true + +[[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.columns]] + +[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.columns.Column] +table = "User" +column = "username" + +[[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.where_clauses]] +table = "User" +column = ".ownerId" +condition_str = "= $1" +index = "" + [resolvers.createTask] operation_name = "createTask" @@ -70,49 +202,6 @@ value = "'$list.$id'" column = "user_username" value = "'$user.$username'" -[resolvers.updateList] -operation_name = "updateList" - -[resolvers.updateList.resolver.QueryBuilder] -command_type = "SQLUpdate" - -[resolvers.updateList.resolver.QueryBuilder.command] -table = "List" -command_type = "SQLSelect" -columns = [] -nested_modify_tables = [] -aggregate_result = true - -[[resolvers.updateList.resolver.QueryBuilder.command.modify_values]] -column = "title" -value = "'$title'" - -[[resolvers.updateList.resolver.QueryBuilder.command.where_clauses]] -table = "List" -column = "id" -condition_str = "= '$id'" -index = "" - -[resolvers.deleteTask] -operation_name = "deleteTask" - -[resolvers.deleteTask.resolver.QueryBuilder] -command_type = "SQLDelete" - -[resolvers.deleteTask.resolver.QueryBuilder.command] -table = "Task" -command_type = "SQLSelect" -columns = [] -modify_values = [] -nested_modify_tables = [] -aggregate_result = true - -[[resolvers.deleteTask.resolver.QueryBuilder.command.where_clauses]] -table = "Task" -column = "id" -condition_str = "= '$id'" -index = "" - [resolvers.deleteList] operation_name = "deleteList" @@ -132,92 +221,3 @@ table = "List" column = "id" condition_str = "= '$id'" index = "" - -[resolvers.createUser] -operation_name = "createUser" - -[resolvers.createUser.resolver.QueryBuilder] -command_type = "SQLInsert" - -[resolvers.createUser.resolver.QueryBuilder.command] -table = "User" -command_type = "SQLSelect" -columns = [] -nested_modify_tables = [] -where_clauses = [] -aggregate_result = true - -[[resolvers.createUser.resolver.QueryBuilder.command.modify_values]] -column = "username" -value = "'$username'" - -[[resolvers.createUser.resolver.QueryBuilder.command.modify_values]] -column = "name" -value = "'$name'" - -[resolvers._bb_user_User] -operation_name = "_bb_user_User" - -[resolvers._bb_user_User.resolver.InternalQueryBuilder] -command_type = "SQLSelect" - -[resolvers._bb_user_User.resolver.InternalQueryBuilder.command] -table = "User" -command_type = "SQLSelect" -modify_values = [] -nested_modify_tables = [] -aggregate_result = true - -[[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.columns]] - -[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.columns.Column] -table = "User" -column = "username" - -[[resolvers._bb_user_User.resolver.InternalQueryBuilder.command.where_clauses]] -table = "User" -column = ".ownerId" -condition_str = "= $1" -index = "" - -[resolvers.createList] -operation_name = "createList" - -[resolvers.createList.resolver.QueryBuilder] -command_type = "SQLInsert" - -[resolvers.createList.resolver.QueryBuilder.command] -table = "List" -command_type = "SQLSelect" -columns = [] -nested_modify_tables = [] -where_clauses = [] -aggregate_result = true - -[[resolvers.createList.resolver.QueryBuilder.command.modify_values]] -column = "title" -value = "'$title'" - -[[resolvers.createList.resolver.QueryBuilder.command.modify_values]] -column = "user_username" -value = "'$user.$username'" - -[resolvers.getUser] -operation_name = "getUser" - -[resolvers.getUser.resolver.QueryBuilder] -command_type = "SQLSelect" - -[resolvers.getUser.resolver.QueryBuilder.command] -table = "User" -command_type = "SQLSelect" -columns = [] -modify_values = [] -nested_modify_tables = [] -aggregate_result = true - -[[resolvers.getUser.resolver.QueryBuilder.command.where_clauses]] -table = "User" -column = "username" -condition_str = "= '$username'" -index = "" diff --git a/src/App.vue b/src/App.vue index e86881c..2e77ce7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/src/components/auth/Login.vue b/src/components/auth/Login.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/oidc/oidc-client.js b/src/oidc/oidc-client.js deleted file mode 100644 index 42fb745..0000000 --- a/src/oidc/oidc-client.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Configure oidc-client library. - * - */ - -import Oidc from 'oidc-client'; - -Oidc.Log.logger = console; -Oidc.Log.level = (process.env.NODE_ENV === 'production') ? Oidc.Log.ERROR : Oidc.Log.DEBUG; - -// OIDC configuration -let oidcProviderDomain = 'https://basebox-test-1.eu.auth0.com'; -let clientId = '5wl8hQV1thh07rScSoJ3aN56ETuXWprg'; -let scopes = "openid profile email" - -let instance; - -// OIDC Client -export const getOidcClient = () => { - if (instance) { - return instance; - } - - instance = new Oidc.UserManager({ - userStore: new Oidc.WebStorageStateStore(), - authority: oidcProviderDomain, - client_id: clientId, - redirect_uri: window.location.origin + '/callback', - response_type: 'code', - scope: scopes, - post_logout_redirect_uri: window.location.origin + '/home?action=logout', - accessTokenExpiringNotificationTime: 10, - automaticSilentRenew: false, - filterProtocolClaims: false, - loadUserInfo: true, - includeIdTokenInSilentRenew: false - }); - - instance.events.addAccessTokenExpiring(function() { - // eslint-disable-next-line no-console - console.log('access token expiring') - }) - - instance.events.addAccessTokenExpired(function() { - // eslint-disable-next-line no-console - console.log('access token expired') - }) - - instance.events.addSilentRenewError(function(err) { - // eslint-disable-next-line no-console - console.error('silent renew error', err) - }) - - instance.events.addUserLoaded(function(user) { - // eslint-disable-next-line no-console - console.log('user loaded', user) - }) - - instance.events.addUserSignedIn(function(user) { - // eslint-disable-next-line no-console - console.log('user signed in', user) - }) - - instance.events.addUserUnloaded(function() { - // eslint-disable-next-line no-console - console.log('user unloaded') - }) - - instance.events.addUserSignedOut(function() { - // eslint-disable-next-line no-console - console.log('user signed out') - }) - - instance.events.addUserSessionChanged(function() { - // eslint-disable-next-line no-console - console.log('user session changed') - }) - - return instance; -} diff --git a/src/router/index.js b/src/router/index.js index ed0fae8..ff5c554 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -9,8 +9,7 @@ import {loggedIn, showError, store} from "../store"; import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' import { objectToQueryString } from "../util/net"; -import {oauthCallbackHandler} from "../util/oauth"; -import {storeInit} from "../store"; +import { callbackPath, getOidcUserManager } from "../util/oidc"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -38,24 +37,18 @@ const router = createRouter({ 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.startsWith("/oauth-callback")) { - let queryString = objectToQueryString(to.query); - console.info(`Got /oauth-callback with query string '${queryString}`); - - /* Pass the query string to the handler function */ + /* catch OIDC callback */ + if (to.path === callbackPath) { + const userMgr = getOidcUserManager(); try { - await oauthCallbackHandler(queryString); + const user = await userMgr.signinRedirectCallback(); + console.log("OIDC: login complete, user: ", user); + storeInit(user); + /* redirect to home page */ + return {name: 'home'}; } catch (err) { - const errorMsg = `Failed to get session info from basebox/finish OpenID Connect login: ${err}`; - console.error(errorMsg); - /* show error */ - showError(errorMsg); + console.error("OIDC: login failed: ", err); } - - return {name: 'home'}; } /* if no user is logged in, we redirect to the home page */ diff --git a/src/store.js b/src/store.js index e19ef5e..c51c011 100644 --- a/src/store.js +++ b/src/store.js @@ -18,8 +18,8 @@ export const store = reactive({ /** base URL of basebox broker host */ baseboxHost: import.meta.env.BASEBOX_HOST || "http://127.0.0.1:8080", - /** basebox session data */ - session: {}, + /** User as returned from oidc-client */ + user: null, /** Fatal error message, if any */ fatalError: "", @@ -35,14 +35,15 @@ export const store = reactive({ * Return true if the user is logged in. */ export function loggedIn() { - return store.session && store.session.token; + return store.user !== null; } /** * Clear session data. */ export function clearSession() { - store.session = {}; + store.user = null; + store.userDisplayName = "stranger"; } /** @@ -69,27 +70,17 @@ export function clearError() { * * Additionally, we also create the default list here and save everything in the store. * - * @param session - the session info as returned by the OpenID Connect login. + * @param user - the user as returned from oidc-client. */ -export async function storeInit(session) { +export async function storeInit(user) { /* save user session in the store */ - store.session = { ...session }; + store.user = user; - /* The information in the session object depends on the OpenID Connect provider. - * The following fields are always present: - * - * `token` - basebox session token - * `subject` - unique user id (not a username, rather a number or UUID) - * `id_token_claims` - all fields found in the ID token. - * - * Since we assume Auth0 being the Open ID Connect provider, we use `id_token_claims.nickname` - * as the user name we display in the UI. - */ - store.userDisplayName = session.id_token_claims.nickname; + store.userDisplayName = user.profile.nickname || "Logged In Stranger"; /* The unique user ID is the 'sub' field from the ID token. */ - store.userId = session.subject; + store.userId = user.profile.sub; /* Create user and default list. * We cannot run these requests in parallel, since the user record must exist in the database diff --git a/src/util/oauth.js b/src/util/oauth.js deleted file mode 100644 index 709bfa0..0000000 --- a/src/util/oauth.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * OAuth specific functions. - * - * Part of the basebox sample Todo app. - * https://basebox.io - */ - -import {store} from "../store"; -import {storeInit} from "../store"; - -/** - * Handle OAuth callback and complete login process. - * - * After the user enters her/his credentials at the IdP (Keycloak) login form, she/he - * gets redirected to a client URL; this function handles this request. - * - * This function passes the query string received with the callback to the basebox broker, - * which responds with a session information struct (JSON) like this: - * - * { - * "token":"47caa9df-ac89-45a8-8b22-9c0925d6aed0", - * "username":"tester", - * "first_name":"Fred", - * "last_name":"Feuerstein", - * "roles":[ - * "/user" - * ], - * "claims":{ - * "groups":[ - * "/user" - * ] - * } - * } - * - * The session is then stored in app state. - * - * Then, this function checks if the user is known in the database; since we use Keycloak/OpenID Connect - * for user management, the user might be available in Keycloak, but not in the database. So if the user - * is not there, it is created. - * - * @param queryString - * @returns {Promise} - * @throws Error if the session data cannot be retrieved. - */ -export async function oauthCallbackHandler(queryString) { - - /* Pass query string to the broker to complete the login process. */ - const response = await fetch(`${store.baseboxHost}/oauth/complete-login?${queryString}`, { - method: "POST", - }); - if (!response.ok) { - throw new Error("Failed to get session data: " + response.statusText); - } - - /* Store session data and initialize data store. */ - const rspJson = await response.json(); - await storeInit(rspJson); -} diff --git a/src/util/oidc.js b/src/util/oidc.js new file mode 100644 index 0000000..56ef3d1 --- /dev/null +++ b/src/util/oidc.js @@ -0,0 +1,74 @@ +/** + * Configure oidc-client-ts library and retrieve UserManager instance for authentication. + * + */ + +import { Log, UserManager, WebStorageStateStore } from 'oidc-client-ts'; + +Log.logger = console; +Log.level = (process.env.NODE_ENV === 'production') ? Log.ERROR : Log.DEBUG; + +// OIDC configuration +const oidcProviderDomain = 'https://basebox-test-1.eu.auth0.com'; +const clientId = '5wl8hQV1thh07rScSoJ3aN56ETuXWprg'; +const scopes = "openid profile email"; +export const callbackPath = "/auth/callback" + +/* OIDC UserManager singleton */ +let userMgr = null; + +// OIDC Client +export const getOidcUserManager = () => { + if (userMgr) { + return userMgr; + } + + userMgr = new UserManager({ + userStore: new WebStorageStateStore(), + authority: oidcProviderDomain, + client_id: clientId, + redirect_uri: window.location.origin + callbackPath, + response_type: 'code', + scope: scopes, + post_logout_redirect_uri: window.location.origin + '/home?action=logout', + accessTokenExpiringNotificationTime: 10, + automaticSilentRenew: false, + filterProtocolClaims: false, + loadUserInfo: true, + includeIdTokenInSilentRenew: false + }); + + userMgr.events.addAccessTokenExpiring(function() { + console.info('OIDC: access token expiring') + }) + + userMgr.events.addAccessTokenExpired(function() { + console.info('OIDC: access token expired') + }) + + userMgr.events.addSilentRenewError(function(err) { + console.error('silent renew error', err) + }) + + userMgr.events.addUserLoaded(function(user) { + console.info('OIDC: user loaded', user) + }) + + userMgr.events.addUserSignedIn(function(user) { + console.info('OIDC: user signed in', user) + }) + + userMgr.events.addUserUnloaded(function() { + console.info('OIDC: user unloaded') + }) + + userMgr.events.addUserSignedOut(function() { + console.info('OIDC: user signed out') + }) + + userMgr.events.addUserSessionChanged(function() { + console.info('OIDC: user session changed') + }) + + return userMgr; +} diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index e00b8f6..4f99f68 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -9,6 +9,21 @@ import { store } from "../store"; import {loggedIn} from "../store"; import TodoRoot from "../components/Todo.vue"; +import { getOidcUserManager } from "../util/oidc"; + + +/** + * Start login process. + */ +function doLogin() { + console.log("doLogin"); + const userMgr = getOidcUserManager(); + userMgr.signinRedirect().then(() => { + console.log("signinRedirect"); + }).catch((err) => { + console.error("signinRedirect failed", err); + }); +} @@ -16,11 +31,11 @@ import TodoRoot from "../components/Todo.vue";
-
+

Welcome to basebox' TODO sample app!

Your are currently not logged in.

- +