Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientAssertionCallback,
ClientAssertionConfig,
} from "../account/ClientCredentials.js";
export async function getClientAssertion(
clientAssertion: string | ClientAssertionCallback,
clientId: string,
tokenEndpoint?: string
): Promise<string> {
if (typeof clientAssertion === "string") {
return clientAssertion;
} else {
const config: ClientAssertionConfig = {
clientId: clientId,
tokenEndpoint: tokenEndpoint,
};
return clientAssertion(config);
}
}
+382
View File
@@ -0,0 +1,382 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const Constants = {
LIBRARY_NAME: "MSAL.JS",
SKU: "msal.js.common",
// Prefix for all library cache entries
CACHE_PREFIX: "msal",
// default authority
DEFAULT_AUTHORITY: "https://login.microsoftonline.com/common/",
DEFAULT_AUTHORITY_HOST: "login.microsoftonline.com",
DEFAULT_COMMON_TENANT: "common",
// ADFS String
ADFS: "adfs",
DSTS: "dstsv2",
// Default AAD Instance Discovery Endpoint
AAD_INSTANCE_DISCOVERY_ENDPT:
"https://login.microsoftonline.com/common/discovery/instance?api-version=1.1&authorization_endpoint=",
// CIAM URL
CIAM_AUTH_URL: ".ciamlogin.com",
AAD_TENANT_DOMAIN_SUFFIX: ".onmicrosoft.com",
// Resource delimiter - used for certain cache entries
RESOURCE_DELIM: "|",
// Placeholder for non-existent account ids/objects
NO_ACCOUNT: "NO_ACCOUNT",
// Claims
CLAIMS: "claims",
// Consumer UTID
CONSUMER_UTID: "9188040d-6c67-4c5b-b112-36a304b66dad",
// Default scopes
OPENID_SCOPE: "openid",
PROFILE_SCOPE: "profile",
OFFLINE_ACCESS_SCOPE: "offline_access",
EMAIL_SCOPE: "email",
// Default response type for authorization code flow
CODE_RESPONSE_TYPE: "code",
CODE_GRANT_TYPE: "authorization_code",
RT_GRANT_TYPE: "refresh_token",
FRAGMENT_RESPONSE_MODE: "fragment",
S256_CODE_CHALLENGE_METHOD: "S256",
URL_FORM_CONTENT_TYPE: "application/x-www-form-urlencoded;charset=utf-8",
AUTHORIZATION_PENDING: "authorization_pending",
NOT_DEFINED: "not_defined",
EMPTY_STRING: "",
NOT_APPLICABLE: "N/A",
NOT_AVAILABLE: "Not Available",
FORWARD_SLASH: "/",
IMDS_ENDPOINT: "http://169.254.169.254/metadata/instance/compute/location",
IMDS_VERSION: "2020-06-01",
IMDS_TIMEOUT: 2000,
AZURE_REGION_AUTO_DISCOVER_FLAG: "TryAutoDetect",
REGIONAL_AUTH_PUBLIC_CLOUD_SUFFIX: "login.microsoft.com",
KNOWN_PUBLIC_CLOUDS: [
"login.microsoftonline.com",
"login.windows.net",
"login.microsoft.com",
"sts.windows.net",
],
TOKEN_RESPONSE_TYPE: "token",
ID_TOKEN_RESPONSE_TYPE: "id_token",
SHR_NONCE_VALIDITY: 240,
INVALID_INSTANCE: "invalid_instance",
};
export const HttpStatus = {
SUCCESS: 200,
SUCCESS_RANGE_START: 200,
SUCCESS_RANGE_END: 299,
REDIRECT: 302,
CLIENT_ERROR: 400,
CLIENT_ERROR_RANGE_START: 400,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
NOT_FOUND: 404,
REQUEST_TIMEOUT: 408,
TOO_MANY_REQUESTS: 429,
CLIENT_ERROR_RANGE_END: 499,
SERVER_ERROR: 500,
SERVER_ERROR_RANGE_START: 500,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
SERVER_ERROR_RANGE_END: 599,
MULTI_SIDED_ERROR: 600,
} as const;
export type HttpStatus = (typeof HttpStatus)[keyof typeof HttpStatus];
export const OIDC_DEFAULT_SCOPES = [
Constants.OPENID_SCOPE,
Constants.PROFILE_SCOPE,
Constants.OFFLINE_ACCESS_SCOPE,
];
export const OIDC_SCOPES = [...OIDC_DEFAULT_SCOPES, Constants.EMAIL_SCOPE];
/**
* Request header names
*/
export const HeaderNames = {
CONTENT_TYPE: "Content-Type",
CONTENT_LENGTH: "Content-Length",
RETRY_AFTER: "Retry-After",
CCS_HEADER: "X-AnchorMailbox",
WWWAuthenticate: "WWW-Authenticate",
AuthenticationInfo: "Authentication-Info",
X_MS_REQUEST_ID: "x-ms-request-id",
X_MS_HTTP_VERSION: "x-ms-httpver",
} as const;
export type HeaderNames = (typeof HeaderNames)[keyof typeof HeaderNames];
/**
* Persistent cache keys MSAL which stay while user is logged in.
*/
export const PersistentCacheKeys = {
ACTIVE_ACCOUNT_FILTERS: "active-account-filters", // new cache entry for active_account for a more robust version for browser
} as const;
export type PersistentCacheKeys =
(typeof PersistentCacheKeys)[keyof typeof PersistentCacheKeys];
/**
* String constants related to AAD Authority
*/
export const AADAuthorityConstants = {
COMMON: "common",
ORGANIZATIONS: "organizations",
CONSUMERS: "consumers",
} as const;
export type AADAuthorityConstants =
(typeof AADAuthorityConstants)[keyof typeof AADAuthorityConstants];
/**
* Claims request keys
*/
export const ClaimsRequestKeys = {
ACCESS_TOKEN: "access_token",
XMS_CC: "xms_cc",
} as const;
export type ClaimsRequestKeys =
(typeof ClaimsRequestKeys)[keyof typeof ClaimsRequestKeys];
/**
* we considered making this "enum" in the request instead of string, however it looks like the allowed list of
* prompt values kept changing over past couple of years. There are some undocumented prompt values for some
* internal partners too, hence the choice of generic "string" type instead of the "enum"
*/
export const PromptValue = {
LOGIN: "login",
SELECT_ACCOUNT: "select_account",
CONSENT: "consent",
NONE: "none",
CREATE: "create",
NO_SESSION: "no_session",
};
/**
* allowed values for codeVerifier
*/
export const CodeChallengeMethodValues = {
PLAIN: "plain",
S256: "S256",
};
/**
* allowed values for server response type
*/
export const ServerResponseType = {
QUERY: "query",
FRAGMENT: "fragment",
} as const;
export type ServerResponseType =
(typeof ServerResponseType)[keyof typeof ServerResponseType];
/**
* allowed values for response_mode
*/
export const ResponseMode = {
...ServerResponseType,
FORM_POST: "form_post",
} as const;
export type ResponseMode = (typeof ResponseMode)[keyof typeof ResponseMode];
/**
* allowed grant_type
*/
export const GrantType = {
IMPLICIT_GRANT: "implicit",
AUTHORIZATION_CODE_GRANT: "authorization_code",
CLIENT_CREDENTIALS_GRANT: "client_credentials",
RESOURCE_OWNER_PASSWORD_GRANT: "password",
REFRESH_TOKEN_GRANT: "refresh_token",
DEVICE_CODE_GRANT: "device_code",
JWT_BEARER: "urn:ietf:params:oauth:grant-type:jwt-bearer",
} as const;
export type GrantType = (typeof GrantType)[keyof typeof GrantType];
/**
* Account types in Cache
*/
export const CacheAccountType = {
MSSTS_ACCOUNT_TYPE: "MSSTS",
ADFS_ACCOUNT_TYPE: "ADFS",
MSAV1_ACCOUNT_TYPE: "MSA",
GENERIC_ACCOUNT_TYPE: "Generic", // NTLM, Kerberos, FBA, Basic etc
} as const;
export type CacheAccountType =
(typeof CacheAccountType)[keyof typeof CacheAccountType];
/**
* Separators used in cache
*/
export const Separators = {
CACHE_KEY_SEPARATOR: "-",
CLIENT_INFO_SEPARATOR: ".",
} as const;
export type Separators = (typeof Separators)[keyof typeof Separators];
/**
* Credential Type stored in the cache
*/
export const CredentialType = {
ID_TOKEN: "IdToken",
ACCESS_TOKEN: "AccessToken",
ACCESS_TOKEN_WITH_AUTH_SCHEME: "AccessToken_With_AuthScheme",
REFRESH_TOKEN: "RefreshToken",
} as const;
export type CredentialType =
(typeof CredentialType)[keyof typeof CredentialType];
/**
* Combine all cache types
*/
export const CacheType = {
ADFS: 1001,
MSA: 1002,
MSSTS: 1003,
GENERIC: 1004,
ACCESS_TOKEN: 2001,
REFRESH_TOKEN: 2002,
ID_TOKEN: 2003,
APP_METADATA: 3001,
UNDEFINED: 9999,
} as const;
export type CacheType = (typeof CacheType)[keyof typeof CacheType];
/**
* More Cache related constants
*/
export const APP_METADATA = "appmetadata";
export const CLIENT_INFO = "client_info";
export const THE_FAMILY_ID = "1";
export const AUTHORITY_METADATA_CONSTANTS = {
CACHE_KEY: "authority-metadata",
REFRESH_TIME_SECONDS: 3600 * 24, // 24 Hours
};
export const AuthorityMetadataSource = {
CONFIG: "config",
CACHE: "cache",
NETWORK: "network",
HARDCODED_VALUES: "hardcoded_values",
} as const;
export type AuthorityMetadataSource =
(typeof AuthorityMetadataSource)[keyof typeof AuthorityMetadataSource];
export const SERVER_TELEM_CONSTANTS = {
SCHEMA_VERSION: 5,
MAX_CUR_HEADER_BYTES: 80, // ESTS limit is 100B, set to 80 to provide a 20B buffer
MAX_LAST_HEADER_BYTES: 330, // ESTS limit is 350B, set to 330 to provide a 20B buffer,
MAX_CACHED_ERRORS: 50, // Limit the number of errors that can be stored to prevent uncontrolled size gains
CACHE_KEY: "server-telemetry",
CATEGORY_SEPARATOR: "|",
VALUE_SEPARATOR: ",",
OVERFLOW_TRUE: "1",
OVERFLOW_FALSE: "0",
UNKNOWN_ERROR: "unknown_error",
};
/**
* Type of the authentication request
*/
export const AuthenticationScheme = {
BEARER: "Bearer",
POP: "pop",
SSH: "ssh-cert",
} as const;
export type AuthenticationScheme =
(typeof AuthenticationScheme)[keyof typeof AuthenticationScheme];
/**
* Constants related to throttling
*/
export const ThrottlingConstants = {
// Default time to throttle RequestThumbprint in seconds
DEFAULT_THROTTLE_TIME_SECONDS: 60,
// Default maximum time to throttle in seconds, overrides what the server sends back
DEFAULT_MAX_THROTTLE_TIME_SECONDS: 3600,
// Prefix for storing throttling entries
THROTTLING_PREFIX: "throttling",
// Value assigned to the x-ms-lib-capability header to indicate to the server the library supports throttling
X_MS_LIB_CAPABILITY_VALUE: "retry-after, h429",
};
export const Errors = {
INVALID_GRANT_ERROR: "invalid_grant",
CLIENT_MISMATCH_ERROR: "client_mismatch",
};
/**
* Password grant parameters
*/
export const PasswordGrantConstants = {
username: "username",
password: "password",
} as const;
export type PasswordGrantConstants =
(typeof PasswordGrantConstants)[keyof typeof PasswordGrantConstants];
/**
* Response codes
*/
export const ResponseCodes = {
httpSuccess: 200,
httpBadRequest: 400,
} as const;
export type ResponseCodes = (typeof ResponseCodes)[keyof typeof ResponseCodes];
/**
* Region Discovery Sources
*/
export const RegionDiscoverySources = {
FAILED_AUTO_DETECTION: "1",
INTERNAL_CACHE: "2",
ENVIRONMENT_VARIABLE: "3",
IMDS: "4",
} as const;
export type RegionDiscoverySources =
(typeof RegionDiscoverySources)[keyof typeof RegionDiscoverySources];
/**
* Region Discovery Outcomes
*/
export const RegionDiscoveryOutcomes = {
CONFIGURED_MATCHES_DETECTED: "1",
CONFIGURED_NO_AUTO_DETECTION: "2",
CONFIGURED_NOT_DETECTED: "3",
AUTO_DETECTION_REQUESTED_SUCCESSFUL: "4",
AUTO_DETECTION_REQUESTED_FAILED: "5",
} as const;
export type RegionDiscoveryOutcomes =
(typeof RegionDiscoveryOutcomes)[keyof typeof RegionDiscoveryOutcomes];
/**
* Specifies the reason for fetching the access token from the identity provider
*/
export const CacheOutcome = {
// When a token is found in the cache or the cache is not supposed to be hit when making the request
NOT_APPLICABLE: "0",
// When the token request goes to the identity provider because force_refresh was set to true. Also occurs if claims were requested
FORCE_REFRESH_OR_CLAIMS: "1",
// When the token request goes to the identity provider because no cached access token exists
NO_CACHED_ACCESS_TOKEN: "2",
// When the token request goes to the identity provider because cached access token expired
CACHED_ACCESS_TOKEN_EXPIRED: "3",
// When the token request goes to the identity provider because refresh_in was used and the existing token needs to be refreshed
PROACTIVELY_REFRESHED: "4",
} as const;
export type CacheOutcome = (typeof CacheOutcome)[keyof typeof CacheOutcome];
export const JsonWebTokenTypes = {
Jwt: "JWT",
Jwk: "JWK",
Pop: "pop",
} as const;
export type JsonWebTokenTypes =
(typeof JsonWebTokenTypes)[keyof typeof JsonWebTokenTypes];
export const ONE_DAY_IN_MS = 86400000;
// Token renewal offset default in seconds
export const DEFAULT_TOKEN_RENEWAL_OFFSET_SEC = 300;
+126
View File
@@ -0,0 +1,126 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Logger } from "../logger/Logger.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
/**
* Wraps a function with a performance measurement.
* Usage: invoke(functionToCall, performanceClient, "EventName", "correlationId")(...argsToPassToFunction)
* @param callback
* @param eventName
* @param logger
* @param telemetryClient
* @param correlationId
* @returns
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const invoke = <T extends Array<any>, U>(
callback: (...args: T) => U,
eventName: string,
logger: Logger,
telemetryClient?: IPerformanceClient,
correlationId?: string
) => {
return (...args: T): U => {
logger.trace(`Executing function ${eventName}`);
const inProgressEvent = telemetryClient?.startMeasurement(
eventName,
correlationId
);
if (correlationId) {
// Track number of times this API is called in a single request
const eventCount = eventName + "CallCount";
telemetryClient?.incrementFields(
{ [eventCount]: 1 },
correlationId
);
}
try {
const result = callback(...args);
inProgressEvent?.end({
success: true,
});
logger.trace(`Returning result from ${eventName}`);
return result;
} catch (e) {
logger.trace(`Error occurred in ${eventName}`);
try {
logger.trace(JSON.stringify(e));
} catch (e) {
logger.trace("Unable to print error message.");
}
inProgressEvent?.end(
{
success: false,
},
e
);
throw e;
}
};
};
/**
* Wraps an async function with a performance measurement.
* Usage: invokeAsync(functionToCall, performanceClient, "EventName", "correlationId")(...argsToPassToFunction)
* @param callback
* @param eventName
* @param logger
* @param telemetryClient
* @param correlationId
* @returns
* @internal
*
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const invokeAsync = <T extends Array<any>, U>(
callback: (...args: T) => Promise<U>,
eventName: string,
logger: Logger,
telemetryClient?: IPerformanceClient,
correlationId?: string
) => {
return (...args: T): Promise<U> => {
logger.trace(`Executing function ${eventName}`);
const inProgressEvent = telemetryClient?.startMeasurement(
eventName,
correlationId
);
if (correlationId) {
// Track number of times this API is called in a single request
const eventCount = eventName + "CallCount";
telemetryClient?.incrementFields(
{ [eventCount]: 1 },
correlationId
);
}
telemetryClient?.setPreQueueTime(eventName, correlationId);
return callback(...args)
.then((response) => {
logger.trace(`Returning result from ${eventName}`);
inProgressEvent?.end({
success: true,
});
return response;
})
.catch((e) => {
logger.trace(`Error occurred in ${eventName}`);
try {
logger.trace(JSON.stringify(e));
} catch (e) {
logger.trace("Unable to print error message.");
}
inProgressEvent?.end(
{
success: false,
},
e
);
throw e;
});
};
};
+9
View File
@@ -0,0 +1,9 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Key-Value type to support queryParams, extraQueryParams and claims
*/
export type StringDict = { [key: string]: string };
+120
View File
@@ -0,0 +1,120 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Constants } from "./Constants.js";
import { ICrypto } from "../crypto/ICrypto.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
/**
* Type which defines the object that is stringified, encoded and sent in the state value.
* Contains the following:
* - id - unique identifier for this request
* - ts - timestamp for the time the request was made. Used to ensure that token expiration is not calculated incorrectly.
* - platformState - string value sent from the platform.
*/
export type LibraryStateObject = {
id: string;
meta?: Record<string, string>;
};
/**
* Type which defines the stringified and encoded object sent to the service in the authorize request.
*/
export type RequestStateObject = {
userRequestState: string;
libraryState: LibraryStateObject;
};
/**
* Class which provides helpers for OAuth 2.0 protocol specific values
*/
export class ProtocolUtils {
/**
* Appends user state with random guid, or returns random guid.
* @param userState
* @param randomGuid
*/
static setRequestState(
cryptoObj: ICrypto,
userState?: string,
meta?: Record<string, string>
): string {
const libraryState = ProtocolUtils.generateLibraryState(
cryptoObj,
meta
);
return userState
? `${libraryState}${Constants.RESOURCE_DELIM}${userState}`
: libraryState;
}
/**
* Generates the state value used by the common library.
* @param randomGuid
* @param cryptoObj
*/
static generateLibraryState(
cryptoObj: ICrypto,
meta?: Record<string, string>
): string {
if (!cryptoObj) {
throw createClientAuthError(ClientAuthErrorCodes.noCryptoObject);
}
// Create a state object containing a unique id and the timestamp of the request creation
const stateObj: LibraryStateObject = {
id: cryptoObj.createNewGuid(),
};
if (meta) {
stateObj.meta = meta;
}
const stateString = JSON.stringify(stateObj);
return cryptoObj.base64Encode(stateString);
}
/**
* Parses the state into the RequestStateObject, which contains the LibraryState info and the state passed by the user.
* @param state
* @param cryptoObj
*/
static parseRequestState(
cryptoObj: ICrypto,
state: string
): RequestStateObject {
if (!cryptoObj) {
throw createClientAuthError(ClientAuthErrorCodes.noCryptoObject);
}
if (!state) {
throw createClientAuthError(ClientAuthErrorCodes.invalidState);
}
try {
// Split the state between library state and user passed state and decode them separately
const splitState = state.split(Constants.RESOURCE_DELIM);
const libraryState = splitState[0];
const userState =
splitState.length > 1
? splitState.slice(1).join(Constants.RESOURCE_DELIM)
: Constants.EMPTY_STRING;
const libraryStateString = cryptoObj.base64Decode(libraryState);
const libraryStateObj = JSON.parse(
libraryStateString
) as LibraryStateObject;
return {
userRequestState: userState || Constants.EMPTY_STRING,
libraryState: libraryStateObj,
};
} catch (e) {
throw createClientAuthError(ClientAuthErrorCodes.invalidState);
}
}
}
+106
View File
@@ -0,0 +1,106 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* @hidden
*/
export class StringUtils {
/**
* Check if stringified object is empty
* @param strObj
*/
static isEmptyObj(strObj?: string): boolean {
if (strObj) {
try {
const obj = JSON.parse(strObj);
return Object.keys(obj).length === 0;
} catch (e) {}
}
return true;
}
static startsWith(str: string, search: string): boolean {
return str.indexOf(search) === 0;
}
static endsWith(str: string, search: string): boolean {
return (
str.length >= search.length &&
str.lastIndexOf(search) === str.length - search.length
);
}
/**
* Parses string into an object.
*
* @param query
*/
static queryStringToObject<T>(query: string): T {
const obj: {} = {};
const params = query.split("&");
const decode = (s: string) => decodeURIComponent(s.replace(/\+/g, " "));
params.forEach((pair) => {
if (pair.trim()) {
const [key, value] = pair.split(/=(.+)/g, 2); // Split on the first occurence of the '=' character
if (key && value) {
obj[decode(key)] = decode(value);
}
}
});
return obj as T;
}
/**
* Trims entries in an array.
*
* @param arr
*/
static trimArrayEntries(arr: Array<string>): Array<string> {
return arr.map((entry) => entry.trim());
}
/**
* Removes empty strings from array
* @param arr
*/
static removeEmptyStringsFromArray(arr: Array<string>): Array<string> {
return arr.filter((entry) => {
return !!entry;
});
}
/**
* Attempts to parse a string into JSON
* @param str
*/
static jsonParseHelper<T>(str: string): T | null {
try {
return JSON.parse(str) as T;
} catch (e) {
return null;
}
}
/**
* Tests if a given string matches a given pattern, with support for wildcards and queries.
* @param pattern Wildcard pattern to string match. Supports "*" for wildcards and "?" for queries
* @param input String to match against
*/
static matchPattern(pattern: string, input: string): boolean {
/**
* Wildcard support: https://stackoverflow.com/a/3117248/4888559
* Queries: replaces "?" in string with escaped "\?" for regex test
*/
// eslint-disable-next-line security/detect-non-literal-regexp
const regex: RegExp = new RegExp(
pattern
.replace(/\\/g, "\\\\")
.replace(/\*/g, "[^ ]*")
.replace(/\?/g, "\\?")
);
return regex.test(input);
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Utility functions for managing date and time operations.
*/
/**
* return the current time in Unix time (seconds).
*/
export function nowSeconds(): number {
// Date.getTime() returns in milliseconds.
return Math.round(new Date().getTime() / 1000.0);
}
/**
* check if a token is expired based on given UTC time in seconds.
* @param expiresOn
*/
export function isTokenExpired(expiresOn: string, offset: number): boolean {
// check for access token expiry
const expirationSec = Number(expiresOn) || 0;
const offsetCurrentTimeSec = nowSeconds() + offset;
// If current time + offset is greater than token expiration time, then token is expired.
return offsetCurrentTimeSec > expirationSec;
}
/**
* If the current time is earlier than the time that a token was cached at, we must discard the token
* i.e. The system clock was turned back after acquiring the cached token
* @param cachedAt
* @param offset
*/
export function wasClockTurnedBack(cachedAt: string): boolean {
const cachedAtSec = Number(cachedAt);
return cachedAtSec > nowSeconds();
}
/**
* Waits for t number of milliseconds
* @param t number
* @param value T
*/
export function delay<T>(t: number, value?: T): Promise<T | void> {
return new Promise((resolve) => setTimeout(() => resolve(value), t));
}
+60
View File
@@ -0,0 +1,60 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ServerAuthorizationCodeResponse } from "../response/ServerAuthorizationCodeResponse.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
/**
* Parses hash string from given string. Returns empty string if no hash symbol is found.
* @param hashString
*/
export function stripLeadingHashOrQuery(responseString: string): string {
if (responseString.startsWith("#/")) {
return responseString.substring(2);
} else if (
responseString.startsWith("#") ||
responseString.startsWith("?")
) {
return responseString.substring(1);
}
return responseString;
}
/**
* Returns URL hash as server auth code response object.
*/
export function getDeserializedResponse(
responseString: string
): ServerAuthorizationCodeResponse | null {
// Check if given hash is empty
if (!responseString || responseString.indexOf("=") < 0) {
return null;
}
try {
// Strip the # or ? symbol if present
const normalizedResponse = stripLeadingHashOrQuery(responseString);
// If # symbol was not present, above will return empty string, so give original hash value
const deserializedHash: ServerAuthorizationCodeResponse =
Object.fromEntries(new URLSearchParams(normalizedResponse));
// Check for known response properties
if (
deserializedHash.code ||
deserializedHash.error ||
deserializedHash.error_description ||
deserializedHash.state
) {
return deserializedHash;
}
} catch (e) {
throw createClientAuthError(ClientAuthErrorCodes.hashNotDeserialized);
}
return null;
}