@brionmario-experimental/asgardeo-auth-spa
Version:
Asgardeo Auth SPA SDK to be used in Single-Page Applications.
478 lines (408 loc) • 17.1 kB
text/typescript
/**
* Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. 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_CODE,
AsgardeoAuthClient,
AuthClientConfig,
AuthenticationUtils,
BasicUserInfo,
CryptoHelper,
DataLayer,
DecodedIDTokenPayload,
FetchResponse,
GetAuthURLConfig,
OIDCEndpoints,
ResponseMode,
SESSION_STATE,
STATE,
SessionData,
Store
} from "@asgardeo/auth-js";
import {
SILENT_SIGN_IN_STATE,
Storage,
TOKEN_REQUEST_CONFIG_KEY
} from "../constants";
import { AuthenticationHelper, SPAHelper, SessionManagementHelper } from "../helpers";
import { HttpClient, HttpClientInstance } from "../http-client";
import {
HttpError,
HttpRequestConfig,
HttpResponse,
MainThreadClientConfig,
MainThreadClientInterface
} from "../models";
import { SPACustomGrantConfig } from "../models/request-custom-grant";
import { LocalStore, MemoryStore, SessionStore } from "../stores";
import { SPAUtils } from "../utils";
import { SPACryptoUtils } from "../utils/crypto-utils";
const initiateStore = (store: Storage | undefined): Store => {
switch (store) {
case Storage.LocalStorage:
return new LocalStore();
case Storage.SessionStorage:
return new SessionStore();
case Storage.BrowserMemory:
return new MemoryStore();
default:
return new SessionStore();
}
};
export const MainThreadClient = async (
instanceID: number,
config: AuthClientConfig<MainThreadClientConfig>,
getAuthHelper: (
authClient: AsgardeoAuthClient<MainThreadClientConfig>,
spaHelper: SPAHelper<MainThreadClientConfig>
) => AuthenticationHelper<MainThreadClientConfig>
): Promise<MainThreadClientInterface> => {
const _store: Store = initiateStore(config.storage);
const _cryptoUtils: SPACryptoUtils = new SPACryptoUtils();
const _authenticationClient = new AsgardeoAuthClient<MainThreadClientConfig>();
await _authenticationClient.initialize(config, _store, _cryptoUtils, instanceID);
const _spaHelper = new SPAHelper<MainThreadClientConfig>(_authenticationClient);
const _dataLayer = _authenticationClient.getDataLayer();
const _sessionManagementHelper = await SessionManagementHelper(
async () => {
return _authenticationClient.getSignOutURL();
},
config.storage ?? Storage.SessionStorage,
(sessionState: string) => _dataLayer.setSessionDataParameter(SESSION_STATE as keyof SessionData,
sessionState ?? "")
);
const _authenticationHelper = getAuthHelper(_authenticationClient, _spaHelper);
let _getSignOutURLFromSessionStorage: boolean = false;
const _httpClient: HttpClientInstance = HttpClient.getInstance();
let _isHttpHandlerEnabled: boolean = true;
let _httpErrorCallback: (error: HttpError) => void | Promise<void>;
let _httpFinishCallback: () => void;
const attachToken = async (request: HttpRequestConfig): Promise<void> => {
await _authenticationHelper.attachTokenToRequestConfig(request);
}
_httpClient?.init && (await _httpClient.init(true, attachToken));
const setHttpRequestStartCallback = (callback: () => void): void => {
_httpClient?.setHttpRequestStartCallback && _httpClient.setHttpRequestStartCallback(callback);
};
const setHttpRequestSuccessCallback = (callback: (response: HttpResponse) => void): void => {
_httpClient?.setHttpRequestSuccessCallback && _httpClient.setHttpRequestSuccessCallback(callback);
};
const setHttpRequestFinishCallback = (callback: () => void): void => {
_httpClient?.setHttpRequestFinishCallback && _httpClient.setHttpRequestFinishCallback(callback);
};
const setHttpRequestErrorCallback = (callback: (error: HttpError) => void | Promise<void>): void => {
_httpErrorCallback = callback;
};
const httpRequest = async (requestConfig: HttpRequestConfig): Promise<HttpResponse> => {
return await _authenticationHelper.httpRequest(
_httpClient,
requestConfig,
_isHttpHandlerEnabled,
_httpErrorCallback,
_httpFinishCallback
);
};
const httpRequestAll = async (requestConfigs: HttpRequestConfig[]): Promise<HttpResponse[] | undefined> => {
return await _authenticationHelper.httpRequestAll(
requestConfigs,
_httpClient,
_isHttpHandlerEnabled,
_httpErrorCallback,
_httpFinishCallback
);
};
const getHttpClient = (): HttpClientInstance => {
return _httpClient;
};
const enableHttpHandler = (): boolean => {
_authenticationHelper.enableHttpHandler(_httpClient);
_isHttpHandlerEnabled = true;
return true;
};
const disableHttpHandler = (): boolean => {
_authenticationHelper.disableHttpHandler(_httpClient);
_isHttpHandlerEnabled = false;
return true;
};
const checkSession = async (): Promise<void> => {
const oidcEndpoints: OIDCEndpoints = await _authenticationClient.getOIDCServiceEndpoints();
const config = await _dataLayer.getConfigData();
_authenticationHelper.initializeSessionManger(
config,
oidcEndpoints,
async () => (await _authenticationClient.getBasicUserInfo()).sessionState,
async (params?: GetAuthURLConfig): Promise<string> => _authenticationClient.getAuthorizationURL(params),
_sessionManagementHelper
);
};
const shouldStopAuthn = async (): Promise<boolean> => {
return await _sessionManagementHelper.receivePromptNoneResponse(
async (sessionState: string | null) => {
await _dataLayer.setSessionDataParameter(SESSION_STATE as keyof SessionData, sessionState ?? "");
return;
}
);
}
const setSessionStatus = async (sessionStatus: string): Promise<void> => {
await _dataLayer.setSessionStatus(sessionStatus);
}
const signIn = async (
signInConfig?: GetAuthURLConfig,
authorizationCode?: string,
sessionState?: string,
state?: string,
tokenRequestConfig?: {
params: Record<string, unknown>
}
): Promise<BasicUserInfo> => {
const basicUserInfo = await _authenticationHelper.handleSignIn(
shouldStopAuthn,
checkSession,
undefined
);
if(basicUserInfo) {
return basicUserInfo;
} else {
let resolvedAuthorizationCode: string;
let resolvedSessionState: string;
let resolvedState: string;
let resolvedTokenRequestConfig: {
params: Record<string, unknown>
} = { params: {} };
if (config?.responseMode === ResponseMode.formPost && authorizationCode) {
resolvedAuthorizationCode = authorizationCode;
resolvedSessionState = sessionState ?? "";
resolvedState = state ?? "";
} else {
resolvedAuthorizationCode = new URL(window.location.href).searchParams.get(AUTHORIZATION_CODE) ?? "";
resolvedSessionState = new URL(window.location.href).searchParams.get(SESSION_STATE) ?? "";
resolvedState = new URL(window.location.href).searchParams.get(STATE) ?? "";
SPAUtils.removeAuthorizationCode();
}
if (resolvedAuthorizationCode && resolvedState) {
setSessionStatus("true");
const storedTokenRequestConfig = await _dataLayer.getTemporaryDataParameter(TOKEN_REQUEST_CONFIG_KEY);
if (storedTokenRequestConfig && typeof storedTokenRequestConfig === "string") {
resolvedTokenRequestConfig = JSON.parse(storedTokenRequestConfig);
}
return requestAccessToken(resolvedAuthorizationCode, resolvedSessionState,
resolvedState, resolvedTokenRequestConfig);
}
return _authenticationClient.getAuthorizationURL(signInConfig).then(async (url: string) => {
if (config.storage === Storage.BrowserMemory && config.enablePKCE) {
const pkceKey: string = AuthenticationUtils.extractPKCEKeyFromStateParam(resolvedState);
SPAUtils.setPKCE(pkceKey, (await _authenticationClient.getPKCECode(resolvedState)) as string);
}
if (tokenRequestConfig) {
_dataLayer.setTemporaryDataParameter(TOKEN_REQUEST_CONFIG_KEY, JSON.stringify(tokenRequestConfig));
}
location.href = url;
await SPAUtils.waitTillPageRedirect();
return Promise.resolve({
allowedScopes: "",
displayName: "",
email: "",
sessionState: "",
sub: "",
tenantDomain: "",
username: ""
});
});
}
};
const signOut = async (): Promise<boolean> => {
if ((await _authenticationClient.isAuthenticated()) && !_getSignOutURLFromSessionStorage) {
location.href = await _authenticationClient.getSignOutURL();
} else {
location.href = SPAUtils.getSignOutURL(config.clientID, instanceID);
}
_spaHelper.clearRefreshTokenTimeout();
await _dataLayer.removeOIDCProviderMetaData();
await _dataLayer.removeTemporaryData();
await _dataLayer.removeSessionData();
await _dataLayer.removeSessionStatus();
await SPAUtils.waitTillPageRedirect();
return true;
};
const enableRetrievingSignOutURLFromSession = (config: SPACustomGrantConfig) => {
if (config.preventSignOutURLUpdate) {
_getSignOutURLFromSessionStorage = true;
}
}
const requestCustomGrant = async (config: SPACustomGrantConfig):
Promise<BasicUserInfo | FetchResponse> => {
return await _authenticationHelper.requestCustomGrant(
config,
enableRetrievingSignOutURLFromSession
);
};
const refreshAccessToken = async (): Promise<BasicUserInfo> => {
try {
return await _authenticationHelper.refreshAccessToken(
enableRetrievingSignOutURLFromSession
);
} catch (error) {
return Promise.reject(error);
}
};
const revokeAccessToken = async (): Promise<boolean> => {
const timer: number = await _spaHelper.getRefreshTimeoutTimer();
return _authenticationClient
.revokeAccessToken()
.then(() => {
_sessionManagementHelper.reset();
_spaHelper.clearRefreshTokenTimeout(timer);
return Promise.resolve(true);
})
.catch((error) => Promise.reject(error));
};
const requestAccessToken = async (
resolvedAuthorizationCode: string,
resolvedSessionState: string,
resolvedState: string,
tokenRequestConfig?: {
params: Record<string, unknown>
}
): Promise<BasicUserInfo> => {
return await _authenticationHelper.requestAccessToken(
resolvedAuthorizationCode,
resolvedSessionState,
checkSession,
undefined,
resolvedState,
tokenRequestConfig
);
};
const constructSilentSignInUrl = async (
additionalParams: Record<string, string | boolean> = {}
): Promise<string> => {
const config = await _dataLayer.getConfigData();
const urlString: string = await _authenticationClient.getAuthorizationURL({
prompt: "none",
state: SILENT_SIGN_IN_STATE,
...additionalParams
});
// Replace form_post with query
const urlObject = new URL(urlString);
urlObject.searchParams.set("response_mode", "query");
const url: string = urlObject.toString();
if (config.storage === Storage.BrowserMemory && config.enablePKCE) {
const state = urlObject.searchParams.get(STATE);
SPAUtils.setPKCE(
AuthenticationUtils.extractPKCEKeyFromStateParam(state ?? ""),
(await _authenticationClient.getPKCECode(state ?? "")) as string
);
}
return url;
}
/**
* This method checks if there is an active user session in the server by sending a prompt none request.
* If the user is signed in, this method sends a token request. Returns false otherwise.
*
* @return {Promise<BasicUserInfo|boolean} Returns a Promise that resolves with the BasicUserInfo
* if the user is signed in or with `false` if there is no active user session in the server.
*/
const trySignInSilently = async (
additionalParams?: Record<string, string | boolean>,
tokenRequestConfig?: { params: Record<string, unknown> }
): Promise<BasicUserInfo | boolean> => {
return await _authenticationHelper.trySignInSilently(
constructSilentSignInUrl,
requestAccessToken,
_sessionManagementHelper,
additionalParams,
tokenRequestConfig
);
};
const getBasicUserInfo = async (): Promise<BasicUserInfo> => {
return _authenticationHelper.getBasicUserInfo();
};
const getDecodedIDToken = async (): Promise<DecodedIDTokenPayload> => {
return _authenticationHelper.getDecodedIDToken();
};
const getCryptoHelper = async (): Promise<CryptoHelper> => {
return _authenticationHelper.getCryptoHelper();
};
const getIDToken = async (): Promise<string> => {
return _authenticationHelper.getIDToken();
};
const getOIDCServiceEndpoints = async (): Promise<OIDCEndpoints> => {
return _authenticationHelper.getOIDCServiceEndpoints();
};
const getAccessToken = async (): Promise<string> => {
return _authenticationHelper.getAccessToken();
};
const getDataLayer = async (): Promise<DataLayer<MainThreadClientConfig>> => {
return _authenticationHelper.getDataLayer();
};
const getConfigData = async (): Promise<AuthClientConfig<MainThreadClientConfig>> => {
return await _dataLayer.getConfigData();
};
const isAuthenticated = async (): Promise<boolean> => {
return _authenticationHelper.isAuthenticated();
};
const isSessionActive = async (): Promise<boolean> => {
return await _dataLayer.getSessionStatus() === "true";
};
const updateConfig = async (newConfig: Partial<AuthClientConfig<MainThreadClientConfig>>): Promise<void> => {
const existingConfig = await _dataLayer.getConfigData();
const isCheckSessionIframeDifferent: boolean = !(
existingConfig &&
existingConfig.endpoints &&
existingConfig.endpoints.checkSessionIframe &&
newConfig &&
newConfig.endpoints &&
newConfig.endpoints.checkSessionIframe &&
existingConfig.endpoints.checkSessionIframe === newConfig.endpoints.checkSessionIframe
);
const config = { ...existingConfig, ...newConfig };
await _authenticationClient.updateConfig(config);
// Re-initiates check session if the check session endpoint is updated.
if (config.enableOIDCSessionManagement && isCheckSessionIframeDifferent) {
_sessionManagementHelper.reset();
checkSession();
}
};
return {
disableHttpHandler,
enableHttpHandler,
getAccessToken,
getBasicUserInfo,
getConfigData,
getCryptoHelper,
getDataLayer,
getDecodedIDToken,
getHttpClient,
getIDToken,
getOIDCServiceEndpoints,
httpRequest,
httpRequestAll,
isAuthenticated,
isSessionActive,
refreshAccessToken,
requestCustomGrant,
revokeAccessToken,
setHttpRequestErrorCallback,
setHttpRequestFinishCallback,
setHttpRequestStartCallback,
setHttpRequestSuccessCallback,
signIn,
signOut,
trySignInSilently,
updateConfig
};
};