UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

233 lines (232 loc) 10.6 kB
// SPDX-License-Identifier: Apache-2.0 import AbstractConnectManager from './abstractconnectmanager.js'; import { authorizationCodeGrantRequest, calculatePKCECodeChallenge, discoveryRequest, generateRandomCodeVerifier, processAuthorizationCodeResponse, processDiscoveryResponse, processRefreshTokenResponse, refreshTokenGrantRequest, TlsClientAuth, validateAuthResponse } from 'oauth4webapi'; export default class OpenIdConnectManager extends AbstractConnectManager { authorizationServer; silentLoginIframe; sendTimeoutErrorCallbackTimeoutId = -1; get issuerConfig() { return this.context.configManager.Config.oauth.issuer; } authProcessed() { return this.context.urlManager.getParam('authentified') !== null; } isAuthentified() { return this.context.urlManager.getParam('authentified') === 'true'; } isLoggedOut() { return this.context.urlManager.getParam('authentified') === 'false'; } hasAuthError() { return this.context.urlManager.getParam('error') !== null; } async initialize() { window.addEventListener('message', async (event) => { if (event.origin !== window.location.origin) { // potentially dangerous message (unknown origin) return; } await this.handleSilentLoggedInViaIframe(event); }); if (this.isAuthentified()) { if (!this.hasAuthError()) { // We are back from issuer authentication. // Go to the next step with the backend authentication await this.handleLoggedInToIssuer(); } else { // We are back but with an error. this.state.oauth.error = this.context.urlManager.getParam('error'); this.state.oauth.status = 'loginFailed'; } } else if (this.isLoggedOut()) { this.loggedOut(); } } /** * Code verifier for Authorization Code Grant with Proof Key for Code Exchange (PKCE) * It has to be the same when coming back from the issuer after a redirect * Therefore, we keep it in localStorage. */ get codeVerifier() { return this.loadFromLocalStorage('codeVerifier'); } set codeVerifier(value) { this.saveToLocalStorage('codeVerifier', value); } async getAuthorizationServer() { if (!this.authorizationServer) { const response = await discoveryRequest(new URL(this.issuerConfig.url), { algorithm: this.issuerConfig.algorithm }); this.authorizationServer = await processDiscoveryResponse(new URL(this.issuerConfig.url), response); } return this.authorizationServer; } async getAuthorizationUrl(silent) { const authorizationServer = await this.getAuthorizationServer(); if (!authorizationServer.authorization_endpoint) { throw new Error(`No authorization endpoint found for oauth issuer ${this.issuerConfig.url}.`); } // Use Code-Verifier and Code-Challenge this.codeVerifier = generateRandomCodeVerifier(); const code_challenge = await calculatePKCECodeChallenge(this.codeVerifier); // Save the current state of the application before login // If we are in an autologin case, we use the state from the URL // Otherwise we use the current state of the application if (!silent) { this.context.sessionManager.saveStateToSession(); } this.redirectUrl = this.getLoginRedirectUrl(silent); // Redirect user to authorizationServer.authorization_endpoint const authorizationUrl = new URL(authorizationServer.authorization_endpoint); authorizationUrl.searchParams.set('client_id', this.issuerConfig.clientId); authorizationUrl.searchParams.set('redirect_uri', this.redirectUrl); authorizationUrl.searchParams.set('response_type', 'code'); authorizationUrl.searchParams.set('scope', this.issuerConfig.scope); authorizationUrl.searchParams.set('code_challenge', code_challenge); authorizationUrl.searchParams.set('code_challenge_method', this.issuerConfig.codeChallengeMethod); if (silent) { // Add custom parameters for silent login for (const [key, value] of Object.entries(this.issuerConfig.customSilentLoginParams)) { authorizationUrl.searchParams.set(key, value); } } else { // Add custom parameters for login for (const [key, value] of Object.entries(this.issuerConfig.customLoginParams)) { authorizationUrl.searchParams.set(key, value); } } return authorizationUrl; } getClient() { return { client_id: this.issuerConfig.clientId, token_endpoint_auth_method: 'none' }; } async login() { /** * If autologin configuration is activated, we force a loggin at the start of the app * But if we are coming back from the issuer, we are already in a loggin process. * In this case we do not do anything */ if (!this.isAuthentified() || this.hasAuthError()) { console.debug('Auth: 1. Issuer login'); await this.redirectToIssuerLogin(); } } async silentLogin() { /** * If autologin configuration is activated, we force a loggin at the start of the app * But if we are coming back from the issuer, we are already in a loggin process. * In this case we do not do anything */ if (!this.authProcessed()) { await this.silentLoginViaIframe(); } } async logout() { console.debug('Auth: 5. Issuer logout'); await this.logoutFromIssuer(); } async silentLoginViaIframe() { const authorizationUrl = await this.getAuthorizationUrl(true); this.silentLoginIframe = document.createElement('iframe'); this.silentLoginIframe.style.display = 'none'; this.silentLoginIframe.src = authorizationUrl.toString(); document.body.appendChild(this.silentLoginIframe); this.sendTimeoutErrorCallbackTimeoutId = window.setTimeout(() => { window.postMessage({ type: 'OAUTH_ERROR', error: `Timeout: Looks like we did not get any Response within 5s` }, window.location.origin); }, 5000); } async handleSilentLoggedInViaIframe(event) { if (event.data?.type === 'OAUTH_CALLBACK') { const params = new URLSearchParams(event.data.data); const newUrl = new URL(window.location.href); for (const [key, value] of params.entries()) { newUrl.searchParams.set(key, value); } this.context.urlManager.updateUrl(newUrl); await this.handleLoggedInToIssuer(); } else if (event.data?.type === 'OAUTH_ERROR') { console.info('Silent login could not be done : ', event.data.error); this.state.oauth.status = 'loggedOut'; } if (this.silentLoginIframe) { this.silentLoginIframe.remove(); this.silentLoginIframe = undefined; } if (this.sendTimeoutErrorCallbackTimeoutId > -1) { window.clearTimeout(this.sendTimeoutErrorCallbackTimeoutId); } } async redirectToIssuerLogin() { const authorizationUrl = await this.getAuthorizationUrl(false); window.open(authorizationUrl, '_self'); } async handleLoggedInToIssuer() { console.debug('Auth: 2. Issuer login handle'); const authorizationServer = await this.getAuthorizationServer(); const client = this.getClient(); const currentUrl = new URL(document.URL); try { const params = validateAuthResponse(authorizationServer, client, currentUrl); const response = await authorizationCodeGrantRequest(authorizationServer, client, TlsClientAuth(), params, this.redirectUrl, this.codeVerifier); const openIdTokens = await processAuthorizationCodeResponse(authorizationServer, client, response); this.setToken(openIdTokens); this.state.oauth.audience = this.issuerConfig.audience; } catch (error) { this.handleUnknownError(error); } } setToken(tokens) { this.state.oauth.tokens = tokens; // Prepare refresh token if (this.state.oauth.tokens.expires_in) { // Refresh the token 1 minutes before its expiration, // to be sure that it will still be valid when using it. const expiresInMs = (this.state.oauth.tokens.expires_in - 60) * 1000; setTimeout(() => this.refreshToken(), expiresInMs); } this.state.oauth.status = 'issuer.loggedIn'; } async refreshToken() { if (!this.state.oauth.tokens?.refresh_token) { throw new Error('Token cannot be refreshed : there is no refresh-token'); } console.debug('Refreshing token'); const authorizationServer = await this.getAuthorizationServer(); const client = this.getClient(); let openIdTokens; try { const response = await refreshTokenGrantRequest(authorizationServer, client, TlsClientAuth(), this.state.oauth.tokens.refresh_token); openIdTokens = await processRefreshTokenResponse(authorizationServer, client, response); } catch (error) { // an error here means the user was logged out somehow somewhere else. this.handleUnknownError(error); this.loggedOutFromBackend(); } if (openIdTokens) { this.setToken(openIdTokens); } } async logoutFromIssuer() { // Save the current state of the application before logout this.context.sessionManager.saveStateToSession(); const authorizationServer = await this.getAuthorizationServer(); const issuerLogoutUrl = new URL(authorizationServer.end_session_endpoint); const logoutRedirectUrl = this.getLogoutRedirectUrl(); issuerLogoutUrl.searchParams.set('client_id', this.issuerConfig.clientId); issuerLogoutUrl.searchParams.set('post_logout_redirect_uri', logoutRedirectUrl); if (this.state.oauth.tokens?.id_token) { issuerLogoutUrl.searchParams.set('id_token_hint', this.state.oauth.tokens.id_token); } window.open(issuerLogoutUrl, '_self'); } }