Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): add silent signin #733

Merged
merged 9 commits into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MIGRATION_GUIDE_V3_TO_V4.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/context/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
5 changes: 3 additions & 2 deletions packages/context/src/MultiAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/context/src/configurations.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
3 changes: 2 additions & 1 deletion packages/context/src/oidc/OidcProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ sessionLostComponent=SessionLost }) => {
<LoadingComponent/>
) : (
<>
<OidcRoutes redirect_uri={configuration.redirect_uri}
<OidcRoutes redirect_uri={configuration.redirect_uri}
silent_redirect_uri={configuration.silent_redirect_uri}
callbackSuccessComponent={callbackSuccessComponent}
callbackErrorComponent={callbackErrorComponent}
authenticatingComponent={authenticatingComponent}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useEffect, PropsWithChildren} from 'react';
import Oidc from "../../vanilla/oidc";
import {OidcSecure} from "../../OidcSecure";

const CallBack = ({configurationName}) =>{
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<any> = ({configurationName }) => {
return <OidcSecure configurationName={configurationName}>
<CallBack configurationName={configurationName}/>
</OidcSecure>;
};

export default CallbackManager;
9 changes: 9 additions & 0 deletions packages/context/src/oidc/core/routes/OidcRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -20,13 +21,15 @@ type OidcRoutesProps = {
authenticatingComponent?: ComponentType;
configurationName:string;
redirect_uri: string;
silent_redirect_uri?: string;
};

const OidcRoutes: FC<PropsWithChildren<OidcRoutesProps>> = ({
callbackErrorComponent,
callbackSuccessComponent,
authenticatingComponent,
redirect_uri,
silent_redirect_uri,
children, configurationName
}) => {
// This exist because in next.js window outside useEffect is null
Expand All @@ -43,6 +46,12 @@ const OidcRoutes: FC<PropsWithChildren<OidcRoutesProps>> = ({

const callbackPath = getPath(redirect_uri);

if(silent_redirect_uri){
if(path === getPath(silent_redirect_uri)){
return <SilentCallbackComponent configurationName={configurationName} />
}
}

switch (path) {
case callbackPath:
return <CallbackComponent callBackError={callbackErrorComponent} callBackSuccess={callbackSuccessComponent} configurationName={configurationName} />;
Expand Down
8 changes: 5 additions & 3 deletions packages/context/src/oidc/vanilla/initWorker.ts
Original file line number Diff line number Diff line change
@@ -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])){
Expand Down Expand Up @@ -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 = () => {
Expand Down
61 changes: 59 additions & 2 deletions packages/context/src/oidc/vanilla/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vanilla/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down