@openpass/openpass-js-sdk
Version:
OpenPass SSO JavaScript SDK
159 lines • 10.7 kB
JavaScript
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