@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
285 lines (284 loc) • 12.5 kB
JavaScript
import AbstractConnectManager from './abstractconnectmanager';
import ShareManager from '../share/sharemanager';
import { authorizationCodeGrantRequest, calculatePKCECodeChallenge, discoveryRequest, generateRandomCodeVerifier, processAuthorizationCodeResponse, processDiscoveryResponse, processRefreshTokenResponse, refreshTokenGrantRequest, TlsClientAuth, validateAuthResponse } from 'oauth4webapi';
import UserDataManager from '../userdata/userdatamanager';
export default class OpenIdConnectManager extends AbstractConnectManager {
constructor() {
super(...arguments);
Object.defineProperty(this, "authorizationServer", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "storagePath", {
enumerable: true,
configurable: true,
writable: true,
value: 'oAuth'
});
Object.defineProperty(this, "silentLoginIframe", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
}
get issuerConfig() {
return this.configManager.Config.oauth.issuer;
}
authProcessed() {
return new URL(window.location.href).searchParams.has('authentified');
}
isAuthentified() {
return new URL(window.location.href).searchParams.get('authentified') === 'true';
}
hasAuthError() {
return new URL(window.location.href).searchParams.has('error');
}
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.resetUrlHistory(false);
this.handleErrorFromIssuer();
}
}
}
/**
* 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);
}
/**
* The RedirectUrl has to be the same for all call to the issuer
* Because the authentication is given for a specific redirectUrl.
* Therefore, we keep it in localStorage.
*/
get redirectUrl() {
return this.loadFromLocalStorage('redirectUrl');
}
set redirectUrl(value) {
this.saveToLocalStorage('redirectUrl', value);
}
/**
* The currentState cannot just be passed as parameter in the URL
* Because the URL-length is limited.
* Therefore, we keep it in localStorage.
*/
get currentState() {
return this.loadFromLocalStorage('currentState');
}
set currentState(value) {
if (value.startsWith('#')) {
value = value.substring(1);
}
this.saveToLocalStorage('currentState', value);
}
loadFromLocalStorage(path) {
return UserDataManager.getInstance().getUserData(`${this.storagePath}.${path}`, true) ?? '';
}
saveToLocalStorage(path, value) {
return UserDataManager.getInstance().saveUserData(`${this.storagePath}.${path}`, value, true);
}
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
this.currentState = silent ? window.location.hash : ShareManager.getInstance().getStateToShare();
// Redirect user to authorizationServer.authorization_endpoint
const authorizationUrl = new URL(authorizationServer.authorization_endpoint);
this.redirectUrl = this.getLoginRedirectUrl(silent);
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) {
authorizationUrl.searchParams.set('prompt', 'none');
}
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();
}
getLoginRedirectUrl(silent) {
if (silent) {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}silentlogincallback.html?authentified=true`;
}
return `${window.location.protocol}//${window.location.host}${window.location.pathname}?authentified=true`;
}
getLogoutRedirectUrl() {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
}
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);
}
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);
}
window.history.replaceState(null, '', newUrl.toString());
await this.handleLoggedInToIssuer();
}
else if (event.data?.type === 'OAUTH_ERROR') {
console.error('Silent login error', event.data.error);
}
document.body.removeChild(this.silentLoginIframe);
this.silentLoginIframe = undefined;
}
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;
// Remove oauth URL parameters
this.resetUrlHistory(true);
}
catch (error) {
this.manageError(error);
}
}
setToken(tokens) {
this.state.oauth.tokens = tokens;
// Prepare refresh token
if (this.state.oauth.tokens.expires_in) {
const expiresInMs = this.state.oauth.tokens.expires_in * 1000;
setTimeout(() => this.refreshToken(), expiresInMs);
}
this.state.oauth.status = 'issuer.loggedIn';
}
resetUrlHistory(authentified) {
let newUrl;
if (authentified) {
newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}#${this.currentState}`;
}
else {
newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?authentified=${authentified}#${this.currentState}`;
}
window.history.replaceState(null, '', newUrl);
}
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.
const errorMsg = `OAuth: Refresh token failed: ${error.message}`;
console.error(errorMsg);
this.resetUrlHistory(false);
this.loggedOutFromBackend();
}
if (openIdTokens) {
this.setToken(openIdTokens);
}
}
manageError(error) {
this.state.oauth.status = 'loginFailed';
const errorMsg = `OAuth: Issuer login failed: ${error.message}`;
console.error(errorMsg);
this.resetUrlHistory(false);
this.handleErrorFromIssuer();
}
async logoutFromIssuer() {
// Save the current state of the application before logout
this.currentState = ShareManager.getInstance().getStateToShare();
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');
}
}