UNPKG

oidc-client-rx

Version:

ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications

424 lines (423 loc) 21.9 kB
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 };