oidc-client-rx
Version:
ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications
424 lines (423 loc) • 22.1 kB
JavaScript
import { Injectable, inject } from "@outposts/injection-js";
import { of } from "rxjs";
import { map } from "rxjs/operators";
import { FlowsDataService } from "../../flows/flows-data.service.js";
import { HttpParams } from "../../http/index.js";
import { LoggerService } from "../../logging/logger.service.js";
import { StoragePersistenceService } from "../../storage/storage-persistence.service.js";
import { JwtWindowCryptoService } from "../../validation/jwt-window-crypto.service.js";
import { FlowHelper } from "../flowHelper/flow-helper.service.js";
import { UriEncoder } from "./uri-encoder.js";
function _ts_decorate(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : null === desc ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if ("object" == typeof Reflect && "function" == typeof Reflect.decorate) r = Reflect.decorate(decorators, target, key, desc);
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
const CALLBACK_PARAMS_TO_CHECK = [
'code',
'state',
'token',
'id_token'
];
const AUTH0_ENDPOINT = 'auth0.com';
class UrlService {
getUrlParameter(urlToCheck, name) {
if (!urlToCheck) return '';
if (!name) return '';
name = name.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp(`[\\?&#]${name}=([^&#]*)`);
const results = regex.exec(urlToCheck);
return null === results ? '' : decodeURIComponent(results[1]);
}
getUrlWithoutQueryParameters(url) {
const u = new URL(url.toString());
const keys = [];
for (const key of Array.from(u.searchParams.keys()))keys.push(key);
keys.forEach((key)=>{
u.searchParams.delete(key);
});
return u;
}
queryParametersExist(expected, actual) {
let r = true;
expected.forEach((_v, k)=>{
if (!actual.has(k)) r = false;
});
return r;
}
isCallbackFromSts(currentUrl, config) {
if (null == config ? void 0 : config.checkRedirectUrlWhenCheckingIfIsCallback) {
const currentUrlInstance = new URL(currentUrl);
const redirectUrl = this.getRedirectUrl(config);
if (!redirectUrl) {
this.loggerService.logError(config, 'UrlService.isCallbackFromSts: could not get redirectUrl from config, was: ', redirectUrl);
return false;
}
const redirectUriUrlInstance = new URL(redirectUrl);
const redirectUriWithoutQueryParams = this.getUrlWithoutQueryParameters(redirectUriUrlInstance).toString();
const currentUrlWithoutQueryParams = this.getUrlWithoutQueryParameters(currentUrlInstance).toString();
const redirectUriQueryParamsArePresentInCurrentUrl = this.queryParametersExist(redirectUriUrlInstance.searchParams, currentUrlInstance.searchParams);
if (redirectUriWithoutQueryParams !== currentUrlWithoutQueryParams || !redirectUriQueryParamsArePresentInCurrentUrl) {
this.loggerService.logDebug(config, 'UrlService.isCallbackFromSts: configured redirectUrl does not match with the current url');
return false;
}
}
return CALLBACK_PARAMS_TO_CHECK.some((x)=>!!this.getUrlParameter(currentUrl, x));
}
getRefreshSessionSilentRenewUrl(config, customParams) {
if (this.flowHelper.isCurrentFlowCodeFlow(config)) return this.createUrlCodeFlowWithSilentRenew(config, customParams);
return of(this.createUrlImplicitFlowWithSilentRenew(config, customParams));
}
getAuthorizeParUrl(requestUri, configuration) {
const authWellKnownEndPoints = this.storagePersistenceService.read('authWellKnownEndPoints', configuration);
if (!authWellKnownEndPoints) {
this.loggerService.logError(configuration, 'authWellKnownEndpoints is undefined');
return null;
}
const authorizationEndpoint = authWellKnownEndPoints.authorizationEndpoint;
if (!authorizationEndpoint) {
this.loggerService.logError(configuration, `Can not create an authorize URL when authorizationEndpoint is '${authorizationEndpoint}'`);
return null;
}
const { clientId } = configuration;
if (!clientId) {
this.loggerService.logError(configuration, 'getAuthorizeParUrl could not add clientId because it was: ', clientId);
return null;
}
const urlParts = authorizationEndpoint.split('?');
const authorizationUrl = urlParts[0];
const existingParams = urlParts[1];
let params = this.createHttpParams(existingParams);
params = params.set('request_uri', requestUri);
params = params.append('client_id', clientId);
return `${authorizationUrl}?${params}`;
}
getAuthorizeUrl(config, authOptions) {
if (!config) return of(null);
if (this.flowHelper.isCurrentFlowCodeFlow(config)) return this.createUrlCodeFlowAuthorize(config, authOptions);
return of(this.createUrlImplicitFlowAuthorize(config, authOptions) || '');
}
getEndSessionEndpoint(configuration) {
const authWellKnownEndPoints = this.storagePersistenceService.read('authWellKnownEndPoints', configuration);
const endSessionEndpoint = null == authWellKnownEndPoints ? void 0 : authWellKnownEndPoints.endSessionEndpoint;
if (!endSessionEndpoint) return {
url: '',
existingParams: ''
};
const urlParts = endSessionEndpoint.split('?');
const url = urlParts[0];
const existingParams = urlParts[1] ?? '';
return {
url,
existingParams
};
}
getEndSessionUrl(configuration, customParams) {
if (!configuration) return null;
const idToken = this.storagePersistenceService.getIdToken(configuration);
const { customParamsEndSessionRequest } = configuration;
const mergedParams = {
...customParamsEndSessionRequest,
...customParams
};
return this.createEndSessionUrl(idToken, configuration, mergedParams);
}
createRevocationEndpointBodyAccessToken(token, configuration) {
const clientId = this.getClientId(configuration);
const clientSecret = this.getClientSecret(configuration);
if (!clientId) return null;
let params = this.createHttpParams();
params = params.set('client_id', clientId);
params = params.set('token', token);
params = params.set('token_type_hint', 'access_token');
if (clientSecret) params = params.set('client_secret', clientSecret);
return params.toString();
}
createRevocationEndpointBodyRefreshToken(token, configuration) {
const clientId = this.getClientId(configuration);
const clientSecret = this.getClientSecret(configuration);
if (!clientId) return null;
let params = this.createHttpParams();
params = params.set('client_id', clientId);
params = params.set('token', token);
params = params.set('token_type_hint', 'refresh_token');
if (clientSecret) params = params.set('client_secret', clientSecret);
return params.toString();
}
getRevocationEndpointUrl(configuration) {
const authWellKnownEndPoints = this.storagePersistenceService.read('authWellKnownEndPoints', configuration);
const revocationEndpoint = null == authWellKnownEndPoints ? void 0 : authWellKnownEndPoints.revocationEndpoint;
if (!revocationEndpoint) return null;
const urlParts = revocationEndpoint.split('?');
return urlParts[0];
}
createBodyForCodeFlowCodeRequest(code, configuration, customTokenParams) {
const clientId = this.getClientId(configuration);
const clientSecret = this.getClientSecret(configuration);
if (!clientId) return null;
let params = this.createHttpParams();
params = params.set('grant_type', 'authorization_code');
params = params.set('client_id', clientId);
if (clientSecret) params = params.set('client_secret', clientSecret);
if (!configuration.disablePkce) {
const codeVerifier = this.flowsDataService.getCodeVerifier(configuration);
if (!codeVerifier) {
this.loggerService.logError(configuration, 'CodeVerifier is not set ', codeVerifier);
return null;
}
params = params.set('code_verifier', codeVerifier);
}
params = params.set('code', code);
if (customTokenParams) params = this.appendCustomParams({
...customTokenParams
}, params);
const silentRenewUrl = this.getSilentRenewUrl(configuration);
if (this.flowsDataService.isSilentRenewRunning(configuration) && silentRenewUrl) {
params = params.set('redirect_uri', silentRenewUrl);
return params.toString();
}
const redirectUrl = this.getRedirectUrl(configuration);
if (!redirectUrl) return null;
params = params.set('redirect_uri', redirectUrl);
return params.toString();
}
createBodyForCodeFlowRefreshTokensRequest(refreshToken, configuration, customParamsRefresh) {
const clientId = this.getClientId(configuration);
const clientSecret = this.getClientSecret(configuration);
if (!clientId) return null;
let params = this.createHttpParams();
params = params.set('grant_type', 'refresh_token');
params = params.set('client_id', clientId);
params = params.set('refresh_token', refreshToken);
if (clientSecret) params = params.set('client_secret', clientSecret);
if (customParamsRefresh) params = this.appendCustomParams({
...customParamsRefresh
}, params);
return params.toString();
}
createBodyForParCodeFlowRequest(configuration, authOptions) {
const redirectUrl = this.getRedirectUrl(configuration, authOptions);
if (!redirectUrl) return of(null);
const state = this.flowsDataService.getExistingOrCreateAuthStateControl(configuration);
const nonce = this.flowsDataService.createNonce(configuration);
this.loggerService.logDebug(configuration, `Authorize created. adding myautostate: ${state}`);
const codeVerifier = this.flowsDataService.createCodeVerifier(configuration);
return this.jwtWindowCryptoService.generateCodeChallenge(codeVerifier).pipe(map((codeChallenge)=>{
const { clientId, responseType, scope, hdParam, customParamsAuthRequest } = configuration;
let params = this.createHttpParams('');
params = params.set('client_id', clientId ?? '');
params = params.append('redirect_uri', redirectUrl);
params = params.append('response_type', responseType ?? '');
params = params.append('scope', scope ?? '');
params = params.append('nonce', nonce);
params = params.append('state', state);
params = params.append('code_challenge', codeChallenge);
params = params.append('code_challenge_method', 'S256');
if (hdParam) params = params.append('hd', hdParam);
if (customParamsAuthRequest) params = this.appendCustomParams({
...customParamsAuthRequest
}, params);
if (null == authOptions ? void 0 : authOptions.customParams) params = this.appendCustomParams({
...authOptions.customParams
}, params);
return params.toString();
}));
}
getPostLogoutRedirectUrl(configuration) {
const { postLogoutRedirectUri } = configuration;
if (!postLogoutRedirectUri) {
this.loggerService.logError(configuration, 'could not get postLogoutRedirectUri, was: ', postLogoutRedirectUri);
return null;
}
return postLogoutRedirectUri;
}
createEndSessionUrl(idTokenHint, configuration, customParamsEndSession) {
if (this.isAuth0Endpoint(configuration)) return this.composeAuth0Endpoint(configuration);
const { url, existingParams } = this.getEndSessionEndpoint(configuration);
if (!url) return null;
let params = this.createHttpParams(existingParams);
if (idTokenHint) params = params.set('id_token_hint', idTokenHint);
const postLogoutRedirectUri = this.getPostLogoutRedirectUrl(configuration);
if (postLogoutRedirectUri) params = params.append('post_logout_redirect_uri', postLogoutRedirectUri);
if (customParamsEndSession) params = this.appendCustomParams({
...customParamsEndSession
}, params);
return `${url}?${params}`;
}
createAuthorizeUrl(codeChallenge, redirectUrl, nonce, state, configuration, prompt, customRequestParams) {
const authWellKnownEndPoints = this.storagePersistenceService.read('authWellKnownEndPoints', configuration);
const authorizationEndpoint = null == authWellKnownEndPoints ? void 0 : authWellKnownEndPoints.authorizationEndpoint;
if (!authorizationEndpoint) {
this.loggerService.logError(configuration, `Can not create an authorize URL when authorizationEndpoint is '${authorizationEndpoint}'`);
return '';
}
const { clientId, responseType, scope, hdParam, customParamsAuthRequest } = configuration;
if (!clientId) {
this.loggerService.logError(configuration, 'createAuthorizeUrl could not add clientId because it was: ', clientId);
return '';
}
if (!responseType) {
this.loggerService.logError(configuration, 'createAuthorizeUrl could not add responseType because it was: ', responseType);
return '';
}
if (!scope) {
this.loggerService.logError(configuration, 'createAuthorizeUrl could not add scope because it was: ', scope);
return '';
}
const urlParts = authorizationEndpoint.split('?');
const authorizationUrl = urlParts[0];
const existingParams = urlParts[1];
let params = this.createHttpParams(existingParams);
params = params.set('client_id', clientId);
params = params.append('redirect_uri', redirectUrl);
params = params.append('response_type', responseType);
params = params.append('scope', scope);
params = params.append('nonce', nonce);
params = params.append('state', state);
if (this.flowHelper.isCurrentFlowCodeFlow(configuration)) {
params = params.append('code_challenge', codeChallenge);
params = params.append('code_challenge_method', 'S256');
}
const mergedParams = {
...customParamsAuthRequest,
...customRequestParams
};
if (Object.keys(mergedParams).length > 0) params = this.appendCustomParams({
...mergedParams
}, params);
if (prompt) params = this.overWriteParam(params, 'prompt', prompt);
if (hdParam) params = params.append('hd', hdParam);
return `${authorizationUrl}?${params}`;
}
createUrlImplicitFlowWithSilentRenew(configuration, customParams) {
const state = this.flowsDataService.getExistingOrCreateAuthStateControl(configuration);
const nonce = this.flowsDataService.createNonce(configuration);
const silentRenewUrl = this.getSilentRenewUrl(configuration);
if (!silentRenewUrl) return null;
this.loggerService.logDebug(configuration, 'RefreshSession created. adding myautostate: ', state);
const authWellKnownEndPoints = this.storagePersistenceService.read('authWellKnownEndPoints', configuration);
if (authWellKnownEndPoints) return this.createAuthorizeUrl('', silentRenewUrl, nonce, state, configuration, 'none', customParams);
this.loggerService.logError(configuration, 'authWellKnownEndpoints is undefined');
return null;
}
createUrlCodeFlowWithSilentRenew(configuration, customParams) {
const state = this.flowsDataService.getExistingOrCreateAuthStateControl(configuration);
const nonce = this.flowsDataService.createNonce(configuration);
this.loggerService.logDebug(configuration, `RefreshSession created. adding myautostate: ${state}`);
const codeVerifier = this.flowsDataService.createCodeVerifier(configuration);
return this.jwtWindowCryptoService.generateCodeChallenge(codeVerifier).pipe(map((codeChallenge)=>{
const silentRenewUrl = this.getSilentRenewUrl(configuration);
if (!silentRenewUrl) return '';
const authWellKnownEndPoints = this.storagePersistenceService.read('authWellKnownEndPoints', configuration);
if (authWellKnownEndPoints) return this.createAuthorizeUrl(codeChallenge, silentRenewUrl, nonce, state, configuration, 'none', customParams);
this.loggerService.logWarning(configuration, 'authWellKnownEndpoints is undefined');
return '';
}));
}
createUrlImplicitFlowAuthorize(configuration, authOptions) {
const state = this.flowsDataService.getExistingOrCreateAuthStateControl(configuration);
const nonce = this.flowsDataService.createNonce(configuration);
this.loggerService.logDebug(configuration, `Authorize created. adding myautostate: ${state}`);
const redirectUrl = this.getRedirectUrl(configuration, authOptions);
if (!redirectUrl) return null;
const authWellKnownEndPoints = this.storagePersistenceService.read('authWellKnownEndPoints', configuration);
if (authWellKnownEndPoints) {
const { customParams } = authOptions || {};
return this.createAuthorizeUrl('', redirectUrl, nonce, state, configuration, '', customParams);
}
this.loggerService.logError(configuration, 'authWellKnownEndpoints is undefined');
return null;
}
createUrlCodeFlowAuthorize(config, authOptions) {
const state = this.flowsDataService.getExistingOrCreateAuthStateControl(config);
const nonce = this.flowsDataService.createNonce(config);
this.loggerService.logDebug(config, `Authorize created. adding myautostate: ${state}`);
const redirectUrl = this.getRedirectUrl(config, authOptions);
if (!redirectUrl) return of(null);
return this.getCodeChallenge(config).pipe(map((codeChallenge)=>{
const authWellKnownEndPoints = this.storagePersistenceService.read('authWellKnownEndPoints', config);
if (authWellKnownEndPoints) {
const { customParams } = authOptions || {};
return this.createAuthorizeUrl(codeChallenge, redirectUrl, nonce, state, config, '', customParams);
}
this.loggerService.logError(config, 'authWellKnownEndpoints is undefined');
return '';
}));
}
getCodeChallenge(config) {
if (config.disablePkce) return of('');
const codeVerifier = this.flowsDataService.createCodeVerifier(config);
return this.jwtWindowCryptoService.generateCodeChallenge(codeVerifier);
}
getRedirectUrl(configuration, authOptions) {
let { redirectUrl } = configuration;
if (null == authOptions ? void 0 : authOptions.redirectUrl) redirectUrl = authOptions.redirectUrl;
if (!redirectUrl) {
this.loggerService.logError(configuration, 'could not get redirectUrl, was: ', redirectUrl);
return null;
}
return redirectUrl;
}
getSilentRenewUrl(configuration) {
const { silentRenewUrl } = configuration;
if (!silentRenewUrl) {
this.loggerService.logError(configuration, 'could not get silentRenewUrl, was: ', silentRenewUrl);
return null;
}
return silentRenewUrl;
}
getClientId(configuration) {
const { clientId } = configuration;
if (!clientId) {
this.loggerService.logError(configuration, 'could not get clientId, was: ', clientId);
return null;
}
return clientId;
}
getClientSecret(configuration) {
const { clientSecret } = configuration;
if (!clientSecret) {
this.loggerService.logDebug(configuration, 'could not get clientSecret');
return null;
}
return clientSecret;
}
appendCustomParams(customParams, params) {
for (const [key, value] of Object.entries({
...customParams
}))params = params.append(key, value.toString());
return params;
}
overWriteParam(params, key, value) {
return params.set(key, value);
}
createHttpParams(existingParams) {
existingParams = existingParams ?? '';
return new HttpParams({
fromString: existingParams,
encoder: new UriEncoder()
});
}
isAuth0Endpoint(configuration) {
const { authority, useCustomAuth0Domain } = configuration;
if (!authority) return false;
return authority.endsWith(AUTH0_ENDPOINT) || Boolean(useCustomAuth0Domain);
}
composeAuth0Endpoint(configuration) {
const { authority, clientId } = configuration;
const postLogoutRedirectUrl = this.getPostLogoutRedirectUrl(configuration);
return `${authority}/v2/logout?client_id=${clientId}&returnTo=${postLogoutRedirectUrl}`;
}
constructor(){
this.loggerService = inject(LoggerService);
this.flowsDataService = inject(FlowsDataService);
this.flowHelper = inject(FlowHelper);
this.storagePersistenceService = inject(StoragePersistenceService);
this.jwtWindowCryptoService = inject(JwtWindowCryptoService);
}
}
UrlService = _ts_decorate([
Injectable()
], UrlService);
export { UrlService };