UNPKG

@openpass/openpass-js-sdk

Version:
159 lines 10.7 kB
import { config, getOpenPassApiBaseUrl } from "../config"; import { PARAM_CODE_CHALLENGE_METHOD_VALUE } from "./constants"; import { generateCodeChallenge, generateCodeVerifier } from "./utils/pkce"; import { generateStateValue } from "./utils/state"; import { buildAuthorizeUrl, urlHasStateAndCodeOrErrorParameters, parseAuthRedirectUrlParams, redirectUrlMatchesCurrentUrl } from "./url"; import { ERROR_CODE_INVALID_AUTH_SESSION, ERROR_CODE_INVALID_REDIRECT } from "./error/codes"; import { AuthError, SdkError } from "./error/errors"; import { sendSdkTelemetryErrorEvent, sendSdkTelemetryInfoEvent } from "./utils/sdkTelemetry"; import { reportAnalyticsEvent, addAnalyticsDimension } from "./utils/userAnalytics"; /** * Class which handles the redirect authorization flow. * To realize this flow this class implements the Authorization Code Flow with Proof Key for Code Exchange (PKCE). * The redirect flow involves redirecting to an authorization service, and then handle the login response send via the redirect. */ export default class RedirectAuth { constructor(openPassOptions, signInStateRepository, openPassAuthClient) { this.openPassOptions = openPassOptions; this.openPassApiClient = openPassAuthClient; this.signInStateRepository = signInStateRepository; } /** * Initiate the login flow by redirecting to the authorization server * @param options the login options */ async signIn(options) { var _a, _b; if (!options.redirectUrl) { throw new SdkError("Error redirectUrl is invalid. Please use a valid redirectUrl"); } // Report analytics event for redirect sign-in started reportAnalyticsEvent("sign-in-with-redirect-started", { source: options.source }); try { const verifier = generateCodeVerifier(); const authSession = { clientState: options.clientState, clientId: this.openPassOptions.clientId, redirectUrl: options.redirectUrl, codeVerifier: verifier, codeChallenge: await generateCodeChallenge(verifier), codeChallengeMethod: PARAM_CODE_CHALLENGE_METHOD_VALUE, state: generateStateValue(), loginHint: options.loginHint, disableLoginHintEditing: options.disableLoginHintEditing, originatingUri: (_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.href, allowUnverifiedEmail: (_b = options.allowUnverifiedEmail) !== null && _b !== void 0 ? _b : false, }; this.signInStateRepository.add(authSession); const loginUri = buildAuthorizeUrl(getOpenPassApiBaseUrl(this.openPassOptions.baseUrl), config.SSO_AUTHORIZE_PATH, authSession, options.source, options.customQueryParameters); window.location.href = loginUri; } catch (error) { console.error("Unable to start a sign-in session", error); sendSdkTelemetryErrorEvent("RedirectAuth.signIn", error, this.openPassApiClient); throw error; } } /** * Determines if a redirect authorization session is in progress, and the current browser url meets the conditions to consider it as a valid redirection from the * authorization server for the current login session. */ isAuthenticationRedirect() { try { // Parse the current URL query parameters const currentPageAuthenticationParameters = parseAuthRedirectUrlParams(window.location.search); // First check if the current URL has state and code, or error parameters. if (!urlHasStateAndCodeOrErrorParameters(currentPageAuthenticationParameters)) { return false; } // Get the current auth session, this contains the redirect url to check against. const authSession = this.signInStateRepository.get(); // If there is no auth session at this point, then we are unable to complete the redirect flow. if (!authSession) { console.warn("No authentication session found when checking authentication redirect, ensure a call to signIn via redirect has been made first"); sendSdkTelemetryInfoEvent("RedirectAuth.isAuthenticationRedirect", "No auth session found", this.openPassApiClient); return false; } // If there is not redirect url in the auth session, then we are unable to complete the redirect flow. if (!authSession.redirectUrl) { console.warn("No redirect url found in the auth session, ensure a call to signIn via redirect has been made first"); sendSdkTelemetryInfoEvent("RedirectAuth.isAuthenticationRedirect", "Auth session found, but redirect url was not set", this.openPassApiClient); return false; } // Check if the current URL is a valid redirect URL for the current auth session, including checking the redirect url is the same as the current page url (just origin and path). const redirectUrlMatchesCurrentUrlValue = redirectUrlMatchesCurrentUrl(window.location.href, authSession.redirectUrl); sendSdkTelemetryInfoEvent("RedirectAuth.isAuthenticationRedirect", `validAuthenticationRedirectUrl is ${redirectUrlMatchesCurrentUrlValue}`, this.openPassApiClient); return redirectUrlMatchesCurrentUrlValue; } catch (error) { console.error("Unable to check if the current URL is a valid redirect URL for the current auth session", error); sendSdkTelemetryErrorEvent("RedirectAuth.isAuthenticationRedirect", error, this.openPassApiClient); throw error; } } /** * Handles a login redirect and attempts to complete the authorization flow. */ async handleAuthenticationRedirect() { try { // Parse the current URL query parameters const currentPageAuthenticationParameters = parseAuthRedirectUrlParams(window.location.search); // First check if the current URL has state and code, or error parameters. if (!urlHasStateAndCodeOrErrorParameters(currentPageAuthenticationParameters)) { throw new AuthError(ERROR_CODE_INVALID_REDIRECT, "The current URL does not contain expected state and code or error parameters, cannot handle redirect", ""); } // Get the current auth session which contains all the required security parameters to complete the sign-in. const authSession = this.signInStateRepository.get(); // If there is no auth session at this point, then we are unable to complete the redirect flow. if (!authSession) { console.error("No auth session found, cannot handle redirect. Ensure a call to signIn via redirect has been made first"); throw new AuthError(ERROR_CODE_INVALID_REDIRECT, "No auth session found, cannot handle redirect. Ensure a call to signIn via redirect has been made first", ""); } // If there is not redirect url in the auth session, then we are unable to complete the redirect flow. if (!authSession.redirectUrl) { console.error("No redirect url found in the auth session, ensure a call to signIn via redirect has been made first"); throw new AuthError(ERROR_CODE_INVALID_REDIRECT, "No redirect url found in the auth session, ensure a call to signIn via redirect has been made first", ""); } // Check if the current URL is a valid redirect URL for the current auth session, including checking the redirect url is the same as the current page url (just origin and path). if (!redirectUrlMatchesCurrentUrl(window.location.href, authSession.redirectUrl)) { console.error("The current URL does not match the expected redirect URL, cannot handle redirect"); throw new AuthError(ERROR_CODE_INVALID_REDIRECT, "The current URL does not match the expected redirect URL, cannot handle redirect", ""); } // Remove the auth session from the repository, we will no longer need it whether the rest of this method passes or fails. this.signInStateRepository.remove(); // If there is a error parameter on the URL, then we have a error response from the authorization server. if (currentPageAuthenticationParameters.error) { throw new AuthError(currentPageAuthenticationParameters.error, currentPageAuthenticationParameters.errorDescription ? currentPageAuthenticationParameters.errorDescription : "", currentPageAuthenticationParameters.errorUri ? currentPageAuthenticationParameters.errorUri : "", authSession.clientState); } if (currentPageAuthenticationParameters.state !== authSession.state) { console.error("The state parameter in the redirect URL does not match the state in the auth session, cannot complete the redirect flow"); throw new AuthError(ERROR_CODE_INVALID_AUTH_SESSION, "The state parameter in the redirect URL does not match the state in the auth session", "", authSession.clientState); } const tokens = await this.openPassApiClient.exchangeAuthCodeForTokens(currentPageAuthenticationParameters.code, authSession); const { idToken, rawIdToken, accessToken, refreshToken, tokenType, expiresIn } = tokens; const response = { clientState: authSession.clientState, originatingUri: authSession.originatingUri, idToken: idToken, rawIdToken: rawIdToken, accessToken: accessToken, rawAccessToken: accessToken, refreshToken: refreshToken, tokenType: tokenType, expiresIn: expiresIn, }; // Report analytics for successful authentication if (response.idToken) { reportAnalyticsEvent("authentication-completed", { method: "redirect" }); addAnalyticsDimension("is_authenticated", "true"); addAnalyticsDimension("op_user_id", response.idToken.sub || ""); } return response; } catch (error) { sendSdkTelemetryErrorEvent("RedirectAuth.handleAuthenticationRedirect", error, this.openPassApiClient); throw error; } } } //# sourceMappingURL=redirect.js.map