@axa-fr/react-oidc
v7.23.1
Published
OpenID Connect & OAuth authentication using react
Downloads
54,042
Readme
@axa-fr/react-oidc
@axa-fr/oidc-client the lightest and securest library to manage authentication with OpenID Connect (OIDC) and OAuth2 protocol. It is compatible with all OIDC providers. @axa-fr/oidc-client is a pure javascript library. It works with any JavaScript framework or library.
We provide a wrapper @axa-fr/react-oidc for React (compatible next.js) and we expect soon to provide one for Vue, Angular and Svelte.
- Try the React demo at https://black-rock-0dc6b0d03.1.azurestaticapps.net/ (most advanced)
- Try the pure javascript demo at https://icy-glacier-004ab4303.2.azurestaticapps.net/
About
@axa-fr/react is:
- Secure :
- With Demonstrating Proof of Possession (DPoP), your access_token and refresh_token are not usable outside your browser context (big protection)
- With the use of Service Worker, your tokens (refresh_token and/or access_token) are not accessible to the JavaScript client code (if you follow good practices from
FAQ
section) - OIDC using client side Code Credential Grant with pkce only
- Lightweight : Unpacked Size on npm is 274 kB
- 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
- Multiple Authentication :
- You can authenticate many times to the same provider with different scope (for example 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
- Flexible :
- Work with Service Worker (more secure) and without for older browser (less secure).
- You can disable Service Worker if you want (but less secure) and just use SessionStorage or LocalStorage mode.
The service worker catch access_token and refresh_token that will never be accessible to the client.
Getting Started
npm install @axa-fr/react-oidc --save
# To install or update OidcServiceWorker.js file, you can run
node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public
# If you have a "public" folder, the 2 files will be created :
# ./public/OidcServiceWorker.js <-- will be updated at each "npm install"
# ./public/OidcTrustedDomains.js <-- won't be updated if already exist
WARNING : If you use Service Worker mode, the OidcServiceWorker.js file should always be up to date with the version of the library. You may setup a postinstall script in your package.json file to update it at each npm install. For example :
"scripts": {
...
"postinstall": "node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public"
},
If you need a very secure mode where refresh_token and access_token will be hide behind a service worker that will proxify requests. The only file you should edit is "OidcTrustedDomains.js".
// OidcTrustedDomains.js
// Add bellow trusted domains, access tokens will automatically injected to be send to
// trusted domain can also be a path like https://www.myapi.com/users,
// then all subroute like https://www.myapi.com/useers/1 will be authorized to send access_token to.
// Domains used by OIDC server must be also declared here
const trustedDomains = {
default: {
oidcDomains: ['https://demo.duendesoftware.com'],
accessTokenDomains: ['https://www.myapi.com/users'],
},
};
// Service worker will continue to give access token to the JavaScript client
// Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some
// scenarios which require it. For example, to send it via websocket connection.
trustedDomains.config_show_access_token = {
oidcDomains: ['https://demo.duendesoftware.com'],
accessTokenDomains: ['https://www.myapi.com/users'],
showAccessToken: true,
// convertAllRequestsToCorsExceptNavigate: false, // default value is false
// setAccessTokenToNavigateRequests: true, // default value is true
};
// DPoP (Demonstrating Proof of Possession) will be activated for the following domains
trustedDomains.config_with_dpop = {
domains: ['https://demo.duendesoftware.com'],
demonstratingProofOfPossession: true,
demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: true, // default value is false, inject DPOP token only when DPOP header is present
// Optional, more details bellow
/*demonstratingProofOfPossessionConfiguration: {
importKeyAlgorithm: {
name: 'ECDSA',
namedCurve: 'P-256',
hash: {name: 'ES256'}
},
signAlgorithm: {name: 'ECDSA', hash: {name: 'SHA-256'}},
generateKeyAlgorithm: {
name: 'ECDSA',
namedCurve: 'P-256'
},
digestAlgorithm: { name: 'SHA-256' },
jwtHeaderAlgorithm : 'ES256'
}*/
};
// Setting allowMultiTabLogin to true will enable storing login-specific parameters (state, nonce, code verifier)
// separately for each tab. This will prevent errors when logins are initiated from multiple tabs.
trustedDomains.config_multi_tab_login = {
domains: ['https://demo.duendesoftware.com'],
allowMultiTabLogin: true,
};
Run The Demo
git clone https://github.com/AxaFrance/oidc-client.git
cd oidc-client
pnpm install
cd /examples/react-oidc-demo
pnpm install
pnpm start
# then navigate to http://localhost:4200
Examples
Application startup
The library is router agnostic and will use native History API.
The default routes used internally :
- www.your-app.fr/authentication/callback
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { OidcProvider } from '@axa-fr/react-oidc';
import Header from './Layout/Header';
import Routes from './Router';
// This configuration use hybrid mode
// ServiceWorker are used if available (more secure) else tokens are given to the client
// You need to give inside your code the "access_token" when using fetch
const configuration = {
client_id: 'interactive.public.short',
redirect_uri: window.location.origin + '/authentication/callback',
silent_redirect_uri: window.location.origin + '/authentication/silent-callback',
scope: 'openid profile email api offline_access', // offline_access scope allow your client to retrieve the refresh_token
authority: 'https://demo.duendesoftware.com',
service_worker_relative_url: '/OidcServiceWorker.js', // just comment that line to disable service worker mode
service_worker_only: false,
demonstrating_proof_of_possession: false,
};
const App = () => (
<OidcProvider configuration={configuration}>
<Router>
<Header />
<Routes />
</Router>
</OidcProvider>
);
render(<App />, document.getElementById('root'));
const configuration = {
loadingComponent: ReactComponent, // you can inject your own loading component
sessionLostComponent: ReactComponent, // you can inject your own session lost component
authenticating: ReactComponent, // you can inject your own authenticating component
authenticatingErrorComponent: ReactComponent,
callbackSuccessComponent: ReactComponent, // you can inject your own call back success component
serviceWorkerNotSupportedComponent: ReactComponent, // you can inject your page that explains you require a more modern browser
onSessionLost: Function, // If set, "sessionLostComponent" is not displayed, and onSessionLost callback is called instead
configuration: {
client_id: String.isRequired, // oidc client id
redirect_uri: String.isRequired, // oidc redirect url
silent_redirect_uri: String, // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore sessions
silent_login_uri: String, // Optional, route that triggers the signin
silent_login_timeout: Number, // Optional, default is 12000 milliseconds
scope: String.isRequired, // oidc scope (you need to set "offline_access")
authority: String.isRequired,
storage: Storage, // Default sessionStorage, you can set localStorage, but it is less secure against XSS attacks
authority_configuration: {
// Optional for providers that do not implement OIDC server auto-discovery via a .wellknown URL
authorization_endpoint: String,
token_endpoint: String,
userinfo_endpoint: String,
end_session_endpoint: String,
revocation_endpoint: String,
check_session_iframe: String,
issuer: String,
},
refresh_time_before_tokens_expiration_in_second: Number, // default is 120 seconds
service_worker_relative_url: String,
service_worker_keep_alive_path: String, // default is "/"
service_worker_only: Boolean, // default false
service_worker_activate: () => boolean, // you can take the control of the service worker default activation which use user agent string
service_worker_update_require_callback: (registration:any, stopKeepAlive:Function) => Promise<void>, // callback called when service worker need to be updated, you can take the control of the update process
service_worker_register: (url: string) => Promise<ServiceWorkerRegistration>, // Optional, you can take the control of the service worker registration
extras: StringMap | undefined, // ex: {'prompt': 'consent', 'access_type': 'offline'} list of key/value that is sent to the OIDC server (more info: https://github.com/openid/AppAuth-JS)
token_request_extras: StringMap | undefined, // ex: {'prompt': 'consent', 'access_type': 'offline'} list of key/value that is sent to the OIDC server during token request (more info: https://github.com/openid/AppAuth-JS)
withCustomHistory: Function, // Override history modification, return an instance with replaceState(url, stateHistory) implemented (like History.replaceState())
authority_time_cache_wellknowurl_in_second: 60 * 60, // Time to cache in seconds of the openid well-known URL, default is 1 hour
authority_timeout_wellknowurl_in_millisecond: 10000, // Timeout in milliseconds of the openid well-known URL, default is 10 seconds, then an error is thrown
monitor_session: Boolean, // Add OpenID monitor session, default is false (more information https://openid.net/specs/openid-connect-session-1_0.html), if you need to set it to true consider https://infi.nl/nieuws/spa-necromancy/
onLogoutFromAnotherTab: Function, // Optional, can be set to override the default behavior, this function is triggered when a user with the same subject is logged out from another tab when session_monitor is active
onLogoutFromSameTab: Function, // Optional, can be set to override the default behavior, this function is triggered when a user is logged out from the same tab when session_monitor is active
token_renew_mode: String, // Optional, update tokens based on the selected token(s) lifetime: "access_token_or_id_token_invalid" (default), "access_token_invalid", "id_token_invalid"
token_automatic_renew_mode: TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted, // Optional, default is TokenAutomaticRenewMode.AutomaticBeforeTokensExpiration
// TokenAutomaticRenewMode.AutomaticBeforeTokensExpiration: renew tokens automatically before they expire
// TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted: renew tokens automatically only when fetch is executed
// It requires you to use fetch given by hook useOidcFetch(fetch) or HOC withOidcFetch(fetch)(Component)
logout_tokens_to_invalidate: Array<string>, // Optional tokens to invalidate during logout, default: ['access_token', 'refresh_token']
location: ILOidcLocation, // Optional, default is window.location, you can inject your own location object respecting the ILOidcLocation interface
demonstrating_proof_of_possession: Boolean, // Optional, default is false, if true, the the Demonstrating Proof of Possession will be activated //https://www.rfc-editor.org/rfc/rfc9449.html#name-protected-resource-access
demonstrating_proof_of_possession_configuration: DemonstratingProofOfPossessionConfiguration // Optional, more details bellow
},
};
demonstrating_proof_of_possession_configuration: DemonstratingProofOfPossessionConfiguration // Optional, more details bellow
};
interface DemonstratingProofOfPossessionConfiguration
{
generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams,
digestAlgorithm: AlgorithmIdentifier,
importKeyAlgorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm,
signAlgorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams,
jwtHeaderAlgorithm: string
};
// default value of demonstrating_proof_of_possession_configuration
const defaultDemonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration ={
importKeyAlgorithm: {
name: 'ECDSA',
namedCurve: 'P-256',
hash: {name: 'ES256'}
},
signAlgorithm: {name: 'ECDSA', hash: {name: 'SHA-256'}},
generateKeyAlgorithm: {
name: 'ECDSA',
namedCurve: 'P-256'
},
digestAlgorithm: { name: 'SHA-256' },
jwtHeaderAlgorithm : 'ES256'
};
How to consume
"useOidc" returns all props from the Hook :
import React from 'react';
import { useOidc } from './oidc';
export const Home = () => {
const { login, logout, renewTokens, isAuthenticated } = useOidc();
return (
<div className="container-fluid mt-3">
<div className="card">
<div className="card-body">
<h5 className="card-title">Welcome !!!</h5>
<p className="card-text">React Demo Application protected by OpenId Connect</p>
{!isAuthenticated && (
<button type="button" className="btn btn-primary" onClick={() => login('/profile')}>
Login
</button>
)}
{isAuthenticated && (
<button type="button" className="btn btn-primary" onClick={() => logout()}>
logout
</button>
)}
{isAuthenticated && (
<button type="button" className="btn btn-primary" onClick={() => renewTokens()}>
renewTokens
</button>
)}
</div>
</div>
</div>
);
};
The Hook method exposes :
- isAuthenticated : if the user is logged in or not
- logout: logout function (return a promise)
- login: login function 'return a promise'
- renewTokens: renew tokens function 'return a promise'
How to secure a component
OidcSecure
component trigger authentication in case user is not authenticated. So, the children of that component can be accessible only once you are connected.
import React from 'react';
import { OidcSecure } from '@axa-fr/react-oidc';
const AdminSecure = () => (
<OidcSecure>
<h1>My sub component</h1>
</OidcSecure>
);
// adding the oidc user in the props
export default AdminSecure;
How to secure a component: HOC method
withOidcSecure
will act the same as OidcSecure
,it will also trigger authentication in case the user is not authenticated.
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { withOidcSecure } from '@axa-fr/react-oidc';
import Home from '../Pages/Home';
import Dashboard from '../Pages/Dashboard';
import Admin from '../Pages/Admin';
const Routes = () => (
<Switch>
<Route exact path="/" component={Home} />
<Route path="/dashboard" component={withOidcSecure(Dashboard)} />
<Route path="/admin" component={Admin} />
<Route path="/home" component={Home} />
</Switch>
);
export default Routes;
How to get "Access Token": Hook method
import { useOidcAccessToken } from '@axa-fr/react-oidc';
const DisplayAccessToken = () => {
const { accessToken, accessTokenPayload } = useOidcAccessToken();
if (!accessToken) {
return <p>you are not authentified</p>;
}
return (
<div className="card text-white bg-info mb-3">
<div className="card-body">
<h5 className="card-title">Access Token</h5>
<p style={{ color: 'red', backgroundColor: 'white' }}>
Please consider to configure the ServiceWorker in order to protect your application from
XSRF attacks. ""access_token" and "refresh_token" will never be accessible from your
client side javascript.
</p>
{<p className="card-text">{JSON.stringify(accessToken)}</p>}
{accessTokenPayload != null && (
<p className="card-text">{JSON.stringify(accessTokenPayload)}</p>
)}
</div>
</div>
);
};
How to get IDToken: Hook method
import { useOidcIdToken } from '@axa-fr/react-oidc';
const DisplayIdToken = () => {
const { idToken, idTokenPayload } = useOidcIdToken();
if (!idToken) {
return <p>you are not authentified</p>;
}
return (
<div className="card text-white bg-info mb-3">
<div className="card-body">
<h5 className="card-title">ID Token</h5>
{<p className="card-text">{JSON.stringify(idToken)}</p>}
{idTokenPayload != null && <p className="card-text">{JSON.stringify(idTokenPayload)}</p>}
</div>
</div>
);
};
How to get User Information: Hook method
import { useOidcUser, UserStatus } from '@axa-fr/react-oidc';
const DisplayUserInfo = () => {
const { oidcUser, oidcUserLoadingState } = useOidcUser();
switch (oidcUserLoadingState) {
case UserStatus.Loading:
return <p>User Information are loading</p>;
case UserStatus.Unauthenticated:
return <p>you are not authenticated</p>;
case UserStatus.LoadingError:
return <p>Fail to load user information</p>;
default:
return (
<div className="card text-white bg-success mb-3">
<div className="card-body">
<h5 className="card-title">User information</h5>
<p className="card-text">{JSON.stringify(oidcUser)}</p>
</div>
</div>
);
}
};
How to get a fetch that inject Access_Token: Hook method
If you are not using the service worker. The Fetch function needs to send AccessToken. This hook will give you a wrapped fetch that adds the access token for you.
import React, { useEffect, useState } from 'react';
import { useOidcFetch, OidcSecure } from '@axa-fr/react-oidc';
const DisplayUserInfo = ({ fetch }) => {
const [oidcUser, setOidcUser] = useState(null);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
const fetchUserInfoAsync = async () => {
const res = await fetch('https://demo.duendesoftware.com/connect/userinfo');
if (res.status != 200) {
return null;
}
return res.json();
};
let isMounted = true;
fetchUserInfoAsync().then(userInfo => {
if (isMounted) {
setLoading(false);
setOidcUser(userInfo);
}
});
return () => {
isMounted = false;
};
}, []);
if (isLoading) {
return <>Loading</>;
}
return (
<div className="container mt-3">
<div className="card text-white bg-success mb-3">
<div className="card-body">
<h5 className="card-title">User information</h5>
{oidcUser != null && <p className="card-text">{JSON.stringify(oidcUser)}</p>}
</div>
</div>
</div>
);
};
export const FetchUserHook = () => {
const { fetch } = useOidcFetch();
return (
<OidcSecure>
<DisplayUserInfo fetch={fetch} />
</OidcSecure>
);
};
How to get a fetch that inject Access_Token : HOC method
If your are not using the service worker. Fetch function need to send AccessToken. This HOC give you a wrapped fetch that add the access token for you.
import React, { useEffect, useState } from 'react';
import { useOidcFetch, OidcSecure } from '@axa-fr/react-oidc';
const DisplayUserInfo = ({ fetch }) => {
const [oidcUser, setOidcUser] = useState(null);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
const fetchUserInfoAsync = async () => {
const res = await fetch('https://demo.duendesoftware.com/connect/userinfo');
if (res.status != 200) {
return null;
}
return res.json();
};
let isMounted = true;
fetchUserInfoAsync().then(userInfo => {
if (isMounted) {
setLoading(false);
setOidcUser(userInfo);
}
});
return () => {
isMounted = false;
};
}, []);
if (isLoading) {
return <>Loading</>;
}
return (
<div className="container mt-3">
<div className="card text-white bg-success mb-3">
<div className="card-body">
<h5 className="card-title">User information</h5>
{oidcUser != null && <p className="card-text">{JSON.stringify(oidcUser)}</p>}
</div>
</div>
</div>
);
};
const UserInfoWithFetchHoc = withOidcFetch(fetch)(DisplayUserInfo);
export const FetchUserHoc = () => (
<OidcSecure>
<UserInfoWithFetchHoc />
</OidcSecure>
);
Components override
You can inject your own components.
All components definition receive props configurationName
. Please checkout the demo for more complete example.
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { OidcProvider } from '@axa-fr/react-oidc';
import Header from './Layout/Header';
import Routes from './Router';
// This configuration use hybrid mode
// ServiceWorker are used if available (more secure) else tokens are given to the client
// You need to give inside your code the "access_token" when using fetch
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',
service_worker_only: false,
};
const Loading = () => <p>Loading</p>;
const AuthenticatingError = () => <p>Authenticating error</p>;
const Authenticating = () => <p>Authenticating</p>;
const SessionLost = () => <p>Session Lost</p>;
const ServiceWorkerNotSupported = () => <p>Not supported</p>;
const CallBackSuccess = () => <p>Success</p>;
//const [isSessionLost, setIsSessionLost] = useState(false);
//const onSessionLost = ()=>{
// setIsSessionLost(true);
//}
const App = () => (
<OidcProvider
configuration={configuration}
loadingComponent={Loading}
authenticatingErrorComponent={AuthenticatingError}
authenticatingComponent={Authenticating}
sessionLostComponent={SessionLost}
//onSessionLost={onSessionLost} // If set "sessionLostComponent" is not displayed and onSessionLost callback is called instead
serviceWorkerNotSupportedComponent={ServiceWorkerNotSupported}
callbackSuccessComponent={CallBackSuccess}
>
{/* isSessionLost && <SessionLost />*/}
<Router>
<Header />
<Routes />
</Router>
</OidcProvider>
);
render(<App />, document.getElementById('root'));
How It Works
These components encapsulate the use of "@axa-fr/vanilla-oidc" in order to hide workflow complexity. Internally, native History API is used to be router library agnostic.
More information about OIDC
- French : Augmentez la sécurité et la simplicité de votre Système d’Information OpenID Connect
- English : Increase the security and simplicity of your information system with openid connect
- English: youtube OIDC
- French: youtube OIDC
NextJS
To work with NextJS you need to inject your own history surcharge like the sample below.
component/layout.js
import { OidcProvider } from '@axa-fr/react-oidc';
import { useRouter } from 'next/router';
const configuration = {
client_id: 'interactive.public.short',
redirect_uri: 'http://localhost:3001/#authentication/callback',
silent_redirect_uri: 'http://localhost:3001/#authentication/silent-callback', // Optional activate silent-login that use cookies between OIDC server and client javascript to restore the session
scope: 'openid profile email api offline_access',
authority: 'https://demo.duendesoftware.com',
};
const onEvent = (configurationName, eventName, data) => {
console.log(`oidc:${configurationName}:${eventName}`, data);
};
export default function Layout({ children }) {
const router = useRouter();
const withCustomHistory = () => {
return {
replaceState: url => {
router
.replace({
pathname: url,
})
.then(() => {
window.dispatchEvent(new Event('popstate'));
});
},
};
};
return (
<>
<OidcProvider
configuration={configuration}
onEvent={onEvent}
withCustomHistory={withCustomHistory}
>
<main>{children}</main>
</OidcProvider>
</>
);
}
For more information checkout the NextJS React OIDC demo
Hash route
react-oidc
work also with hash router.
export const configurationIdentityServerWithHash = {
client_id: 'interactive.public.short',
redirect_uri: window.location.origin + '#authentication-callback',
silent_redirect_uri: window.location.origin + '#authentication-silent-callback',
scope: 'openid profile email api offline_access',
authority: 'https://demo.duendesoftware.com',
refresh_time_before_tokens_expiration_in_second: 70,
service_worker_relative_url: '/OidcServiceWorker.js',
service_worker_only: false,
};