Add PBandGraphOauth scaffold and auth UI
This commit is contained in:
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* 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 = {
|
||||
ID_TOKEN: "idtoken",
|
||||
CLIENT_INFO: "client.info",
|
||||
ADAL_ID_TOKEN: "adal.idtoken",
|
||||
ERROR: "error",
|
||||
ERROR_DESC: "error.description",
|
||||
ACTIVE_ACCOUNT: "active-account", // Legacy active-account cache key, use new key instead
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user