From 36a9190614dc8910759168e7d7ab78064d8947f3 Mon Sep 17 00:00:00 2001 From: Guillaume Chervet <52236059+guillaume-chervet@users.noreply.github.com> Date: Mon, 21 Mar 2022 11:39:01 +0100 Subject: [PATCH] feat(auth): add silent signin (#733) --- MIGRATION_GUIDE_V3_TO_V4.md | 1 + packages/context/README.md | 4 +- packages/context/package.json | 2 +- packages/context/src/MultiAuth.tsx | 5 +- packages/context/src/configurations.ts | 1 + packages/context/src/oidc/OidcProvider.tsx | 3 +- .../SilentCallback.component.tsx | 31 ++++++++++ .../src/oidc/core/routes/OidcRoutes.tsx | 9 +++ .../context/src/oidc/vanilla/initWorker.ts | 8 ++- packages/context/src/oidc/vanilla/oidc.ts | 61 ++++++++++++++++++- packages/vanilla/package.json | 2 +- readme.md | 3 +- 12 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 packages/context/src/oidc/core/default-component/SilentCallback.component.tsx diff --git a/MIGRATION_GUIDE_V3_TO_V4.md b/MIGRATION_GUIDE_V3_TO_V4.md index fa7d490f9..500525e12 100644 --- a/MIGRATION_GUIDE_V3_TO_V4.md +++ b/MIGRATION_GUIDE_V3_TO_V4.md @@ -84,6 +84,7 @@ const propTypes = { configuration: PropTypes.shape({ client_id: PropTypes.string.isRequired, // oidc client id redirect_uri: PropTypes.string.isRequired, // oidc redirect url + silent_redirect_uri: PropTypes.string, // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore sessions scope: PropTypes.string.isRequired, // oidc scope (you need to set "offline_access") authority: PropTypes.string.isRequired, refresh_time_before_tokens_expiration_in_second: PropTypes.number, diff --git a/packages/context/README.md b/packages/context/README.md index 8377cbb6d..0950bcb4e 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -27,7 +27,7 @@ It use AppAuthJS behind the scene. - **Simple** : - refresh_token and access_token are auto refreshed in background - with the use of the Service Worker, you do not need to inject the access_token in every fetch, you have only to configure OidcTrustedDomains.js file -- **No cookies problem** : No silent signin mode inside in iframe +- **No cookies problem** : You can disable silent signin (that internally use an iframe) - **Multiple Authentification** : - You can authenticate many times to the same provider with different scope (for exemple you can acquire a new 'payment' scope for a payment) - You can authenticate to multiple different providers inside the same SPA (single page application) website @@ -107,6 +107,7 @@ import Routes from './Router'; const configuration = { client_id: 'interactive.public.short', redirect_uri: 'http://localhost:4200/authentication/callback', + silent_redirect_uri: 'http://localhost:4200/authentication/silent-callback', scope: 'openid profile email api offline_access', authority: 'https://demo.identityserver.io', service_worker_relative_url:'/OidcServiceWorker.js', @@ -137,6 +138,7 @@ const propTypes = { configuration: PropTypes.shape({ client_id: PropTypes.string.isRequired, // oidc client id redirect_uri: PropTypes.string.isRequired, // oidc redirect url + silent_redirect_uri: PropTypes.string, // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore sessions scope: PropTypes.string.isRequired, // oidc scope (you need to set "offline_access") authority: PropTypes.string.isRequired, refresh_time_before_tokens_expiration_in_second: PropTypes.number, diff --git a/packages/context/package.json b/packages/context/package.json index 2679295d0..b5533d651 100644 --- a/packages/context/package.json +++ b/packages/context/package.json @@ -1,6 +1,6 @@ { "name": "@axa-fr/react-oidc-context", - "version": "4.4.0-alpha.0", + "version": "4.5.0-alpha.0", "private": false, "main": "dist/index.js", "jsnext:main": "dist/index.js", diff --git a/packages/context/src/MultiAuth.tsx b/packages/context/src/MultiAuth.tsx index 705cf7a19..b34eacf8b 100644 --- a/packages/context/src/MultiAuth.tsx +++ b/packages/context/src/MultiAuth.tsx @@ -32,9 +32,10 @@ if(!sessionStorage.configurationName){ export const MultiAuthContainer = () => { const [configurationName, setConfigurationName] = useState(sessionStorage.configurationName); const callBack = window.location.origin+"/multi-auth/authentification/callback2"; + const silent_redirect_uri = window.location.origin+"/multi-auth/authentification/silent-callback2"; const configurations = { - "config_1": {...configurationIdentityServer, redirect_uri:callBack}, - "config_2": {...configurationIdentityServer, redirect_uri:callBack, scope: 'openid profile email api'} + "config_1": {...configurationIdentityServer, redirect_uri:callBack, silent_redirect_uri}, + "config_2": {...configurationIdentityServer, redirect_uri:callBack, silent_redirect_uri, scope: 'openid profile email api'} } const handleConfigurationChange = (event) => { const configurationName = event.target.value; diff --git a/packages/context/src/configurations.ts b/packages/context/src/configurations.ts index 5c2be86b5..28589e52f 100644 --- a/packages/context/src/configurations.ts +++ b/packages/context/src/configurations.ts @@ -1,6 +1,7 @@ export const configurationIdentityServer = { client_id: 'interactive.public.short', // interactive.public.short redirect_uri: window.location.origin+'/authentication/callback', // http://localhost:4200/authentication/callback + silent_redirect_uri: window.location.origin+'/authentication/silent-callback', scope: 'openid profile email api offline_access', authority: 'https://demo.identityserver.io', refresh_time_before_tokens_expiration_in_second: 70, diff --git a/packages/context/src/oidc/OidcProvider.tsx b/packages/context/src/oidc/OidcProvider.tsx index e363451b8..707a97746 100644 --- a/packages/context/src/oidc/OidcProvider.tsx +++ b/packages/context/src/oidc/OidcProvider.tsx @@ -135,7 +135,8 @@ sessionLostComponent=SessionLost }) => { ) : ( <> - { + const getOidc = Oidc.get; + useEffect(() => { + let isMounted = true; + const playCallbackAsync = async () => { + if(isMounted) { + const oidc = getOidc(configurationName); + oidc.silentSigninCallbackFromIFrame(); + } + }; + playCallbackAsync(); + + return () => { + isMounted = false; + }; + },[]); + + return <>; +} + +const CallbackManager: PropsWithChildren = ({configurationName }) => { + return + + ; +}; + +export default CallbackManager; \ No newline at end of file diff --git a/packages/context/src/oidc/core/routes/OidcRoutes.tsx b/packages/context/src/oidc/core/routes/OidcRoutes.tsx index c44908b14..d969dc9a8 100644 --- a/packages/context/src/oidc/core/routes/OidcRoutes.tsx +++ b/packages/context/src/oidc/core/routes/OidcRoutes.tsx @@ -2,6 +2,7 @@ import React, { ComponentType, FC, PropsWithChildren, useEffect, useState } from import PropTypes from 'prop-types'; import { getPath } from './route-utils'; import CallbackComponent from '../default-component/Callback.component'; +import SilentCallbackComponent from "../default-component/SilentCallback.component"; import ServiceWorkerInstall from "../default-component/ServiceWorkerInstall.component"; const propTypes = { @@ -20,6 +21,7 @@ type OidcRoutesProps = { authenticatingComponent?: ComponentType; configurationName:string; redirect_uri: string; + silent_redirect_uri?: string; }; const OidcRoutes: FC> = ({ @@ -27,6 +29,7 @@ const OidcRoutes: FC> = ({ callbackSuccessComponent, authenticatingComponent, redirect_uri, + silent_redirect_uri, children, configurationName }) => { // This exist because in next.js window outside useEffect is null @@ -43,6 +46,12 @@ const OidcRoutes: FC> = ({ const callbackPath = getPath(redirect_uri); + if(silent_redirect_uri){ + if(path === getPath(silent_redirect_uri)){ + return + } + } + switch (path) { case callbackPath: return ; diff --git a/packages/context/src/oidc/vanilla/initWorker.ts b/packages/context/src/oidc/vanilla/initWorker.ts index 823dd03bd..1728421ee 100644 --- a/packages/context/src/oidc/vanilla/initWorker.ts +++ b/packages/context/src/oidc/vanilla/initWorker.ts @@ -1,4 +1,6 @@ -function get_browser() { +import timer from "./timer" + +function get_browser() { let ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; if(/trident/i.test(M[1])){ @@ -30,8 +32,8 @@ let keepAliveServiceWorkerTimeoutId = null; -const sleepAsync = (milliseconds) => { - return new Promise(resolve => setTimeout(resolve, milliseconds)) +export const sleepAsync = (milliseconds) => { + return new Promise(resolve => timer.setTimeout(resolve, milliseconds)) } const keepAlive = () => { diff --git a/packages/context/src/oidc/vanilla/oidc.ts b/packages/context/src/oidc/vanilla/oidc.ts index 9ad8a6492..b54b20cf5 100644 --- a/packages/context/src/oidc/vanilla/oidc.ts +++ b/packages/context/src/oidc/vanilla/oidc.ts @@ -11,7 +11,7 @@ import { TokenRequest } from '@openid/appauth'; import {NoHashQueryStringUtils} from './noHashQueryStringUtils'; -import {initWorkerAsync} from './initWorker' +import {initWorkerAsync, sleepAsync} from './initWorker' import {MemoryStorageBackend} from "./memoryStorageBackend"; import {initSession} from "./initSession"; import timer from './timer'; @@ -50,6 +50,7 @@ export interface StringMap { export type Configuration = { client_id: string, redirect_uri: string, + silent_redirect_uri?:string, scope: string, authority: string, refresh_time_before_tokens_expiration_in_second?: number, @@ -168,6 +169,9 @@ const eventNames = { tryKeepExistingSessionAsync_begin: "tryKeepExistingSessionAsync_begin", tryKeepExistingSessionAsync_end: "tryKeepExistingSessionAsync_end", tryKeepExistingSessionAsync_error: "tryKeepExistingSessionAsync_error", + silentSigninAsync_begin: "silentSigninAsync_begin", + silentSigninAsync_end: "silentSigninAsync_end", + silentSigninAsync_error: "silentSigninAsync_error", } export class Oidc { @@ -224,7 +228,55 @@ export class Oidc { return oidcDatabase[name]; } static eventNames = eventNames; + + silentSigninCallbackFromIFrame(){ + window.top.postMessage(`${this.configurationName}_oidc_tokens:${JSON.stringify(this.tokens)}`, window.location.origin); + } + async silentSigninAsync() { + if (!this.configuration.silent_redirect_uri) { + return Promise.resolve(null); + } + this.publishEvent(eventNames.silentSigninAsync_begin, {}); + const link = this.configuration.silent_redirect_uri; + const iframe = document.createElement('iframe'); + iframe.width = "0px"; + iframe.height = "0px"; + iframe.id = `${this.configurationName}_oidc_iframe`; + iframe.setAttribute("src", link); + document.body.appendChild(iframe); + const self = this; + const promise = new Promise((resolve, reject) => { + try { + let isResolved = false; + window.onmessage = function (e) { + const key = `${self.configurationName}_oidc_tokens:`; + if (e.data && typeof (e.data) === "string" && e.data.startsWith(key)) { + + if (!isResolved) { + self.publishEvent(eventNames.silentSigninAsync_end, {}); + resolve(JSON.parse(e.data.replace(key, ''))); + iframe.remove(); + isResolved = true; + } + } + }; + setTimeout(() => { + if (!isResolved) { + reject("timeout"); + self.publishEvent(eventNames.silentSigninAsync_error, new Error("timeout")); + iframe.remove(); + isResolved = true; + } + }, 8000); + } catch (e) { + iframe.remove(); + reject(e); + self.publishEvent(eventNames.silentSigninAsync_error, e); + } + }); + return promise; + } async initAsync(authority) { const oidcServerConfiguration = await AuthorizationServiceConfiguration.fetchFromIssuer(authority, new FetchRequestor()); return oidcServerConfiguration; @@ -399,7 +451,7 @@ export class Oidc { authorizationHandler.completeAuthorizationRequestIfPossible(); }); return promise; - } catch(exception){ + } catch(exception) { console.error(exception); this.publishEvent(eventNames.loginCallbackAsync_error, exception); throw exception; @@ -432,6 +484,11 @@ export class Oidc { return token_response; } catch(exception) { console.error(exception); + const silent_token_response =await this.silentSigninAsync(); + if(silent_token_response){ + return silent_token_response; + } + this.publishEvent( silentEvent ? eventNames.refreshTokensAsync_silent_error :eventNames.refreshTokensAsync_error, exception); return null; } diff --git a/packages/vanilla/package.json b/packages/vanilla/package.json index 163f3b78a..1f2216646 100644 --- a/packages/vanilla/package.json +++ b/packages/vanilla/package.json @@ -1,6 +1,6 @@ { "name": "@axa-fr/vanilla-oidc", - "version": "4.4.0", + "version": "4.5.0-alpha.0", "private": false, "main": "dist/index.js", "jsnext:main": "dist/index.js", diff --git a/readme.md b/readme.md index bd3126cd7..b881682af 100644 --- a/readme.md +++ b/readme.md @@ -44,7 +44,7 @@ For migrating from v3 to v4 checkout our [`migration guide v3 to v4`](./MIGRATIO - **Simple** : - refresh_token and access_token are auto refreshed in background - with the use of the Service Worker, you do not need to inject the access_token in every fetch, you have only to configure OidcTrustedDomains.js file -- **No cookies problem** : No silent signin mode inside in iframe +- **No cookies problem** : You can disable silent signin (that internally use an iframe) - **Multiple Authentification** : - You can authenticate many times to the same provider with different scope (for exemple you can acquire a new 'payment' scope for a payment) - You can authenticate to multiple different providers inside the same SPA (single page application) website @@ -96,6 +96,7 @@ import Routes from './Router'; const configuration = { client_id: 'interactive.public.short', redirect_uri: 'http://localhost:4200/authentication/callback', + silent_redirect_uri: 'http://localhost:4200/authentication/silent-callback', // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore the session scope: 'openid profile email api offline_access', authority: 'https://demo.identityserver.io', service_worker_relative_url:'/OidcServiceWorker.js',