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',