From bc41cd1c7594cf4ae847ae5720e8096d9467cdc1 Mon Sep 17 00:00:00 2001 From: Curt Tudor Date: Fri, 1 Nov 2024 13:50:04 -0600 Subject: [PATCH] id_token -> access_token refactor --- package.json | 3 +- src/constants.js | 2 + src/oidc/utils.js | 71 ++++++++++++++----- src/runtime.js | 176 ++++++++++++++++++++++++++++++++++++++-------- yarn.lock | 13 ++-- 5 files changed, 211 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index a1fab72..3e0d7db 100644 --- a/package.json +++ b/package.json @@ -96,10 +96,11 @@ "@auth0/auth0-spa-js": "^2.0.4", "@azure/msal-browser": "^2.38.0", "@babel/runtime": "^7.17.9", - "@openziti/ziti-browzer-core": "^0.50.0", + "@openziti/ziti-browzer-core": "^0.51.0", "bowser": "^2.11.0", "cookie-interceptor": "^1.0.0", "core-js": "^3.22.8", + "date-fns": "^4.1.0", "events": "^3.3.0", "js-base64": "^3.7.2", "jwt-decode": "^3.1.2", diff --git a/src/constants.js b/src/constants.js index 04d964f..1bba8f1 100644 --- a/src/constants.js +++ b/src/constants.js @@ -53,6 +53,8 @@ const ZBR_CONSTANTS = ZBR_ERROR_CODE_WSS_ROUTER_CONNECTION_ERROR: 1016, ZBR_ERROR_CODE_CONTROLLER_CONNECTION_ERROR: 1017, ZBR_ERROR_CODE_PKCS_LOGIN_ERROR: 1018, + ZBR_ERROR_CODE_ORIGIN_TRIAL_EXPIRED: 1019, + ZBR_ERROR_CODE_NO_API_AUDIENCE: 1020, }; diff --git a/src/oidc/utils.js b/src/oidc/utils.js index a6097c0..17d7846 100644 --- a/src/oidc/utils.js +++ b/src/oidc/utils.js @@ -28,7 +28,7 @@ import { processDiscoveryResponse, validateAuthResponse } from 'oauth4webapi'; -import { isEqual } from 'lodash-es'; +import { isEqual, isUndefined } from 'lodash-es'; import jwtDecode from 'jwt-decode'; import { ZBR_CONSTANTS } from '../constants'; @@ -110,10 +110,15 @@ export const PKCEState = { set: (state) => sessionStorage.setItem(window.btoa('pkce_state'), state), unset: () => sessionStorage.removeItem(window.btoa('pkce_state')) }; -export const PKCEToken = { - get: () => sessionStorage.getItem('BrowZer_token'), - set: (state) => sessionStorage.setItem('BrowZer_token', state), - unset: () => sessionStorage.removeItem('BrowZer_token') +export const PKCE_id_Token = { + get: () => sessionStorage.getItem('BrowZer_id_token'), + set: (state) => sessionStorage.setItem('BrowZer_id_token', state), + unset: () => sessionStorage.removeItem('BrowZer_id_token') +}; +export const PKCE_access_Token = { + get: () => sessionStorage.getItem('BrowZer_access_token'), + set: (state) => sessionStorage.setItem('BrowZer_access_token', state), + unset: () => sessionStorage.removeItem('BrowZer_access_token') }; export const PKCEAuthorizationServer = { get: () => {return JSON.parse(sessionStorage.getItem('BrowZer_oidc_config'))}, @@ -236,7 +241,32 @@ export const pkceLogin = async (oidcConfig, redirectURI) => { const authorizationServerConsentScreen = new URL(authorizationServer.authorization_endpoint); authorizationServerConsentScreen.searchParams.set('client_id', oidcConfig.client_id); - // authorizationServerConsentScreen.searchParams.set('audience', 'https://browzermost.ziti.netfoundry.io'); + + /** + * If Auth0 is the IdP, then we need to add an audience parm (perhaps other things as well) in order to get a valid access_token + */ + let asurl = new URL(authorizationServer.authorization_endpoint); + if (asurl.hostname.includes('auth0.com')) { + /** + * If we are configured with authorization_endpoint_parms, then use it + */ + if (!isUndefined(oidcConfig.authorization_endpoint_parms)) { + const params = new URLSearchParams(oidcConfig.authorization_endpoint_parms); + let parmArray = Array.from(params.entries()); + parmArray.forEach((parm) => { + if (isEqual(parm[0].toLowerCase(), 'audience')) { // ignore any non-audience URL parms that were passed in (Auth0 dislikes them) + authorizationServerConsentScreen.searchParams.set(parm[0], parm[1]); + } + }); + } + /** + * If we were NOT configured with authorization_endpoint_parms, we will default to using an audience value equal + * to the browZer app's URL (i.e. we assume that an 'API' was created in Auth0 that matches that URL) + */ + else { + authorizationServerConsentScreen.searchParams.set('audience', `https://${window.zitiBrowzerRuntime.zitiConfig.browzer.bootstrapper.self.host}`); + } + } authorizationServerConsentScreen.searchParams.set('code_challenge', codeChallange); authorizationServerConsentScreen.searchParams.set('code_challenge_method', 'S256'); authorizationServerConsentScreen.searchParams.set('redirect_uri', redirectURI); @@ -338,9 +368,10 @@ export const pkceCallback = async (oidcConfig, redirectURI) => { } let { id_token } = result; - let { access_token } = result; - - PKCEToken.set(id_token); + PKCE_id_Token.set(id_token); + + let { access_token } = result; + PKCE_access_Token.set(access_token); }; @@ -355,15 +386,16 @@ export const pkceLogout = async (oidcConfig, redirectURI) => { const {authorizationServer} = await validateAndGetOIDCForPKCE(oidcConfig); // Pull the token from session storage - let access_token = PKCEToken.get(); + let id_token = PKCE_id_Token.get(); if (authorizationServer.end_session_endpoint) { const authorizationServerLogoutURL = new URL(authorizationServer.end_session_endpoint); - if (!isEqual(access_token, null)) { - authorizationServerLogoutURL.searchParams.set('id_token_hint', access_token); - PKCEToken.unset(); + if (!isEqual(id_token, null)) { + authorizationServerLogoutURL.searchParams.set('id_token_hint', id_token); + PKCE_id_Token.unset(); + PKCE_access_Token.unset(); } authorizationServerLogoutURL.searchParams.set('client_id', oidcConfig.client_id); authorizationServerLogoutURL.searchParams.set('post_logout_redirect_uri', redirectURI); @@ -391,9 +423,9 @@ export const pkceLogout = async (oidcConfig, redirectURI) => { if (asurl.hostname.includes('auth0.com')) { let isExpired = false; - if (access_token) { - let decoded_access_token = jwtDecode(access_token); - let exp = decoded_access_token.exp; + if (id_token) { + let decoded_id_token = jwtDecode(id_token); + let exp = decoded_id_token.exp; if (Date.now() >= exp * 1000) { isExpired = true; } @@ -402,12 +434,13 @@ export const pkceLogout = async (oidcConfig, redirectURI) => { } let url; - if (!isEqual(access_token, null) && !isExpired) { - url = `${asurl.protocol}//${asurl.hostname}/v2/logout?id_token_hint=${access_token}client_id=${oidcConfig.client_id}&returnTo=${redirectURI}`; + if (!isEqual(id_token, null) && !isExpired) { + url = `${asurl.protocol}//${asurl.hostname}/v2/logout?id_token_hint=${id_token}client_id=${oidcConfig.client_id}&returnTo=${redirectURI}`; } else { url = `${asurl.protocol}//${asurl.hostname}/v2/logout?client_id=${oidcConfig.client_id}&returnTo=${redirectURI}`; } - PKCEToken.unset(); + PKCE_id_Token.unset(); + PKCE_access_Token.unset(); window.location = url; } else { diff --git a/src/runtime.js b/src/runtime.js index 27da418..34d0e73 100644 --- a/src/runtime.js +++ b/src/runtime.js @@ -39,14 +39,16 @@ import { Auth0Client } from '@auth0/auth0-spa-js'; import Bowser from 'bowser'; import * as msal from '@azure/msal-browser'; import { stringify } from './urlon'; -import * as oidc from 'oauth4webapi' +import * as oidc from 'oauth4webapi'; +import { format } from 'date-fns'; import { getPKCERedirectURI, pkceLogin, pkceLogout, pkceLogoutIsNeeded, pkceCallback, - PKCEToken, + PKCE_id_Token, + PKCE_access_Token, } from './oidc/utils'; import { jspi } from "wasm-feature-detect"; import {eruda} from './tool-button/eruda'; @@ -129,6 +131,23 @@ function isTokenExpired(access_token) { return isExpired; } +/** + * + */ +function isOriginTrialTokenExpired(token) { + try { + const expirationTime = token.payload.expiry * 1000; // Convert to milliseconds + const currentTime = Date.now(); + if ( currentTime > expirationTime ) { + return expirationTime + } else { + return false; + } + } catch (error) { + return Date.now(); // If there's an error, assume the token is invalid or expired + } +} + /** * */ @@ -139,6 +158,7 @@ function getOIDCConfig() { name: 'ZitiBrowzerRuntimeOIDCConfig', issuer: window.zitiBrowzerRuntime.zitiConfig.idp.host, client_id: window.zitiBrowzerRuntime.zitiConfig.idp.clientId, + authorization_endpoint_parms: window.zitiBrowzerRuntime.zitiConfig.idp.authorization_endpoint_parms, scopes: ['openid', 'email'], enablePKCEAuthentication: true, token_endpoint_auth_method: 'none', @@ -550,10 +570,10 @@ class ZitiBrowzerRuntime { position: 'center', insert: 'after', theme: 'compact', - pool: 10, + pool: 3, sticky: false, progressbar: true, - headerText: 'OpenZiti browZer', + headerText: 'OpenZiti BrowZer', effect: 'slide', closer: false, life: 3000, @@ -706,6 +726,17 @@ class ZitiBrowzerRuntime { } + originTrialTokenExpiredEventHandler(originTrialExpiredEvent) { + + window.zitiBrowzerRuntime.browzer_error({ + status: 409, + code: ZBR_CONSTANTS.ZBR_ERROR_CODE_ORIGIN_TRIAL_EXPIRED, + title: `OriginTrial expiration for feature [${originTrialExpiredEvent.feature}]`, + message: `Token for origin [${originTrialExpiredEvent.expectedOrigin}] expired at[${format(new Date(originTrialExpiredEvent.expirationTime), 'MMMM dd, yyyy HH:mm:ss')}]` + }); + + } + noConfigForServiceEventHandler(noConfigForServiceEvent) { this.logger.trace(`noConfigForServiceEventHandler() `, noConfigForServiceEvent); @@ -848,6 +879,51 @@ class ZitiBrowzerRuntime { } + idTokenDeprecationEventHandler(deprecationEvent) { + + this.logger.trace(`idTokenDeprecationEventHandler() `, deprecationEvent); + + let link = `Please visit this link for details regarding configuration to use access_tokens.`; + + let idTokenDeprecationRenderDone = sessionStorage.getItem('idTokenDeprecationRenderDone'); + + if (isNull(idTokenDeprecationRenderDone)) { idTokenDeprecationRenderDone = 0} + + if (idTokenDeprecationRenderDone < 3) { + idTokenDeprecationRenderDone++; + sessionStorage.setItem('idTokenDeprecationRenderDone', idTokenDeprecationRenderDone); + window.zitiBrowzerRuntime.toastWarningSticky(`DEPRECATION NOTICE:
Your BrowZer app is configured to use the id_token from your IdP.
Authentication via id_token is deprecated.
${link}`); + } + } + + accessTokenInvalidEventHandler(accessTokenInvalidEvent) { + + this.logger.trace(`accessTokenInvalidEventHandler() `, accessTokenInvalidEvent); + + window.zitiBrowzerRuntime.browzer_error({ + status: 511, + code: ZBR_CONSTANTS.ZBR_ERROR_CODE_INVALID_IDP_CONFIG, + title: `IdP[${accessTokenInvalidEvent.idp_host}] produced an invalid access_token`, + message: `'audience' may not be configured in the BrowZer Bootstrapper` + }); + + } + + accessTokenMissingAPIAudienceEventHandler(event) { + + const parts = event.audience.split('&'); + + + window.zitiBrowzerRuntime.browzer_error({ + status: 511, + code: ZBR_CONSTANTS.ZBR_ERROR_CODE_NO_API_AUDIENCE, + title: `IdP[${event.idp_host}] cannot produce a valid access_token`, + message: `On the IdP, please create an API with 'identifier' of ${parts[0]}` + }); + + } + + channelConnectFailEventHandler(channelConnectFailEvent) { this.logger.trace(`channelConnectFailEventHandler() `, channelConnectFailEvent); @@ -1288,16 +1364,23 @@ class ZitiBrowzerRuntime { this.logger.trace(`initialize() Loaded via BOOTSTRAPPER`); - // Pull the token from session storage - this.zitiConfig.access_token = PKCEToken.get(); + // Pull the tokens from session storage + this.zitiConfig.id_token = PKCE_id_Token.get(); + this.zitiConfig.access_token = PKCE_access_Token.get(); + + let invalidAccessToken = false; // If we have a token, determine if it has expired if (!isEqual(this.zitiConfig.access_token, null)) { this.logger.trace(`initialize() session token found`); - if (isTokenExpired(this.zitiConfig.access_token)) { - this.isAuthenticated = false; - } else { - this.isAuthenticated = true; + try { + if (isTokenExpired(this.zitiConfig.access_token)) { + this.isAuthenticated = false; + } else { + this.isAuthenticated = true; + } + } catch (e) { + invalidAccessToken = true; } } else { this.logger.trace(`initialize() session token NOT found`); @@ -1307,8 +1390,17 @@ class ZitiBrowzerRuntime { // If we don't have a valid token yet if (!this.isAuthenticated) { - // If we are coming back from an IdP redirect, obtain the token by leveraging the URL parms - if (window.location.search.includes("code=") && window.location.search.includes("state=")) { + // If we are coming back from an IdP redirect, obtain the token by leveraging the URL parms. + if (window.location.search.includes("error=access_denied")) { + const params = new URLSearchParams(window.location.search); + // e.g. error_description=Service not found: https://mattermost.ziti.netfoundry.io + this.accessTokenMissingAPIAudienceEventHandler({ + idp_host: window.zitiBrowzerRuntime.zitiConfig.idp.host, + audience: params.get('error_description').replace('Service not found:',''), + }); + await delay(5000); + } + else if (window.location.search.includes("code=") && window.location.search.includes("state=")) { this.logger.trace(`initialize() calling pkceCallback`); @@ -1317,7 +1409,8 @@ class ZitiBrowzerRuntime { window.zitiBrowzerRuntime.pkceCallbackErrorEventHandler(error); }); - this.zitiConfig.access_token = PKCEToken.get(); + this.zitiConfig.id_token = PKCE_id_Token.get(); + this.zitiConfig.access_token = PKCE_access_Token.get(); window.location.replace(getPKCERedirectURI().toString()); @@ -1335,13 +1428,13 @@ class ZitiBrowzerRuntime { // Local data indicates that the user is not authenticated, however, the IdP might still think the authentication // is alive/valid (a common Auth0 situation), so, we will force/tell the IdP to do a logout. - if (await pkceLogoutIsNeeded(getOIDCConfig())) { + if (!invalidAccessToken && await pkceLogoutIsNeeded(getOIDCConfig())) { let logoutInitiated = this.getCookie( this.authTokenName + '_logout_initiated' ); if (isEqual(logoutInitiated, '')) { document.cookie = this.authTokenName + '_logout_initiated' + "=" + "yes" + "; path=/"; this.logger.trace(`initialize() calling pkceLogout`); pkceLogout( getOIDCConfig(), getPKCERedirectURI().toString() ); - await delay(1000); // we need to pause a bit or the 'login' call below will cancel the 'logout' + await delay(3000); // we need to pause a bit or the 'login' call below will cancel the 'logout' } document.cookie = this.authTokenName + '_logout_initiated'+'=; Max-Age=-99999999;'; } @@ -1373,7 +1466,8 @@ class ZitiBrowzerRuntime { this.logger.trace(`initialize() Loaded via ZBSW`); // Pull the token from session storage - this.zitiConfig.access_token = PKCEToken.get(); + this.zitiConfig.id_token = PKCE_id_Token.get(); + this.zitiConfig.access_token = PKCE_access_Token.get(); if (isEqual(this.zitiConfig.access_token, null)) { @@ -1386,7 +1480,10 @@ class ZitiBrowzerRuntime { this.logger.trace(`initialize() session token ACQUIRED from ZBSW`); - PKCEToken.set(swVersionObject.zitiConfig.access_token); + PKCE_id_Token.set(swVersionObject.zitiConfig.id_token); + this.zitiConfig.id_token = swVersionObject.zitiConfig.id_token; + PKCE_access_Token.set(swVersionObject.zitiConfig.access_token); + this.zitiConfig.access_token = swVersionObject.zitiConfig.access_token } else { @@ -1421,6 +1518,7 @@ class ZitiBrowzerRuntime { sdkRevision: buildInfo.sdkRevision, token_type: this.zitiConfig.token_type, + id_token: this.zitiConfig.id_token, access_token: this.zitiConfig.access_token, apiSessionHeartbeatTimeMin: (1), @@ -1440,23 +1538,12 @@ class ZitiBrowzerRuntime { this.zitiConfig.jspi = await this.shouldUseJSPI(); // determine which WASM to instantiate - await this.zitiContext.initialize({ - loadWASM: !options.loadedViaBootstrapper, // instantiate the WASM ONLY if we were not injected by the browZer Bootstrapper - jspi: this.zitiConfig.jspi, // indicate which WASM to instantiate - target: this.zitiConfig.browzer.bootstrapper.target - }); - - this.initialized = true; - - this.logger.trace(`ZitiBrowzerRuntime ${this._uuid} has been initialized`); - - await this.zitiContext.listControllerVersion(); - this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_IDP_AUTH_HEALTH, this.idpAuthHealthEventHandler); this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_NO_CONFIG_FOR_SERVICE, this.noConfigForServiceEventHandler); this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_NO_SERVICE, this.noServiceEventHandler); this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_SESSION_CREATION_ERROR, this.sessionCreationErrorEventHandler); this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_INVALID_AUTH, this.invalidAuthEventHandler); + this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_DEPRECATION_ID_TOKEN, this.idTokenDeprecationEventHandler); this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_CHANNEL_CONNECT_FAIL, this.channelConnectFailEventHandler); this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_NO_WSS_ROUTERS, this.noWSSRoutersEventHandler); this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_XGRESS, this.xgressEventHandler); @@ -1465,6 +1552,18 @@ class ZitiBrowzerRuntime { this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_WSS_ROUTER_CONNECTION_ERROR, this.wssERConnectionErrorEventHandler); this.zitiContext.on(ZITI_CONSTANTS.ZITI_EVENT_CONTROLLER_CONNECTION_ERROR, this.controllerConnectionErrorEventHandler); + await this.zitiContext.initialize({ + loadWASM: !options.loadedViaBootstrapper, // instantiate the WASM ONLY if we were not injected by the browZer Bootstrapper + jspi: this.zitiConfig.jspi, // indicate which WASM to instantiate + target: this.zitiConfig.browzer.bootstrapper.target + }); + + this.initialized = true; + + this.logger.trace(`ZitiBrowzerRuntime ${this._uuid} has been initialized`); + + await this.zitiContext.listControllerVersion(); + if (options.eruda) { this.zitiConfig.eruda = true; } else { @@ -1525,6 +1624,13 @@ class ZitiBrowzerRuntime { setTimeout(self._toast, 1000, self, content, type); } } + _toastSticky(self, content, type) { + if (self.polipop) { + self.polipop.add({content: content, title: `OpenZiti BrowZer`, type: type, life: 15*1000}); + } else { + setTimeout(self._toast, 1000, self, content, type); + } + } toastInfo(content) { this._toast(this, content, `info`); @@ -1554,6 +1660,9 @@ class ZitiBrowzerRuntime { toastWarning(content) { this._toast(this, content, `warning`); } + toastWarningSticky(content) { + this._toastSticky(this, content, `warning`); + } toastError(content) { this._toast(this, content, `error`); } @@ -1592,11 +1701,18 @@ if (isUndefined(window.zitiBrowzerRuntime)) { } catch (e) { window.zitiBrowzerRuntime.originTrialTokenInvalidEventHandler({}); } - console.log('decodedOriginTrialToken: ', decodedOriginTrialToken) let currentOriginURL = new URL( window.location.origin ); let actualOrigin = currentOriginURL.hostname.split(/\./).slice(-2).join('.'); let originTrialURL = new URL( decodedOriginTrialToken.payload.origin ); let expectedOrigin = originTrialURL.hostname.split(/\./).slice(-2).join('.'); + let originTrialExpirationTime = isOriginTrialTokenExpired(decodedOriginTrialToken); + if (originTrialExpirationTime) { + window.zitiBrowzerRuntime.originTrialTokenExpiredEventHandler({ + feature: decodedOriginTrialToken.payload.feature, + expectedOrigin: `*.${expectedOrigin}`, + expirationTime: originTrialExpirationTime + }); + } if (!isEqual(actualOrigin, expectedOrigin)) { window.zitiBrowzerRuntime.originTrialSubDomainMismatchEventHandler({ feature: decodedOriginTrialToken.payload.feature, diff --git a/yarn.lock b/yarn.lock index 172204c..efea2c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1205,10 +1205,10 @@ "@types/emscripten" "^1.39.6" "@wasmer/wasi" "^1.0.2" -"@openziti/ziti-browzer-core@^0.50.0": - version "0.50.0" - resolved "https://registry.yarnpkg.com/@openziti/ziti-browzer-core/-/ziti-browzer-core-0.50.0.tgz#9de686babba6200ba7e8262acd06b90e09764e9c" - integrity sha512-CfdVexI7ibsFKe0kx1z2Eo3t1N6Xp7Td59UuF7k7eBzIGCC27pVrAz+qjQ0/PV4dGrOBGxqaultgNFn/BUhosw== +"@openziti/ziti-browzer-core@^0.51.0": + version "0.51.0" + resolved "https://registry.yarnpkg.com/@openziti/ziti-browzer-core/-/ziti-browzer-core-0.51.0.tgz#7b9979bd80cf5a306aaef29af73a8426f63557b7" + integrity sha512-z3+/7S7etxDhfoyULXYn7X6k6WB+KtNxdFaZS79DOA1NNxaz/AYl4zzQ80D3lFOD3ZjSUZJW+VshyFMOowhenQ== dependencies: "@openziti/libcrypto-js" "^0.24.0" "@openziti/ziti-browzer-edge-client" "^0.7.0" @@ -2503,6 +2503,11 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +date-fns@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== + debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"