oidc-client-rx
Version:
ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications
424 lines (423 loc) • 21.9 kB
JavaScript
import { Injectable, inject } from "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 (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 = 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 = 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 (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 = 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 (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 };