@asgardeo/auth-js
Version:
Asgardeo Auth JS SDK to be used in JavaScript and TypeScript applications.
689 lines (570 loc) • 26.5 kB
text/typescript
/**
* Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved.
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
AUTHORIZATION_ENDPOINT,
FetchCredentialTypes,
OIDC_SCOPE,
OP_CONFIG_INITIATED,
SESSION_STATE,
SIGN_OUT_SUCCESS_PARAM,
STATE
} from "../constants";
import { DataLayer } from "../data";
import { AsgardeoAuthException } from "../exception";
import { AuthenticationHelper, CryptoHelper } from "../helpers";
import {
AuthClientConfig,
AuthenticatedUserInfo,
AuthorizationURLParams,
BasicUserInfo,
CryptoUtils,
CustomGrantConfig,
DecodedIDTokenPayload,
FetchRequestConfig,
FetchResponse,
OIDCEndpoints,
OIDCProviderMetaData,
SessionData,
StrictAuthClientConfig,
TokenResponse
} from "../models";
import { AuthenticationUtils } from "../utils";
export class AuthenticationCore<T> {
private _dataLayer: DataLayer<T>;
private _config: () => Promise<AuthClientConfig>;
private _oidcProviderMetaData: () => Promise<OIDCProviderMetaData>;
private _authenticationHelper: AuthenticationHelper<T>;
private _cryptoUtils: CryptoUtils;
private _cryptoHelper: CryptoHelper;
public constructor(dataLayer: DataLayer<T>, cryptoUtils: CryptoUtils) {
this._cryptoUtils = cryptoUtils;
this._cryptoHelper = new CryptoHelper(cryptoUtils);
this._authenticationHelper = new AuthenticationHelper(dataLayer, this._cryptoHelper);
this._dataLayer = dataLayer;
this._config = async () => await this._dataLayer.getConfigData();
this._oidcProviderMetaData = async () => await this._dataLayer.getOIDCProviderMetaData();
}
public async getAuthorizationURLParams(
config?: AuthorizationURLParams,
userID?: string
): Promise<Map<string, string>> {
const configData: StrictAuthClientConfig = await this._config();
const authorizeRequestParams: Map<string, string> = new Map<
string,
string
>();
authorizeRequestParams.set("response_type", "code");
authorizeRequestParams.set("client_id", configData.clientID);
if (configData.clientSecret && configData.clientSecret.trim().length > 0) {
authorizeRequestParams.set("client_secret", configData.clientSecret);
}
let scope: string = OIDC_SCOPE;
if (configData.scope && configData.scope.length > 0) {
if (!configData.scope.includes(OIDC_SCOPE)) {
configData.scope.push(OIDC_SCOPE);
}
scope = configData.scope.join(" ");
}
authorizeRequestParams.set("scope", scope);
authorizeRequestParams.set("redirect_uri", configData.signInRedirectURL);
if (configData.responseMode) {
authorizeRequestParams.set("response_mode", configData.responseMode);
}
const pkceKey: string = await this._authenticationHelper.generatePKCEKey(
userID
);
if (configData.enablePKCE) {
const codeVerifier: string = this._cryptoHelper?.getCodeVerifier();
const codeChallenge: string =
this._cryptoHelper?.getCodeChallenge(codeVerifier);
await this._dataLayer.setTemporaryDataParameter(
pkceKey,
codeVerifier,
userID
);
authorizeRequestParams.set("code_challenge_method", "S256");
authorizeRequestParams.set("code_challenge", codeChallenge);
}
if (configData.prompt) {
authorizeRequestParams.set("prompt", configData.prompt);
}
const customParams: AuthorizationURLParams | undefined = config;
if (customParams) {
for (const [ key, value ] of Object.entries(customParams)) {
if (key != "" && value != "" && key !== STATE) {
authorizeRequestParams.set(key, value.toString());
}
}
}
authorizeRequestParams.set(
STATE,
AuthenticationUtils.generateStateParamForRequestCorrelation(
pkceKey,
customParams ? customParams[STATE]?.toString() : ""
)
);
return authorizeRequestParams;
}
public async getAuthorizationURL(config?: AuthorizationURLParams, userID?: string): Promise<string> {
const authorizeEndpoint: string = (await this._dataLayer.getOIDCProviderMetaDataParameter(
AUTHORIZATION_ENDPOINT as keyof OIDCProviderMetaData
)) as string;
if (!authorizeEndpoint || authorizeEndpoint.trim().length === 0) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-GAU-NF01",
"No authorization endpoint found.",
"No authorization endpoint was found in the OIDC provider meta data from the well-known endpoint " +
"or the authorization endpoint passed to the SDK is empty."
);
}
const authorizeRequest: URL = new URL(authorizeEndpoint);
const authorizeRequestParams: Map<string, string> =
await this.getAuthorizationURLParams(config, userID);
for (const [ key, value ] of authorizeRequestParams.entries()) {
authorizeRequest.searchParams.append(key, value);
}
return authorizeRequest.toString();
}
public async requestAccessToken(
authorizationCode: string,
sessionState: string,
state: string,
userID?: string,
tokenRequestConfig?: {
params: Record<string, unknown>
}
): Promise<TokenResponse> {
const tokenEndpoint: string | undefined = (await this._oidcProviderMetaData()).token_endpoint;
const configData: StrictAuthClientConfig = await this._config();
if (!tokenEndpoint || tokenEndpoint.trim().length === 0) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT1-NF01",
"Token endpoint not found.",
"No token endpoint was found in the OIDC provider meta data returned by the well-known endpoint " +
"or the token endpoint passed to the SDK is empty."
);
}
sessionState && (await this._dataLayer.setSessionDataParameter(
SESSION_STATE as keyof SessionData, sessionState, userID));
const body: URLSearchParams = new URLSearchParams();
body.set("client_id", configData.clientID);
if (configData.clientSecret && configData.clientSecret.trim().length > 0) {
body.set("client_secret", configData.clientSecret);
}
const code: string = authorizationCode;
body.set("code", code);
body.set("grant_type", "authorization_code");
body.set("redirect_uri", configData.signInRedirectURL);
if (tokenRequestConfig?.params) {
Object.entries(tokenRequestConfig.params).forEach(([ key, value ]: [key: string, value: unknown]) => {
body.append(key, value as string);
});
}
if (configData.enablePKCE) {
body.set(
"code_verifier", `${await this._dataLayer.getTemporaryDataParameter(
AuthenticationUtils.extractPKCEKeyFromStateParam(state),
userID
)}`
);
await this._dataLayer.removeTemporaryDataParameter(
AuthenticationUtils.extractPKCEKeyFromStateParam(state),
userID
);
}
let tokenResponse: Response;
try {
tokenResponse = await fetch(tokenEndpoint, {
body: body,
credentials: configData.sendCookiesInRequests
? FetchCredentialTypes.Include
: FetchCredentialTypes.SameOrigin,
headers: new Headers(AuthenticationUtils.getTokenRequestHeaders()),
method: "POST"
});
} catch (error: any) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT1-NE02",
"Requesting access token failed",
error ?? "The request to get the access token from the server failed."
);
}
if (!tokenResponse.ok) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT1-HE03",
`Requesting access token failed with ${tokenResponse.statusText}`,
await tokenResponse.json()
);
}
return await this._authenticationHelper.handleTokenResponse(tokenResponse, userID);
}
public async refreshAccessToken(userID?: string): Promise<TokenResponse> {
const tokenEndpoint: string | undefined = (await this._oidcProviderMetaData()).token_endpoint;
const configData: StrictAuthClientConfig = await this._config();
const sessionData: SessionData = await this._dataLayer.getSessionData(userID);
if (!sessionData.refresh_token) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT2-NF01",
"No refresh token found.",
"There was no refresh token found. Asgardeo doesn't return a " +
"refresh token if the refresh token grant is not enabled."
);
}
if (!tokenEndpoint || tokenEndpoint.trim().length === 0) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT2-NF02",
"No refresh token endpoint found.",
"No refresh token endpoint was in the OIDC provider meta data returned by the well-known " +
"endpoint or the refresh token endpoint passed to the SDK is empty."
);
}
const body: string[] = [];
body.push(`client_id=${ configData.clientID }`);
body.push(`refresh_token=${ sessionData.refresh_token }`);
body.push("grant_type=refresh_token");
if (configData.clientSecret && configData.clientSecret.trim().length > 0) {
body.push(`client_secret=${ configData.clientSecret }`);
}
let tokenResponse: Response;
try {
tokenResponse = await fetch(tokenEndpoint, {
body: body.join("&"),
credentials: configData.sendCookiesInRequests
? FetchCredentialTypes.Include
: FetchCredentialTypes.SameOrigin,
headers: new Headers(AuthenticationUtils.getTokenRequestHeaders()),
method: "POST"
});
} catch (error: any) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT2-NR03",
"Refresh access token request failed.",
error ?? "The request to refresh the access token failed."
);
}
if (!tokenResponse.ok) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT2-HE04",
`Refreshing access token failed with ${tokenResponse.statusText}`,
await tokenResponse.json()
);
}
return this._authenticationHelper.handleTokenResponse(tokenResponse, userID);
}
public async revokeAccessToken(userID?: string): Promise<FetchResponse> {
const revokeTokenEndpoint: string | undefined = (await this._oidcProviderMetaData()).revocation_endpoint;
const configData: StrictAuthClientConfig = await this._config();
if (!revokeTokenEndpoint || revokeTokenEndpoint.trim().length === 0) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT3-NF01",
"No revoke access token endpoint found.",
"No revoke access token endpoint was found in the OIDC provider meta data returned by " +
"the well-known endpoint or the revoke access token endpoint passed to the SDK is empty."
);
}
const body: string[] = [];
body.push(`client_id=${ configData.clientID }`);
body.push(`token=${ (await this._dataLayer.getSessionData(userID)).access_token }`);
body.push("token_type_hint=access_token");
if (configData.clientSecret && configData.clientSecret.trim().length > 0) {
body.push(`client_secret=${ configData.clientSecret }`);
}
let response: Response;
try {
response = await fetch(revokeTokenEndpoint, {
body: body.join("&"),
credentials: configData.sendCookiesInRequests
? FetchCredentialTypes.Include
: FetchCredentialTypes.SameOrigin,
headers: new Headers(AuthenticationUtils.getTokenRequestHeaders()),
method: "POST"
});
} catch (error: any) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT3-NE02",
"The request to revoke access token failed.",
error ?? "The request sent to revoke the access token failed."
);
}
if (response.status !== 200 || !response.ok) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RAT3-HE03",
`Invalid response status received for revoke access token request (${response.statusText}).`,
await response.json()
);
}
this._authenticationHelper.clearUserSessionData(userID);
return Promise.resolve(response);
}
public async requestCustomGrant(
customGrantParams: CustomGrantConfig,
userID?: string
): Promise<TokenResponse | FetchResponse> {
const oidcProviderMetadata: OIDCProviderMetaData = await this._oidcProviderMetaData();
const configData: StrictAuthClientConfig = await this._config();
let tokenEndpoint: string | undefined;
if (customGrantParams.tokenEndpoint && customGrantParams.tokenEndpoint.trim().length !== 0) {
tokenEndpoint = customGrantParams.tokenEndpoint;
} else {
tokenEndpoint = oidcProviderMetadata.token_endpoint;
}
if (!tokenEndpoint || tokenEndpoint.trim().length === 0) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RCG-NF01",
"Token endpoint not found.",
"No token endpoint was found in the OIDC provider meta data returned by the well-known endpoint " +
"or the token endpoint passed to the SDK is empty."
);
}
const data: string[] = await Promise.all(
Object.entries(customGrantParams.data).map(async ([ key, value ]: [ key: string, value: any ]) => {
const newValue: string = await this._authenticationHelper.replaceCustomGrantTemplateTags(
value as string,
userID
);
return `${ key }=${ newValue }`;
})
);
let requestHeaders: Record<string, any> = {
...AuthenticationUtils.getTokenRequestHeaders()
};
if (customGrantParams.attachToken) {
requestHeaders = {
...requestHeaders,
Authorization: `Bearer ${ (await this._dataLayer.getSessionData(userID)).access_token }`
};
}
const requestConfig: FetchRequestConfig = {
body: data.join("&"),
credentials: configData.sendCookiesInRequests
? FetchCredentialTypes.Include
: FetchCredentialTypes.SameOrigin,
headers: new Headers(requestHeaders),
method: "POST"
};
let response: Response;
try {
response = await fetch(tokenEndpoint, requestConfig);
} catch (error: any) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RCG-NE02",
"The custom grant request failed.",
error ?? "The request sent to get the custom grant failed."
);
}
if (response.status !== 200 || !response.ok) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-RCG-HE03",
`Invalid response status received for the custom grant request. (${response.statusText})`,
await response.json()
);
}
if (customGrantParams.returnsSession) {
return this._authenticationHelper.handleTokenResponse(response, userID);
} else {
return Promise.resolve(await response.json());
}
}
public async getBasicUserInfo(userID?: string): Promise<BasicUserInfo> {
const sessionData: SessionData = await this._dataLayer.getSessionData(userID);
const authenticatedUser: AuthenticatedUserInfo = this._authenticationHelper
.getAuthenticatedUserInfo(sessionData?.id_token);
let basicUserInfo: BasicUserInfo = {
allowedScopes: sessionData.scope,
sessionState: sessionData.session_state
};
Object.keys(authenticatedUser).forEach((key: string) => {
if (
authenticatedUser[ key ] === undefined ||
authenticatedUser[ key ] === "" ||
authenticatedUser[ key ] === null
) {
delete authenticatedUser[ key ];
}
});
basicUserInfo = { ...basicUserInfo, ...authenticatedUser };
return basicUserInfo;
}
public async getDecodedIDToken(userID?: string): Promise<DecodedIDTokenPayload> {
const idToken: string = (await this._dataLayer.getSessionData(userID)).id_token;
const payload: DecodedIDTokenPayload = this._cryptoHelper.decodeIDToken(idToken);
return payload;
}
public async getCryptoHelper(): Promise<CryptoHelper> {
return this._cryptoHelper;
}
public async getIDToken(userID?: string): Promise<string> {
return (await this._dataLayer.getSessionData(userID)).id_token;
}
public async getOIDCProviderMetaData(forceInit: boolean): Promise<void> {
const configData: StrictAuthClientConfig = await this._config();
if (!forceInit && (await this._dataLayer.getTemporaryDataParameter(OP_CONFIG_INITIATED))) {
return Promise.resolve();
}
const wellKnownEndpoint: string = (configData as any).wellKnownEndpoint;
if (wellKnownEndpoint) {
let response: Response;
try {
response = await fetch(wellKnownEndpoint);
if (response.status !== 200 || !response.ok) {
throw new Error();
}
} catch {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-GOPMD-HE01",
"Invalid well-known response",
"The well known endpoint response has been failed with an error."
);
}
await this._dataLayer.setOIDCProviderMetaData(
await this._authenticationHelper.resolveEndpoints(await response.json())
);
await this._dataLayer.setTemporaryDataParameter(OP_CONFIG_INITIATED, true);
return Promise.resolve();
} else if ((configData as any).baseUrl) {
try {
await this._dataLayer.setOIDCProviderMetaData(
await this._authenticationHelper.resolveEndpointsByBaseURL());
} catch (error: any) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-GOPMD-IV02",
"Resolving endpoints failed.",
error ?? "Resolving endpoints by base url failed."
);
}
await this._dataLayer.setTemporaryDataParameter(OP_CONFIG_INITIATED, true);
return Promise.resolve();
} else {
await this._dataLayer.setOIDCProviderMetaData(
await this._authenticationHelper.resolveEndpointsExplicitly());
await this._dataLayer.setTemporaryDataParameter(OP_CONFIG_INITIATED, true);
return Promise.resolve();
}
}
public async getOIDCServiceEndpoints(): Promise<OIDCEndpoints> {
const oidcProviderMetaData: OIDCProviderMetaData = await this._oidcProviderMetaData();
return {
authorizationEndpoint: oidcProviderMetaData.authorization_endpoint ?? "",
checkSessionIframe: oidcProviderMetaData.check_session_iframe ?? "",
endSessionEndpoint: oidcProviderMetaData.end_session_endpoint ?? "",
introspectionEndpoint: oidcProviderMetaData.introspection_endpoint ?? "",
issuer: oidcProviderMetaData.issuer ?? "",
jwksUri: oidcProviderMetaData.jwks_uri ?? "",
registrationEndpoint: oidcProviderMetaData.registration_endpoint ?? "",
revocationEndpoint: oidcProviderMetaData.revocation_endpoint ?? "",
tokenEndpoint: oidcProviderMetaData.token_endpoint ?? "",
userinfoEndpoint: oidcProviderMetaData.userinfo_endpoint ?? ""
};
}
public async getSignOutURL(userID?: string): Promise<string> {
const logoutEndpoint: string | undefined = (await this._oidcProviderMetaData())?.end_session_endpoint;
const configData: StrictAuthClientConfig = await this._config();
if (!logoutEndpoint || logoutEndpoint.trim().length === 0) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-GSOU-NF01",
"Sign-out endpoint not found.",
"No sign-out endpoint was found in the OIDC provider meta data returned by the well-known endpoint " +
"or the sign-out endpoint passed to the SDK is empty."
);
}
const callbackURL: string = configData?.signOutRedirectURL ?? configData?.signInRedirectURL;
if (!callbackURL || callbackURL.trim().length === 0) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-GSOU-NF03",
"No sign-out redirect URL found.",
"The sign-out redirect URL cannot be found or the URL passed to the SDK is empty. " +
"No sign-in redirect URL has been found either. "
);
}
const queryParams: URLSearchParams = new URLSearchParams();
queryParams.set("post_logout_redirect_uri", callbackURL);
if (configData.sendIdTokenInLogoutRequest) {
const idToken: string = (await this._dataLayer.getSessionData(userID))?.id_token;
if (!idToken || idToken.trim().length === 0) {
throw new AsgardeoAuthException(
"JS-AUTH_CORE-GSOU-NF02",
"ID token not found.",
"No ID token could be found. Either the session information is lost or you have not signed in."
);
}
queryParams.set("id_token_hint", idToken);
} else {
queryParams.set("client_id", configData.clientID);
}
queryParams.set("state", SIGN_OUT_SUCCESS_PARAM);
return `${logoutEndpoint}?${queryParams.toString()}`;
}
public async clearUserSessionData(userID?: string): Promise<void> {
await this._authenticationHelper.clearUserSessionData(userID);
}
public async getAccessToken(userID?: string): Promise<string> {
return (await this._dataLayer.getSessionData(userID))?.access_token;
}
/**
* The created timestamp of the token response in milliseconds.
*
* @param userID - User ID
* @returns Created at timestamp of the token response in milliseconds.
*/
public async getCreatedAt(userID?: string): Promise<number> {
return (await this._dataLayer.getSessionData(userID))?.created_at;
}
/**
* The expires timestamp of the token response in seconds.
*
* @param userID - User ID
* @returns Expires in timestamp of the token response in seconds.
*/
public async getExpiresIn(userID?: string): Promise<string> {
return (await this._dataLayer.getSessionData(userID))?.expires_in;
}
public async isAuthenticated(userID?: string): Promise<boolean> {
const isAccessTokenAvailable: boolean = Boolean(await this.getAccessToken(userID));
// Check if the access token is expired.
const createdAt: number = await this.getCreatedAt(userID);
// Get the expires in value.
const expiresInString: string = await this.getExpiresIn(userID);
// If the expires in value is not available, the token is invalid and the user is not authenticated.
if (!expiresInString) {
return false;
}
// Convert to milliseconds.
const expiresIn: number = parseInt(expiresInString) * 1000;
const currentTime: number = new Date().getTime();
const isAccessTokenValid: boolean = (createdAt + expiresIn) > currentTime;
const isAuthenticated: boolean = isAccessTokenAvailable && isAccessTokenValid;
return isAuthenticated;
}
public async getPKCECode(state: string, userID?: string): Promise<string> {
return (await this._dataLayer.getTemporaryDataParameter(
AuthenticationUtils.extractPKCEKeyFromStateParam(state),
userID
)) as string;
}
public async setPKCECode(pkce: string, state: string, userID?: string): Promise<void> {
return await this._dataLayer.setTemporaryDataParameter(
AuthenticationUtils.extractPKCEKeyFromStateParam(state),
pkce,
userID
);
}
public async updateConfig(config: Partial<AuthClientConfig<T>>): Promise<void> {
await this._dataLayer.setConfigData(config);
await this.getOIDCProviderMetaData(true);
}
}