diff --git a/.env.example b/.env.example index 43b7b27f8..57842b095 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ VUE_APP_VIEWER_BASE_URL=https://viewer-staging.bimdata.io VUE_APP_OIDC_CLIENT_ID= VUE_APP_MAPBOX_TOKEN=pk.eyJ1IjoiYmltZGF0YSIsImEiOiJjanRrM3ljcW8yeHB1NGFwODNjZGY1czd2In0.EvPADmXo97ZLl4A_7vs59A VUE_APP_MAX_UPLOAD_SIZE=1000000 +VUE_APP_AUTHORIZED_IDENTITY_PROVIDERS=bimdataconnect # External pages VUE_APP_URL_BIMDATACONNECT=https://connect-staging.bimdata.io diff --git a/src/config/oidcConfig.js b/src/config/oidcConfig.js index cccc6c404..6c530e569 100644 --- a/src/config/oidcConfig.js +++ b/src/config/oidcConfig.js @@ -7,6 +7,7 @@ const APP_BASE_URL = process.env.VUE_APP_BASE_URL; const AUTHORITY = `${process.env.VUE_APP_IAM_BASE_URL}/auth/realms/bimdata`; const OIDC_ENDPOINT = `${AUTHORITY}/protocol/openid-connect`; const CLIENT_ID = process.env.VUE_APP_OIDC_CLIENT_ID; +const AUTHORIZED_IDENTITY_PROVIDERS = process.env.VUE_APP_AUTHORIZED_IDENTITY_PROVIDERS; export const oidcConfig = { // Auth request config @@ -37,5 +38,8 @@ export const oidcConfig = { userinfo_endpoint: `${OIDC_ENDPOINT}/userinfo`, end_session_endpoint: `${OIDC_ENDPOINT}/logout`, jwks_uri: `${OIDC_ENDPOINT}/certs` - } + }, + + // Limit authorized identity providers + authorizedIdentityProviders: AUTHORIZED_IDENTITY_PROVIDERS ? AUTHORIZED_IDENTITY_PROVIDERS.split(',') : [], }; diff --git a/src/server/AuthService.js b/src/server/AuthService.js index 62798d9d7..fc760bde0 100644 --- a/src/server/AuthService.js +++ b/src/server/AuthService.js @@ -1,4 +1,4 @@ -import { UserManager, WebStorageStateStore } from "oidc-client"; +import { User, UserManager, WebStorageStateStore } from "oidc-client"; import { oidcConfig } from "@/config/oidcConfig"; const userManager = new UserManager({ @@ -6,6 +6,49 @@ const userManager = new UserManager({ userStore: new WebStorageStateStore({ store: window.localStorage }) }); +/* +Monkey patch oidcUserManager to hijack login with force logout +*/ +async function signinEndWithForcedLogout (url, args = {}) { + const signinResponse = await this.processSigninResponse(url); + const authorizedIdentityProviders = oidcConfig.authorizedIdentityProviders; + if (authorizedIdentityProviders.length) { + const identityProvider = signinResponse.profile.preferred_username.split('.')[0]; + if (!authorizedIdentityProviders.includes(identityProvider)) { + const params = new URLSearchParams({ + post_logout_redirect_uri: oidcConfig.post_logout_redirect_uri, + id_token_hint: signinResponse.id_token, + initiating_idp: identityProvider, + }); + const redirectUrl = oidcConfig.metadata.end_session_endpoint + '?' + params.toString(); + window.location.replace(redirectUrl); + await new Promise((resolve, reject) => { + // Wait for window.location.replace to trigger + setTimeout(resolve, 5000); + }); + } + } + + const user = new User(signinResponse) + + if (args.current_sub) { + if (args.current_sub !== user.profile.sub) { + console.debug('UserManager._signinEnd: current user does not match user returned from signin. sub from signin: ', user.profile.sub) + throw new Error('login_required') + } else { + console.debug('UserManager._signinEnd: current user matches user returned from signin') + } + } + await this.storeUser(user) + console.debug('UserManager._signinEnd: user stored') + + this._events.load(user) + return user +} +signinEndWithForcedLogout.bind(userManager) + +userManager._signinEnd = signinEndWithForcedLogout + class AuthServive { getUser() { return userManager.getUser(); diff --git a/src/state/auth.js b/src/state/auth.js index 22a85bb3e..14aecee0d 100644 --- a/src/state/auth.js +++ b/src/state/auth.js @@ -2,6 +2,32 @@ import { reactive, readonly, toRefs, watchEffect } from "vue"; import apiClient from "@/server/api-client"; import AuthService from "@/server/AuthService"; +const parseJwt = (token) => { + try { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace('-', '+').replace('_', '/'); + return JSON.parse(window.atob(base64)); + } catch (error) { + return {}; + } +} + +const tokenExp = (token) => { + if (token) { + const parsed = parseJwt(token); + return parsed.exp ? parsed.exp * 1000 : null; + } + return null; +} + +const tokenIsExpired = (token) => { + const tokenExpiryTime = tokenExp(token); + if (tokenExpiryTime) { + return tokenExpiryTime < new Date().getTime(); + } + return false; +} + const state = reactive({ isAuthenticated: false, accessToken: null @@ -15,6 +41,10 @@ const authenticate = async redirectPath => { } if (!state.isAuthenticated) { if (user.expired) { + if (tokenIsExpired(user.refresh_token)) { + await AuthService.signIn(redirectPath); + return; + } // Refresh access token silently user = await AuthService.signInSilent(); }