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
+161
View File
@@ -0,0 +1,161 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TokenClaims } from "./TokenClaims.js";
/**
* Account object with the following signature:
* - homeAccountId - Home account identifier for this account object
* - environment - Entity which issued the token represented by the domain of the issuer (e.g. login.microsoftonline.com)
* - tenantId - Full tenant or organizational id that this account belongs to
* - username - preferred_username claim of the id_token that represents this account
* - localAccountId - Local, tenant-specific account identifer for this account object, usually used in legacy cases
* - name - Full name for the account, including given name and family name
* - idToken - raw ID token
* - idTokenClaims - Object contains claims from ID token
* - nativeAccountId - The user's native account ID
* - tenantProfiles - Map of tenant profile objects for each tenant that the account has authenticated with in the browser
*/
export type AccountInfo = {
homeAccountId: string;
environment: string;
tenantId: string;
username: string;
localAccountId: string;
name?: string;
idToken?: string;
idTokenClaims?: TokenClaims & {
[key: string]:
| string
| number
| string[]
| object
| undefined
| unknown;
};
nativeAccountId?: string;
authorityType?: string;
tenantProfiles?: Map<string, TenantProfile>;
};
/**
* Account details that vary across tenants for the same user
*/
export type TenantProfile = Pick<
AccountInfo,
"tenantId" | "localAccountId" | "name"
> & {
/**
* - isHomeTenant - True if this is the home tenant profile of the account, false if it's a guest tenant profile
*/
isHomeTenant?: boolean;
};
export type ActiveAccountFilters = {
homeAccountId: string;
localAccountId: string;
tenantId?: string;
};
/**
* Returns true if tenantId matches the utid portion of homeAccountId
* @param tenantId
* @param homeAccountId
* @returns
*/
export function tenantIdMatchesHomeTenant(
tenantId?: string,
homeAccountId?: string
): boolean {
return (
!!tenantId &&
!!homeAccountId &&
tenantId === homeAccountId.split(".")[1]
);
}
/**
* Build tenant profile
* @param homeAccountId - Home account identifier for this account object
* @param localAccountId - Local account identifer for this account object
* @param tenantId - Full tenant or organizational id that this account belongs to
* @param idTokenClaims - Claims from the ID token
* @returns
*/
export function buildTenantProfile(
homeAccountId: string,
localAccountId: string,
tenantId: string,
idTokenClaims?: TokenClaims
): TenantProfile {
if (idTokenClaims) {
const { oid, sub, tid, name, tfp, acr } = idTokenClaims;
/**
* Since there is no way to determine if the authority is AAD or B2C, we exhaust all the possible claims that can serve as tenant ID with the following precedence:
* tid - TenantID claim that identifies the tenant that issued the token in AAD. Expected in all AAD ID tokens, not present in B2C ID Tokens.
* tfp - Trust Framework Policy claim that identifies the policy that was used to authenticate the user. Functions as tenant for B2C scenarios.
* acr - Authentication Context Class Reference claim used only with older B2C policies. Fallback in case tfp is not present, but likely won't be present anyway.
*/
const tenantId = tid || tfp || acr || "";
return {
tenantId: tenantId,
localAccountId: oid || sub || "",
name: name,
isHomeTenant: tenantIdMatchesHomeTenant(tenantId, homeAccountId),
};
} else {
return {
tenantId,
localAccountId,
isHomeTenant: tenantIdMatchesHomeTenant(tenantId, homeAccountId),
};
}
}
/**
* Replaces account info that varies by tenant profile sourced from the ID token claims passed in with the tenant-specific account info
* @param baseAccountInfo
* @param idTokenClaims
* @returns
*/
export function updateAccountTenantProfileData(
baseAccountInfo: AccountInfo,
tenantProfile?: TenantProfile,
idTokenClaims?: TokenClaims,
idTokenSecret?: string
): AccountInfo {
let updatedAccountInfo = baseAccountInfo;
// Tenant Profile overrides passed in account info
if (tenantProfile) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { isHomeTenant, ...tenantProfileOverride } = tenantProfile;
updatedAccountInfo = { ...baseAccountInfo, ...tenantProfileOverride };
}
// ID token claims override passed in account info and tenant profile
if (idTokenClaims) {
// Ignore isHomeTenant, loginHint, and sid which are part of tenant profile but not base account info
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { isHomeTenant, ...claimsSourcedTenantProfile } =
buildTenantProfile(
baseAccountInfo.homeAccountId,
baseAccountInfo.localAccountId,
baseAccountInfo.tenantId,
idTokenClaims
);
updatedAccountInfo = {
...updatedAccountInfo,
...claimsSourcedTenantProfile,
idTokenClaims: idTokenClaims,
idToken: idTokenSecret,
};
return updatedAccountInfo;
}
return updatedAccountInfo;
}
+71
View File
@@ -0,0 +1,71 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TokenClaims } from "./TokenClaims.js";
import {
createClientAuthError,
ClientAuthErrorCodes,
} from "../error/ClientAuthError.js";
/**
* Extract token by decoding the rawToken
*
* @param encodedToken
*/
export function extractTokenClaims(
encodedToken: string,
base64Decode: (input: string) => string
): TokenClaims {
const jswPayload = getJWSPayload(encodedToken);
// token will be decoded to get the username
try {
// base64Decode() should throw an error if there is an issue
const base64Decoded = base64Decode(jswPayload);
return JSON.parse(base64Decoded) as TokenClaims;
} catch (err) {
throw createClientAuthError(ClientAuthErrorCodes.tokenParsingError);
}
}
/**
* decode a JWT
*
* @param authToken
*/
export function getJWSPayload(authToken: string): string {
if (!authToken) {
throw createClientAuthError(ClientAuthErrorCodes.nullOrEmptyToken);
}
const tokenPartsRegex = /^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/;
const matches = tokenPartsRegex.exec(authToken);
if (!matches || matches.length < 4) {
throw createClientAuthError(ClientAuthErrorCodes.tokenParsingError);
}
/**
* const crackedToken = {
* header: matches[1],
* JWSPayload: matches[2],
* JWSSig: matches[3],
* };
*/
return matches[2];
}
/**
* Determine if the token's max_age has transpired
*/
export function checkMaxAge(authTime: number, maxAge: number): void {
/*
* per https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
* To force an immediate re-authentication: If an app requires that a user re-authenticate prior to access,
* provide a value of 0 for the max_age parameter and the AS will force a fresh login.
*/
const fiveMinuteSkew = 300000; // five minutes in milliseconds
if (maxAge === 0 || Date.now() - fiveMinuteSkew > authTime + maxAge) {
throw createClientAuthError(ClientAuthErrorCodes.maxAgeTranspired);
}
}
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type CcsCredential = {
credential: string;
type: CcsCredentialType;
};
export const CcsCredentialType = {
HOME_ACCOUNT_ID: "home_account_id",
UPN: "UPN",
} as const;
export type CcsCredentialType =
(typeof CcsCredentialType)[keyof typeof CcsCredentialType];
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type ClientAssertionConfig = {
clientId: string;
tokenEndpoint?: string;
};
export type ClientAssertionCallback = (
config: ClientAssertionConfig
) => Promise<string>;
/**
* Client Assertion credential for Confidential Clients
*/
export type ClientAssertion = {
assertion: string | ClientAssertionCallback;
assertionType: string;
};
/**
* Client Credentials set for Confidential Clients
*/
export type ClientCredentials = {
clientSecret?: string;
clientAssertion?: ClientAssertion;
};
+66
View File
@@ -0,0 +1,66 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
createClientAuthError,
ClientAuthErrorCodes,
} from "../error/ClientAuthError.js";
import { Separators, Constants } from "../utils/Constants.js";
/**
* Client info object which consists of two IDs. Need to add more info here.
*/
export type ClientInfo = {
uid: string;
utid: string;
};
/**
* Function to build a client info object from server clientInfo string
* @param rawClientInfo
* @param crypto
*/
export function buildClientInfo(
rawClientInfo: string,
base64Decode: (input: string) => string
): ClientInfo {
if (!rawClientInfo) {
throw createClientAuthError(ClientAuthErrorCodes.clientInfoEmptyError);
}
try {
const decodedClientInfo: string = base64Decode(rawClientInfo);
return JSON.parse(decodedClientInfo) as ClientInfo;
} catch (e) {
throw createClientAuthError(
ClientAuthErrorCodes.clientInfoDecodingError
);
}
}
/**
* Function to build a client info object from cached homeAccountId string
* @param homeAccountId
*/
export function buildClientInfoFromHomeAccountId(
homeAccountId: string
): ClientInfo {
if (!homeAccountId) {
throw createClientAuthError(
ClientAuthErrorCodes.clientInfoDecodingError
);
}
const clientInfoParts: string[] = homeAccountId.split(
Separators.CLIENT_INFO_SEPARATOR,
2
);
return {
uid: clientInfoParts[0],
utid:
clientInfoParts.length < 2
? Constants.EMPTY_STRING
: clientInfoParts[1],
};
}
+98
View File
@@ -0,0 +1,98 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Type which describes Id Token claims known by MSAL.
*/
export type TokenClaims = {
/**
* Audience
*/
aud?: string;
/**
* Issuer
*/
iss?: string;
/**
* Issued at
*/
iat?: number;
/**
* Not valid before
*/
nbf?: number;
/**
* Immutable object identifier, this ID uniquely identifies the user across applications
*/
oid?: string;
/**
* Immutable subject identifier, this is a pairwise identifier - it is unique to a particular application ID
*/
sub?: string;
/**
* Users' tenant or '9188040d-6c67-4c5b-b112-36a304b66dad' for personal accounts.
*/
tid?: string;
/**
* Trusted Framework Policy (B2C) The name of the policy that was used to acquire the ID token.
*/
tfp?: string;
/**
* Authentication Context Class Reference (B2C) Used only with older policies.
*/
acr?: string;
ver?: string;
upn?: string;
preferred_username?: string;
login_hint?: string;
emails?: string[];
name?: string;
nonce?: string;
/**
* Expiration
*/
exp?: number;
home_oid?: string;
sid?: string;
cloud_instance_host_name?: string;
cnf?: {
kid: string;
};
x5c_ca?: string[];
ts?: number;
at?: string;
u?: string;
p?: string;
m?: string;
roles?: string[];
amr?: string[];
idp?: string;
auth_time?: number;
/**
* Region of the resource tenant
*/
tenant_region_scope?: string;
tenant_region_sub_scope?: string;
};
/**
* Gets tenantId from available ID token claims to set as credential realm with the following precedence:
* 1. tid - if the token is acquired from an Azure AD tenant tid will be present
* 2. tfp - if the token is acquired from a modern B2C tenant tfp should be present
* 3. acr - if the token is acquired from a legacy B2C tenant acr should be present
* Downcased to match the realm case-insensitive comparison requirements
* @param idTokenClaims
* @returns
*/
export function getTenantIdFromIdTokenClaims(
idTokenClaims?: TokenClaims
): string | null {
if (idTokenClaims) {
const tenantId =
idTokenClaims.tid || idTokenClaims.tfp || idTokenClaims.acr;
return tenantId || null;
}
return null;
}
File diff suppressed because it is too large Load Diff
+74
View File
@@ -0,0 +1,74 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Authority, formatAuthorityUri } from "./Authority.js";
import { INetworkModule } from "../network/INetworkModule.js";
import {
createClientAuthError,
ClientAuthErrorCodes,
} from "../error/ClientAuthError.js";
import { ICacheManager } from "../cache/interface/ICacheManager.js";
import { AuthorityOptions } from "./AuthorityOptions.js";
import { Logger } from "../logger/Logger.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
/**
* Create an authority object of the correct type based on the url
* Performs basic authority validation - checks to see if the authority is of a valid type (i.e. aad, b2c, adfs)
*
* Also performs endpoint discovery.
*
* @param authorityUri
* @param networkClient
* @param protocolMode
* @internal
*/
export async function createDiscoveredInstance(
authorityUri: string,
networkClient: INetworkModule,
cacheManager: ICacheManager,
authorityOptions: AuthorityOptions,
logger: Logger,
correlationId: string,
performanceClient?: IPerformanceClient
): Promise<Authority> {
performanceClient?.addQueueMeasurement(
PerformanceEvents.AuthorityFactoryCreateDiscoveredInstance,
correlationId
);
const authorityUriFinal = Authority.transformCIAMAuthority(
formatAuthorityUri(authorityUri)
);
// Initialize authority and perform discovery endpoint check.
const acquireTokenAuthority: Authority = new Authority(
authorityUriFinal,
networkClient,
cacheManager,
authorityOptions,
logger,
correlationId,
performanceClient
);
try {
await invokeAsync(
acquireTokenAuthority.resolveEndpointsAsync.bind(
acquireTokenAuthority
),
PerformanceEvents.AuthorityResolveEndpointsAsync,
logger,
performanceClient,
correlationId
)();
return acquireTokenAuthority;
} catch (e) {
throw createClientAuthError(
ClientAuthErrorCodes.endpointResolutionError
);
}
}
+211
View File
@@ -0,0 +1,211 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Logger } from "../logger/Logger.js";
import { UrlString } from "../url/UrlString.js";
import { AuthorityMetadataSource } from "../utils/Constants.js";
import { StaticAuthorityOptions } from "./AuthorityOptions.js";
import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata.js";
import { CloudInstanceDiscoveryResponse } from "./CloudInstanceDiscoveryResponse.js";
import { OpenIdConfigResponse } from "./OpenIdConfigResponse.js";
type RawMetadata = {
endpointMetadata: { [key: string]: OpenIdConfigResponse };
instanceDiscoveryMetadata: CloudInstanceDiscoveryResponse;
};
export const rawMetdataJSON: RawMetadata = {
endpointMetadata: {
"login.microsoftonline.com": {
token_endpoint:
"https://login.microsoftonline.com/{tenantid}/oauth2/v2.0/token",
jwks_uri:
"https://login.microsoftonline.com/{tenantid}/discovery/v2.0/keys",
issuer: "https://login.microsoftonline.com/{tenantid}/v2.0",
authorization_endpoint:
"https://login.microsoftonline.com/{tenantid}/oauth2/v2.0/authorize",
end_session_endpoint:
"https://login.microsoftonline.com/{tenantid}/oauth2/v2.0/logout",
},
"login.chinacloudapi.cn": {
token_endpoint:
"https://login.chinacloudapi.cn/{tenantid}/oauth2/v2.0/token",
jwks_uri:
"https://login.chinacloudapi.cn/{tenantid}/discovery/v2.0/keys",
issuer: "https://login.partner.microsoftonline.cn/{tenantid}/v2.0",
authorization_endpoint:
"https://login.chinacloudapi.cn/{tenantid}/oauth2/v2.0/authorize",
end_session_endpoint:
"https://login.chinacloudapi.cn/{tenantid}/oauth2/v2.0/logout",
},
"login.microsoftonline.us": {
token_endpoint:
"https://login.microsoftonline.us/{tenantid}/oauth2/v2.0/token",
jwks_uri:
"https://login.microsoftonline.us/{tenantid}/discovery/v2.0/keys",
issuer: "https://login.microsoftonline.us/{tenantid}/v2.0",
authorization_endpoint:
"https://login.microsoftonline.us/{tenantid}/oauth2/v2.0/authorize",
end_session_endpoint:
"https://login.microsoftonline.us/{tenantid}/oauth2/v2.0/logout",
},
},
instanceDiscoveryMetadata: {
tenant_discovery_endpoint:
"https://{canonicalAuthority}/v2.0/.well-known/openid-configuration",
metadata: [
{
preferred_network: "login.microsoftonline.com",
preferred_cache: "login.windows.net",
aliases: [
"login.microsoftonline.com",
"login.windows.net",
"login.microsoft.com",
"sts.windows.net",
],
},
{
preferred_network: "login.partner.microsoftonline.cn",
preferred_cache: "login.partner.microsoftonline.cn",
aliases: [
"login.partner.microsoftonline.cn",
"login.chinacloudapi.cn",
],
},
{
preferred_network: "login.microsoftonline.de",
preferred_cache: "login.microsoftonline.de",
aliases: ["login.microsoftonline.de"],
},
{
preferred_network: "login.microsoftonline.us",
preferred_cache: "login.microsoftonline.us",
aliases: [
"login.microsoftonline.us",
"login.usgovcloudapi.net",
],
},
{
preferred_network: "login-us.microsoftonline.com",
preferred_cache: "login-us.microsoftonline.com",
aliases: ["login-us.microsoftonline.com"],
},
],
},
};
export const EndpointMetadata = rawMetdataJSON.endpointMetadata;
export const InstanceDiscoveryMetadata =
rawMetdataJSON.instanceDiscoveryMetadata;
export const InstanceDiscoveryMetadataAliases: Set<String> = new Set();
InstanceDiscoveryMetadata.metadata.forEach(
(metadataEntry: CloudDiscoveryMetadata) => {
metadataEntry.aliases.forEach((alias: string) => {
InstanceDiscoveryMetadataAliases.add(alias);
});
}
);
/**
* Attempts to get an aliases array from the static authority metadata sources based on the canonical authority host
* @param staticAuthorityOptions
* @param logger
* @returns
*/
export function getAliasesFromStaticSources(
staticAuthorityOptions: StaticAuthorityOptions,
logger?: Logger
): string[] {
let staticAliases: string[] | undefined;
const canonicalAuthority = staticAuthorityOptions.canonicalAuthority;
if (canonicalAuthority) {
const authorityHost = new UrlString(
canonicalAuthority
).getUrlComponents().HostNameAndPort;
staticAliases =
getAliasesFromMetadata(
authorityHost,
staticAuthorityOptions.cloudDiscoveryMetadata?.metadata,
AuthorityMetadataSource.CONFIG,
logger
) ||
getAliasesFromMetadata(
authorityHost,
InstanceDiscoveryMetadata.metadata,
AuthorityMetadataSource.HARDCODED_VALUES,
logger
) ||
staticAuthorityOptions.knownAuthorities;
}
return staticAliases || [];
}
/**
* Returns aliases for from the raw cloud discovery metadata passed in
* @param authorityHost
* @param rawCloudDiscoveryMetadata
* @returns
*/
export function getAliasesFromMetadata(
authorityHost?: string,
cloudDiscoveryMetadata?: CloudDiscoveryMetadata[],
source?: AuthorityMetadataSource,
logger?: Logger
): string[] | null {
logger?.trace(`getAliasesFromMetadata called with source: ${source}`);
if (authorityHost && cloudDiscoveryMetadata) {
const metadata = getCloudDiscoveryMetadataFromNetworkResponse(
cloudDiscoveryMetadata,
authorityHost
);
if (metadata) {
logger?.trace(
`getAliasesFromMetadata: found cloud discovery metadata in ${source}, returning aliases`
);
return metadata.aliases;
} else {
logger?.trace(
`getAliasesFromMetadata: did not find cloud discovery metadata in ${source}`
);
}
}
return null;
}
/**
* Get cloud discovery metadata for common authorities
*/
export function getCloudDiscoveryMetadataFromHardcodedValues(
authorityHost: string
): CloudDiscoveryMetadata | null {
const metadata = getCloudDiscoveryMetadataFromNetworkResponse(
InstanceDiscoveryMetadata.metadata,
authorityHost
);
return metadata;
}
/**
* Searches instance discovery network response for the entry that contains the host in the aliases list
* @param response
* @param authority
*/
export function getCloudDiscoveryMetadataFromNetworkResponse(
response: CloudDiscoveryMetadata[],
authorityHost: string
): CloudDiscoveryMetadata | null {
for (let i = 0; i < response.length; i++) {
const metadata = response[i];
if (metadata.aliases.includes(authorityHost)) {
return metadata;
}
}
return null;
}
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ProtocolMode } from "./ProtocolMode.js";
import { OIDCOptions } from "./OIDCOptions.js";
import { AzureRegionConfiguration } from "./AzureRegionConfiguration.js";
import { CloudInstanceDiscoveryResponse } from "./CloudInstanceDiscoveryResponse.js";
export type AuthorityOptions = {
protocolMode: ProtocolMode;
OIDCOptions?: OIDCOptions | null;
knownAuthorities: Array<string>;
cloudDiscoveryMetadata: string;
authorityMetadata: string;
skipAuthorityMetadataCache?: boolean;
azureRegionConfiguration?: AzureRegionConfiguration;
authority?: string;
};
export type StaticAuthorityOptions = Partial<
Pick<AuthorityOptions, "knownAuthorities">
> & {
canonicalAuthority?: string;
cloudDiscoveryMetadata?: CloudInstanceDiscoveryResponse;
};
export const AzureCloudInstance = {
// AzureCloudInstance is not specified.
None: "none",
// Microsoft Azure public cloud
AzurePublic: "https://login.microsoftonline.com",
// Microsoft PPE
AzurePpe: "https://login.windows-ppe.net",
// Microsoft Chinese national/regional cloud
AzureChina: "https://login.chinacloudapi.cn",
// Microsoft German national/regional cloud ("Black Forest")
AzureGermany: "https://login.microsoftonline.de",
// US Government cloud
AzureUsGovernment: "https://login.microsoftonline.us",
} as const;
export type AzureCloudInstance =
(typeof AzureCloudInstance)[keyof typeof AzureCloudInstance];
+15
View File
@@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Authority types supported by MSAL.
*/
export const AuthorityType = {
Default: 0,
Adfs: 1,
Dsts: 2,
Ciam: 3,
} as const;
export type AuthorityType = (typeof AuthorityType)[keyof typeof AuthorityType];
+7
View File
@@ -0,0 +1,7 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
// The type here is a string instead of enum as the list of regional end points supported is not final yet.
export type AzureRegion = string;
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AzureRegion } from "./AzureRegion.js";
/*
* AzureRegionConfiguration
* - preferredAzureRegion - Preferred azure region from the user
* - environmentRegionFunc - Environment specific way of fetching the region from the environment
*/
export type AzureRegionConfiguration = {
azureRegion?: AzureRegion;
environmentRegion: string | undefined;
};
@@ -0,0 +1,10 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type CloudDiscoveryMetadata = {
preferred_network: string;
preferred_cache: string;
aliases: Array<string>;
};
@@ -0,0 +1,26 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* The OpenID Configuration Endpoint Response type. Used by the authority class to get relevant OAuth endpoints.
*/
export type CloudInstanceDiscoveryErrorResponse = {
error: String;
error_description: String;
error_codes?: Array<Number>;
timestamp?: String;
trace_id?: String;
correlation_id?: String;
error_uri?: String;
};
export function isCloudInstanceDiscoveryErrorResponse(
response: object
): boolean {
return (
response.hasOwnProperty("error") &&
response.hasOwnProperty("error_description")
);
}
@@ -0,0 +1,21 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata.js";
/**
* The OpenID Configuration Endpoint Response type. Used by the authority class to get relevant OAuth endpoints.
*/
export type CloudInstanceDiscoveryResponse = {
tenant_discovery_endpoint: string;
metadata: Array<CloudDiscoveryMetadata>;
};
export function isCloudInstanceDiscoveryResponse(response: object): boolean {
return (
response.hasOwnProperty("tenant_discovery_endpoint") &&
response.hasOwnProperty("metadata")
);
}
+10
View File
@@ -0,0 +1,10 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type ImdsOptions = {
headers?: {
Metadata: string;
};
};
+14
View File
@@ -0,0 +1,14 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ServerResponseType } from "../utils/Constants.js";
/**
* Options for the OIDC protocol mode.
*/
export type OIDCOptions = {
serverResponseType?: ServerResponseType;
defaultScopes?: Array<string>;
};
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Tenant Discovery Response which contains the relevant OAuth endpoints and data needed for authentication and authorization.
*/
export type OpenIdConfigResponse = {
authorization_endpoint: string;
token_endpoint: string;
end_session_endpoint?: string;
issuer: string;
jwks_uri: string;
};
export function isOpenIdConfigResponse(response: object): boolean {
return (
response.hasOwnProperty("authorization_endpoint") &&
response.hasOwnProperty("token_endpoint") &&
response.hasOwnProperty("issuer") &&
response.hasOwnProperty("jwks_uri")
);
}
+13
View File
@@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Protocol modes supported by MSAL.
*/
export const ProtocolMode = {
AAD: "AAD",
OIDC: "OIDC",
} as const;
export type ProtocolMode = (typeof ProtocolMode)[keyof typeof ProtocolMode];
+196
View File
@@ -0,0 +1,196 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { INetworkModule } from "../network/INetworkModule.js";
import { NetworkResponse } from "../network/NetworkResponse.js";
import { IMDSBadResponse } from "../response/IMDSBadResponse.js";
import {
Constants,
RegionDiscoverySources,
ResponseCodes,
} from "../utils/Constants.js";
import { RegionDiscoveryMetadata } from "./RegionDiscoveryMetadata.js";
import { ImdsOptions } from "./ImdsOptions.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import { Logger } from "../logger/Logger.js";
export class RegionDiscovery {
// Network interface to make requests with.
protected networkInterface: INetworkModule;
// Logger
private logger: Logger;
// Performance client
protected performanceClient: IPerformanceClient | undefined;
// CorrelationId
protected correlationId: string | undefined;
// Options for the IMDS endpoint request
protected static IMDS_OPTIONS: ImdsOptions = {
headers: {
Metadata: "true",
},
};
constructor(
networkInterface: INetworkModule,
logger: Logger,
performanceClient?: IPerformanceClient,
correlationId?: string
) {
this.networkInterface = networkInterface;
this.logger = logger;
this.performanceClient = performanceClient;
this.correlationId = correlationId;
}
/**
* Detect the region from the application's environment.
*
* @returns Promise<string | null>
*/
public async detectRegion(
environmentRegion: string | undefined,
regionDiscoveryMetadata: RegionDiscoveryMetadata
): Promise<string | null> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RegionDiscoveryDetectRegion,
this.correlationId
);
// Initialize auto detected region with the region from the envrionment
let autodetectedRegionName = environmentRegion;
// Check if a region was detected from the environment, if not, attempt to get the region from IMDS
if (!autodetectedRegionName) {
const options = RegionDiscovery.IMDS_OPTIONS;
try {
const localIMDSVersionResponse = await invokeAsync(
this.getRegionFromIMDS.bind(this),
PerformanceEvents.RegionDiscoveryGetRegionFromIMDS,
this.logger,
this.performanceClient,
this.correlationId
)(Constants.IMDS_VERSION, options);
if (
localIMDSVersionResponse.status ===
ResponseCodes.httpSuccess
) {
autodetectedRegionName = localIMDSVersionResponse.body;
regionDiscoveryMetadata.region_source =
RegionDiscoverySources.IMDS;
}
// If the response using the local IMDS version failed, try to fetch the current version of IMDS and retry.
if (
localIMDSVersionResponse.status ===
ResponseCodes.httpBadRequest
) {
const currentIMDSVersion = await invokeAsync(
this.getCurrentVersion.bind(this),
PerformanceEvents.RegionDiscoveryGetCurrentVersion,
this.logger,
this.performanceClient,
this.correlationId
)(options);
if (!currentIMDSVersion) {
regionDiscoveryMetadata.region_source =
RegionDiscoverySources.FAILED_AUTO_DETECTION;
return null;
}
const currentIMDSVersionResponse = await invokeAsync(
this.getRegionFromIMDS.bind(this),
PerformanceEvents.RegionDiscoveryGetRegionFromIMDS,
this.logger,
this.performanceClient,
this.correlationId
)(currentIMDSVersion, options);
if (
currentIMDSVersionResponse.status ===
ResponseCodes.httpSuccess
) {
autodetectedRegionName =
currentIMDSVersionResponse.body;
regionDiscoveryMetadata.region_source =
RegionDiscoverySources.IMDS;
}
}
} catch (e) {
regionDiscoveryMetadata.region_source =
RegionDiscoverySources.FAILED_AUTO_DETECTION;
return null;
}
} else {
regionDiscoveryMetadata.region_source =
RegionDiscoverySources.ENVIRONMENT_VARIABLE;
}
// If no region was auto detected from the environment or from the IMDS endpoint, mark the attempt as a FAILED_AUTO_DETECTION
if (!autodetectedRegionName) {
regionDiscoveryMetadata.region_source =
RegionDiscoverySources.FAILED_AUTO_DETECTION;
}
return autodetectedRegionName || null;
}
/**
* Make the call to the IMDS endpoint
*
* @param imdsEndpointUrl
* @returns Promise<NetworkResponse<string>>
*/
private async getRegionFromIMDS(
version: string,
options: ImdsOptions
): Promise<NetworkResponse<string>> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RegionDiscoveryGetRegionFromIMDS,
this.correlationId
);
return this.networkInterface.sendGetRequestAsync<string>(
`${Constants.IMDS_ENDPOINT}?api-version=${version}&format=text`,
options,
Constants.IMDS_TIMEOUT
);
}
/**
* Get the most recent version of the IMDS endpoint available
*
* @returns Promise<string | null>
*/
private async getCurrentVersion(
options: ImdsOptions
): Promise<string | null> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RegionDiscoveryGetCurrentVersion,
this.correlationId
);
try {
const response =
await this.networkInterface.sendGetRequestAsync<IMDSBadResponse>(
`${Constants.IMDS_ENDPOINT}?format=json`,
options
);
// When IMDS endpoint is called without the api version query param, bad request response comes back with latest version.
if (
response.status === ResponseCodes.httpBadRequest &&
response.body &&
response.body["newest-versions"] &&
response.body["newest-versions"].length > 0
) {
return response.body["newest-versions"][0];
}
return null;
} catch (e) {
return null;
}
}
}
@@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
RegionDiscoveryOutcomes,
RegionDiscoverySources,
} from "../utils/Constants.js";
export type RegionDiscoveryMetadata = {
region_used?: string;
region_source?: RegionDiscoverySources;
region_outcome?: RegionDiscoveryOutcomes;
};
@@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountInfo } from "../../account/AccountInfo.js";
import { LoggerOptions } from "../../config/ClientConfiguration.js";
import { NativeRequest } from "../../request/NativeRequest.js";
import { NativeSignOutRequest } from "../../request/NativeSignOutRequest.js";
import { AuthenticationResult } from "../../response/AuthenticationResult.js";
export interface INativeBrokerPlugin {
isBrokerAvailable: boolean;
setLogger(loggerOptions: LoggerOptions): void;
getAccountById(
accountId: string,
correlationId: string
): Promise<AccountInfo>;
getAllAccounts(
clientId: string,
correlationId: string
): Promise<AccountInfo[]>;
acquireTokenSilent(request: NativeRequest): Promise<AuthenticationResult>;
acquireTokenInteractive(
request: NativeRequest,
windowHandle?: Buffer
): Promise<AuthenticationResult>;
signOut(request: NativeSignOutRequest): Promise<void>;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CredentialEntity } from "./CredentialEntity.js";
import { AuthenticationScheme } from "../../utils/Constants.js";
/**
* Access token cache type
*/
export type AccessTokenEntity = CredentialEntity & {
/** Full tenant or organizational identifier that the account belongs to */
realm: string;
/** Permissions that are included in the token, or for refresh tokens, the resource identifier. */
target: string;
/** Absolute device time when entry was created in the cache. */
cachedAt: string;
/** Token expiry time, calculated based on current UTC time in seconds. Represented as a string. */
expiresOn: string;
/** Additional extended expiry time until when token is valid in case of server-side outage. Represented as string in UTC seconds. */
extendedExpiresOn?: string;
/** Used for proactive refresh */
refreshOn?: string;
/** Matches the authentication scheme for which the token was issued (i.e. Bearer or pop) */
tokenType?: AuthenticationScheme;
/** Stringified claims object */
requestedClaims?: string;
/** Matches the SHA 256 hash of the claims object included in the token request */
requestedClaimsHash?: string;
};
+360
View File
@@ -0,0 +1,360 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CacheAccountType, Separators } from "../../utils/Constants.js";
import { Authority } from "../../authority/Authority.js";
import { ICrypto } from "../../crypto/ICrypto.js";
import { ClientInfo, buildClientInfo } from "../../account/ClientInfo.js";
import {
AccountInfo,
TenantProfile,
buildTenantProfile,
} from "../../account/AccountInfo.js";
import {
createClientAuthError,
ClientAuthErrorCodes,
} from "../../error/ClientAuthError.js";
import { AuthorityType } from "../../authority/AuthorityType.js";
import { Logger } from "../../logger/Logger.js";
import {
TokenClaims,
getTenantIdFromIdTokenClaims,
} from "../../account/TokenClaims.js";
import { ProtocolMode } from "../../authority/ProtocolMode.js";
/**
* Type that defines required and optional parameters for an Account field (based on universal cache schema implemented by all MSALs).
*
* Key : Value Schema
*
* Key: <home_account_id>-<environment>-<realm*>
*
* Value Schema:
* {
* homeAccountId: home account identifier for the auth scheme,
* environment: entity that issued the token, represented as a full host
* realm: Full tenant or organizational identifier that the account belongs to
* localAccountId: Original tenant-specific accountID, usually used for legacy cases
* username: primary username that represents the user, usually corresponds to preferred_username in the v2 endpt
* authorityType: Accounts authority type as a string
* name: Full name for the account, including given name and family name,
* lastModificationTime: last time this entity was modified in the cache
* lastModificationApp:
* nativeAccountId: Account identifier on the native device
* tenantProfiles: Array of tenant profile objects for each tenant that the account has authenticated with in the browser
* }
* @internal
*/
export class AccountEntity {
homeAccountId: string;
environment: string;
realm: string;
localAccountId: string;
username: string;
authorityType: string;
clientInfo?: string;
name?: string;
lastModificationTime?: string;
lastModificationApp?: string;
cloudGraphHostName?: string;
msGraphHost?: string;
nativeAccountId?: string;
tenantProfiles?: Array<TenantProfile>;
/**
* Generate Account Id key component as per the schema: <home_account_id>-<environment>
*/
generateAccountId(): string {
const accountId: Array<string> = [this.homeAccountId, this.environment];
return accountId.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Generate Account Cache Key as per the schema: <home_account_id>-<environment>-<realm*>
*/
generateAccountKey(): string {
return AccountEntity.generateAccountCacheKey({
homeAccountId: this.homeAccountId,
environment: this.environment,
tenantId: this.realm,
username: this.username,
localAccountId: this.localAccountId,
});
}
/**
* Returns the AccountInfo interface for this account.
*/
getAccountInfo(): AccountInfo {
return {
homeAccountId: this.homeAccountId,
environment: this.environment,
tenantId: this.realm,
username: this.username,
localAccountId: this.localAccountId,
name: this.name,
nativeAccountId: this.nativeAccountId,
authorityType: this.authorityType,
// Deserialize tenant profiles array into a Map
tenantProfiles: new Map(
(this.tenantProfiles || []).map((tenantProfile) => {
return [tenantProfile.tenantId, tenantProfile];
})
),
};
}
/**
* Returns true if the account entity is in single tenant format (outdated), false otherwise
*/
isSingleTenant(): boolean {
return !this.tenantProfiles;
}
/**
* Generates account key from interface
* @param accountInterface
*/
static generateAccountCacheKey(accountInterface: AccountInfo): string {
const homeTenantId = accountInterface.homeAccountId.split(".")[1];
const accountKey = [
accountInterface.homeAccountId,
accountInterface.environment || "",
homeTenantId || accountInterface.tenantId || "",
];
return accountKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Build Account cache from IdToken, clientInfo and authority/policy. Associated with AAD.
* @param accountDetails
*/
static createAccount(
accountDetails: {
homeAccountId: string;
idTokenClaims?: TokenClaims;
clientInfo?: string;
cloudGraphHostName?: string;
msGraphHost?: string;
environment?: string;
nativeAccountId?: string;
tenantProfiles?: Array<TenantProfile>;
},
authority: Authority,
base64Decode?: (input: string) => string
): AccountEntity {
const account: AccountEntity = new AccountEntity();
if (authority.authorityType === AuthorityType.Adfs) {
account.authorityType = CacheAccountType.ADFS_ACCOUNT_TYPE;
} else if (authority.protocolMode === ProtocolMode.AAD) {
account.authorityType = CacheAccountType.MSSTS_ACCOUNT_TYPE;
} else {
account.authorityType = CacheAccountType.GENERIC_ACCOUNT_TYPE;
}
let clientInfo: ClientInfo | undefined;
if (accountDetails.clientInfo && base64Decode) {
clientInfo = buildClientInfo(
accountDetails.clientInfo,
base64Decode
);
}
account.clientInfo = accountDetails.clientInfo;
account.homeAccountId = accountDetails.homeAccountId;
account.nativeAccountId = accountDetails.nativeAccountId;
const env =
accountDetails.environment ||
(authority && authority.getPreferredCache());
if (!env) {
throw createClientAuthError(
ClientAuthErrorCodes.invalidCacheEnvironment
);
}
account.environment = env;
// non AAD scenarios can have empty realm
account.realm =
clientInfo?.utid ||
getTenantIdFromIdTokenClaims(accountDetails.idTokenClaims) ||
"";
// How do you account for MSA CID here?
account.localAccountId =
clientInfo?.uid ||
accountDetails.idTokenClaims?.oid ||
accountDetails.idTokenClaims?.sub ||
"";
/*
* In B2C scenarios the emails claim is used instead of preferred_username and it is an array.
* In most cases it will contain a single email. This field should not be relied upon if a custom
* policy is configured to return more than 1 email.
*/
const preferredUsername =
accountDetails.idTokenClaims?.preferred_username ||
accountDetails.idTokenClaims?.upn;
const email = accountDetails.idTokenClaims?.emails
? accountDetails.idTokenClaims.emails[0]
: null;
account.username = preferredUsername || email || "";
account.name = accountDetails.idTokenClaims?.name || "";
account.cloudGraphHostName = accountDetails.cloudGraphHostName;
account.msGraphHost = accountDetails.msGraphHost;
if (accountDetails.tenantProfiles) {
account.tenantProfiles = accountDetails.tenantProfiles;
} else {
const tenantProfile = buildTenantProfile(
accountDetails.homeAccountId,
account.localAccountId,
account.realm,
accountDetails.idTokenClaims
);
account.tenantProfiles = [tenantProfile];
}
return account;
}
/**
* Creates an AccountEntity object from AccountInfo
* @param accountInfo
* @param cloudGraphHostName
* @param msGraphHost
* @returns
*/
static createFromAccountInfo(
accountInfo: AccountInfo,
cloudGraphHostName?: string,
msGraphHost?: string
): AccountEntity {
const account: AccountEntity = new AccountEntity();
account.authorityType =
accountInfo.authorityType || CacheAccountType.GENERIC_ACCOUNT_TYPE;
account.homeAccountId = accountInfo.homeAccountId;
account.localAccountId = accountInfo.localAccountId;
account.nativeAccountId = accountInfo.nativeAccountId;
account.realm = accountInfo.tenantId;
account.environment = accountInfo.environment;
account.username = accountInfo.username;
account.name = accountInfo.name;
account.cloudGraphHostName = cloudGraphHostName;
account.msGraphHost = msGraphHost;
// Serialize tenant profiles map into an array
account.tenantProfiles = Array.from(
accountInfo.tenantProfiles?.values() || []
);
return account;
}
/**
* Generate HomeAccountId from server response
* @param serverClientInfo
* @param authType
*/
static generateHomeAccountId(
serverClientInfo: string,
authType: AuthorityType,
logger: Logger,
cryptoObj: ICrypto,
idTokenClaims?: TokenClaims
): string {
// since ADFS/DSTS do not have tid and does not set client_info
if (
!(
authType === AuthorityType.Adfs ||
authType === AuthorityType.Dsts
)
) {
// for cases where there is clientInfo
if (serverClientInfo) {
try {
const clientInfo = buildClientInfo(
serverClientInfo,
cryptoObj.base64Decode
);
if (clientInfo.uid && clientInfo.utid) {
return `${clientInfo.uid}.${clientInfo.utid}`;
}
} catch (e) {}
}
logger.warning("No client info in response");
}
// default to "sub" claim
return idTokenClaims?.sub || "";
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
static isAccountEntity(entity: object): boolean {
if (!entity) {
return false;
}
return (
entity.hasOwnProperty("homeAccountId") &&
entity.hasOwnProperty("environment") &&
entity.hasOwnProperty("realm") &&
entity.hasOwnProperty("localAccountId") &&
entity.hasOwnProperty("username") &&
entity.hasOwnProperty("authorityType")
);
}
/**
* Helper function to determine whether 2 accountInfo objects represent the same account
* @param accountA
* @param accountB
* @param compareClaims - If set to true idTokenClaims will also be compared to determine account equality
*/
static accountInfoIsEqual(
accountA: AccountInfo | null,
accountB: AccountInfo | null,
compareClaims?: boolean
): boolean {
if (!accountA || !accountB) {
return false;
}
let claimsMatch = true; // default to true so as to not fail comparison below if compareClaims: false
if (compareClaims) {
const accountAClaims = (accountA.idTokenClaims ||
{}) as TokenClaims;
const accountBClaims = (accountB.idTokenClaims ||
{}) as TokenClaims;
// issued at timestamp and nonce are expected to change each time a new id token is acquired
claimsMatch =
accountAClaims.iat === accountBClaims.iat &&
accountAClaims.nonce === accountBClaims.nonce;
}
return (
accountA.homeAccountId === accountB.homeAccountId &&
accountA.localAccountId === accountB.localAccountId &&
accountA.username === accountB.username &&
accountA.tenantId === accountB.tenantId &&
accountA.environment === accountB.environment &&
accountA.nativeAccountId === accountB.nativeAccountId &&
claimsMatch
);
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* App Metadata Cache Type
*/
export type AppMetadataEntity = {
/** clientId of the application */
clientId: string;
/** entity that issued the token, represented as a full host */
environment: string;
/** Family identifier, '1' represents Microsoft Family */
familyId?: string;
};
@@ -0,0 +1,20 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/** @internal */
export type AuthorityMetadataEntity = {
aliases: Array<string>;
preferred_cache: string;
preferred_network: string;
canonical_authority: string;
authorization_endpoint: string;
token_endpoint: string;
end_session_endpoint?: string;
issuer: string;
aliasesFromNetwork: boolean;
endpointsFromNetwork: boolean;
expiresAt: number;
jwks_uri: string;
};
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { IdTokenEntity } from "./IdTokenEntity.js";
import { AccessTokenEntity } from "./AccessTokenEntity.js";
import { RefreshTokenEntity } from "./RefreshTokenEntity.js";
import { AccountEntity } from "./AccountEntity.js";
import { AppMetadataEntity } from "./AppMetadataEntity.js";
/** @internal */
export type CacheRecord = {
account?: AccountEntity | null;
idToken?: IdTokenEntity | null;
accessToken?: AccessTokenEntity | null;
refreshToken?: RefreshTokenEntity | null;
appMetadata?: AppMetadataEntity | null;
};
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CredentialType, AuthenticationScheme } from "../../utils/Constants.js";
/**
* Credential Cache Type
*/
export type CredentialEntity = {
/** Identifier for the user in their home tenant*/
homeAccountId: string;
/** Entity that issued the token, represented as a full host */
environment: string;
/** Type of credential */
credentialType: CredentialType;
/** Client ID of the application */
clientId: string;
/** Actual credential as a string */
secret: string;
/** Family ID identifier, usually only used for refresh tokens */
familyId?: string;
/** Full tenant or organizational identifier that the account belongs to */
realm?: string;
/** Permissions that are included in the token, or for refresh tokens, the resource identifier. */
target?: string;
/** Matches the SHA 256 hash of the obo_assertion for the OBO flow */
userAssertionHash?: string;
/** Matches the authentication scheme for which the token was issued (i.e. Bearer or pop) */
tokenType?: AuthenticationScheme;
/** KeyId for PoP and SSH tokens stored in the kid claim */
keyId?: string;
/** Matches the SHA 256 hash of the claims object included in the token request */
requestedClaimsHash?: string;
};
+14
View File
@@ -0,0 +1,14 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CredentialEntity } from "./CredentialEntity.js";
/**
* Id Token Cache Type
*/
export type IdTokenEntity = CredentialEntity & {
/** Full tenant or organizational identifier that the account belongs to */
realm: string;
};
@@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CredentialEntity } from "./CredentialEntity.js";
/**
* Refresh Token Cache Type
*/
export type RefreshTokenEntity = CredentialEntity & {
expiresOn?: string;
};
@@ -0,0 +1,11 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type ServerTelemetryEntity = {
failedRequests: Array<string | number>;
errors: string[];
cacheHits: number;
nativeBrokerErrorCode?: string;
};
+14
View File
@@ -0,0 +1,14 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type ThrottlingEntity = {
// Unix-time value representing the expiration of the throttle
throttleTime: number;
// Information provided by the server
error?: string;
errorCodes?: Array<string>;
errorMessage?: string;
subError?: string;
};
+228
View File
@@ -0,0 +1,228 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountFilter } from "../utils/CacheTypes.js";
import { CacheRecord } from "../entities/CacheRecord.js";
import { AccountEntity } from "../entities/AccountEntity.js";
import { AccountInfo } from "../../account/AccountInfo.js";
import { AppMetadataEntity } from "../entities/AppMetadataEntity.js";
import { ServerTelemetryEntity } from "../entities/ServerTelemetryEntity.js";
import { ThrottlingEntity } from "../entities/ThrottlingEntity.js";
import { IdTokenEntity } from "../entities/IdTokenEntity.js";
import { AccessTokenEntity } from "../entities/AccessTokenEntity.js";
import { RefreshTokenEntity } from "../entities/RefreshTokenEntity.js";
import { AuthorityMetadataEntity } from "../entities/AuthorityMetadataEntity.js";
import { StoreInCache } from "../../request/StoreInCache.js";
export interface ICacheManager {
/**
* fetch the account entity from the platform cache
* @param accountKey
*/
getAccount(accountKey: string): AccountEntity | null;
/**
* set account entity in the platform cache
* @param account
*/
setAccount(account: AccountEntity, correlationId: string): Promise<void>;
/**
* Returns true if the given key matches our account key schema. Also matches homeAccountId and/or tenantId if provided
* @param key
* @param homeAccountId
* @param tenantId
* @returns
*/
isAccountKey(
key: string,
homeAccountId?: string,
tenantId?: string
): boolean;
/**
* fetch the idToken entity from the platform cache
* @param idTokenKey
*/
getIdTokenCredential(idTokenKey: string): IdTokenEntity | null;
/**
* set idToken entity to the platform cache
* @param idToken
*/
setIdTokenCredential(
idToken: IdTokenEntity,
correlationId: string
): Promise<void>;
/**
* fetch the idToken entity from the platform cache
* @param accessTokenKey
*/
getAccessTokenCredential(accessTokenKey: string): AccessTokenEntity | null;
/**
* set idToken entity to the platform cache
* @param accessToken
*/
setAccessTokenCredential(
accessToken: AccessTokenEntity,
correlationId: string
): Promise<void>;
/**
* fetch the idToken entity from the platform cache
* @param refreshTokenKey
*/
getRefreshTokenCredential(
refreshTokenKey: string
): RefreshTokenEntity | null;
/**
* set idToken entity to the platform cache
* @param refreshToken
*/
setRefreshTokenCredential(
refreshToken: RefreshTokenEntity,
correlationId: string
): Promise<void>;
/**
* fetch appMetadata entity from the platform cache
* @param appMetadataKey
*/
getAppMetadata(appMetadataKey: string): AppMetadataEntity | null;
/**
* set appMetadata entity to the platform cache
* @param appMetadata
*/
setAppMetadata(appMetadata: AppMetadataEntity): void;
/**
* fetch server telemetry entity from the platform cache
* @param serverTelemetryKey
*/
getServerTelemetry(
serverTelemetryKey: string
): ServerTelemetryEntity | null;
/**
* set server telemetry entity to the platform cache
* @param serverTelemetryKey
* @param serverTelemetry
*/
setServerTelemetry(
serverTelemetryKey: string,
serverTelemetry: ServerTelemetryEntity
): void;
/**
* fetch cloud discovery metadata entity from the platform cache
* @param key
*/
getAuthorityMetadata(key: string): AuthorityMetadataEntity | null;
/**
* Get cache keys for authority metadata
*/
getAuthorityMetadataKeys(): Array<string>;
/**
* set cloud discovery metadata entity to the platform cache
* @param key
* @param value
*/
setAuthorityMetadata(key: string, value: AuthorityMetadataEntity): void;
/**
* Provide an alias to find a matching AuthorityMetadataEntity in cache
* @param host
*/
getAuthorityMetadataByAlias(host: string): AuthorityMetadataEntity | null;
/**
* given an authority generates the cache key for authorityMetadata
* @param authority
*/
generateAuthorityMetadataCacheKey(authority: string): string;
/**
* fetch throttling entity from the platform cache
* @param throttlingCacheKey
*/
getThrottlingCache(throttlingCacheKey: string): ThrottlingEntity | null;
/**
* set throttling entity to the platform cache
* @param throttlingCacheKey
* @param throttlingCache
*/
setThrottlingCache(
throttlingCacheKey: string,
throttlingCache: ThrottlingEntity
): void;
/**
* Returns all accounts in cache
*/
getAllAccounts(): AccountInfo[];
/**
* saves a cache record
* @param cacheRecord
*/
saveCacheRecord(
cacheRecord: CacheRecord,
correlationId: string,
storeInCache?: StoreInCache
): Promise<void>;
/**
* retrieve accounts matching all provided filters; if no filter is set, get all accounts
* @param homeAccountId
* @param environment
* @param realm
*/
getAccountsFilteredBy(filter: AccountFilter): AccountEntity[];
/**
* Get AccountInfo object based on provided filters
* @param filter
*/
getAccountInfoFilteredBy(filter: AccountFilter): AccountInfo | null;
/**
* Removes all accounts and related tokens from cache.
*/
removeAllAccounts(): Promise<void>;
/**
* returns a boolean if the given account is removed
* @param account
*/
removeAccount(accountKey: string): Promise<void>;
/**
* returns a boolean if the given account is removed
* @param account
*/
removeAccountContext(account: AccountEntity): Promise<void>;
/**
* @param key
*/
removeIdToken(key: string): void;
/**
* @param key
*/
removeAccessToken(key: string): Promise<void>;
/**
* @param key
*/
removeRefreshToken(key: string): void;
}
+11
View File
@@ -0,0 +1,11 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TokenCacheContext } from "../persistence/TokenCacheContext.js";
export interface ICachePlugin {
beforeCacheAccess: (tokenCacheContext: TokenCacheContext) => Promise<void>;
afterCacheAccess: (tokenCacheContext: TokenCacheContext) => Promise<void>;
}
@@ -0,0 +1,9 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface ISerializableTokenCache {
deserialize: (cache: string) => void;
serialize: () => string;
}
@@ -0,0 +1,39 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ISerializableTokenCache } from "../interface/ISerializableTokenCache.js";
/**
* This class instance helps track the memory changes facilitating
* decisions to read from and write to the persistent cache
*/ export class TokenCacheContext {
/**
* boolean indicating cache change
*/
hasChanged: boolean;
/**
* serializable token cache interface
*/
cache: ISerializableTokenCache;
constructor(tokenCache: ISerializableTokenCache, hasChanged: boolean) {
this.cache = tokenCache;
this.hasChanged = hasChanged;
}
/**
* boolean which indicates the changes in cache
*/
get cacheHasChanged(): boolean {
return this.hasChanged;
}
/**
* function to retrieve the token cache
*/
get tokenCache(): ISerializableTokenCache {
return this.cache;
}
}
+463
View File
@@ -0,0 +1,463 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { extractTokenClaims } from "../../account/AuthToken.js";
import { TokenClaims } from "../../account/TokenClaims.js";
import { CloudDiscoveryMetadata } from "../../authority/CloudDiscoveryMetadata.js";
import { OpenIdConfigResponse } from "../../authority/OpenIdConfigResponse.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../../error/ClientAuthError.js";
import {
APP_METADATA,
AUTHORITY_METADATA_CONSTANTS,
AuthenticationScheme,
CredentialType,
SERVER_TELEM_CONSTANTS,
Separators,
ThrottlingConstants,
} from "../../utils/Constants.js";
import * as TimeUtils from "../../utils/TimeUtils.js";
import { AccessTokenEntity } from "../entities/AccessTokenEntity.js";
import { AppMetadataEntity } from "../entities/AppMetadataEntity.js";
import { AuthorityMetadataEntity } from "../entities/AuthorityMetadataEntity.js";
import { CredentialEntity } from "../entities/CredentialEntity.js";
import { IdTokenEntity } from "../entities/IdTokenEntity.js";
import { RefreshTokenEntity } from "../entities/RefreshTokenEntity.js";
/**
* Cache Key: <home_account_id>-<environment>-<credential_type>-<client_id or familyId>-<realm>-<scopes>-<claims hash>-<scheme>
* IdToken Example: uid.utid-login.microsoftonline.com-idtoken-app_client_id-contoso.com
* AccessToken Example: uid.utid-login.microsoftonline.com-accesstoken-app_client_id-contoso.com-scope1 scope2--pop
* RefreshToken Example: uid.utid-login.microsoftonline.com-refreshtoken-1-contoso.com
* @param credentialEntity
* @returns
*/
export function generateCredentialKey(
credentialEntity: CredentialEntity
): string {
const credentialKey = [
generateAccountId(credentialEntity),
generateCredentialId(credentialEntity),
generateTarget(credentialEntity),
generateClaimsHash(credentialEntity),
generateScheme(credentialEntity),
];
return credentialKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Create IdTokenEntity
* @param homeAccountId
* @param authenticationResult
* @param clientId
* @param authority
*/
export function createIdTokenEntity(
homeAccountId: string,
environment: string,
idToken: string,
clientId: string,
tenantId: string
): IdTokenEntity {
const idTokenEntity: IdTokenEntity = {
credentialType: CredentialType.ID_TOKEN,
homeAccountId: homeAccountId,
environment: environment,
clientId: clientId,
secret: idToken,
realm: tenantId,
};
return idTokenEntity;
}
/**
* Create AccessTokenEntity
* @param homeAccountId
* @param environment
* @param accessToken
* @param clientId
* @param tenantId
* @param scopes
* @param expiresOn
* @param extExpiresOn
*/
export function createAccessTokenEntity(
homeAccountId: string,
environment: string,
accessToken: string,
clientId: string,
tenantId: string,
scopes: string,
expiresOn: number,
extExpiresOn: number,
base64Decode: (input: string) => string,
refreshOn?: number,
tokenType?: AuthenticationScheme,
userAssertionHash?: string,
keyId?: string,
requestedClaims?: string,
requestedClaimsHash?: string
): AccessTokenEntity {
const atEntity: AccessTokenEntity = {
homeAccountId: homeAccountId,
credentialType: CredentialType.ACCESS_TOKEN,
secret: accessToken,
cachedAt: TimeUtils.nowSeconds().toString(),
expiresOn: expiresOn.toString(),
extendedExpiresOn: extExpiresOn.toString(),
environment: environment,
clientId: clientId,
realm: tenantId,
target: scopes,
tokenType: tokenType || AuthenticationScheme.BEARER,
};
if (userAssertionHash) {
atEntity.userAssertionHash = userAssertionHash;
}
if (refreshOn) {
atEntity.refreshOn = refreshOn.toString();
}
if (requestedClaims) {
atEntity.requestedClaims = requestedClaims;
atEntity.requestedClaimsHash = requestedClaimsHash;
}
/*
* Create Access Token With Auth Scheme instead of regular access token
* Cast to lower to handle "bearer" from ADFS
*/
if (
atEntity.tokenType?.toLowerCase() !==
AuthenticationScheme.BEARER.toLowerCase()
) {
atEntity.credentialType = CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME;
switch (atEntity.tokenType) {
case AuthenticationScheme.POP:
// Make sure keyId is present and add it to credential
const tokenClaims: TokenClaims | null = extractTokenClaims(
accessToken,
base64Decode
);
if (!tokenClaims?.cnf?.kid) {
throw createClientAuthError(
ClientAuthErrorCodes.tokenClaimsCnfRequiredForSignedJwt
);
}
atEntity.keyId = tokenClaims.cnf.kid;
break;
case AuthenticationScheme.SSH:
atEntity.keyId = keyId;
}
}
return atEntity;
}
/**
* Create RefreshTokenEntity
* @param homeAccountId
* @param authenticationResult
* @param clientId
* @param authority
*/
export function createRefreshTokenEntity(
homeAccountId: string,
environment: string,
refreshToken: string,
clientId: string,
familyId?: string,
userAssertionHash?: string,
expiresOn?: number
): RefreshTokenEntity {
const rtEntity: RefreshTokenEntity = {
credentialType: CredentialType.REFRESH_TOKEN,
homeAccountId: homeAccountId,
environment: environment,
clientId: clientId,
secret: refreshToken,
};
if (userAssertionHash) {
rtEntity.userAssertionHash = userAssertionHash;
}
if (familyId) {
rtEntity.familyId = familyId;
}
if (expiresOn) {
rtEntity.expiresOn = expiresOn.toString();
}
return rtEntity;
}
export function isCredentialEntity(entity: object): boolean {
return (
entity.hasOwnProperty("homeAccountId") &&
entity.hasOwnProperty("environment") &&
entity.hasOwnProperty("credentialType") &&
entity.hasOwnProperty("clientId") &&
entity.hasOwnProperty("secret")
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isAccessTokenEntity(entity: object): boolean {
if (!entity) {
return false;
}
return (
isCredentialEntity(entity) &&
entity.hasOwnProperty("realm") &&
entity.hasOwnProperty("target") &&
(entity["credentialType"] === CredentialType.ACCESS_TOKEN ||
entity["credentialType"] ===
CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME)
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isIdTokenEntity(entity: object): boolean {
if (!entity) {
return false;
}
return (
isCredentialEntity(entity) &&
entity.hasOwnProperty("realm") &&
entity["credentialType"] === CredentialType.ID_TOKEN
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isRefreshTokenEntity(entity: object): boolean {
if (!entity) {
return false;
}
return (
isCredentialEntity(entity) &&
entity["credentialType"] === CredentialType.REFRESH_TOKEN
);
}
/**
* Generate Account Id key component as per the schema: <home_account_id>-<environment>
*/
function generateAccountId(credentialEntity: CredentialEntity): string {
const accountId: Array<string> = [
credentialEntity.homeAccountId,
credentialEntity.environment,
];
return accountId.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Generate Credential Id key component as per the schema: <credential_type>-<client_id>-<realm>
*/
function generateCredentialId(credentialEntity: CredentialEntity): string {
const clientOrFamilyId =
credentialEntity.credentialType === CredentialType.REFRESH_TOKEN
? credentialEntity.familyId || credentialEntity.clientId
: credentialEntity.clientId;
const credentialId: Array<string> = [
credentialEntity.credentialType,
clientOrFamilyId,
credentialEntity.realm || "",
];
return credentialId.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Generate target key component as per schema: <target>
*/
function generateTarget(credentialEntity: CredentialEntity): string {
return (credentialEntity.target || "").toLowerCase();
}
/**
* Generate requested claims key component as per schema: <requestedClaims>
*/
function generateClaimsHash(credentialEntity: CredentialEntity): string {
return (credentialEntity.requestedClaimsHash || "").toLowerCase();
}
/**
* Generate scheme key componenet as per schema: <scheme>
*/
function generateScheme(credentialEntity: CredentialEntity): string {
/*
* PoP Tokens and SSH certs include scheme in cache key
* Cast to lowercase to handle "bearer" from ADFS
*/
return credentialEntity.tokenType &&
credentialEntity.tokenType.toLowerCase() !==
AuthenticationScheme.BEARER.toLowerCase()
? credentialEntity.tokenType.toLowerCase()
: "";
}
/**
* validates if a given cache entry is "Telemetry", parses <key,value>
* @param key
* @param entity
*/
export function isServerTelemetryEntity(key: string, entity?: object): boolean {
const validateKey: boolean =
key.indexOf(SERVER_TELEM_CONSTANTS.CACHE_KEY) === 0;
let validateEntity: boolean = true;
if (entity) {
validateEntity =
entity.hasOwnProperty("failedRequests") &&
entity.hasOwnProperty("errors") &&
entity.hasOwnProperty("cacheHits");
}
return validateKey && validateEntity;
}
/**
* validates if a given cache entry is "Throttling", parses <key,value>
* @param key
* @param entity
*/
export function isThrottlingEntity(key: string, entity?: object): boolean {
let validateKey: boolean = false;
if (key) {
validateKey = key.indexOf(ThrottlingConstants.THROTTLING_PREFIX) === 0;
}
let validateEntity: boolean = true;
if (entity) {
validateEntity = entity.hasOwnProperty("throttleTime");
}
return validateKey && validateEntity;
}
/**
* Generate AppMetadata Cache Key as per the schema: appmetadata-<environment>-<client_id>
*/
export function generateAppMetadataKey({
environment,
clientId,
}: AppMetadataEntity): string {
const appMetaDataKeyArray: Array<string> = [
APP_METADATA,
environment,
clientId,
];
return appMetaDataKeyArray
.join(Separators.CACHE_KEY_SEPARATOR)
.toLowerCase();
}
/*
* Validates an entity: checks for all expected params
* @param entity
*/
export function isAppMetadataEntity(key: string, entity: object): boolean {
if (!entity) {
return false;
}
return (
key.indexOf(APP_METADATA) === 0 &&
entity.hasOwnProperty("clientId") &&
entity.hasOwnProperty("environment")
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isAuthorityMetadataEntity(
key: string,
entity: object
): boolean {
if (!entity) {
return false;
}
return (
key.indexOf(AUTHORITY_METADATA_CONSTANTS.CACHE_KEY) === 0 &&
entity.hasOwnProperty("aliases") &&
entity.hasOwnProperty("preferred_cache") &&
entity.hasOwnProperty("preferred_network") &&
entity.hasOwnProperty("canonical_authority") &&
entity.hasOwnProperty("authorization_endpoint") &&
entity.hasOwnProperty("token_endpoint") &&
entity.hasOwnProperty("issuer") &&
entity.hasOwnProperty("aliasesFromNetwork") &&
entity.hasOwnProperty("endpointsFromNetwork") &&
entity.hasOwnProperty("expiresAt") &&
entity.hasOwnProperty("jwks_uri")
);
}
/**
* Reset the exiresAt value
*/
export function generateAuthorityMetadataExpiresAt(): number {
return (
TimeUtils.nowSeconds() +
AUTHORITY_METADATA_CONSTANTS.REFRESH_TIME_SECONDS
);
}
export function updateAuthorityEndpointMetadata(
authorityMetadata: AuthorityMetadataEntity,
updatedValues: OpenIdConfigResponse,
fromNetwork: boolean
): void {
authorityMetadata.authorization_endpoint =
updatedValues.authorization_endpoint;
authorityMetadata.token_endpoint = updatedValues.token_endpoint;
authorityMetadata.end_session_endpoint = updatedValues.end_session_endpoint;
authorityMetadata.issuer = updatedValues.issuer;
authorityMetadata.endpointsFromNetwork = fromNetwork;
authorityMetadata.jwks_uri = updatedValues.jwks_uri;
}
export function updateCloudDiscoveryMetadata(
authorityMetadata: AuthorityMetadataEntity,
updatedValues: CloudDiscoveryMetadata,
fromNetwork: boolean
): void {
authorityMetadata.aliases = updatedValues.aliases;
authorityMetadata.preferred_cache = updatedValues.preferred_cache;
authorityMetadata.preferred_network = updatedValues.preferred_network;
authorityMetadata.aliasesFromNetwork = fromNetwork;
}
/**
* Returns whether or not the data needs to be refreshed
*/
export function isAuthorityMetadataExpired(
metadata: AuthorityMetadataEntity
): boolean {
return metadata.expiresAt <= TimeUtils.nowSeconds();
}
+105
View File
@@ -0,0 +1,105 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountEntity } from "../entities/AccountEntity.js";
import { IdTokenEntity } from "../entities/IdTokenEntity.js";
import { AccessTokenEntity } from "../entities/AccessTokenEntity.js";
import { RefreshTokenEntity } from "../entities/RefreshTokenEntity.js";
import { AppMetadataEntity } from "../entities/AppMetadataEntity.js";
import { ServerTelemetryEntity } from "../entities/ServerTelemetryEntity.js";
import { ThrottlingEntity } from "../entities/ThrottlingEntity.js";
import { AuthorityMetadataEntity } from "../entities/AuthorityMetadataEntity.js";
import { AuthenticationScheme } from "../../utils/Constants.js";
import { ScopeSet } from "../../request/ScopeSet.js";
import { AccountInfo } from "../../account/AccountInfo.js";
/** @internal */
export type AccountCache = Record<string, AccountEntity>;
/** @internal */
export type IdTokenCache = Record<string, IdTokenEntity>;
/** @internal */
export type AccessTokenCache = Record<string, AccessTokenEntity>;
/** @internal */
export type RefreshTokenCache = Record<string, RefreshTokenEntity>;
/** @internal */
export type AppMetadataCache = Record<string, AppMetadataEntity>;
/**
* Object type of all accepted cache types
* @internal
*/
export type ValidCacheType =
| AccountEntity
| IdTokenEntity
| AccessTokenEntity
| RefreshTokenEntity
| AppMetadataEntity
| AuthorityMetadataEntity
| ServerTelemetryEntity
| ThrottlingEntity
| string;
/**
* Object type of all credential types
* @internal
*/
export type ValidCredentialType =
| IdTokenEntity
| AccessTokenEntity
| RefreshTokenEntity;
/**
* Account: <home_account_id>-<environment>-<realm*>
*/
export type AccountFilter = Omit<
Partial<AccountInfo>,
"idToken" | "idTokenClaims"
> & {
realm?: string;
loginHint?: string;
sid?: string;
isHomeTenant?: boolean;
};
export type TenantProfileFilter = Pick<
AccountFilter,
| "localAccountId"
| "loginHint"
| "name"
| "sid"
| "isHomeTenant"
| "username"
>;
/**
* Credential: <home_account_id*>-<environment>-<credential_type>-<client_id>-<realm*>-<target*>-<scheme*>
*/
export type CredentialFilter = {
homeAccountId?: string;
environment?: string;
credentialType?: string;
clientId?: string;
familyId?: string;
realm?: string;
target?: ScopeSet;
userAssertionHash?: string;
tokenType?: AuthenticationScheme;
keyId?: string;
requestedClaimsHash?: string;
};
/**
* AppMetadata: appmetadata-<environment>-<client_id>
*/
export type AppMetadataFilter = {
environment?: string;
clientId?: string;
};
export type TokenKeys = {
idToken: string[];
accessToken: string[];
refreshToken: string[];
};
+832
View File
@@ -0,0 +1,832 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseClient } from "./BaseClient.js";
import { CommonAuthorizationUrlRequest } from "../request/CommonAuthorizationUrlRequest.js";
import { CommonAuthorizationCodeRequest } from "../request/CommonAuthorizationCodeRequest.js";
import { Authority } from "../authority/Authority.js";
import { RequestParameterBuilder } from "../request/RequestParameterBuilder.js";
import {
GrantType,
AuthenticationScheme,
PromptValue,
Separators,
HeaderNames,
} from "../utils/Constants.js";
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
import {
ClientConfiguration,
isOidcProtocolMode,
} from "../config/ClientConfiguration.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import { NetworkResponse } from "../network/NetworkResponse.js";
import { ResponseHandler } from "../response/ResponseHandler.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { StringUtils } from "../utils/StringUtils.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { UrlString } from "../url/UrlString.js";
import { ServerAuthorizationCodeResponse } from "../response/ServerAuthorizationCodeResponse.js";
import { CommonEndSessionRequest } from "../request/CommonEndSessionRequest.js";
import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js";
import { RequestThumbprint } from "../network/RequestThumbprint.js";
import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import { AccountInfo } from "../account/AccountInfo.js";
import {
buildClientInfoFromHomeAccountId,
buildClientInfo,
} from "../account/ClientInfo.js";
import { CcsCredentialType, CcsCredential } from "../account/CcsCredential.js";
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import { RequestValidator } from "../request/RequestValidator.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import { ClientAssertion } from "../account/ClientCredentials.js";
import { getClientAssertion } from "../utils/ClientAssertionUtils.js";
/**
* Oauth2.0 Authorization Code client
* @internal
*/
export class AuthorizationCodeClient extends BaseClient {
// Flag to indicate if client is for hybrid spa auth code redemption
protected includeRedirectUri: boolean = true;
private oidcDefaultScopes;
constructor(
configuration: ClientConfiguration,
performanceClient?: IPerformanceClient
) {
super(configuration, performanceClient);
this.oidcDefaultScopes =
this.config.authOptions.authority.options.OIDCOptions?.defaultScopes;
}
/**
* Creates the URL of the authorization request letting the user input credentials and consent to the
* application. The URL target the /authorize endpoint of the authority configured in the
* application object.
*
* Once the user inputs their credentials and consents, the authority will send a response to the redirect URI
* sent in the request and should contain an authorization code, which can then be used to acquire tokens via
* acquireToken(AuthorizationCodeRequest)
* @param request
*/
async getAuthCodeUrl(
request: CommonAuthorizationUrlRequest
): Promise<string> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.GetAuthCodeUrl,
request.correlationId
);
const queryString = await invokeAsync(
this.createAuthCodeUrlQueryString.bind(this),
PerformanceEvents.AuthClientCreateQueryString,
this.logger,
this.performanceClient,
request.correlationId
)(request);
return UrlString.appendQueryString(
this.authority.authorizationEndpoint,
queryString
);
}
/**
* API to acquire a token in exchange of 'authorization_code` acquired by the user in the first leg of the
* authorization_code_grant
* @param request
*/
async acquireToken(
request: CommonAuthorizationCodeRequest,
authCodePayload?: AuthorizationCodePayload
): Promise<AuthenticationResult> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.AuthClientAcquireToken,
request.correlationId
);
if (!request.code) {
throw createClientAuthError(
ClientAuthErrorCodes.requestCannotBeMade
);
}
const reqTimestamp = TimeUtils.nowSeconds();
const response = await invokeAsync(
this.executeTokenRequest.bind(this),
PerformanceEvents.AuthClientExecuteTokenRequest,
this.logger,
this.performanceClient,
request.correlationId
)(this.authority, request);
// Retrieve requestId from response headers
const requestId = response.headers?.[HeaderNames.X_MS_REQUEST_ID];
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.config.serializableCache,
this.config.persistencePlugin,
this.performanceClient
);
// Validate response. This function throws a server error if an error is returned by the server.
responseHandler.validateTokenResponse(response.body);
return invokeAsync(
responseHandler.handleServerTokenResponse.bind(responseHandler),
PerformanceEvents.HandleServerTokenResponse,
this.logger,
this.performanceClient,
request.correlationId
)(
response.body,
this.authority,
reqTimestamp,
request,
authCodePayload,
undefined,
undefined,
undefined,
requestId
);
}
/**
* Handles the hash fragment response from public client code request. Returns a code response used by
* the client to exchange for a token in acquireToken.
* @param hashFragment
*/
handleFragmentResponse(
serverParams: ServerAuthorizationCodeResponse,
cachedState: string
): AuthorizationCodePayload {
// Handle responses.
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
null,
null
);
// Get code response
responseHandler.validateServerAuthorizationCodeResponse(
serverParams,
cachedState
);
// throw when there is no auth code in the response
if (!serverParams.code) {
throw createClientAuthError(
ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse
);
}
return serverParams as AuthorizationCodePayload;
}
/**
* Used to log out the current user, and redirect the user to the postLogoutRedirectUri.
* Default behaviour is to redirect the user to `window.location.href`.
* @param authorityUri
*/
getLogoutUri(logoutRequest: CommonEndSessionRequest): string {
// Throw error if logoutRequest is null/undefined
if (!logoutRequest) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.logoutRequestEmpty
);
}
const queryString = this.createLogoutUrlQueryString(logoutRequest);
// Construct logout URI
return UrlString.appendQueryString(
this.authority.endSessionEndpoint,
queryString
);
}
/**
* Executes POST request to token endpoint
* @param authority
* @param request
*/
private async executeTokenRequest(
authority: Authority,
request: CommonAuthorizationCodeRequest
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.AuthClientExecuteTokenRequest,
request.correlationId
);
const queryParametersString = this.createTokenQueryParameters(request);
const endpoint = UrlString.appendQueryString(
authority.tokenEndpoint,
queryParametersString
);
const requestBody = await invokeAsync(
this.createTokenRequestBody.bind(this),
PerformanceEvents.AuthClientCreateTokenRequestBody,
this.logger,
this.performanceClient,
request.correlationId
)(request);
let ccsCredential: CcsCredential | undefined = undefined;
if (request.clientInfo) {
try {
const clientInfo = buildClientInfo(
request.clientInfo,
this.cryptoUtils.base64Decode
);
ccsCredential = {
credential: `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
type: CcsCredentialType.HOME_ACCOUNT_ID,
};
} catch (e) {
this.logger.verbose(
"Could not parse client info for CCS Header: " + e
);
}
}
const headers: Record<string, string> = this.createTokenRequestHeaders(
ccsCredential || request.ccsCredential
);
const thumbprint: RequestThumbprint = {
clientId:
request.tokenBodyParameters?.clientId ||
this.config.authOptions.clientId,
authority: authority.canonicalAuthority,
scopes: request.scopes,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
return invokeAsync(
this.executePostToTokenEndpoint.bind(this),
PerformanceEvents.AuthorizationCodeClientExecutePostToTokenEndpoint,
this.logger,
this.performanceClient,
request.correlationId
)(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId,
PerformanceEvents.AuthorizationCodeClientExecutePostToTokenEndpoint
);
}
/**
* Generates a map for all the params to be sent to the service
* @param request
*/
private async createTokenRequestBody(
request: CommonAuthorizationCodeRequest
): Promise<string> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.AuthClientCreateTokenRequestBody,
request.correlationId
);
const parameterBuilder = new RequestParameterBuilder(
request.correlationId,
this.performanceClient
);
parameterBuilder.addClientId(
request.embeddedClientId ||
request.tokenBodyParameters?.[AADServerParamKeys.CLIENT_ID] ||
this.config.authOptions.clientId
);
/*
* For hybrid spa flow, there will be a code but no verifier
* In this scenario, don't include redirect uri as auth code will not be bound to redirect URI
*/
if (!this.includeRedirectUri) {
// Just validate
RequestValidator.validateRedirectUri(request.redirectUri);
} else {
// Validate and include redirect uri
parameterBuilder.addRedirectUri(request.redirectUri);
}
// Add scope array, parameter builder will add default scopes and dedupe
parameterBuilder.addScopes(
request.scopes,
true,
this.oidcDefaultScopes
);
// add code: user set, not validated
parameterBuilder.addAuthorizationCode(request.code);
// Add library metadata
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
parameterBuilder.addApplicationTelemetry(
this.config.telemetry.application
);
parameterBuilder.addThrottling();
if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) {
parameterBuilder.addServerTelemetry(this.serverTelemetryManager);
}
// add code_verifier if passed
if (request.codeVerifier) {
parameterBuilder.addCodeVerifier(request.codeVerifier);
}
if (this.config.clientCredentials.clientSecret) {
parameterBuilder.addClientSecret(
this.config.clientCredentials.clientSecret
);
}
if (this.config.clientCredentials.clientAssertion) {
const clientAssertion: ClientAssertion =
this.config.clientCredentials.clientAssertion;
parameterBuilder.addClientAssertion(
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
parameterBuilder.addClientAssertionType(
clientAssertion.assertionType
);
}
parameterBuilder.addGrantType(GrantType.AUTHORIZATION_CODE_GRANT);
parameterBuilder.addClientInfo();
if (request.authenticationScheme === AuthenticationScheme.POP) {
const popTokenGenerator = new PopTokenGenerator(
this.cryptoUtils,
this.performanceClient
);
let reqCnfData;
if (!request.popKid) {
const generatedReqCnfData = await invokeAsync(
popTokenGenerator.generateCnf.bind(popTokenGenerator),
PerformanceEvents.PopTokenGenerateCnf,
this.logger,
this.performanceClient,
request.correlationId
)(request, this.logger);
reqCnfData = generatedReqCnfData.reqCnfString;
} else {
reqCnfData = this.cryptoUtils.encodeKid(request.popKid);
}
// SPA PoP requires full Base64Url encoded req_cnf string (unhashed)
parameterBuilder.addPopToken(reqCnfData);
} else if (request.authenticationScheme === AuthenticationScheme.SSH) {
if (request.sshJwk) {
parameterBuilder.addSshJwk(request.sshJwk);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshJwk
);
}
}
if (
!StringUtils.isEmptyObj(request.claims) ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
let ccsCred: CcsCredential | undefined = undefined;
if (request.clientInfo) {
try {
const clientInfo = buildClientInfo(
request.clientInfo,
this.cryptoUtils.base64Decode
);
ccsCred = {
credential: `${clientInfo.uid}${Separators.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
type: CcsCredentialType.HOME_ACCOUNT_ID,
};
} catch (e) {
this.logger.verbose(
"Could not parse client info for CCS Header: " + e
);
}
} else {
ccsCred = request.ccsCredential;
}
// Adds these as parameters in the request instead of headers to prevent CORS preflight request
if (this.config.systemOptions.preventCorsPreflight && ccsCred) {
switch (ccsCred.type) {
case CcsCredentialType.HOME_ACCOUNT_ID:
try {
const clientInfo = buildClientInfoFromHomeAccountId(
ccsCred.credential
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"Could not parse home account ID for CCS Header: " +
e
);
}
break;
case CcsCredentialType.UPN:
parameterBuilder.addCcsUpn(ccsCred.credential);
break;
}
}
if (request.embeddedClientId) {
parameterBuilder.addBrokerParameters({
brokerClientId: this.config.authOptions.clientId,
brokerRedirectUri: this.config.authOptions.redirectUri,
});
}
if (request.tokenBodyParameters) {
parameterBuilder.addExtraQueryParameters(
request.tokenBodyParameters
);
}
// Add hybrid spa parameters if not already provided
if (
request.enableSpaAuthorizationCode &&
(!request.tokenBodyParameters ||
!request.tokenBodyParameters[
AADServerParamKeys.RETURN_SPA_CODE
])
) {
parameterBuilder.addExtraQueryParameters({
[AADServerParamKeys.RETURN_SPA_CODE]: "1",
});
}
return parameterBuilder.createQueryString();
}
/**
* This API validates the `AuthorizationCodeUrlRequest` and creates a URL
* @param request
*/
private async createAuthCodeUrlQueryString(
request: CommonAuthorizationUrlRequest
): Promise<string> {
// generate the correlationId if not set by the user and add
const correlationId =
request.correlationId ||
this.config.cryptoInterface.createNewGuid();
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.AuthClientCreateQueryString,
correlationId
);
const parameterBuilder = new RequestParameterBuilder(
correlationId,
this.performanceClient
);
parameterBuilder.addClientId(
request.embeddedClientId ||
request.extraQueryParameters?.[AADServerParamKeys.CLIENT_ID] ||
this.config.authOptions.clientId
);
const requestScopes = [
...(request.scopes || []),
...(request.extraScopesToConsent || []),
];
parameterBuilder.addScopes(requestScopes, true, this.oidcDefaultScopes);
// validate the redirectUri (to be a non null value)
parameterBuilder.addRedirectUri(request.redirectUri);
parameterBuilder.addCorrelationId(correlationId);
// add response_mode. If not passed in it defaults to query.
parameterBuilder.addResponseMode(request.responseMode);
// add response_type = code
parameterBuilder.addResponseTypeCode();
// add library info parameters
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
if (!isOidcProtocolMode(this.config)) {
parameterBuilder.addApplicationTelemetry(
this.config.telemetry.application
);
}
// add client_info=1
parameterBuilder.addClientInfo();
if (request.codeChallenge && request.codeChallengeMethod) {
parameterBuilder.addCodeChallengeParams(
request.codeChallenge,
request.codeChallengeMethod
);
}
if (request.prompt) {
parameterBuilder.addPrompt(request.prompt);
}
if (request.domainHint) {
parameterBuilder.addDomainHint(request.domainHint);
this.performanceClient?.addFields(
{ domainHintFromRequest: true },
correlationId
);
}
this.performanceClient?.addFields(
{ prompt: request.prompt },
correlationId
);
// Add sid or loginHint with preference for login_hint claim (in request) -> sid -> loginHint (upn/email) -> username of AccountInfo object
if (request.prompt !== PromptValue.SELECT_ACCOUNT) {
// AAD will throw if prompt=select_account is passed with an account hint
if (request.sid && request.prompt === PromptValue.NONE) {
// SessionID is only used in silent calls
this.logger.verbose(
"createAuthCodeUrlQueryString: Prompt is none, adding sid from request"
);
parameterBuilder.addSid(request.sid);
this.performanceClient?.addFields(
{ sidFromRequest: true },
correlationId
);
} else if (request.account) {
const accountSid = this.extractAccountSid(request.account);
let accountLoginHintClaim = this.extractLoginHint(
request.account
);
if (accountLoginHintClaim && request.domainHint) {
this.logger.warning(
`AuthorizationCodeClient.createAuthCodeUrlQueryString: "domainHint" param is set, skipping opaque "login_hint" claim. Please consider not passing domainHint`
);
accountLoginHintClaim = null;
}
// If login_hint claim is present, use it over sid/username
if (accountLoginHintClaim) {
this.logger.verbose(
"createAuthCodeUrlQueryString: login_hint claim present on account"
);
parameterBuilder.addLoginHint(accountLoginHintClaim);
this.performanceClient?.addFields(
{ loginHintFromClaim: true },
correlationId
);
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.account.homeAccountId
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header"
);
}
} else if (accountSid && request.prompt === PromptValue.NONE) {
/*
* If account and loginHint are provided, we will check account first for sid before adding loginHint
* SessionId is only used in silent calls
*/
this.logger.verbose(
"createAuthCodeUrlQueryString: Prompt is none, adding sid from account"
);
parameterBuilder.addSid(accountSid);
this.performanceClient?.addFields(
{ sidFromClaim: true },
correlationId
);
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.account.homeAccountId
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header"
);
}
} else if (request.loginHint) {
this.logger.verbose(
"createAuthCodeUrlQueryString: Adding login_hint from request"
);
parameterBuilder.addLoginHint(request.loginHint);
parameterBuilder.addCcsUpn(request.loginHint);
this.performanceClient?.addFields(
{ loginHintFromRequest: true },
correlationId
);
} else if (request.account.username) {
// Fallback to account username if provided
this.logger.verbose(
"createAuthCodeUrlQueryString: Adding login_hint from account"
);
parameterBuilder.addLoginHint(request.account.username);
this.performanceClient?.addFields(
{ loginHintFromUpn: true },
correlationId
);
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.account.homeAccountId
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header"
);
}
}
} else if (request.loginHint) {
this.logger.verbose(
"createAuthCodeUrlQueryString: No account, adding login_hint from request"
);
parameterBuilder.addLoginHint(request.loginHint);
parameterBuilder.addCcsUpn(request.loginHint);
this.performanceClient?.addFields(
{ loginHintFromRequest: true },
correlationId
);
}
} else {
this.logger.verbose(
"createAuthCodeUrlQueryString: Prompt is select_account, ignoring account hints"
);
}
if (request.nonce) {
parameterBuilder.addNonce(request.nonce);
}
if (request.state) {
parameterBuilder.addState(request.state);
}
if (
request.claims ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
if (request.embeddedClientId) {
parameterBuilder.addBrokerParameters({
brokerClientId: this.config.authOptions.clientId,
brokerRedirectUri: this.config.authOptions.redirectUri,
});
}
this.addExtraQueryParams(request, parameterBuilder);
if (request.platformBroker) {
// signal ests that this is a WAM call
parameterBuilder.addNativeBroker();
// pass the req_cnf for POP
if (request.authenticationScheme === AuthenticationScheme.POP) {
const popTokenGenerator = new PopTokenGenerator(
this.cryptoUtils
);
// req_cnf is always sent as a string for SPAs
let reqCnfData;
if (!request.popKid) {
const generatedReqCnfData = await invokeAsync(
popTokenGenerator.generateCnf.bind(popTokenGenerator),
PerformanceEvents.PopTokenGenerateCnf,
this.logger,
this.performanceClient,
request.correlationId
)(request, this.logger);
reqCnfData = generatedReqCnfData.reqCnfString;
} else {
reqCnfData = this.cryptoUtils.encodeKid(request.popKid);
}
parameterBuilder.addPopToken(reqCnfData);
}
}
return parameterBuilder.createQueryString();
}
/**
* This API validates the `EndSessionRequest` and creates a URL
* @param request
*/
private createLogoutUrlQueryString(
request: CommonEndSessionRequest
): string {
const parameterBuilder = new RequestParameterBuilder(
request.correlationId,
this.performanceClient
);
if (request.postLogoutRedirectUri) {
parameterBuilder.addPostLogoutRedirectUri(
request.postLogoutRedirectUri
);
}
if (request.correlationId) {
parameterBuilder.addCorrelationId(request.correlationId);
}
if (request.idTokenHint) {
parameterBuilder.addIdTokenHint(request.idTokenHint);
}
if (request.state) {
parameterBuilder.addState(request.state);
}
if (request.logoutHint) {
parameterBuilder.addLogoutHint(request.logoutHint);
}
this.addExtraQueryParams(request, parameterBuilder);
return parameterBuilder.createQueryString();
}
private addExtraQueryParams(
request: CommonAuthorizationUrlRequest | CommonEndSessionRequest,
parameterBuilder: RequestParameterBuilder
) {
const hasRequestInstanceAware =
request.extraQueryParameters &&
request.extraQueryParameters.hasOwnProperty("instance_aware");
// Set instance_aware flag if config auth param is set
if (!hasRequestInstanceAware && this.config.authOptions.instanceAware) {
request.extraQueryParameters = request.extraQueryParameters || {};
request.extraQueryParameters["instance_aware"] = "true";
}
if (request.extraQueryParameters) {
parameterBuilder.addExtraQueryParameters(
request.extraQueryParameters
);
}
}
/**
* Helper to get sid from account. Returns null if idTokenClaims are not present or sid is not present.
* @param account
*/
private extractAccountSid(account: AccountInfo): string | null {
return account.idTokenClaims?.sid || null;
}
private extractLoginHint(account: AccountInfo): string | null {
return account.idTokenClaims?.login_hint || null;
}
}
+303
View File
@@ -0,0 +1,303 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientConfiguration,
buildClientConfiguration,
CommonClientConfiguration,
} from "../config/ClientConfiguration.js";
import {
INetworkModule,
NetworkRequestOptions,
} from "../network/INetworkModule.js";
import { NetworkResponse } from "../network/NetworkResponse.js";
import { ICrypto } from "../crypto/ICrypto.js";
import { Authority } from "../authority/Authority.js";
import { Logger } from "../logger/Logger.js";
import { Constants, HeaderNames } from "../utils/Constants.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import { CacheManager } from "../cache/CacheManager.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { RequestThumbprint } from "../network/RequestThumbprint.js";
import { version, name } from "../packageMetadata.js";
import { CcsCredential, CcsCredentialType } from "../account/CcsCredential.js";
import { buildClientInfoFromHomeAccountId } from "../account/ClientInfo.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { RequestParameterBuilder } from "../request/RequestParameterBuilder.js";
import { BaseAuthRequest } from "../request/BaseAuthRequest.js";
import { createDiscoveredInstance } from "../authority/AuthorityFactory.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { ThrottlingUtils } from "../network/ThrottlingUtils.js";
import { AuthError } from "../error/AuthError.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { NetworkError } from "../error/NetworkError.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
/**
* Base application class which will construct requests to send to and handle responses from the Microsoft STS using the authorization code flow.
* @internal
*/
export abstract class BaseClient {
// Logger object
public logger: Logger;
// Application config
protected config: CommonClientConfiguration;
// Crypto Interface
protected cryptoUtils: ICrypto;
// Storage Interface
protected cacheManager: CacheManager;
// Network Interface
protected networkClient: INetworkModule;
// Server Telemetry Manager
protected serverTelemetryManager: ServerTelemetryManager | null;
// Default authority object
public authority: Authority;
// Performance telemetry client
protected performanceClient?: IPerformanceClient;
protected constructor(
configuration: ClientConfiguration,
performanceClient?: IPerformanceClient
) {
// Set the configuration
this.config = buildClientConfiguration(configuration);
// Initialize the logger
this.logger = new Logger(this.config.loggerOptions, name, version);
// Initialize crypto
this.cryptoUtils = this.config.cryptoInterface;
// Initialize storage interface
this.cacheManager = this.config.storageInterface;
// Set the network interface
this.networkClient = this.config.networkInterface;
// Set TelemetryManager
this.serverTelemetryManager = this.config.serverTelemetryManager;
// set Authority
this.authority = this.config.authOptions.authority;
// set performance telemetry client
this.performanceClient = performanceClient;
}
/**
* Creates default headers for requests to token endpoint
*/
protected createTokenRequestHeaders(
ccsCred?: CcsCredential
): Record<string, string> {
const headers: Record<string, string> = {};
headers[HeaderNames.CONTENT_TYPE] = Constants.URL_FORM_CONTENT_TYPE;
if (!this.config.systemOptions.preventCorsPreflight && ccsCred) {
switch (ccsCred.type) {
case CcsCredentialType.HOME_ACCOUNT_ID:
try {
const clientInfo = buildClientInfoFromHomeAccountId(
ccsCred.credential
);
headers[
HeaderNames.CCS_HEADER
] = `Oid:${clientInfo.uid}@${clientInfo.utid}`;
} catch (e) {
this.logger.verbose(
"Could not parse home account ID for CCS Header: " +
e
);
}
break;
case CcsCredentialType.UPN:
headers[
HeaderNames.CCS_HEADER
] = `UPN: ${ccsCred.credential}`;
break;
}
}
return headers;
}
/**
* Http post to token endpoint
* @param tokenEndpoint
* @param queryString
* @param headers
* @param thumbprint
*/
protected async executePostToTokenEndpoint(
tokenEndpoint: string,
queryString: string,
headers: Record<string, string>,
thumbprint: RequestThumbprint,
correlationId: string,
queuedEvent?: string
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
if (queuedEvent) {
this.performanceClient?.addQueueMeasurement(
queuedEvent,
correlationId
);
}
const response =
await this.sendPostRequest<ServerAuthorizationTokenResponse>(
thumbprint,
tokenEndpoint,
{ body: queryString, headers: headers },
correlationId
);
if (
this.config.serverTelemetryManager &&
response.status < 500 &&
response.status !== 429
) {
// Telemetry data successfully logged by server, clear Telemetry cache
this.config.serverTelemetryManager.clearTelemetryCache();
}
return response;
}
/**
* Wraps sendPostRequestAsync with necessary preflight and postflight logic
* @param thumbprint - Request thumbprint for throttling
* @param tokenEndpoint - Endpoint to make the POST to
* @param options - Body and Headers to include on the POST request
* @param correlationId - CorrelationId for telemetry
*/
async sendPostRequest<T extends ServerAuthorizationTokenResponse>(
thumbprint: RequestThumbprint,
tokenEndpoint: string,
options: NetworkRequestOptions,
correlationId: string
): Promise<NetworkResponse<T>> {
ThrottlingUtils.preProcess(this.cacheManager, thumbprint);
let response;
try {
response = await invokeAsync(
this.networkClient.sendPostRequestAsync.bind(
this.networkClient
)<T>,
PerformanceEvents.NetworkClientSendPostRequestAsync,
this.logger,
this.performanceClient,
correlationId
)(tokenEndpoint, options);
const responseHeaders = response.headers || {};
this.performanceClient?.addFields(
{
refreshTokenSize: response.body.refresh_token?.length || 0,
httpVerToken:
responseHeaders[HeaderNames.X_MS_HTTP_VERSION] || "",
requestId:
responseHeaders[HeaderNames.X_MS_REQUEST_ID] || "",
},
correlationId
);
} catch (e) {
if (e instanceof NetworkError) {
const responseHeaders = e.responseHeaders;
if (responseHeaders) {
this.performanceClient?.addFields(
{
httpVerToken:
responseHeaders[
HeaderNames.X_MS_HTTP_VERSION
] || "",
requestId:
responseHeaders[HeaderNames.X_MS_REQUEST_ID] ||
"",
contentTypeHeader:
responseHeaders[HeaderNames.CONTENT_TYPE] ||
undefined,
contentLengthHeader:
responseHeaders[HeaderNames.CONTENT_LENGTH] ||
undefined,
httpStatus: e.httpStatus,
},
correlationId
);
}
throw e.error;
}
if (e instanceof AuthError) {
throw e;
} else {
throw createClientAuthError(ClientAuthErrorCodes.networkError);
}
}
ThrottlingUtils.postProcess(this.cacheManager, thumbprint, response);
return response;
}
/**
* Updates the authority object of the client. Endpoint discovery must be completed.
* @param updatedAuthority
*/
async updateAuthority(
cloudInstanceHostname: string,
correlationId: string
): Promise<void> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.UpdateTokenEndpointAuthority,
correlationId
);
const cloudInstanceAuthorityUri = `https://${cloudInstanceHostname}/${this.authority.tenant}/`;
const cloudInstanceAuthority = await createDiscoveredInstance(
cloudInstanceAuthorityUri,
this.networkClient,
this.cacheManager,
this.authority.options,
this.logger,
correlationId,
this.performanceClient
);
this.authority = cloudInstanceAuthority;
}
/**
* Creates query string for the /token request
* @param request
*/
createTokenQueryParameters(request: BaseAuthRequest): string {
const parameterBuilder = new RequestParameterBuilder(
request.correlationId,
this.performanceClient
);
if (request.embeddedClientId) {
parameterBuilder.addBrokerParameters({
brokerClientId: this.config.authOptions.clientId,
brokerRedirectUri: this.config.authOptions.redirectUri,
});
}
if (request.tokenQueryParameters) {
parameterBuilder.addExtraQueryParameters(
request.tokenQueryParameters
);
}
parameterBuilder.addCorrelationId(request.correlationId);
return parameterBuilder.createQueryString();
}
}
+501
View File
@@ -0,0 +1,501 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientConfiguration,
isOidcProtocolMode,
} from "../config/ClientConfiguration.js";
import { BaseClient } from "./BaseClient.js";
import { CommonRefreshTokenRequest } from "../request/CommonRefreshTokenRequest.js";
import { Authority } from "../authority/Authority.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import { RequestParameterBuilder } from "../request/RequestParameterBuilder.js";
import {
GrantType,
AuthenticationScheme,
Errors,
HeaderNames,
} from "../utils/Constants.js";
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
import { ResponseHandler } from "../response/ResponseHandler.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js";
import { StringUtils } from "../utils/StringUtils.js";
import { RequestThumbprint } from "../network/RequestThumbprint.js";
import { NetworkResponse } from "../network/NetworkResponse.js";
import { CommonSilentFlowRequest } from "../request/CommonSilentFlowRequest.js";
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import {
createClientAuthError,
ClientAuthErrorCodes,
} from "../error/ClientAuthError.js";
import { ServerError } from "../error/ServerError.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import { UrlString } from "../url/UrlString.js";
import { CcsCredentialType } from "../account/CcsCredential.js";
import { buildClientInfoFromHomeAccountId } from "../account/ClientInfo.js";
import {
InteractionRequiredAuthError,
InteractionRequiredAuthErrorCodes,
createInteractionRequiredAuthError,
} from "../error/InteractionRequiredAuthError.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { invoke, invokeAsync } from "../utils/FunctionWrappers.js";
import { generateCredentialKey } from "../cache/utils/CacheHelpers.js";
import { ClientAssertion } from "../account/ClientCredentials.js";
import { getClientAssertion } from "../utils/ClientAssertionUtils.js";
const DEFAULT_REFRESH_TOKEN_EXPIRATION_OFFSET_SECONDS = 300; // 5 Minutes
/**
* OAuth2.0 refresh token client
* @internal
*/
export class RefreshTokenClient extends BaseClient {
constructor(
configuration: ClientConfiguration,
performanceClient?: IPerformanceClient
) {
super(configuration, performanceClient);
}
public async acquireToken(
request: CommonRefreshTokenRequest
): Promise<AuthenticationResult> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientAcquireToken,
request.correlationId
);
const reqTimestamp = TimeUtils.nowSeconds();
const response = await invokeAsync(
this.executeTokenRequest.bind(this),
PerformanceEvents.RefreshTokenClientExecuteTokenRequest,
this.logger,
this.performanceClient,
request.correlationId
)(request, this.authority);
// Retrieve requestId from response headers
const requestId = response.headers?.[HeaderNames.X_MS_REQUEST_ID];
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.config.serializableCache,
this.config.persistencePlugin
);
responseHandler.validateTokenResponse(response.body);
return invokeAsync(
responseHandler.handleServerTokenResponse.bind(responseHandler),
PerformanceEvents.HandleServerTokenResponse,
this.logger,
this.performanceClient,
request.correlationId
)(
response.body,
this.authority,
reqTimestamp,
request,
undefined,
undefined,
true,
request.forceCache,
requestId
);
}
/**
* Gets cached refresh token and attaches to request, then calls acquireToken API
* @param request
*/
public async acquireTokenByRefreshToken(
request: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
// Cannot renew token if no request object is given.
if (!request) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.tokenRequestEmpty
);
}
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientAcquireTokenByRefreshToken,
request.correlationId
);
// We currently do not support silent flow for account === null use cases; This will be revisited for confidential flow usecases
if (!request.account) {
throw createClientAuthError(
ClientAuthErrorCodes.noAccountInSilentRequest
);
}
// try checking if FOCI is enabled for the given application
const isFOCI = this.cacheManager.isAppMetadataFOCI(
request.account.environment
);
// if the app is part of the family, retrive a Family refresh token if present and make a refreshTokenRequest
if (isFOCI) {
try {
return await invokeAsync(
this.acquireTokenWithCachedRefreshToken.bind(this),
PerformanceEvents.RefreshTokenClientAcquireTokenWithCachedRefreshToken,
this.logger,
this.performanceClient,
request.correlationId
)(request, true);
} catch (e) {
const noFamilyRTInCache =
e instanceof InteractionRequiredAuthError &&
e.errorCode ===
InteractionRequiredAuthErrorCodes.noTokensFound;
const clientMismatchErrorWithFamilyRT =
e instanceof ServerError &&
e.errorCode === Errors.INVALID_GRANT_ERROR &&
e.subError === Errors.CLIENT_MISMATCH_ERROR;
// if family Refresh Token (FRT) cache acquisition fails or if client_mismatch error is seen with FRT, reattempt with application Refresh Token (ART)
if (noFamilyRTInCache || clientMismatchErrorWithFamilyRT) {
return invokeAsync(
this.acquireTokenWithCachedRefreshToken.bind(this),
PerformanceEvents.RefreshTokenClientAcquireTokenWithCachedRefreshToken,
this.logger,
this.performanceClient,
request.correlationId
)(request, false);
// throw in all other cases
} else {
throw e;
}
}
}
// fall back to application refresh token acquisition
return invokeAsync(
this.acquireTokenWithCachedRefreshToken.bind(this),
PerformanceEvents.RefreshTokenClientAcquireTokenWithCachedRefreshToken,
this.logger,
this.performanceClient,
request.correlationId
)(request, false);
}
/**
* makes a network call to acquire tokens by exchanging RefreshToken available in userCache; throws if refresh token is not cached
* @param request
*/
private async acquireTokenWithCachedRefreshToken(
request: CommonSilentFlowRequest,
foci: boolean
) {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientAcquireTokenWithCachedRefreshToken,
request.correlationId
);
// fetches family RT or application RT based on FOCI value
const refreshToken = invoke(
this.cacheManager.getRefreshToken.bind(this.cacheManager),
PerformanceEvents.CacheManagerGetRefreshToken,
this.logger,
this.performanceClient,
request.correlationId
)(
request.account,
foci,
undefined,
this.performanceClient,
request.correlationId
);
if (!refreshToken) {
throw createInteractionRequiredAuthError(
InteractionRequiredAuthErrorCodes.noTokensFound
);
}
if (
refreshToken.expiresOn &&
TimeUtils.isTokenExpired(
refreshToken.expiresOn,
request.refreshTokenExpirationOffsetSeconds ||
DEFAULT_REFRESH_TOKEN_EXPIRATION_OFFSET_SECONDS
)
) {
this.performanceClient?.addFields(
{ rtExpiresOnMs: Number(refreshToken.expiresOn) },
request.correlationId
);
throw createInteractionRequiredAuthError(
InteractionRequiredAuthErrorCodes.refreshTokenExpired
);
}
// attach cached RT size to the current measurement
const refreshTokenRequest: CommonRefreshTokenRequest = {
...request,
refreshToken: refreshToken.secret,
authenticationScheme:
request.authenticationScheme || AuthenticationScheme.BEARER,
ccsCredential: {
credential: request.account.homeAccountId,
type: CcsCredentialType.HOME_ACCOUNT_ID,
},
};
try {
return await invokeAsync(
this.acquireToken.bind(this),
PerformanceEvents.RefreshTokenClientAcquireToken,
this.logger,
this.performanceClient,
request.correlationId
)(refreshTokenRequest);
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
this.performanceClient?.addFields(
{ rtExpiresOnMs: Number(refreshToken.expiresOn) },
request.correlationId
);
if (e.subError === InteractionRequiredAuthErrorCodes.badToken) {
// Remove bad refresh token from cache
this.logger.verbose(
"acquireTokenWithRefreshToken: bad refresh token, removing from cache"
);
const badRefreshTokenKey =
generateCredentialKey(refreshToken);
this.cacheManager.removeRefreshToken(badRefreshTokenKey);
}
}
throw e;
}
}
/**
* Constructs the network message and makes a NW call to the underlying secure token service
* @param request
* @param authority
*/
private async executeTokenRequest(
request: CommonRefreshTokenRequest,
authority: Authority
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientExecuteTokenRequest,
request.correlationId
);
const queryParametersString = this.createTokenQueryParameters(request);
const endpoint = UrlString.appendQueryString(
authority.tokenEndpoint,
queryParametersString
);
const requestBody = await invokeAsync(
this.createTokenRequestBody.bind(this),
PerformanceEvents.RefreshTokenClientCreateTokenRequestBody,
this.logger,
this.performanceClient,
request.correlationId
)(request);
const headers: Record<string, string> = this.createTokenRequestHeaders(
request.ccsCredential
);
const thumbprint: RequestThumbprint = {
clientId:
request.tokenBodyParameters?.clientId ||
this.config.authOptions.clientId,
authority: authority.canonicalAuthority,
scopes: request.scopes,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
return invokeAsync(
this.executePostToTokenEndpoint.bind(this),
PerformanceEvents.RefreshTokenClientExecutePostToTokenEndpoint,
this.logger,
this.performanceClient,
request.correlationId
)(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId,
PerformanceEvents.RefreshTokenClientExecutePostToTokenEndpoint
);
}
/**
* Helper function to create the token request body
* @param request
*/
private async createTokenRequestBody(
request: CommonRefreshTokenRequest
): Promise<string> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.RefreshTokenClientCreateTokenRequestBody,
request.correlationId
);
const correlationId = request.correlationId;
const parameterBuilder = new RequestParameterBuilder(
correlationId,
this.performanceClient
);
parameterBuilder.addClientId(
request.embeddedClientId ||
request.tokenBodyParameters?.[AADServerParamKeys.CLIENT_ID] ||
this.config.authOptions.clientId
);
if (request.redirectUri) {
parameterBuilder.addRedirectUri(request.redirectUri);
}
parameterBuilder.addScopes(
request.scopes,
true,
this.config.authOptions.authority.options.OIDCOptions?.defaultScopes
);
parameterBuilder.addGrantType(GrantType.REFRESH_TOKEN_GRANT);
parameterBuilder.addClientInfo();
parameterBuilder.addLibraryInfo(this.config.libraryInfo);
parameterBuilder.addApplicationTelemetry(
this.config.telemetry.application
);
parameterBuilder.addThrottling();
if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) {
parameterBuilder.addServerTelemetry(this.serverTelemetryManager);
}
parameterBuilder.addRefreshToken(request.refreshToken);
if (this.config.clientCredentials.clientSecret) {
parameterBuilder.addClientSecret(
this.config.clientCredentials.clientSecret
);
}
if (this.config.clientCredentials.clientAssertion) {
const clientAssertion: ClientAssertion =
this.config.clientCredentials.clientAssertion;
parameterBuilder.addClientAssertion(
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
parameterBuilder.addClientAssertionType(
clientAssertion.assertionType
);
}
if (request.authenticationScheme === AuthenticationScheme.POP) {
const popTokenGenerator = new PopTokenGenerator(
this.cryptoUtils,
this.performanceClient
);
let reqCnfData;
if (!request.popKid) {
const generatedReqCnfData = await invokeAsync(
popTokenGenerator.generateCnf.bind(popTokenGenerator),
PerformanceEvents.PopTokenGenerateCnf,
this.logger,
this.performanceClient,
request.correlationId
)(request, this.logger);
reqCnfData = generatedReqCnfData.reqCnfString;
} else {
reqCnfData = this.cryptoUtils.encodeKid(request.popKid);
}
// SPA PoP requires full Base64Url encoded req_cnf string (unhashed)
parameterBuilder.addPopToken(reqCnfData);
} else if (request.authenticationScheme === AuthenticationScheme.SSH) {
if (request.sshJwk) {
parameterBuilder.addSshJwk(request.sshJwk);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshJwk
);
}
}
if (
!StringUtils.isEmptyObj(request.claims) ||
(this.config.authOptions.clientCapabilities &&
this.config.authOptions.clientCapabilities.length > 0)
) {
parameterBuilder.addClaims(
request.claims,
this.config.authOptions.clientCapabilities
);
}
if (
this.config.systemOptions.preventCorsPreflight &&
request.ccsCredential
) {
switch (request.ccsCredential.type) {
case CcsCredentialType.HOME_ACCOUNT_ID:
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.ccsCredential.credential
);
parameterBuilder.addCcsOid(clientInfo);
} catch (e) {
this.logger.verbose(
"Could not parse home account ID for CCS Header: " +
e
);
}
break;
case CcsCredentialType.UPN:
parameterBuilder.addCcsUpn(
request.ccsCredential.credential
);
break;
}
}
if (request.embeddedClientId) {
parameterBuilder.addBrokerParameters({
brokerClientId: this.config.authOptions.clientId,
brokerRedirectUri: this.config.authOptions.redirectUri,
});
}
if (request.tokenBodyParameters) {
parameterBuilder.addExtraQueryParameters(
request.tokenBodyParameters
);
}
return parameterBuilder.createQueryString();
}
}
+211
View File
@@ -0,0 +1,211 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseClient } from "./BaseClient.js";
import { ClientConfiguration } from "../config/ClientConfiguration.js";
import { CommonSilentFlowRequest } from "../request/CommonSilentFlowRequest.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { ResponseHandler } from "../response/ResponseHandler.js";
import { CacheRecord } from "../cache/entities/CacheRecord.js";
import { CacheOutcome } from "../utils/Constants.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { StringUtils } from "../utils/StringUtils.js";
import { checkMaxAge, extractTokenClaims } from "../account/AuthToken.js";
import { TokenClaims } from "../account/TokenClaims.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import { getTenantFromAuthorityString } from "../authority/Authority.js";
/** @internal */
export class SilentFlowClient extends BaseClient {
constructor(
configuration: ClientConfiguration,
performanceClient?: IPerformanceClient
) {
super(configuration, performanceClient);
}
/**
* Retrieves token from cache or throws an error if it must be refreshed.
* @param request
*/
async acquireCachedToken(
request: CommonSilentFlowRequest
): Promise<[AuthenticationResult, CacheOutcome]> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.SilentFlowClientAcquireCachedToken,
request.correlationId
);
let lastCacheOutcome: CacheOutcome = CacheOutcome.NOT_APPLICABLE;
if (
request.forceRefresh ||
(!this.config.cacheOptions.claimsBasedCachingEnabled &&
!StringUtils.isEmptyObj(request.claims))
) {
// Must refresh due to present force_refresh flag.
this.setCacheOutcome(
CacheOutcome.FORCE_REFRESH_OR_CLAIMS,
request.correlationId
);
throw createClientAuthError(
ClientAuthErrorCodes.tokenRefreshRequired
);
}
// We currently do not support silent flow for account === null use cases; This will be revisited for confidential flow usecases
if (!request.account) {
throw createClientAuthError(
ClientAuthErrorCodes.noAccountInSilentRequest
);
}
const requestTenantId =
request.account.tenantId ||
getTenantFromAuthorityString(request.authority);
const tokenKeys = this.cacheManager.getTokenKeys();
const cachedAccessToken = this.cacheManager.getAccessToken(
request.account,
request,
tokenKeys,
requestTenantId,
this.performanceClient,
request.correlationId
);
if (!cachedAccessToken) {
// must refresh due to non-existent access_token
this.setCacheOutcome(
CacheOutcome.NO_CACHED_ACCESS_TOKEN,
request.correlationId
);
throw createClientAuthError(
ClientAuthErrorCodes.tokenRefreshRequired
);
} else if (
TimeUtils.wasClockTurnedBack(cachedAccessToken.cachedAt) ||
TimeUtils.isTokenExpired(
cachedAccessToken.expiresOn,
this.config.systemOptions.tokenRenewalOffsetSeconds
)
) {
// must refresh due to the expires_in value
this.setCacheOutcome(
CacheOutcome.CACHED_ACCESS_TOKEN_EXPIRED,
request.correlationId
);
throw createClientAuthError(
ClientAuthErrorCodes.tokenRefreshRequired
);
} else if (
cachedAccessToken.refreshOn &&
TimeUtils.isTokenExpired(cachedAccessToken.refreshOn, 0)
) {
// must refresh (in the background) due to the refresh_in value
lastCacheOutcome = CacheOutcome.PROACTIVELY_REFRESHED;
// don't throw ClientAuthError.createRefreshRequiredError(), return cached token instead
}
const environment =
request.authority || this.authority.getPreferredCache();
const cacheRecord: CacheRecord = {
account: this.cacheManager.readAccountFromCache(request.account),
accessToken: cachedAccessToken,
idToken: this.cacheManager.getIdToken(
request.account,
tokenKeys,
requestTenantId,
this.performanceClient,
request.correlationId
),
refreshToken: null,
appMetadata:
this.cacheManager.readAppMetadataFromCache(environment),
};
this.setCacheOutcome(lastCacheOutcome, request.correlationId);
if (this.config.serverTelemetryManager) {
this.config.serverTelemetryManager.incrementCacheHits();
}
return [
await invokeAsync(
this.generateResultFromCacheRecord.bind(this),
PerformanceEvents.SilentFlowClientGenerateResultFromCacheRecord,
this.logger,
this.performanceClient,
request.correlationId
)(cacheRecord, request),
lastCacheOutcome,
];
}
private setCacheOutcome(
cacheOutcome: CacheOutcome,
correlationId: string
): void {
this.serverTelemetryManager?.setCacheOutcome(cacheOutcome);
this.performanceClient?.addFields(
{
cacheOutcome: cacheOutcome,
},
correlationId
);
if (cacheOutcome !== CacheOutcome.NOT_APPLICABLE) {
this.logger.info(
`Token refresh is required due to cache outcome: ${cacheOutcome}`
);
}
}
/**
* Helper function to build response object from the CacheRecord
* @param cacheRecord
*/
private async generateResultFromCacheRecord(
cacheRecord: CacheRecord,
request: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.SilentFlowClientGenerateResultFromCacheRecord,
request.correlationId
);
let idTokenClaims: TokenClaims | undefined;
if (cacheRecord.idToken) {
idTokenClaims = extractTokenClaims(
cacheRecord.idToken.secret,
this.config.cryptoInterface.base64Decode
);
}
// token max_age check
if (request.maxAge || request.maxAge === 0) {
const authTime = idTokenClaims?.auth_time;
if (!authTime) {
throw createClientAuthError(
ClientAuthErrorCodes.authTimeNotFound
);
}
checkMaxAge(authTime, request.maxAge);
}
return ResponseHandler.generateAuthenticationResult(
this.cryptoUtils,
this.authority,
cacheRecord,
true,
request,
idTokenClaims
);
}
}
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Extensibility interface, which allows the app developer to return a token, based on the passed-in parameters, instead of fetching tokens from
* the Identity Provider (AAD).
* Developers need to construct and return an AppTokenProviderResult object back to MSAL. MSAL will cache the token response
* in the same way it would do if the result were comming from AAD.
* This extensibility point is only defined for the client_credential flow, i.e. acquireTokenByClientCredential and
* meant for Azure SDK to enhance Managed Identity support.
*/
export interface IAppTokenProvider {
(
appTokenProviderParameters: AppTokenProviderParameters
): Promise<AppTokenProviderResult>;
}
/**
* Input object for the IAppTokenProvider extensiblity. MSAL will create this object, which can be used
* to help create an AppTokenProviderResult.
*
* - correlationId - the correlation Id associated with the request
* - tenantId - the tenant Id for which the token must be provided
* - scopes - the scopes for which the token must be provided
* - claims - any extra claims that the token must satisfy
*/
export type AppTokenProviderParameters = {
readonly correlationId?: string;
readonly tenantId: string;
readonly scopes: Array<string>;
readonly claims?: string;
};
/**
* Output object for IAppTokenProvider extensiblity.
*
* - accessToken - the actual access token, typically in JWT format, that satisfies the request data AppTokenProviderParameters
* - expiresInSeconds - how long the tokens has before expiry, in seconds. Similar to the "expires_in" field in an AAD token response.
* - refreshInSeconds - how long the token has before it should be proactively refreshed. Similar to the "refresh_in" field in an AAD token response.
*/
export type AppTokenProviderResult = {
accessToken: string;
expiresInSeconds: number;
refreshInSeconds?: number;
};
+291
View File
@@ -0,0 +1,291 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { INetworkModule } from "../network/INetworkModule.js";
import { DEFAULT_CRYPTO_IMPLEMENTATION, ICrypto } from "../crypto/ICrypto.js";
import { ILoggerCallback, Logger, LogLevel } from "../logger/Logger.js";
import {
Constants,
DEFAULT_TOKEN_RENEWAL_OFFSET_SEC,
} from "../utils/Constants.js";
import { version } from "../packageMetadata.js";
import { Authority } from "../authority/Authority.js";
import { AzureCloudInstance } from "../authority/AuthorityOptions.js";
import { CacheManager, DefaultStorageClass } from "../cache/CacheManager.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { ICachePlugin } from "../cache/interface/ICachePlugin.js";
import { ISerializableTokenCache } from "../cache/interface/ISerializableTokenCache.js";
import { ClientCredentials } from "../account/ClientCredentials.js";
import { ProtocolMode } from "../authority/ProtocolMode.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
/**
* Use the configuration object to configure MSAL Modules and initialize the base interfaces for MSAL.
*
* This object allows you to configure important elements of MSAL functionality:
* - authOptions - Authentication for application
* - cryptoInterface - Implementation of crypto functions
* - libraryInfo - Library metadata
* - telemetry - Telemetry options and data
* - loggerOptions - Logging for application
* - networkInterface - Network implementation
* - storageInterface - Storage implementation
* - systemOptions - Additional library options
* - clientCredentials - Credentials options for confidential clients
* @internal
*/
export type ClientConfiguration = {
authOptions: AuthOptions;
systemOptions?: SystemOptions;
loggerOptions?: LoggerOptions;
cacheOptions?: CacheOptions;
storageInterface?: CacheManager;
networkInterface?: INetworkModule;
cryptoInterface?: ICrypto;
clientCredentials?: ClientCredentials;
libraryInfo?: LibraryInfo;
telemetry?: TelemetryOptions;
serverTelemetryManager?: ServerTelemetryManager | null;
persistencePlugin?: ICachePlugin | null;
serializableCache?: ISerializableTokenCache | null;
};
export type CommonClientConfiguration = {
authOptions: Required<AuthOptions>;
systemOptions: Required<SystemOptions>;
loggerOptions: Required<LoggerOptions>;
cacheOptions: Required<CacheOptions>;
storageInterface: CacheManager;
networkInterface: INetworkModule;
cryptoInterface: Required<ICrypto>;
libraryInfo: LibraryInfo;
telemetry: Required<TelemetryOptions>;
serverTelemetryManager: ServerTelemetryManager | null;
clientCredentials: ClientCredentials;
persistencePlugin: ICachePlugin | null;
serializableCache: ISerializableTokenCache | null;
};
/**
* Use this to configure the auth options in the ClientConfiguration object
*
* - clientId - Client ID of your app registered with our Application registration portal : https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview in Microsoft Identity Platform
* - authority - You can configure a specific authority, defaults to " " or "https://login.microsoftonline.com/common"
* - knownAuthorities - An array of URIs that are known to be valid. Used in B2C scenarios.
* - cloudDiscoveryMetadata - A string containing the cloud discovery response. Used in AAD scenarios.
* - clientCapabilities - Array of capabilities which will be added to the claims.access_token.xms_cc request property on every network request.
* - protocolMode - Enum that represents the protocol that msal follows. Used for configuring proper endpoints.
* - skipAuthorityMetadataCache - A flag to choose whether to use or not use the local metadata cache during authority initialization. Defaults to false.
* - instanceAware - A flag of whether the STS will send back additional parameters to specify where the tokens should be retrieved from.
* - redirectUri - The redirect URI where authentication responses can be received by your application. It must exactly match one of the redirect URIs registered in the Azure portal.
* @internal
*/
export type AuthOptions = {
clientId: string;
authority: Authority;
redirectUri: string;
clientCapabilities?: Array<string>;
azureCloudOptions?: AzureCloudOptions;
skipAuthorityMetadataCache?: boolean;
instanceAware?: boolean;
};
/**
* Use this to configure token renewal info in the Configuration object
*
* - tokenRenewalOffsetSeconds - Sets the window of offset needed to renew the token before expiry
*/
export type SystemOptions = {
tokenRenewalOffsetSeconds?: number;
preventCorsPreflight?: boolean;
};
/**
* Use this to configure the logging that MSAL does, by configuring logger options in the Configuration object
*
* - loggerCallback - Callback for logger
* - piiLoggingEnabled - Sets whether pii logging is enabled
* - logLevel - Sets the level at which logging happens
* - correlationId - Sets the correlationId printed by the logger
*/
export type LoggerOptions = {
loggerCallback?: ILoggerCallback;
piiLoggingEnabled?: boolean;
logLevel?: LogLevel;
correlationId?: string;
};
/**
* Use this to configure credential cache preferences in the ClientConfiguration object
*
* - claimsBasedCachingEnabled - Sets whether tokens should be cached based on the claims hash. Default is false.
*/
export type CacheOptions = {
claimsBasedCachingEnabled?: boolean;
};
/**
* Library-specific options
*/
export type LibraryInfo = {
sku: string;
version: string;
cpu: string;
os: string;
};
/**
* AzureCloudInstance specific options
*
* - azureCloudInstance - string enum providing short notation for soverign and public cloud authorities
* - tenant - provision to provide the tenant info
*/
export type AzureCloudOptions = {
azureCloudInstance: AzureCloudInstance;
tenant?: string;
};
export type TelemetryOptions = {
application: ApplicationTelemetry;
};
/**
* Telemetry information sent on request
* - appName: Unique string name of an application
* - appVersion: Version of the application using MSAL
*/
export type ApplicationTelemetry = {
appName: string;
appVersion: string;
};
export const DEFAULT_SYSTEM_OPTIONS: Required<SystemOptions> = {
tokenRenewalOffsetSeconds: DEFAULT_TOKEN_RENEWAL_OFFSET_SEC,
preventCorsPreflight: false,
};
const DEFAULT_LOGGER_IMPLEMENTATION: Required<LoggerOptions> = {
loggerCallback: () => {
// allow users to not set loggerCallback
},
piiLoggingEnabled: false,
logLevel: LogLevel.Info,
correlationId: Constants.EMPTY_STRING,
};
const DEFAULT_CACHE_OPTIONS: Required<CacheOptions> = {
claimsBasedCachingEnabled: false,
};
const DEFAULT_NETWORK_IMPLEMENTATION: INetworkModule = {
async sendGetRequestAsync<T>(): Promise<T> {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
async sendPostRequestAsync<T>(): Promise<T> {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
};
const DEFAULT_LIBRARY_INFO: LibraryInfo = {
sku: Constants.SKU,
version: version,
cpu: Constants.EMPTY_STRING,
os: Constants.EMPTY_STRING,
};
const DEFAULT_CLIENT_CREDENTIALS: ClientCredentials = {
clientSecret: Constants.EMPTY_STRING,
clientAssertion: undefined,
};
const DEFAULT_AZURE_CLOUD_OPTIONS: AzureCloudOptions = {
azureCloudInstance: AzureCloudInstance.None,
tenant: `${Constants.DEFAULT_COMMON_TENANT}`,
};
const DEFAULT_TELEMETRY_OPTIONS: Required<TelemetryOptions> = {
application: {
appName: "",
appVersion: "",
},
};
/**
* Function that sets the default options when not explicitly configured from app developer
*
* @param Configuration
*
* @returns Configuration
*/
export function buildClientConfiguration({
authOptions: userAuthOptions,
systemOptions: userSystemOptions,
loggerOptions: userLoggerOption,
cacheOptions: userCacheOptions,
storageInterface: storageImplementation,
networkInterface: networkImplementation,
cryptoInterface: cryptoImplementation,
clientCredentials: clientCredentials,
libraryInfo: libraryInfo,
telemetry: telemetry,
serverTelemetryManager: serverTelemetryManager,
persistencePlugin: persistencePlugin,
serializableCache: serializableCache,
}: ClientConfiguration): CommonClientConfiguration {
const loggerOptions = {
...DEFAULT_LOGGER_IMPLEMENTATION,
...userLoggerOption,
};
return {
authOptions: buildAuthOptions(userAuthOptions),
systemOptions: { ...DEFAULT_SYSTEM_OPTIONS, ...userSystemOptions },
loggerOptions: loggerOptions,
cacheOptions: { ...DEFAULT_CACHE_OPTIONS, ...userCacheOptions },
storageInterface:
storageImplementation ||
new DefaultStorageClass(
userAuthOptions.clientId,
DEFAULT_CRYPTO_IMPLEMENTATION,
new Logger(loggerOptions)
),
networkInterface:
networkImplementation || DEFAULT_NETWORK_IMPLEMENTATION,
cryptoInterface: cryptoImplementation || DEFAULT_CRYPTO_IMPLEMENTATION,
clientCredentials: clientCredentials || DEFAULT_CLIENT_CREDENTIALS,
libraryInfo: { ...DEFAULT_LIBRARY_INFO, ...libraryInfo },
telemetry: { ...DEFAULT_TELEMETRY_OPTIONS, ...telemetry },
serverTelemetryManager: serverTelemetryManager || null,
persistencePlugin: persistencePlugin || null,
serializableCache: serializableCache || null,
};
}
/**
* Construct authoptions from the client and platform passed values
* @param authOptions
*/
function buildAuthOptions(authOptions: AuthOptions): Required<AuthOptions> {
return {
clientCapabilities: [],
azureCloudOptions: DEFAULT_AZURE_CLOUD_OPTIONS,
skipAuthorityMetadataCache: false,
instanceAware: false,
...authOptions,
};
}
/**
* Returns true if config has protocolMode set to ProtocolMode.OIDC, false otherwise
* @param ClientConfiguration
*/
export function isOidcProtocolMode(config: ClientConfiguration): boolean {
return (
config.authOptions.authority.options.protocolMode === ProtocolMode.OIDC
);
}
+60
View File
@@ -0,0 +1,60 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const CLIENT_ID = "client_id";
export const REDIRECT_URI = "redirect_uri";
export const RESPONSE_TYPE = "response_type";
export const RESPONSE_MODE = "response_mode";
export const GRANT_TYPE = "grant_type";
export const CLAIMS = "claims";
export const SCOPE = "scope";
export const ERROR = "error";
export const ERROR_DESCRIPTION = "error_description";
export const ACCESS_TOKEN = "access_token";
export const ID_TOKEN = "id_token";
export const REFRESH_TOKEN = "refresh_token";
export const EXPIRES_IN = "expires_in";
export const REFRESH_TOKEN_EXPIRES_IN = "refresh_token_expires_in";
export const STATE = "state";
export const NONCE = "nonce";
export const PROMPT = "prompt";
export const SESSION_STATE = "session_state";
export const CLIENT_INFO = "client_info";
export const CODE = "code";
export const CODE_CHALLENGE = "code_challenge";
export const CODE_CHALLENGE_METHOD = "code_challenge_method";
export const CODE_VERIFIER = "code_verifier";
export const CLIENT_REQUEST_ID = "client-request-id";
export const X_CLIENT_SKU = "x-client-SKU";
export const X_CLIENT_VER = "x-client-VER";
export const X_CLIENT_OS = "x-client-OS";
export const X_CLIENT_CPU = "x-client-CPU";
export const X_CLIENT_CURR_TELEM = "x-client-current-telemetry";
export const X_CLIENT_LAST_TELEM = "x-client-last-telemetry";
export const X_MS_LIB_CAPABILITY = "x-ms-lib-capability";
export const X_APP_NAME = "x-app-name";
export const X_APP_VER = "x-app-ver";
export const POST_LOGOUT_URI = "post_logout_redirect_uri";
export const ID_TOKEN_HINT = "id_token_hint";
export const DEVICE_CODE = "device_code";
export const CLIENT_SECRET = "client_secret";
export const CLIENT_ASSERTION = "client_assertion";
export const CLIENT_ASSERTION_TYPE = "client_assertion_type";
export const TOKEN_TYPE = "token_type";
export const REQ_CNF = "req_cnf";
export const OBO_ASSERTION = "assertion";
export const REQUESTED_TOKEN_USE = "requested_token_use";
export const ON_BEHALF_OF = "on_behalf_of";
export const FOCI = "foci";
export const CCS_HEADER = "X-AnchorMailbox";
export const RETURN_SPA_CODE = "return_spa_code";
export const NATIVE_BROKER = "nativebroker";
export const LOGOUT_HINT = "logout_hint";
export const SID = "sid";
export const LOGIN_HINT = "login_hint";
export const DOMAIN_HINT = "domain_hint";
export const X_CLIENT_EXTRA_SKU = "x-client-xtra-sku";
export const BROKER_CLIENT_ID = "brk_client_id";
export const BROKER_REDIRECT_URI = "brk_redirect_uri";
+126
View File
@@ -0,0 +1,126 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { BaseAuthRequest } from "../request/BaseAuthRequest.js";
import { ShrOptions, SignedHttpRequest } from "./SignedHttpRequest.js";
/**
* The PkceCodes type describes the structure
* of objects that contain PKCE code
* challenge and verifier pairs
*/
export type PkceCodes = {
verifier: string;
challenge: string;
};
export type SignedHttpRequestParameters = Pick<
BaseAuthRequest,
| "resourceRequestMethod"
| "resourceRequestUri"
| "shrClaims"
| "shrNonce"
| "shrOptions"
> & {
correlationId?: string;
};
/**
* Interface for crypto functions used by library
*/
export interface ICrypto {
/**
* Creates a guid randomly.
*/
createNewGuid(): string;
/**
* base64 Encode string
* @param input
*/
base64Encode(input: string): string;
/**
* base64 decode string
* @param input
*/
base64Decode(input: string): string;
/**
* base64 URL safe encoded string
*/
base64UrlEncode(input: string): string;
/**
* Stringifies and base64Url encodes input public key
* @param inputKid
* @returns Base64Url encoded public key
*/
encodeKid(inputKid: string): string;
/**
* Generates an JWK RSA S256 Thumbprint
* @param request
*/
getPublicKeyThumbprint(
request: SignedHttpRequestParameters
): Promise<string>;
/**
* Removes cryptographic keypair from key store matching the keyId passed in
* @param kid
*/
removeTokenBindingKey(kid: string): Promise<boolean>;
/**
* Removes all cryptographic keys from IndexedDB storage
*/
clearKeystore(): Promise<boolean>;
/**
* Returns a signed proof-of-possession token with a given acces token that contains a cnf claim with the required kid.
* @param accessToken
*/
signJwt(
payload: SignedHttpRequest,
kid: string,
shrOptions?: ShrOptions,
correlationId?: string
): Promise<string>;
/**
* Returns the SHA-256 hash of an input string
* @param plainText
*/
hashString(plainText: string): Promise<string>;
}
export const DEFAULT_CRYPTO_IMPLEMENTATION: ICrypto = {
createNewGuid: (): string => {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
base64Decode: (): string => {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
base64Encode: (): string => {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
base64UrlEncode: (): string => {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
encodeKid: (): string => {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
async getPublicKeyThumbprint(): Promise<string> {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
async removeTokenBindingKey(): Promise<boolean> {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
async clearKeystore(): Promise<boolean> {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
async signJwt(): Promise<string> {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
async hashString(): Promise<string> {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
},
};
+9
View File
@@ -0,0 +1,9 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface IGuidGenerator {
generateGuid(): string;
isGuid(guid: string): boolean;
}
+58
View File
@@ -0,0 +1,58 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
JoseHeaderErrorCodes,
createJoseHeaderError,
} from "../error/JoseHeaderError.js";
import { JsonWebTokenTypes } from "../utils/Constants.js";
export type JoseHeaderOptions = {
typ?: JsonWebTokenTypes;
alg?: string;
kid?: string;
};
/** @internal */
export class JoseHeader {
public typ?: JsonWebTokenTypes;
public alg?: string;
public kid?: string;
constructor(options: JoseHeaderOptions) {
this.typ = options.typ;
this.alg = options.alg;
this.kid = options.kid;
}
/**
* Builds SignedHttpRequest formatted JOSE Header from the
* JOSE Header options provided or previously set on the object and returns
* the stringified header object.
* Throws if keyId or algorithm aren't provided since they are required for Access Token Binding.
* @param shrHeaderOptions
* @returns
*/
static getShrHeaderString(shrHeaderOptions: JoseHeaderOptions): string {
// KeyID is required on the SHR header
if (!shrHeaderOptions.kid) {
throw createJoseHeaderError(JoseHeaderErrorCodes.missingKidError);
}
// Alg is required on the SHR header
if (!shrHeaderOptions.alg) {
throw createJoseHeaderError(JoseHeaderErrorCodes.missingAlgError);
}
const shrHeader = new JoseHeader({
// Access Token PoP headers must have type pop, but the type header can be overriden for special cases
typ: shrHeaderOptions.typ || JsonWebTokenTypes.Pop,
kid: shrHeaderOptions.kid,
alg: shrHeaderOptions.alg,
});
return JSON.stringify(shrHeader);
}
}
+160
View File
@@ -0,0 +1,160 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ICrypto, SignedHttpRequestParameters } from "./ICrypto.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import { UrlString } from "../url/UrlString.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import { Logger } from "../logger/Logger.js";
/**
* See eSTS docs for more info.
* - A kid element, with the value containing an RFC 7638-compliant JWK thumbprint that is base64 encoded.
* - xms_ksl element, representing the storage location of the key's secret component on the client device. One of two values:
* - sw: software storage
* - uhw: hardware storage
*/
type ReqCnf = {
kid: string;
xms_ksl: KeyLocation;
};
export type ReqCnfData = {
kid: string;
reqCnfString: string;
};
const KeyLocation = {
SW: "sw",
UHW: "uhw",
} as const;
export type KeyLocation = (typeof KeyLocation)[keyof typeof KeyLocation];
/** @internal */
export class PopTokenGenerator {
private cryptoUtils: ICrypto;
private performanceClient?: IPerformanceClient;
constructor(cryptoUtils: ICrypto, performanceClient?: IPerformanceClient) {
this.cryptoUtils = cryptoUtils;
this.performanceClient = performanceClient;
}
/**
* Generates the req_cnf validated at the RP in the POP protocol for SHR parameters
* and returns an object containing the keyid, the full req_cnf string and the req_cnf string hash
* @param request
* @returns
*/
async generateCnf(
request: SignedHttpRequestParameters,
logger: Logger
): Promise<ReqCnfData> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.PopTokenGenerateCnf,
request.correlationId
);
const reqCnf = await invokeAsync(
this.generateKid.bind(this),
PerformanceEvents.PopTokenGenerateCnf,
logger,
this.performanceClient,
request.correlationId
)(request);
const reqCnfString: string = this.cryptoUtils.base64UrlEncode(
JSON.stringify(reqCnf)
);
return {
kid: reqCnf.kid,
reqCnfString,
};
}
/**
* Generates key_id for a SHR token request
* @param request
* @returns
*/
async generateKid(request: SignedHttpRequestParameters): Promise<ReqCnf> {
this.performanceClient?.addQueueMeasurement(
PerformanceEvents.PopTokenGenerateKid,
request.correlationId
);
const kidThumbprint = await this.cryptoUtils.getPublicKeyThumbprint(
request
);
return {
kid: kidThumbprint,
xms_ksl: KeyLocation.SW,
};
}
/**
* Signs the POP access_token with the local generated key-pair
* @param accessToken
* @param request
* @returns
*/
async signPopToken(
accessToken: string,
keyId: string,
request: SignedHttpRequestParameters
): Promise<string> {
return this.signPayload(accessToken, keyId, request);
}
/**
* Utility function to generate the signed JWT for an access_token
* @param payload
* @param kid
* @param request
* @param claims
* @returns
*/
async signPayload(
payload: string,
keyId: string,
request: SignedHttpRequestParameters,
claims?: object
): Promise<string> {
// Deconstruct request to extract SHR parameters
const {
resourceRequestMethod,
resourceRequestUri,
shrClaims,
shrNonce,
shrOptions,
} = request;
const resourceUrlString = resourceRequestUri
? new UrlString(resourceRequestUri)
: undefined;
const resourceUrlComponents = resourceUrlString?.getUrlComponents();
return this.cryptoUtils.signJwt(
{
at: payload,
ts: TimeUtils.nowSeconds(),
m: resourceRequestMethod?.toUpperCase(),
u: resourceUrlComponents?.HostNameAndPort,
nonce: shrNonce || this.cryptoUtils.createNewGuid(),
p: resourceUrlComponents?.AbsolutePath,
q: resourceUrlComponents?.QueryString
? [[], resourceUrlComponents.QueryString]
: undefined,
client_claims: shrClaims || undefined,
...claims,
},
keyId,
shrOptions,
request.correlationId
);
}
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { JoseHeaderOptions } from "./JoseHeader.js";
export type SignedHttpRequest = {
at?: string;
cnf?: object;
m?: string;
u?: string;
p?: string;
q?: [Array<string>, string];
ts?: number;
nonce?: string;
client_claims?: string;
};
export type ShrOptions = {
header: JoseHeaderOptions;
};
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Constants } from "../utils/Constants.js";
import * as AuthErrorCodes from "./AuthErrorCodes.js";
export { AuthErrorCodes };
export const AuthErrorMessages = {
[AuthErrorCodes.unexpectedError]: "Unexpected error in authentication.",
[AuthErrorCodes.postRequestFailed]:
"Post request failed from the network, could be a 4xx/5xx or a network unavailability. Please check the exact error code for details.",
};
/**
* AuthErrorMessage class containing string constants used by error codes and messages.
* @deprecated Use AuthErrorCodes instead
*/
export const AuthErrorMessage = {
unexpectedError: {
code: AuthErrorCodes.unexpectedError,
desc: AuthErrorMessages[AuthErrorCodes.unexpectedError],
},
postRequestFailed: {
code: AuthErrorCodes.postRequestFailed,
desc: AuthErrorMessages[AuthErrorCodes.postRequestFailed],
},
};
/**
* General error class thrown by the MSAL.js library.
*/
export class AuthError extends Error {
/**
* Short string denoting error
*/
errorCode: string;
/**
* Detailed description of error
*/
errorMessage: string;
/**
* Describes the subclass of an error
*/
subError: string;
/**
* CorrelationId associated with the error
*/
correlationId: string;
constructor(errorCode?: string, errorMessage?: string, suberror?: string) {
const errorString = errorMessage
? `${errorCode}: ${errorMessage}`
: errorCode;
super(errorString);
Object.setPrototypeOf(this, AuthError.prototype);
this.errorCode = errorCode || Constants.EMPTY_STRING;
this.errorMessage = errorMessage || Constants.EMPTY_STRING;
this.subError = suberror || Constants.EMPTY_STRING;
this.name = "AuthError";
}
setCorrelationId(correlationId: string): void {
this.correlationId = correlationId;
}
}
export function createAuthError(
code: string,
additionalMessage?: string
): AuthError {
return new AuthError(
code,
additionalMessage
? `${AuthErrorMessages[code]} ${additionalMessage}`
: AuthErrorMessages[code]
);
}
+10
View File
@@ -0,0 +1,10 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* AuthErrorMessage class containing string constants used by error codes and messages.
*/
export const unexpectedError = "unexpected_error";
export const postRequestFailed = "post_request_failed";
+44
View File
@@ -0,0 +1,44 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as CacheErrorCodes from "./CacheErrorCodes.js";
export { CacheErrorCodes };
export const CacheErrorMessages = {
[CacheErrorCodes.cacheQuotaExceededErrorCode]:
"Exceeded cache storage capacity.",
[CacheErrorCodes.cacheUnknownErrorCode]:
"Unexpected error occurred when using cache storage.",
};
/**
* Error thrown when there is an error with the cache
*/
export class CacheError extends Error {
/**
* Short string denoting error
*/
errorCode: string;
/**
* Detailed description of error
*/
errorMessage: string;
constructor(errorCode: string, errorMessage?: string) {
const message =
errorMessage ||
(CacheErrorMessages[errorCode]
? CacheErrorMessages[errorCode]
: CacheErrorMessages[CacheErrorCodes.cacheUnknownErrorCode]);
super(`${errorCode}: ${message}`);
Object.setPrototypeOf(this, CacheError.prototype);
this.name = "CacheError";
this.errorCode = errorCode;
this.errorMessage = message;
}
}
+7
View File
@@ -0,0 +1,7 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const cacheQuotaExceededErrorCode = "cache_quota_exceeded";
export const cacheUnknownErrorCode = "cache_error_unknown";
+341
View File
@@ -0,0 +1,341 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "./AuthError.js";
import * as ClientAuthErrorCodes from "./ClientAuthErrorCodes.js";
export { ClientAuthErrorCodes }; // Allow importing as "ClientAuthErrorCodes";
/**
* ClientAuthErrorMessage class containing string constants used by error codes and messages.
*/
export const ClientAuthErrorMessages = {
[ClientAuthErrorCodes.clientInfoDecodingError]:
"The client info could not be parsed/decoded correctly",
[ClientAuthErrorCodes.clientInfoEmptyError]: "The client info was empty",
[ClientAuthErrorCodes.tokenParsingError]: "Token cannot be parsed",
[ClientAuthErrorCodes.nullOrEmptyToken]: "The token is null or empty",
[ClientAuthErrorCodes.endpointResolutionError]:
"Endpoints cannot be resolved",
[ClientAuthErrorCodes.networkError]: "Network request failed",
[ClientAuthErrorCodes.openIdConfigError]:
"Could not retrieve endpoints. Check your authority and verify the .well-known/openid-configuration endpoint returns the required endpoints.",
[ClientAuthErrorCodes.hashNotDeserialized]:
"The hash parameters could not be deserialized",
[ClientAuthErrorCodes.invalidState]: "State was not the expected format",
[ClientAuthErrorCodes.stateMismatch]: "State mismatch error",
[ClientAuthErrorCodes.stateNotFound]: "State not found",
[ClientAuthErrorCodes.nonceMismatch]: "Nonce mismatch error",
[ClientAuthErrorCodes.authTimeNotFound]:
"Max Age was requested and the ID token is missing the auth_time variable." +
" auth_time is an optional claim and is not enabled by default - it must be enabled." +
" See https://aka.ms/msaljs/optional-claims for more information.",
[ClientAuthErrorCodes.maxAgeTranspired]:
"Max Age is set to 0, or too much time has elapsed since the last end-user authentication.",
[ClientAuthErrorCodes.multipleMatchingTokens]:
"The cache contains multiple tokens satisfying the requirements. " +
"Call AcquireToken again providing more requirements such as authority or account.",
[ClientAuthErrorCodes.multipleMatchingAccounts]:
"The cache contains multiple accounts satisfying the given parameters. Please pass more info to obtain the correct account",
[ClientAuthErrorCodes.multipleMatchingAppMetadata]:
"The cache contains multiple appMetadata satisfying the given parameters. Please pass more info to obtain the correct appMetadata",
[ClientAuthErrorCodes.requestCannotBeMade]:
"Token request cannot be made without authorization code or refresh token.",
[ClientAuthErrorCodes.cannotRemoveEmptyScope]:
"Cannot remove null or empty scope from ScopeSet",
[ClientAuthErrorCodes.cannotAppendScopeSet]: "Cannot append ScopeSet",
[ClientAuthErrorCodes.emptyInputScopeSet]:
"Empty input ScopeSet cannot be processed",
[ClientAuthErrorCodes.deviceCodePollingCancelled]:
"Caller has cancelled token endpoint polling during device code flow by setting DeviceCodeRequest.cancel = true.",
[ClientAuthErrorCodes.deviceCodeExpired]: "Device code is expired.",
[ClientAuthErrorCodes.deviceCodeUnknownError]:
"Device code stopped polling for unknown reasons.",
[ClientAuthErrorCodes.noAccountInSilentRequest]:
"Please pass an account object, silent flow is not supported without account information",
[ClientAuthErrorCodes.invalidCacheRecord]:
"Cache record object was null or undefined.",
[ClientAuthErrorCodes.invalidCacheEnvironment]:
"Invalid environment when attempting to create cache entry",
[ClientAuthErrorCodes.noAccountFound]:
"No account found in cache for given key.",
[ClientAuthErrorCodes.noCryptoObject]: "No crypto object detected.",
[ClientAuthErrorCodes.unexpectedCredentialType]:
"Unexpected credential type.",
[ClientAuthErrorCodes.invalidAssertion]:
"Client assertion must meet requirements described in https://tools.ietf.org/html/rfc7515",
[ClientAuthErrorCodes.invalidClientCredential]:
"Client credential (secret, certificate, or assertion) must not be empty when creating a confidential client. An application should at most have one credential",
[ClientAuthErrorCodes.tokenRefreshRequired]:
"Cannot return token from cache because it must be refreshed. This may be due to one of the following reasons: forceRefresh parameter is set to true, claims have been requested, there is no cached access token or it is expired.",
[ClientAuthErrorCodes.userTimeoutReached]:
"User defined timeout for device code polling reached",
[ClientAuthErrorCodes.tokenClaimsCnfRequiredForSignedJwt]:
"Cannot generate a POP jwt if the token_claims are not populated",
[ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse]:
"Server response does not contain an authorization code to proceed",
[ClientAuthErrorCodes.bindingKeyNotRemoved]:
"Could not remove the credential's binding key from storage.",
[ClientAuthErrorCodes.endSessionEndpointNotSupported]:
"The provided authority does not support logout",
[ClientAuthErrorCodes.keyIdMissing]:
"A keyId value is missing from the requested bound token's cache record and is required to match the token to it's stored binding key.",
[ClientAuthErrorCodes.noNetworkConnectivity]:
"No network connectivity. Check your internet connection.",
[ClientAuthErrorCodes.userCanceled]: "User cancelled the flow.",
[ClientAuthErrorCodes.missingTenantIdError]:
"A tenant id - not common, organizations, or consumers - must be specified when using the client_credentials flow.",
[ClientAuthErrorCodes.methodNotImplemented]:
"This method has not been implemented",
[ClientAuthErrorCodes.nestedAppAuthBridgeDisabled]:
"The nested app auth bridge is disabled",
};
/**
* String constants used by error codes and messages.
* @deprecated Use ClientAuthErrorCodes instead
*/
export const ClientAuthErrorMessage = {
clientInfoDecodingError: {
code: ClientAuthErrorCodes.clientInfoDecodingError,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.clientInfoDecodingError
],
},
clientInfoEmptyError: {
code: ClientAuthErrorCodes.clientInfoEmptyError,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.clientInfoEmptyError
],
},
tokenParsingError: {
code: ClientAuthErrorCodes.tokenParsingError,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.tokenParsingError],
},
nullOrEmptyToken: {
code: ClientAuthErrorCodes.nullOrEmptyToken,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.nullOrEmptyToken],
},
endpointResolutionError: {
code: ClientAuthErrorCodes.endpointResolutionError,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.endpointResolutionError
],
},
networkError: {
code: ClientAuthErrorCodes.networkError,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.networkError],
},
unableToGetOpenidConfigError: {
code: ClientAuthErrorCodes.openIdConfigError,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.openIdConfigError],
},
hashNotDeserialized: {
code: ClientAuthErrorCodes.hashNotDeserialized,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.hashNotDeserialized],
},
invalidStateError: {
code: ClientAuthErrorCodes.invalidState,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.invalidState],
},
stateMismatchError: {
code: ClientAuthErrorCodes.stateMismatch,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.stateMismatch],
},
stateNotFoundError: {
code: ClientAuthErrorCodes.stateNotFound,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.stateNotFound],
},
nonceMismatchError: {
code: ClientAuthErrorCodes.nonceMismatch,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.nonceMismatch],
},
authTimeNotFoundError: {
code: ClientAuthErrorCodes.authTimeNotFound,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.authTimeNotFound],
},
maxAgeTranspired: {
code: ClientAuthErrorCodes.maxAgeTranspired,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.maxAgeTranspired],
},
multipleMatchingTokens: {
code: ClientAuthErrorCodes.multipleMatchingTokens,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.multipleMatchingTokens
],
},
multipleMatchingAccounts: {
code: ClientAuthErrorCodes.multipleMatchingAccounts,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.multipleMatchingAccounts
],
},
multipleMatchingAppMetadata: {
code: ClientAuthErrorCodes.multipleMatchingAppMetadata,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.multipleMatchingAppMetadata
],
},
tokenRequestCannotBeMade: {
code: ClientAuthErrorCodes.requestCannotBeMade,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.requestCannotBeMade],
},
removeEmptyScopeError: {
code: ClientAuthErrorCodes.cannotRemoveEmptyScope,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.cannotRemoveEmptyScope
],
},
appendScopeSetError: {
code: ClientAuthErrorCodes.cannotAppendScopeSet,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.cannotAppendScopeSet
],
},
emptyInputScopeSetError: {
code: ClientAuthErrorCodes.emptyInputScopeSet,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.emptyInputScopeSet],
},
DeviceCodePollingCancelled: {
code: ClientAuthErrorCodes.deviceCodePollingCancelled,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.deviceCodePollingCancelled
],
},
DeviceCodeExpired: {
code: ClientAuthErrorCodes.deviceCodeExpired,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.deviceCodeExpired],
},
DeviceCodeUnknownError: {
code: ClientAuthErrorCodes.deviceCodeUnknownError,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.deviceCodeUnknownError
],
},
NoAccountInSilentRequest: {
code: ClientAuthErrorCodes.noAccountInSilentRequest,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.noAccountInSilentRequest
],
},
invalidCacheRecord: {
code: ClientAuthErrorCodes.invalidCacheRecord,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.invalidCacheRecord],
},
invalidCacheEnvironment: {
code: ClientAuthErrorCodes.invalidCacheEnvironment,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.invalidCacheEnvironment
],
},
noAccountFound: {
code: ClientAuthErrorCodes.noAccountFound,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.noAccountFound],
},
noCryptoObj: {
code: ClientAuthErrorCodes.noCryptoObject,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.noCryptoObject],
},
unexpectedCredentialType: {
code: ClientAuthErrorCodes.unexpectedCredentialType,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.unexpectedCredentialType
],
},
invalidAssertion: {
code: ClientAuthErrorCodes.invalidAssertion,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.invalidAssertion],
},
invalidClientCredential: {
code: ClientAuthErrorCodes.invalidClientCredential,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.invalidClientCredential
],
},
tokenRefreshRequired: {
code: ClientAuthErrorCodes.tokenRefreshRequired,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.tokenRefreshRequired
],
},
userTimeoutReached: {
code: ClientAuthErrorCodes.userTimeoutReached,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.userTimeoutReached],
},
tokenClaimsRequired: {
code: ClientAuthErrorCodes.tokenClaimsCnfRequiredForSignedJwt,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.tokenClaimsCnfRequiredForSignedJwt
],
},
noAuthorizationCodeFromServer: {
code: ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse
],
},
bindingKeyNotRemovedError: {
code: ClientAuthErrorCodes.bindingKeyNotRemoved,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.bindingKeyNotRemoved
],
},
logoutNotSupported: {
code: ClientAuthErrorCodes.endSessionEndpointNotSupported,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.endSessionEndpointNotSupported
],
},
keyIdMissing: {
code: ClientAuthErrorCodes.keyIdMissing,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.keyIdMissing],
},
noNetworkConnectivity: {
code: ClientAuthErrorCodes.noNetworkConnectivity,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.noNetworkConnectivity
],
},
userCanceledError: {
code: ClientAuthErrorCodes.userCanceled,
desc: ClientAuthErrorMessages[ClientAuthErrorCodes.userCanceled],
},
missingTenantIdError: {
code: ClientAuthErrorCodes.missingTenantIdError,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.missingTenantIdError
],
},
nestedAppAuthBridgeDisabled: {
code: ClientAuthErrorCodes.nestedAppAuthBridgeDisabled,
desc: ClientAuthErrorMessages[
ClientAuthErrorCodes.nestedAppAuthBridgeDisabled
],
},
};
/**
* Error thrown when there is an error in the client code running on the browser.
*/
export class ClientAuthError extends AuthError {
constructor(errorCode: string, additionalMessage?: string) {
super(
errorCode,
additionalMessage
? `${ClientAuthErrorMessages[errorCode]}: ${additionalMessage}`
: ClientAuthErrorMessages[errorCode]
);
this.name = "ClientAuthError";
Object.setPrototypeOf(this, ClientAuthError.prototype);
}
}
export function createClientAuthError(
errorCode: string,
additionalMessage?: string
): ClientAuthError {
return new ClientAuthError(errorCode, additionalMessage);
}
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const clientInfoDecodingError = "client_info_decoding_error";
export const clientInfoEmptyError = "client_info_empty_error";
export const tokenParsingError = "token_parsing_error";
export const nullOrEmptyToken = "null_or_empty_token";
export const endpointResolutionError = "endpoints_resolution_error";
export const networkError = "network_error";
export const openIdConfigError = "openid_config_error";
export const hashNotDeserialized = "hash_not_deserialized";
export const invalidState = "invalid_state";
export const stateMismatch = "state_mismatch";
export const stateNotFound = "state_not_found";
export const nonceMismatch = "nonce_mismatch";
export const authTimeNotFound = "auth_time_not_found";
export const maxAgeTranspired = "max_age_transpired";
export const multipleMatchingTokens = "multiple_matching_tokens";
export const multipleMatchingAccounts = "multiple_matching_accounts";
export const multipleMatchingAppMetadata = "multiple_matching_appMetadata";
export const requestCannotBeMade = "request_cannot_be_made";
export const cannotRemoveEmptyScope = "cannot_remove_empty_scope";
export const cannotAppendScopeSet = "cannot_append_scopeset";
export const emptyInputScopeSet = "empty_input_scopeset";
export const deviceCodePollingCancelled = "device_code_polling_cancelled";
export const deviceCodeExpired = "device_code_expired";
export const deviceCodeUnknownError = "device_code_unknown_error";
export const noAccountInSilentRequest = "no_account_in_silent_request";
export const invalidCacheRecord = "invalid_cache_record";
export const invalidCacheEnvironment = "invalid_cache_environment";
export const noAccountFound = "no_account_found";
export const noCryptoObject = "no_crypto_object";
export const unexpectedCredentialType = "unexpected_credential_type";
export const invalidAssertion = "invalid_assertion";
export const invalidClientCredential = "invalid_client_credential";
export const tokenRefreshRequired = "token_refresh_required";
export const userTimeoutReached = "user_timeout_reached";
export const tokenClaimsCnfRequiredForSignedJwt =
"token_claims_cnf_required_for_signedjwt";
export const authorizationCodeMissingFromServerResponse =
"authorization_code_missing_from_server_response";
export const bindingKeyNotRemoved = "binding_key_not_removed";
export const endSessionEndpointNotSupported =
"end_session_endpoint_not_supported";
export const keyIdMissing = "key_id_missing";
export const noNetworkConnectivity = "no_network_connectivity";
export const userCanceled = "user_canceled";
export const missingTenantIdError = "missing_tenant_id_error";
export const methodNotImplemented = "method_not_implemented";
export const nestedAppAuthBridgeDisabled = "nested_app_auth_bridge_disabled";
+210
View File
@@ -0,0 +1,210 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "./AuthError.js";
import * as ClientConfigurationErrorCodes from "./ClientConfigurationErrorCodes.js";
export { ClientConfigurationErrorCodes };
export const ClientConfigurationErrorMessages = {
[ClientConfigurationErrorCodes.redirectUriEmpty]:
"A redirect URI is required for all calls, and none has been set.",
[ClientConfigurationErrorCodes.claimsRequestParsingError]:
"Could not parse the given claims request object.",
[ClientConfigurationErrorCodes.authorityUriInsecure]:
"Authority URIs must use https. Please see here for valid authority configuration options: https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-js-initializing-client-applications#configuration-options",
[ClientConfigurationErrorCodes.urlParseError]:
"URL could not be parsed into appropriate segments.",
[ClientConfigurationErrorCodes.urlEmptyError]: "URL was empty or null.",
[ClientConfigurationErrorCodes.emptyInputScopesError]:
"Scopes cannot be passed as null, undefined or empty array because they are required to obtain an access token.",
[ClientConfigurationErrorCodes.invalidPromptValue]:
"Please see here for valid configuration options: https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_common.html#commonauthorizationurlrequest",
[ClientConfigurationErrorCodes.invalidClaims]:
"Given claims parameter must be a stringified JSON object.",
[ClientConfigurationErrorCodes.tokenRequestEmpty]:
"Token request was empty and not found in cache.",
[ClientConfigurationErrorCodes.logoutRequestEmpty]:
"The logout request was null or undefined.",
[ClientConfigurationErrorCodes.invalidCodeChallengeMethod]:
'code_challenge_method passed is invalid. Valid values are "plain" and "S256".',
[ClientConfigurationErrorCodes.pkceParamsMissing]:
"Both params: code_challenge and code_challenge_method are to be passed if to be sent in the request",
[ClientConfigurationErrorCodes.invalidCloudDiscoveryMetadata]:
"Invalid cloudDiscoveryMetadata provided. Must be a stringified JSON object containing tenant_discovery_endpoint and metadata fields",
[ClientConfigurationErrorCodes.invalidAuthorityMetadata]:
"Invalid authorityMetadata provided. Must by a stringified JSON object containing authorization_endpoint, token_endpoint, issuer fields.",
[ClientConfigurationErrorCodes.untrustedAuthority]:
"The provided authority is not a trusted authority. Please include this authority in the knownAuthorities config parameter.",
[ClientConfigurationErrorCodes.missingSshJwk]:
"Missing sshJwk in SSH certificate request. A stringified JSON Web Key is required when using the SSH authentication scheme.",
[ClientConfigurationErrorCodes.missingSshKid]:
"Missing sshKid in SSH certificate request. A string that uniquely identifies the public SSH key is required when using the SSH authentication scheme.",
[ClientConfigurationErrorCodes.missingNonceAuthenticationHeader]:
"Unable to find an authentication header containing server nonce. Either the Authentication-Info or WWW-Authenticate headers must be present in order to obtain a server nonce.",
[ClientConfigurationErrorCodes.invalidAuthenticationHeader]:
"Invalid authentication header provided",
[ClientConfigurationErrorCodes.cannotSetOIDCOptions]:
"Cannot set OIDCOptions parameter. Please change the protocol mode to OIDC or use a non-Microsoft authority.",
[ClientConfigurationErrorCodes.cannotAllowPlatformBroker]:
"Cannot set allowPlatformBroker parameter to true when not in AAD protocol mode.",
[ClientConfigurationErrorCodes.authorityMismatch]:
"Authority mismatch error. Authority provided in login request or PublicClientApplication config does not match the environment of the provided account. Please use a matching account or make an interactive request to login to this authority.",
};
/**
* ClientConfigurationErrorMessage class containing string constants used by error codes and messages.
* @deprecated Use ClientConfigurationErrorCodes instead
*/
export const ClientConfigurationErrorMessage = {
redirectUriNotSet: {
code: ClientConfigurationErrorCodes.redirectUriEmpty,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.redirectUriEmpty
],
},
claimsRequestParsingError: {
code: ClientConfigurationErrorCodes.claimsRequestParsingError,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.claimsRequestParsingError
],
},
authorityUriInsecure: {
code: ClientConfigurationErrorCodes.authorityUriInsecure,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.authorityUriInsecure
],
},
urlParseError: {
code: ClientConfigurationErrorCodes.urlParseError,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.urlParseError
],
},
urlEmptyError: {
code: ClientConfigurationErrorCodes.urlEmptyError,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.urlEmptyError
],
},
emptyScopesError: {
code: ClientConfigurationErrorCodes.emptyInputScopesError,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.emptyInputScopesError
],
},
invalidPrompt: {
code: ClientConfigurationErrorCodes.invalidPromptValue,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.invalidPromptValue
],
},
invalidClaimsRequest: {
code: ClientConfigurationErrorCodes.invalidClaims,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.invalidClaims
],
},
tokenRequestEmptyError: {
code: ClientConfigurationErrorCodes.tokenRequestEmpty,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.tokenRequestEmpty
],
},
logoutRequestEmptyError: {
code: ClientConfigurationErrorCodes.logoutRequestEmpty,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.logoutRequestEmpty
],
},
invalidCodeChallengeMethod: {
code: ClientConfigurationErrorCodes.invalidCodeChallengeMethod,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.invalidCodeChallengeMethod
],
},
invalidCodeChallengeParams: {
code: ClientConfigurationErrorCodes.pkceParamsMissing,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.pkceParamsMissing
],
},
invalidCloudDiscoveryMetadata: {
code: ClientConfigurationErrorCodes.invalidCloudDiscoveryMetadata,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.invalidCloudDiscoveryMetadata
],
},
invalidAuthorityMetadata: {
code: ClientConfigurationErrorCodes.invalidAuthorityMetadata,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.invalidAuthorityMetadata
],
},
untrustedAuthority: {
code: ClientConfigurationErrorCodes.untrustedAuthority,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.untrustedAuthority
],
},
missingSshJwk: {
code: ClientConfigurationErrorCodes.missingSshJwk,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.missingSshJwk
],
},
missingSshKid: {
code: ClientConfigurationErrorCodes.missingSshKid,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.missingSshKid
],
},
missingNonceAuthenticationHeader: {
code: ClientConfigurationErrorCodes.missingNonceAuthenticationHeader,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.missingNonceAuthenticationHeader
],
},
invalidAuthenticationHeader: {
code: ClientConfigurationErrorCodes.invalidAuthenticationHeader,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.invalidAuthenticationHeader
],
},
cannotSetOIDCOptions: {
code: ClientConfigurationErrorCodes.cannotSetOIDCOptions,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.cannotSetOIDCOptions
],
},
cannotAllowPlatformBroker: {
code: ClientConfigurationErrorCodes.cannotAllowPlatformBroker,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.cannotAllowPlatformBroker
],
},
authorityMismatch: {
code: ClientConfigurationErrorCodes.authorityMismatch,
desc: ClientConfigurationErrorMessages[
ClientConfigurationErrorCodes.authorityMismatch
],
},
};
/**
* Error thrown when there is an error in configuration of the MSAL.js library.
*/
export class ClientConfigurationError extends AuthError {
constructor(errorCode: string) {
super(errorCode, ClientConfigurationErrorMessages[errorCode]);
this.name = "ClientConfigurationError";
Object.setPrototypeOf(this, ClientConfigurationError.prototype);
}
}
export function createClientConfigurationError(
errorCode: string
): ClientConfigurationError {
return new ClientConfigurationError(errorCode);
}
@@ -0,0 +1,28 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const redirectUriEmpty = "redirect_uri_empty";
export const claimsRequestParsingError = "claims_request_parsing_error";
export const authorityUriInsecure = "authority_uri_insecure";
export const urlParseError = "url_parse_error";
export const urlEmptyError = "empty_url_error";
export const emptyInputScopesError = "empty_input_scopes_error";
export const invalidPromptValue = "invalid_prompt_value";
export const invalidClaims = "invalid_claims";
export const tokenRequestEmpty = "token_request_empty";
export const logoutRequestEmpty = "logout_request_empty";
export const invalidCodeChallengeMethod = "invalid_code_challenge_method";
export const pkceParamsMissing = "pkce_params_missing";
export const invalidCloudDiscoveryMetadata = "invalid_cloud_discovery_metadata";
export const invalidAuthorityMetadata = "invalid_authority_metadata";
export const untrustedAuthority = "untrusted_authority";
export const missingSshJwk = "missing_ssh_jwk";
export const missingSshKid = "missing_ssh_kid";
export const missingNonceAuthenticationHeader =
"missing_nonce_authentication_header";
export const invalidAuthenticationHeader = "invalid_authentication_header";
export const cannotSetOIDCOptions = "cannot_set_OIDCOptions";
export const cannotAllowPlatformBroker = "cannot_allow_platform_broker";
export const authorityMismatch = "authority_mismatch";
@@ -0,0 +1,158 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Constants } from "../utils/Constants.js";
import { AuthError } from "./AuthError.js";
import * as InteractionRequiredAuthErrorCodes from "./InteractionRequiredAuthErrorCodes.js";
export { InteractionRequiredAuthErrorCodes };
/**
* InteractionRequiredServerErrorMessage contains string constants used by error codes and messages returned by the server indicating interaction is required
*/
export const InteractionRequiredServerErrorMessage = [
InteractionRequiredAuthErrorCodes.interactionRequired,
InteractionRequiredAuthErrorCodes.consentRequired,
InteractionRequiredAuthErrorCodes.loginRequired,
InteractionRequiredAuthErrorCodes.badToken,
];
export const InteractionRequiredAuthSubErrorMessage = [
"message_only",
"additional_action",
"basic_action",
"user_password_expired",
"consent_required",
"bad_token",
];
const InteractionRequiredAuthErrorMessages = {
[InteractionRequiredAuthErrorCodes.noTokensFound]:
"No refresh token found in the cache. Please sign-in.",
[InteractionRequiredAuthErrorCodes.nativeAccountUnavailable]:
"The requested account is not available in the native broker. It may have been deleted or logged out. Please sign-in again using an interactive API.",
[InteractionRequiredAuthErrorCodes.refreshTokenExpired]:
"Refresh token has expired.",
[InteractionRequiredAuthErrorCodes.badToken]:
"Identity provider returned bad_token due to an expired or invalid refresh token. Please invoke an interactive API to resolve.",
};
/**
* Interaction required errors defined by the SDK
* @deprecated Use InteractionRequiredAuthErrorCodes instead
*/
export const InteractionRequiredAuthErrorMessage = {
noTokensFoundError: {
code: InteractionRequiredAuthErrorCodes.noTokensFound,
desc: InteractionRequiredAuthErrorMessages[
InteractionRequiredAuthErrorCodes.noTokensFound
],
},
native_account_unavailable: {
code: InteractionRequiredAuthErrorCodes.nativeAccountUnavailable,
desc: InteractionRequiredAuthErrorMessages[
InteractionRequiredAuthErrorCodes.nativeAccountUnavailable
],
},
bad_token: {
code: InteractionRequiredAuthErrorCodes.badToken,
desc: InteractionRequiredAuthErrorMessages[
InteractionRequiredAuthErrorCodes.badToken
],
},
};
/**
* Error thrown when user interaction is required.
*/
export class InteractionRequiredAuthError extends AuthError {
/**
* The time the error occured at
*/
timestamp: string;
/**
* TraceId associated with the error
*/
traceId: string;
/**
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/claims-challenge.md
*
* A string with extra claims needed for the token request to succeed
* web site: redirect the user to the authorization page and set the extra claims
* web api: include the claims in the WWW-Authenticate header that are sent back to the client so that it knows to request a token with the extra claims
* desktop application or browser context: include the claims when acquiring the token interactively
* app to app context (client_credentials): include the claims in the AcquireTokenByClientCredential request
*/
claims: string;
/**
* Server error number;
*/
readonly errorNo?: string;
constructor(
errorCode?: string,
errorMessage?: string,
subError?: string,
timestamp?: string,
traceId?: string,
correlationId?: string,
claims?: string,
errorNo?: string
) {
super(errorCode, errorMessage, subError);
Object.setPrototypeOf(this, InteractionRequiredAuthError.prototype);
this.timestamp = timestamp || Constants.EMPTY_STRING;
this.traceId = traceId || Constants.EMPTY_STRING;
this.correlationId = correlationId || Constants.EMPTY_STRING;
this.claims = claims || Constants.EMPTY_STRING;
this.name = "InteractionRequiredAuthError";
this.errorNo = errorNo;
}
}
/**
* Helper function used to determine if an error thrown by the server requires interaction to resolve
* @param errorCode
* @param errorString
* @param subError
*/
export function isInteractionRequiredError(
errorCode?: string,
errorString?: string,
subError?: string
): boolean {
const isInteractionRequiredErrorCode =
!!errorCode &&
InteractionRequiredServerErrorMessage.indexOf(errorCode) > -1;
const isInteractionRequiredSubError =
!!subError &&
InteractionRequiredAuthSubErrorMessage.indexOf(subError) > -1;
const isInteractionRequiredErrorDesc =
!!errorString &&
InteractionRequiredServerErrorMessage.some((irErrorCode) => {
return errorString.indexOf(irErrorCode) > -1;
});
return (
isInteractionRequiredErrorCode ||
isInteractionRequiredErrorDesc ||
isInteractionRequiredSubError
);
}
/**
* Creates an InteractionRequiredAuthError
*/
export function createInteractionRequiredAuthError(
errorCode: string
): InteractionRequiredAuthError {
return new InteractionRequiredAuthError(
errorCode,
InteractionRequiredAuthErrorMessages[errorCode]
);
}
@@ -0,0 +1,15 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
// Codes defined by MSAL
export const noTokensFound = "no_tokens_found";
export const nativeAccountUnavailable = "native_account_unavailable";
export const refreshTokenExpired = "refresh_token_expired";
// Codes potentially returned by server
export const interactionRequired = "interaction_required";
export const consentRequired = "consent_required";
export const loginRequired = "login_required";
export const badToken = "bad_token";
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "./AuthError.js";
import * as JoseHeaderErrorCodes from "./JoseHeaderErrorCodes.js";
export { JoseHeaderErrorCodes };
export const JoseHeaderErrorMessages = {
[JoseHeaderErrorCodes.missingKidError]:
"The JOSE Header for the requested JWT, JWS or JWK object requires a keyId to be configured as the 'kid' header claim. No 'kid' value was provided.",
[JoseHeaderErrorCodes.missingAlgError]:
"The JOSE Header for the requested JWT, JWS or JWK object requires an algorithm to be specified as the 'alg' header claim. No 'alg' value was provided.",
};
/**
* Error thrown when there is an error in the client code running on the browser.
*/
export class JoseHeaderError extends AuthError {
constructor(errorCode: string, errorMessage?: string) {
super(errorCode, errorMessage);
this.name = "JoseHeaderError";
Object.setPrototypeOf(this, JoseHeaderError.prototype);
}
}
/** Returns JoseHeaderError object */
export function createJoseHeaderError(code: string): JoseHeaderError {
return new JoseHeaderError(code, JoseHeaderErrorMessages[code]);
}
+7
View File
@@ -0,0 +1,7 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const missingKidError = "missing_kid_error";
export const missingAlgError = "missing_alg_error";
+44
View File
@@ -0,0 +1,44 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "./AuthError.js";
/**
* Represents network related errors
*/
export class NetworkError extends AuthError {
error: AuthError;
httpStatus?: number;
responseHeaders?: Record<string, string>;
constructor(
error: AuthError,
httpStatus?: number,
responseHeaders?: Record<string, string>
) {
super(error.errorCode, error.errorMessage, error.subError);
Object.setPrototypeOf(this, NetworkError.prototype);
this.name = "NetworkError";
this.error = error;
this.httpStatus = httpStatus;
this.responseHeaders = responseHeaders;
}
}
/**
* Creates NetworkError object for a failed network request
* @param error - Error to be thrown back to the caller
* @param httpStatus - Status code of the network request
* @param responseHeaders - Response headers of the network request, when available
* @returns NetworkError object
*/
export function createNetworkError(
error: AuthError,
httpStatus?: number,
responseHeaders?: Record<string, string>
): NetworkError {
return new NetworkError(error, httpStatus, responseHeaders);
}
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "./AuthError.js";
/**
* Error thrown when there is an error with the server code, for example, unavailability.
*/
export class ServerError extends AuthError {
/**
* Server error number;
*/
readonly errorNo?: string;
/**
* Http status number;
*/
readonly status?: number;
constructor(
errorCode?: string,
errorMessage?: string,
subError?: string,
errorNo?: string,
status?: number
) {
super(errorCode, errorMessage, subError);
this.name = "ServerError";
this.errorNo = errorNo;
this.status = status;
Object.setPrototypeOf(this, ServerError.prototype);
}
}
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export { SignedHttpRequest, ShrOptions } from "./crypto/SignedHttpRequest.js";
export { JoseHeader } from "./crypto/JoseHeader.js";
export { ExternalTokenResponse } from "./response/ExternalTokenResponse.js";
export {
IPerformanceClient,
PerformanceCallbackFunction,
InProgressPerformanceEvent,
QueueMeasurement,
} from "./telemetry/performance/IPerformanceClient.js";
export {
IntFields,
PerformanceEvent,
PerformanceEvents,
PerformanceEventStatus,
SubMeasurement,
} from "./telemetry/performance/PerformanceEvent.js";
export { IPerformanceMeasurement } from "./telemetry/performance/IPerformanceMeasurement.js";
export {
PerformanceClient,
PreQueueEvent,
} from "./telemetry/performance/PerformanceClient.js";
export { StubPerformanceClient } from "./telemetry/performance/StubPerformanceClient.js";
export { PopTokenGenerator } from "./crypto/PopTokenGenerator.js";
+189
View File
@@ -0,0 +1,189 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as AuthToken from "./account/AuthToken.js";
import * as AuthorityFactory from "./authority/AuthorityFactory.js";
import * as CacheHelpers from "./cache/utils/CacheHelpers.js";
import * as TimeUtils from "./utils/TimeUtils.js";
import * as UrlUtils from "./utils/UrlUtils.js";
import * as AADServerParamKeys from "./constants/AADServerParamKeys.js";
export { AuthToken };
export { AuthorityFactory };
export { CacheHelpers };
export { TimeUtils };
export { UrlUtils };
export { AADServerParamKeys };
export { AuthorizationCodeClient } from "./client/AuthorizationCodeClient.js";
export { RefreshTokenClient } from "./client/RefreshTokenClient.js";
export { SilentFlowClient } from "./client/SilentFlowClient.js";
export { BaseClient } from "./client/BaseClient.js";
export {
AuthOptions,
SystemOptions,
LoggerOptions,
CacheOptions,
DEFAULT_SYSTEM_OPTIONS,
AzureCloudOptions,
ApplicationTelemetry,
} from "./config/ClientConfiguration.js";
export { ClientConfiguration } from "./config/ClientConfiguration.js";
export {
AccountInfo,
ActiveAccountFilters,
TenantProfile,
updateAccountTenantProfileData,
tenantIdMatchesHomeTenant,
buildTenantProfile,
} from "./account/AccountInfo.js";
export {
TokenClaims,
getTenantIdFromIdTokenClaims,
} from "./account/TokenClaims.js";
export { TokenClaims as IdTokenClaims } from "./account/TokenClaims.js";
export { CcsCredential, CcsCredentialType } from "./account/CcsCredential.js";
export {
ClientInfo,
buildClientInfo,
buildClientInfoFromHomeAccountId,
} from "./account/ClientInfo.js";
export {
Authority,
formatAuthorityUri,
buildStaticAuthorityOptions,
} from "./authority/Authority.js";
export {
AuthorityOptions,
AzureCloudInstance,
StaticAuthorityOptions,
} from "./authority/AuthorityOptions.js";
export { AuthorityType } from "./authority/AuthorityType.js";
export { ProtocolMode } from "./authority/ProtocolMode.js";
export { OIDCOptions } from "./authority/OIDCOptions.js";
export { CacheManager, DefaultStorageClass } from "./cache/CacheManager.js";
export {
AccountCache,
AccountFilter,
AccessTokenCache,
IdTokenCache,
RefreshTokenCache,
AppMetadataCache,
CredentialFilter,
ValidCacheType,
ValidCredentialType,
TokenKeys,
} from "./cache/utils/CacheTypes.js";
export { CacheRecord } from "./cache/entities/CacheRecord.js";
export { CredentialEntity } from "./cache/entities/CredentialEntity.js";
export { AppMetadataEntity } from "./cache/entities/AppMetadataEntity.js";
export { AccountEntity } from "./cache/entities/AccountEntity.js";
export { IdTokenEntity } from "./cache/entities/IdTokenEntity.js";
export { AccessTokenEntity } from "./cache/entities/AccessTokenEntity.js";
export { RefreshTokenEntity } from "./cache/entities/RefreshTokenEntity.js";
export { ServerTelemetryEntity } from "./cache/entities/ServerTelemetryEntity.js";
export { AuthorityMetadataEntity } from "./cache/entities/AuthorityMetadataEntity.js";
export { ThrottlingEntity } from "./cache/entities/ThrottlingEntity.js";
export {
INetworkModule,
NetworkRequestOptions,
StubbedNetworkModule,
} from "./network/INetworkModule.js";
export { NetworkResponse } from "./network/NetworkResponse.js";
export { ThrottlingUtils } from "./network/ThrottlingUtils.js";
export { RequestThumbprint } from "./network/RequestThumbprint.js";
export { IUri } from "./url/IUri.js";
export { UrlString } from "./url/UrlString.js";
export {
ICrypto,
PkceCodes,
DEFAULT_CRYPTO_IMPLEMENTATION,
SignedHttpRequestParameters,
} from "./crypto/ICrypto.js";
export { BaseAuthRequest } from "./request/BaseAuthRequest.js";
export { CommonAuthorizationUrlRequest } from "./request/CommonAuthorizationUrlRequest.js";
export { CommonAuthorizationCodeRequest } from "./request/CommonAuthorizationCodeRequest.js";
export { CommonRefreshTokenRequest } from "./request/CommonRefreshTokenRequest.js";
export { CommonSilentFlowRequest } from "./request/CommonSilentFlowRequest.js";
export { CommonEndSessionRequest } from "./request/CommonEndSessionRequest.js";
export { RequestParameterBuilder } from "./request/RequestParameterBuilder.js";
export { StoreInCache } from "./request/StoreInCache.js";
export { AzureRegion } from "./authority/AzureRegion.js";
export { AzureRegionConfiguration } from "./authority/AzureRegionConfiguration.js";
export { AuthenticationResult } from "./response/AuthenticationResult.js";
export { AuthorizationCodePayload } from "./response/AuthorizationCodePayload.js";
export { ServerAuthorizationCodeResponse } from "./response/ServerAuthorizationCodeResponse.js";
export { ServerAuthorizationTokenResponse } from "./response/ServerAuthorizationTokenResponse.js";
export {
ResponseHandler,
buildAccountToCache,
} from "./response/ResponseHandler.js";
export { ScopeSet } from "./request/ScopeSet.js";
export { AuthenticationHeaderParser } from "./request/AuthenticationHeaderParser.js";
export { ILoggerCallback, LogLevel, Logger } from "./logger/Logger.js";
export {
InteractionRequiredAuthError,
InteractionRequiredAuthErrorCodes,
InteractionRequiredAuthErrorMessage,
createInteractionRequiredAuthError,
} from "./error/InteractionRequiredAuthError.js";
export {
AuthError,
AuthErrorMessage,
AuthErrorCodes,
createAuthError,
} from "./error/AuthError.js";
export { ServerError } from "./error/ServerError.js";
export { NetworkError, createNetworkError } from "./error/NetworkError.js";
export { CacheError, CacheErrorCodes } from "./error/CacheError.js";
export {
ClientAuthError,
ClientAuthErrorMessage,
ClientAuthErrorCodes,
createClientAuthError,
} from "./error/ClientAuthError.js";
export {
ClientConfigurationError,
ClientConfigurationErrorMessage,
ClientConfigurationErrorCodes,
createClientConfigurationError,
} from "./error/ClientConfigurationError.js";
export {
Constants,
OIDC_DEFAULT_SCOPES,
PromptValue,
PersistentCacheKeys,
ServerResponseType,
ResponseMode,
CacheOutcome,
CredentialType,
CacheType,
CacheAccountType,
AuthenticationScheme,
CodeChallengeMethodValues,
PasswordGrantConstants,
ThrottlingConstants,
ClaimsRequestKeys,
HeaderNames,
Errors,
THE_FAMILY_ID,
ONE_DAY_IN_MS,
GrantType,
AADAuthorityConstants,
HttpStatus,
DEFAULT_TOKEN_RENEWAL_OFFSET_SEC,
JsonWebTokenTypes,
} from "./utils/Constants.js";
export { StringUtils } from "./utils/StringUtils.js";
export { StringDict } from "./utils/MsalTypes.js";
export {
ProtocolUtils,
RequestStateObject,
LibraryStateObject,
} from "./utils/ProtocolUtils.js";
export * from "./utils/FunctionWrappers.js";
export { ServerTelemetryManager } from "./telemetry/server/ServerTelemetryManager.js";
export { ServerTelemetryRequest } from "./telemetry/server/ServerTelemetryRequest.js";
export { version } from "./packageMetadata.js";
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as ClientAssertionUtils from "./utils/ClientAssertionUtils.js";
export { ClientAssertionUtils };
export {
IAppTokenProvider,
AppTokenProviderParameters,
AppTokenProviderResult,
} from "./config/AppTokenProvider.js";
export { INativeBrokerPlugin } from "./broker/nativeBroker/INativeBrokerPlugin.js";
export { ICachePlugin } from "./cache/interface/ICachePlugin.js";
export { TokenCacheContext } from "./cache/persistence/TokenCacheContext.js";
export { ISerializableTokenCache } from "./cache/interface/ISerializableTokenCache.js";
export { CommonClientCredentialRequest } from "./request/CommonClientCredentialRequest.js";
export { CommonOnBehalfOfRequest } from "./request/CommonOnBehalfOfRequest.js";
export { CommonDeviceCodeRequest } from "./request/CommonDeviceCodeRequest.js";
export { CommonUsernamePasswordRequest } from "./request/CommonUsernamePasswordRequest.js";
export { NativeRequest } from "./request/NativeRequest.js";
export { NativeSignOutRequest } from "./request/NativeSignOutRequest.js";
export {
ClientAssertion,
ClientAssertionConfig,
ClientAssertionCallback,
} from "./account/ClientCredentials.js";
export {
DeviceCodeResponse,
ServerDeviceCodeResponse,
} from "./response/DeviceCodeResponse.js";
export { getClientAssertion } from "./utils/ClientAssertionUtils.js";
export { IGuidGenerator } from "./crypto/IGuidGenerator.js";
+11
View File
@@ -0,0 +1,11 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* This file is the entrypoint when importing with the browser subpath e.g. "import { someExport } from @azure/msal-common/browser"
* Additional exports should be added to the applicable exports-*.ts files
*/
export * from "./exports-common.js";
export * from "./exports-browser-only.js";
+11
View File
@@ -0,0 +1,11 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* This file is the entrypoint when importing with the node subpath e.g. "import { someExport } from @azure/msal-common/node"
* Additional exports should be added to the applicable exports-*.ts files
*/
export * from "./exports-common.js";
export * from "./exports-node-only.js";
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* @packageDocumentation
* @module @azure/msal-common
*/
/**
* This file is the entrypoint when importing without a specific subpath e.g. "import { someExport } from @azure/msal-common"
* Additional exports should be added to the applicable exports-*.ts files
*/
export * from "./exports-common.js";
export * from "./exports-browser-only.js";
export * from "./exports-node-only.js";
+272
View File
@@ -0,0 +1,272 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { LoggerOptions } from "../config/ClientConfiguration.js";
import { Constants } from "../utils/Constants.js";
/**
* Options for logger messages.
*/
export type LoggerMessageOptions = {
logLevel: LogLevel;
containsPii?: boolean;
context?: string;
correlationId?: string;
};
/**
* Log message level.
*/
export enum LogLevel {
Error,
Warning,
Info,
Verbose,
Trace,
}
/**
* Callback to send the messages to.
*/
export interface ILoggerCallback {
(level: LogLevel, message: string, containsPii: boolean): void;
}
/**
* Class which facilitates logging of messages to a specific place.
*/
export class Logger {
// Correlation ID for request, usually set by user.
private correlationId: string;
// Current log level, defaults to info.
private level: LogLevel = LogLevel.Info;
// Boolean describing whether PII logging is allowed.
private piiLoggingEnabled: boolean;
// Callback to send messages to.
private localCallback: ILoggerCallback;
// Package name implementing this logger
private packageName: string;
// Package version implementing this logger
private packageVersion: string;
constructor(
loggerOptions: LoggerOptions,
packageName?: string,
packageVersion?: string
) {
const defaultLoggerCallback = () => {
return;
};
const setLoggerOptions =
loggerOptions || Logger.createDefaultLoggerOptions();
this.localCallback =
setLoggerOptions.loggerCallback || defaultLoggerCallback;
this.piiLoggingEnabled = setLoggerOptions.piiLoggingEnabled || false;
this.level =
typeof setLoggerOptions.logLevel === "number"
? setLoggerOptions.logLevel
: LogLevel.Info;
this.correlationId =
setLoggerOptions.correlationId || Constants.EMPTY_STRING;
this.packageName = packageName || Constants.EMPTY_STRING;
this.packageVersion = packageVersion || Constants.EMPTY_STRING;
}
private static createDefaultLoggerOptions(): LoggerOptions {
return {
loggerCallback: () => {
// allow users to not set loggerCallback
},
piiLoggingEnabled: false,
logLevel: LogLevel.Info,
};
}
/**
* Create new Logger with existing configurations.
*/
public clone(
packageName: string,
packageVersion: string,
correlationId?: string
): Logger {
return new Logger(
{
loggerCallback: this.localCallback,
piiLoggingEnabled: this.piiLoggingEnabled,
logLevel: this.level,
correlationId: correlationId || this.correlationId,
},
packageName,
packageVersion
);
}
/**
* Log message with required options.
*/
private logMessage(
logMessage: string,
options: LoggerMessageOptions
): void {
if (
options.logLevel > this.level ||
(!this.piiLoggingEnabled && options.containsPii)
) {
return;
}
const timestamp = new Date().toUTCString();
// Add correlationId to logs if set, correlationId provided on log messages take precedence
const logHeader = `[${timestamp}] : [${
options.correlationId || this.correlationId || ""
}]`;
const log = `${logHeader} : ${this.packageName}@${
this.packageVersion
} : ${LogLevel[options.logLevel]} - ${logMessage}`;
// debug(`msal:${LogLevel[options.logLevel]}${options.containsPii ? "-Pii": Constants.EMPTY_STRING}${options.context ? `:${options.context}` : Constants.EMPTY_STRING}`)(logMessage);
this.executeCallback(
options.logLevel,
log,
options.containsPii || false
);
}
/**
* Execute callback with message.
*/
executeCallback(
level: LogLevel,
message: string,
containsPii: boolean
): void {
if (this.localCallback) {
this.localCallback(level, message, containsPii);
}
}
/**
* Logs error messages.
*/
error(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Error,
containsPii: false,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Logs error messages with PII.
*/
errorPii(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Error,
containsPii: true,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Logs warning messages.
*/
warning(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Warning,
containsPii: false,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Logs warning messages with PII.
*/
warningPii(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Warning,
containsPii: true,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Logs info messages.
*/
info(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Info,
containsPii: false,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Logs info messages with PII.
*/
infoPii(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Info,
containsPii: true,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Logs verbose messages.
*/
verbose(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Verbose,
containsPii: false,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Logs verbose messages with PII.
*/
verbosePii(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Verbose,
containsPii: true,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Logs trace messages.
*/
trace(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Trace,
containsPii: false,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Logs trace messages with PII.
*/
tracePii(message: string, correlationId?: string): void {
this.logMessage(message, {
logLevel: LogLevel.Trace,
containsPii: true,
correlationId: correlationId || Constants.EMPTY_STRING,
});
}
/**
* Returns whether PII Logging is enabled or not.
*/
isPiiLoggingEnabled(): boolean {
return this.piiLoggingEnabled || false;
}
}
+60
View File
@@ -0,0 +1,60 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { NetworkResponse } from "./NetworkResponse.js";
/**
* Options allowed by network request APIs.
*/
export type NetworkRequestOptions = {
headers?: Record<string, string>;
body?: string;
};
/**
* Client network interface to send backend requests.
* @interface
*/
export interface INetworkModule {
/**
* Interface function for async network "GET" requests. Based on the Fetch standard: https://fetch.spec.whatwg.org/
* @param url
* @param requestParams
* @param enableCaching
*/
sendGetRequestAsync<T>(
url: string,
options?: NetworkRequestOptions,
timeout?: number
): Promise<NetworkResponse<T>>;
/**
* Interface function for async network "POST" requests. Based on the Fetch standard: https://fetch.spec.whatwg.org/
* @param url
* @param requestParams
* @param enableCaching
*/
sendPostRequestAsync<T>(
url: string,
options?: NetworkRequestOptions
): Promise<NetworkResponse<T>>;
}
export const StubbedNetworkModule: INetworkModule = {
sendGetRequestAsync: () => {
return Promise.reject(
createClientAuthError(ClientAuthErrorCodes.methodNotImplemented)
);
},
sendPostRequestAsync: () => {
return Promise.reject(
createClientAuthError(ClientAuthErrorCodes.methodNotImplemented)
);
},
};
+10
View File
@@ -0,0 +1,10 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type NetworkResponse<T> = {
headers: Record<string, string>;
body: T;
status: number;
};
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ShrOptions } from "../crypto/SignedHttpRequest.js";
import { AuthenticationScheme } from "../utils/Constants.js";
/**
* Type representing a unique request thumbprint.
*/
export type RequestThumbprint = {
clientId: string;
authority: string;
scopes: Array<string>;
homeAccountIdentifier?: string;
claims?: string;
authenticationScheme?: AuthenticationScheme;
resourceRequestMethod?: string;
resourceRequestUri?: string;
shrClaims?: string;
sshKid?: string;
shrOptions?: ShrOptions;
};
+156
View File
@@ -0,0 +1,156 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { NetworkResponse } from "./NetworkResponse.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import {
HeaderNames,
ThrottlingConstants,
Constants,
} from "../utils/Constants.js";
import { CacheManager } from "../cache/CacheManager.js";
import { ServerError } from "../error/ServerError.js";
import { RequestThumbprint } from "./RequestThumbprint.js";
import { ThrottlingEntity } from "../cache/entities/ThrottlingEntity.js";
import { BaseAuthRequest } from "../request/BaseAuthRequest.js";
/** @internal */
export class ThrottlingUtils {
/**
* Prepares a RequestThumbprint to be stored as a key.
* @param thumbprint
*/
static generateThrottlingStorageKey(thumbprint: RequestThumbprint): string {
return `${ThrottlingConstants.THROTTLING_PREFIX}.${JSON.stringify(
thumbprint
)}`;
}
/**
* Performs necessary throttling checks before a network request.
* @param cacheManager
* @param thumbprint
*/
static preProcess(
cacheManager: CacheManager,
thumbprint: RequestThumbprint
): void {
const key = ThrottlingUtils.generateThrottlingStorageKey(thumbprint);
const value = cacheManager.getThrottlingCache(key);
if (value) {
if (value.throttleTime < Date.now()) {
cacheManager.removeItem(key);
return;
}
throw new ServerError(
value.errorCodes?.join(" ") || Constants.EMPTY_STRING,
value.errorMessage,
value.subError
);
}
}
/**
* Performs necessary throttling checks after a network request.
* @param cacheManager
* @param thumbprint
* @param response
*/
static postProcess(
cacheManager: CacheManager,
thumbprint: RequestThumbprint,
response: NetworkResponse<ServerAuthorizationTokenResponse>
): void {
if (
ThrottlingUtils.checkResponseStatus(response) ||
ThrottlingUtils.checkResponseForRetryAfter(response)
) {
const thumbprintValue: ThrottlingEntity = {
throttleTime: ThrottlingUtils.calculateThrottleTime(
parseInt(response.headers[HeaderNames.RETRY_AFTER])
),
error: response.body.error,
errorCodes: response.body.error_codes,
errorMessage: response.body.error_description,
subError: response.body.suberror,
};
cacheManager.setThrottlingCache(
ThrottlingUtils.generateThrottlingStorageKey(thumbprint),
thumbprintValue
);
}
}
/**
* Checks a NetworkResponse object's status codes against 429 or 5xx
* @param response
*/
static checkResponseStatus(
response: NetworkResponse<ServerAuthorizationTokenResponse>
): boolean {
return (
response.status === 429 ||
(response.status >= 500 && response.status < 600)
);
}
/**
* Checks a NetworkResponse object's RetryAfter header
* @param response
*/
static checkResponseForRetryAfter(
response: NetworkResponse<ServerAuthorizationTokenResponse>
): boolean {
if (response.headers) {
return (
response.headers.hasOwnProperty(HeaderNames.RETRY_AFTER) &&
(response.status < 200 || response.status >= 300)
);
}
return false;
}
/**
* Calculates the Unix-time value for a throttle to expire given throttleTime in seconds.
* @param throttleTime
*/
static calculateThrottleTime(throttleTime: number): number {
const time = throttleTime <= 0 ? 0 : throttleTime;
const currentSeconds = Date.now() / 1000;
return Math.floor(
Math.min(
currentSeconds +
(time || ThrottlingConstants.DEFAULT_THROTTLE_TIME_SECONDS),
currentSeconds +
ThrottlingConstants.DEFAULT_MAX_THROTTLE_TIME_SECONDS
) * 1000
);
}
static removeThrottle(
cacheManager: CacheManager,
clientId: string,
request: BaseAuthRequest,
homeAccountIdentifier?: string
): void {
const thumbprint: RequestThumbprint = {
clientId: clientId,
authority: request.authority,
scopes: request.scopes,
homeAccountIdentifier: homeAccountIdentifier,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
};
const key = this.generateThrottlingStorageKey(thumbprint);
cacheManager.removeItem(key);
}
}
+3
View File
@@ -0,0 +1,3 @@
/* eslint-disable header/header */
export const name = "@azure/msal-common";
export const version = "15.2.0";
@@ -0,0 +1,92 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import { Constants, HeaderNames } from "../utils/Constants.js";
type WWWAuthenticateChallenges = {
nonce?: string;
};
type AuthenticationInfoChallenges = {
nextnonce?: string;
};
/**
* This is a helper class that parses supported HTTP response authentication headers to extract and return
* header challenge values that can be used outside the basic authorization flows.
*/
export class AuthenticationHeaderParser {
private headers: Record<string, string>;
constructor(headers: Record<string, string>) {
this.headers = headers;
}
/**
* This method parses the SHR nonce value out of either the Authentication-Info or WWW-Authenticate authentication headers.
* @returns
*/
getShrNonce(): string {
// Attempt to parse nonce from Authentiacation-Info
const authenticationInfo = this.headers[HeaderNames.AuthenticationInfo];
if (authenticationInfo) {
const authenticationInfoChallenges =
this.parseChallenges<AuthenticationInfoChallenges>(
authenticationInfo
);
if (authenticationInfoChallenges.nextnonce) {
return authenticationInfoChallenges.nextnonce;
}
throw createClientConfigurationError(
ClientConfigurationErrorCodes.invalidAuthenticationHeader
);
}
// Attempt to parse nonce from WWW-Authenticate
const wwwAuthenticate = this.headers[HeaderNames.WWWAuthenticate];
if (wwwAuthenticate) {
const wwwAuthenticateChallenges =
this.parseChallenges<WWWAuthenticateChallenges>(
wwwAuthenticate
);
if (wwwAuthenticateChallenges.nonce) {
return wwwAuthenticateChallenges.nonce;
}
throw createClientConfigurationError(
ClientConfigurationErrorCodes.invalidAuthenticationHeader
);
}
// If neither header is present, throw missing headers error
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingNonceAuthenticationHeader
);
}
/**
* Parses an HTTP header's challenge set into a key/value map.
* @param header
* @returns
*/
private parseChallenges<T>(header: string): T {
const schemeSeparator = header.indexOf(" ");
const challenges = header.substr(schemeSeparator + 1).split(",");
const challengeMap = {} as T;
challenges.forEach((challenge: string) => {
const [key, value] = challenge.split("=");
// Remove escaped quotation marks (', ") from challenge string to keep only the challenge value
challengeMap[key] = unescape(
value.replace(/['"]+/g, Constants.EMPTY_STRING)
);
});
return challengeMap;
}
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthenticationScheme } from "../utils/Constants.js";
import { AzureCloudOptions } from "../config/ClientConfiguration.js";
import { StringDict } from "../utils/MsalTypes.js";
import { StoreInCache } from "./StoreInCache.js";
import { ShrOptions } from "../crypto/SignedHttpRequest.js";
/**
* BaseAuthRequest
* - authority - URL of the authority, the security token service (STS) from which MSAL will acquire tokens. Defaults to https://login.microsoftonline.com/common. If using the same authority for all request, authority should set on client application object and not request, to avoid resolving authority endpoints multiple times.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - scopes - Array of scopes the application is requesting access to.
* - authenticationScheme - The type of token retrieved. Defaults to "Bearer". Can also be type "pop" or "SSH".
* - claims - A stringified claims request which will be added to all /authorize and /token calls
* - shrClaims - A stringified claims object which will be added to a Signed HTTP Request
* - shrNonce - A server-generated timestamp that has been encrypted and base64URL encoded, which will be added to a Signed HTTP Request.
* - shrOptions - An object containing options for the Signed HTTP Request
* - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows.
* - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows.
* - sshJwk - A stringified JSON Web Key representing a public key that can be signed by an SSH certificate.
* - sshKid - Key ID that uniquely identifies the SSH public key mentioned above.
* - azureCloudOptions - Convenience string enums for users to provide public/sovereign cloud ids
* - requestedClaimsHash - SHA 256 hash string of the requested claims string, used as part of an access token cache key so tokens can be filtered by requested claims
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
* - storeInCache - Object containing boolean values indicating whether to store tokens in the cache or not (default is true)
* - scenarioId - Scenario id to track custom user prompts
* - popKid - Key ID to identify the public key for PoP token request
* - embeddedClientId - Embedded client id. When specified, broker client id (brk_client_id) and redirect uri (brk_redirect_uri) params are set with values from the config, overriding the corresponding extra parameters, if present.
*/
export type BaseAuthRequest = {
authority: string;
correlationId: string;
scopes: Array<string>;
authenticationScheme?: AuthenticationScheme;
claims?: string;
shrClaims?: string;
shrNonce?: string;
shrOptions?: ShrOptions;
resourceRequestMethod?: string;
resourceRequestUri?: string;
sshJwk?: string;
sshKid?: string;
azureCloudOptions?: AzureCloudOptions;
requestedClaimsHash?: string;
maxAge?: number;
tokenQueryParameters?: StringDict;
storeInCache?: StoreInCache;
scenarioId?: string;
popKid?: string;
embeddedClientId?: string;
};
@@ -0,0 +1,33 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseAuthRequest } from "./BaseAuthRequest.js";
import { StringDict } from "../utils/MsalTypes.js";
import { CcsCredential } from "../account/CcsCredential.js";
/**
* Request object passed by user to acquire a token from the server exchanging a valid authorization code (second leg of OAuth2.0 Authorization Code flow)
*
* - scopes - Array of scopes the application is requesting access to.
* - claims - A stringified claims request which will be added to all /authorize and /token calls
* - authority: - URL of the authority, the security token service (STS) from which MSAL will acquire tokens. If authority is set on client application object, this will override that value. Overriding the value will cause for authority validation to happen each time. If the same authority will be used for all request, set on the application object instead of the requests.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - redirectUri - The redirect URI of your app, where the authority will redirect to after the user inputs credentials and consents. It must exactly match one of the redirect URIs you registered in the portal
* - code - The authorization_code that the user acquired in the first leg of the flow.
* - codeVerifier - The same code_verifier that was used to obtain the authorization_code. Required if PKCE was used in the authorization code grant request.For more information, see the PKCE RFC: https://tools.ietf.org/html/rfc7636
* - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows.
* - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows.
* - enableSpaAuthCode - Enables the acqusition of a spa authorization code (confidential clients only)
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
*/
export type CommonAuthorizationCodeRequest = BaseAuthRequest & {
code: string;
redirectUri: string;
codeVerifier?: string;
tokenBodyParameters?: StringDict;
enableSpaAuthorizationCode?: boolean;
clientInfo?: string;
ccsCredential?: CcsCredential;
};
@@ -0,0 +1,56 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ResponseMode } from "../utils/Constants.js";
import { StringDict } from "../utils/MsalTypes.js";
import { BaseAuthRequest } from "./BaseAuthRequest.js";
import { AccountInfo } from "../account/AccountInfo.js";
/**
* Request object passed by user to retrieve a Code from the server (first leg of authorization code grant flow)
*
* - scopes - Array of scopes the application is requesting access to.
* - claims - A stringified claims request which will be added to all /authorize and /token calls
* - authority - Url of the authority which the application acquires tokens from.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - redirectUri - The redirect URI where authentication responses can be received by your application. It must exactly match one of the redirect URIs registered in the Azure portal.
* - extraScopesToConsent - Scopes for a different resource when the user needs consent upfront.
* - responseMode - Specifies the method that should be used to send the authentication result to your app. Can be query, form_post, or fragment. If no value is passed in, it defaults to query.
* - codeChallenge - Used to secure authorization code grant via Proof of Key for Code Exchange (PKCE). For more information, see the PKCE RCF:https://tools.ietf.org/html/rfc7636
* - codeChallengeMethod - The method used to encode the code verifier for the code challenge parameter. Can be "plain" or "S256". If excluded, code challenge is assumed to be plaintext. For more information, see the PKCE RCF: https://tools.ietf.org/html/rfc7636
* - state - A value included in the request that is also returned in the token response. A randomly generated unique value is typically used for preventing cross site request forgery attacks. The state is also used to encode information about the user's state in the app before the authentication request occurred.
* - prompt - Indicates the type of user interaction that is required.
* login: will force the user to enter their credentials on that request, negating single-sign on
* none: will ensure that the user isn't presented with any interactive prompt. if request can't be completed via single-sign on, the endpoint will return an interaction_required error
* consent: will the trigger the OAuth consent dialog after the user signs in, asking the user to grant permissions to the app
* select_account: will interrupt single sign-=on providing account selection experience listing all the accounts in session or any remembered accounts or an option to choose to use a different account
* create: will direct the user to the account creation experience instead of the log in experience
* no_session: will not read existing session token when authenticating the user. Upon user being successfully authenticated, EVO wont create a new session for the user. FOR INTERNAL USE ONLY.
* - account - AccountInfo obtained from a getAccount API. Will be used in certain scenarios to generate login_hint if both loginHint and sid params are not provided.
* - loginHint - Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know the username/email address ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the preferred_username claim.
* - sid - Session ID, unique identifier for the session. Available as an optional claim on ID tokens.
* - domainHint - Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain hint is a registered domain for the tenant.
* - extraQueryParameters - String to string map of custom query parameters added to the /authorize call
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
* - nonce - A value included in the request that is returned in the id token. A randomly generated unique value is typically used to mitigate replay attacks.
* - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows.
* - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows.
*/
export type CommonAuthorizationUrlRequest = BaseAuthRequest & {
redirectUri: string;
responseMode: ResponseMode;
account?: AccountInfo;
codeChallenge?: string;
codeChallengeMethod?: string;
domainHint?: string;
extraQueryParameters?: StringDict;
extraScopesToConsent?: Array<string>;
loginHint?: string;
nonce?: string;
prompt?: string;
sid?: string;
state?: string;
platformBroker?: boolean;
};
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseAuthRequest } from "./BaseAuthRequest.js";
import { AzureRegion } from "../authority/AzureRegion.js";
import { ClientAssertion } from "../account/ClientCredentials.js";
/**
* CommonClientCredentialRequest
* - scopes - Array of scopes the application is requesting access to.
* - authority - URL of the authority, the security token service (STS) from which MSAL will acquire tokens.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - skipCache - Skip token cache lookup and force request to authority to get a a new token. Defaults to false.
* - preferredAzureRegionOptions - Options of the user's preferred azure region
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
*/
export type CommonClientCredentialRequest = BaseAuthRequest & {
skipCache?: boolean;
azureRegion?: AzureRegion;
clientAssertion?: ClientAssertion;
};
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { DeviceCodeResponse } from "../response/DeviceCodeResponse.js";
import { StringDict } from "../utils/MsalTypes.js";
import { BaseAuthRequest } from "./BaseAuthRequest.js";
/**
* Parameters for Oauth2 device code flow.
* - scopes - Array of scopes the application is requesting access to.
* - authority: - URL of the authority, the security token service (STS) from which MSAL will acquire tokens. If authority is set on client application object, this will override that value. Overriding the value will cause for authority validation to happen each time. If the same authority will be used for all request, set on the application object instead of the requests.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - deviceCodeCallback - Callback containing device code response. Message should be shown to end user. End user can then navigate to the verification_uri, input the user_code, and input credentials.
* - cancel - Boolean to cancel polling of device code endpoint. While the user authenticates on a separate device, MSAL polls the the token endpoint of security token service for the interval specified in the device code response (usually 15 minutes). To stop polling and cancel the request, set cancel=true.
* - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows.
* - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows.
* - timeout - Timeout period in seconds which the user explicitly configures for the polling of the device code endpoint. At the end of this period; assuming the device code has not expired yet; the device code polling is stopped and the request cancelled. The device code expiration window will always take precedence over this set period.
* - extraQueryParameters - String to string map of custom query parameters added to the query string
*/
// export type CommonDeviceCodeRequest = BaseAuthRequest & {
export type CommonDeviceCodeRequest = Omit<
BaseAuthRequest,
"tokenQueryParameters"
> & {
deviceCodeCallback: (response: DeviceCodeResponse) => void;
cancel?: boolean;
timeout?: number;
extraQueryParameters?: StringDict;
};
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountInfo } from "../account/AccountInfo.js";
import { StringDict } from "../utils/MsalTypes.js";
/**
* CommonEndSessionRequest
* - account - Account object that will be logged out of. All tokens tied to this account will be cleared.
* - postLogoutRedirectUri - URI to navigate to after logout page.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - idTokenHint - ID Token used by B2C to validate logout if required by the policy
* - state - A value included in the request to the logout endpoint which will be returned in the query string upon post logout redirection
* - logoutHint - A string that specifies the account that is being logged out in order to skip the server account picker on logout
* - extraQueryParameters - String to string map of custom query parameters added to the /authorize call
*/
export type CommonEndSessionRequest = {
correlationId: string;
account?: AccountInfo | null;
postLogoutRedirectUri?: string | null;
idTokenHint?: string;
state?: string;
logoutHint?: string;
extraQueryParameters?: StringDict;
};
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseAuthRequest } from "./BaseAuthRequest.js";
/**
* - scopes - Array of scopes the application is requesting access to.
* - authority - URL of the authority, the security token service (STS) from which MSAL will acquire tokens.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - oboAssertion - The access token that was sent to the middle-tier API. This token must have an audience of the app making this OBO request.
* - skipCache - Skip token cache lookup and force request to authority to get a a new token. Defaults to false.
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
*/
export type CommonOnBehalfOfRequest = BaseAuthRequest & {
oboAssertion: string;
skipCache?: boolean;
};
@@ -0,0 +1,28 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseAuthRequest } from "./BaseAuthRequest.js";
import { CcsCredential } from "../account/CcsCredential.js";
import { StringDict } from "../utils/MsalTypes.js";
/**
* CommonRefreshTokenRequest
* - scopes - Array of scopes the application is requesting access to.
* - claims - A stringified claims request which will be added to all /authorize and /token calls
* - authority - URL of the authority, the security token service (STS) from which MSAL will acquire tokens.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - refreshToken - A refresh token returned from a previous request to the Identity provider.
* - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows.
* - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows.
* - forceCache - Force MSAL to cache a refresh token flow response when there is no account in the cache. Used for migration scenarios.
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
*/
export type CommonRefreshTokenRequest = BaseAuthRequest & {
refreshToken: string;
ccsCredential?: CcsCredential;
forceCache?: boolean;
tokenBodyParameters?: StringDict;
redirectUri?: string;
};
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountInfo } from "../account/AccountInfo.js";
import { StringDict } from "../utils/MsalTypes.js";
import { BaseAuthRequest } from "./BaseAuthRequest.js";
/**
* SilentFlow parameters passed by the user to retrieve credentials silently
* - scopes - Array of scopes the application is requesting access to.
* - claims - A stringified claims request which will be added to all /authorize and /token calls. When included on a silent request, cache lookup will be skipped and token will be refreshed.
* - authority - Url of the authority which the application acquires tokens from.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - account - Account entity to lookup the credentials.
* - forceRefresh - Forces silent requests to make network calls if true.
* - resourceRequestMethod - HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows.
* - resourceRequestUri - URI that token will be used for. Used for proof-of-possession flows.
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
*/
export type CommonSilentFlowRequest = BaseAuthRequest & {
/** Account object to lookup the credentials */
account: AccountInfo;
/** Skip cache lookup and forces network call(s) to get fresh tokens */
forceRefresh: boolean;
/** RedirectUri registered on the app registration - only required in brokering scenarios */
redirectUri?: string;
/** Key value pairs to include on the POST body to the /token endpoint */
tokenBodyParameters?: StringDict;
/** If refresh token will expire within the configured value, consider it already expired. Used to pre-emptively invoke interaction when cached refresh token is close to expiry. */
refreshTokenExpirationOffsetSeconds?: number;
};
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseAuthRequest } from "./BaseAuthRequest.js";
/**
* CommonUsernamePassword parameters passed by the user to retrieve credentials
* Note: The latest OAuth 2.0 Security Best Current Practice disallows the password grant entirely. This flow is added for internal testing.
*
* - scopes - Array of scopes the application is requesting access to.
* - claims - A stringified claims request which will be added to all /authorize and /token calls. When included on a silent request, cache lookup will be skipped and token will be refreshed.
* - authority - Url of the authority which the application acquires tokens from.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - username - username of the client
* - password - credentials
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
*/
export type CommonUsernamePasswordRequest = BaseAuthRequest & {
username: string;
password: string;
};
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { StringDict } from "../utils/MsalTypes.js";
export type NativeRequest = {
clientId: string;
authority: string;
correlationId: string;
redirectUri: string;
scopes: Array<string>;
claims?: string;
authenticationScheme?: string;
resourceRequestMethod?: string;
resourceRequestUri?: string;
shrNonce?: string;
accountId?: string;
forceRefresh?: boolean;
extraParameters?: StringDict;
extraScopesToConsent?: Array<string>;
loginHint?: string;
prompt?: string;
};
+10
View File
@@ -0,0 +1,10 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type NativeSignOutRequest = {
clientId: string;
accountId: string;
correlationId: string;
};
+663
View File
@@ -0,0 +1,663 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
Constants,
ResponseMode,
CLIENT_INFO,
AuthenticationScheme,
ClaimsRequestKeys,
PasswordGrantConstants,
OIDC_DEFAULT_SCOPES,
ThrottlingConstants,
HeaderNames,
} from "../utils/Constants.js";
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
import { ScopeSet } from "./ScopeSet.js";
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import { StringDict } from "../utils/MsalTypes.js";
import { RequestValidator } from "./RequestValidator.js";
import {
ApplicationTelemetry,
LibraryInfo,
} from "../config/ClientConfiguration.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { ClientInfo } from "../account/ClientInfo.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
function instrumentBrokerParams(
parameters: Map<string, string>,
correlationId?: string,
performanceClient?: IPerformanceClient
) {
if (!correlationId) {
return;
}
const clientId = parameters.get(AADServerParamKeys.CLIENT_ID);
if (clientId && parameters.has(AADServerParamKeys.BROKER_CLIENT_ID)) {
performanceClient?.addFields(
{
embeddedClientId: clientId,
embeddedRedirectUri: parameters.get(
AADServerParamKeys.REDIRECT_URI
),
},
correlationId
);
}
}
/** @internal */
export class RequestParameterBuilder {
private parameters: Map<string, string>;
private readonly performanceClient?: IPerformanceClient;
private readonly correlationId?: string;
constructor(
correlationId?: string,
performanceClient?: IPerformanceClient
) {
this.parameters = new Map<string, string>();
this.performanceClient = performanceClient;
this.correlationId = correlationId;
}
/**
* add response_type = code
*/
addResponseTypeCode(): void {
this.parameters.set(
AADServerParamKeys.RESPONSE_TYPE,
encodeURIComponent(Constants.CODE_RESPONSE_TYPE)
);
}
/**
* add response_type = token id_token
*/
addResponseTypeForTokenAndIdToken(): void {
this.parameters.set(
AADServerParamKeys.RESPONSE_TYPE,
encodeURIComponent(
`${Constants.TOKEN_RESPONSE_TYPE} ${Constants.ID_TOKEN_RESPONSE_TYPE}`
)
);
}
/**
* add response_mode. defaults to query.
* @param responseMode
*/
addResponseMode(responseMode?: ResponseMode): void {
this.parameters.set(
AADServerParamKeys.RESPONSE_MODE,
encodeURIComponent(responseMode ? responseMode : ResponseMode.QUERY)
);
}
/**
* Add flag to indicate STS should attempt to use WAM if available
*/
addNativeBroker(): void {
this.parameters.set(
AADServerParamKeys.NATIVE_BROKER,
encodeURIComponent("1")
);
}
/**
* add scopes. set addOidcScopes to false to prevent default scopes in non-user scenarios
* @param scopeSet
* @param addOidcScopes
*/
addScopes(
scopes: string[],
addOidcScopes: boolean = true,
defaultScopes: Array<string> = OIDC_DEFAULT_SCOPES
): void {
// Always add openid to the scopes when adding OIDC scopes
if (
addOidcScopes &&
!defaultScopes.includes("openid") &&
!scopes.includes("openid")
) {
defaultScopes.push("openid");
}
const requestScopes = addOidcScopes
? [...(scopes || []), ...defaultScopes]
: scopes || [];
const scopeSet = new ScopeSet(requestScopes);
this.parameters.set(
AADServerParamKeys.SCOPE,
encodeURIComponent(scopeSet.printScopes())
);
}
/**
* add clientId
* @param clientId
*/
addClientId(clientId: string): void {
this.parameters.set(
AADServerParamKeys.CLIENT_ID,
encodeURIComponent(clientId)
);
}
/**
* add redirect_uri
* @param redirectUri
*/
addRedirectUri(redirectUri: string): void {
RequestValidator.validateRedirectUri(redirectUri);
this.parameters.set(
AADServerParamKeys.REDIRECT_URI,
encodeURIComponent(redirectUri)
);
}
/**
* add post logout redirectUri
* @param redirectUri
*/
addPostLogoutRedirectUri(redirectUri: string): void {
RequestValidator.validateRedirectUri(redirectUri);
this.parameters.set(
AADServerParamKeys.POST_LOGOUT_URI,
encodeURIComponent(redirectUri)
);
}
/**
* add id_token_hint to logout request
* @param idTokenHint
*/
addIdTokenHint(idTokenHint: string): void {
this.parameters.set(
AADServerParamKeys.ID_TOKEN_HINT,
encodeURIComponent(idTokenHint)
);
}
/**
* add domain_hint
* @param domainHint
*/
addDomainHint(domainHint: string): void {
this.parameters.set(
AADServerParamKeys.DOMAIN_HINT,
encodeURIComponent(domainHint)
);
}
/**
* add login_hint
* @param loginHint
*/
addLoginHint(loginHint: string): void {
this.parameters.set(
AADServerParamKeys.LOGIN_HINT,
encodeURIComponent(loginHint)
);
}
/**
* Adds the CCS (Cache Credential Service) query parameter for login_hint
* @param loginHint
*/
addCcsUpn(loginHint: string): void {
this.parameters.set(
HeaderNames.CCS_HEADER,
encodeURIComponent(`UPN:${loginHint}`)
);
}
/**
* Adds the CCS (Cache Credential Service) query parameter for account object
* @param loginHint
*/
addCcsOid(clientInfo: ClientInfo): void {
this.parameters.set(
HeaderNames.CCS_HEADER,
encodeURIComponent(`Oid:${clientInfo.uid}@${clientInfo.utid}`)
);
}
/**
* add sid
* @param sid
*/
addSid(sid: string): void {
this.parameters.set(AADServerParamKeys.SID, encodeURIComponent(sid));
}
/**
* add claims
* @param claims
*/
addClaims(claims?: string, clientCapabilities?: Array<string>): void {
const mergedClaims = this.addClientCapabilitiesToClaims(
claims,
clientCapabilities
);
RequestValidator.validateClaims(mergedClaims);
this.parameters.set(
AADServerParamKeys.CLAIMS,
encodeURIComponent(mergedClaims)
);
}
/**
* add correlationId
* @param correlationId
*/
addCorrelationId(correlationId: string): void {
this.parameters.set(
AADServerParamKeys.CLIENT_REQUEST_ID,
encodeURIComponent(correlationId)
);
}
/**
* add library info query params
* @param libraryInfo
*/
addLibraryInfo(libraryInfo: LibraryInfo): void {
// Telemetry Info
this.parameters.set(AADServerParamKeys.X_CLIENT_SKU, libraryInfo.sku);
this.parameters.set(
AADServerParamKeys.X_CLIENT_VER,
libraryInfo.version
);
if (libraryInfo.os) {
this.parameters.set(AADServerParamKeys.X_CLIENT_OS, libraryInfo.os);
}
if (libraryInfo.cpu) {
this.parameters.set(
AADServerParamKeys.X_CLIENT_CPU,
libraryInfo.cpu
);
}
}
/**
* Add client telemetry parameters
* @param appTelemetry
*/
addApplicationTelemetry(appTelemetry: ApplicationTelemetry): void {
if (appTelemetry?.appName) {
this.parameters.set(
AADServerParamKeys.X_APP_NAME,
appTelemetry.appName
);
}
if (appTelemetry?.appVersion) {
this.parameters.set(
AADServerParamKeys.X_APP_VER,
appTelemetry.appVersion
);
}
}
/**
* add prompt
* @param prompt
*/
addPrompt(prompt: string): void {
RequestValidator.validatePrompt(prompt);
this.parameters.set(
`${AADServerParamKeys.PROMPT}`,
encodeURIComponent(prompt)
);
}
/**
* add state
* @param state
*/
addState(state: string): void {
if (state) {
this.parameters.set(
AADServerParamKeys.STATE,
encodeURIComponent(state)
);
}
}
/**
* add nonce
* @param nonce
*/
addNonce(nonce: string): void {
this.parameters.set(
AADServerParamKeys.NONCE,
encodeURIComponent(nonce)
);
}
/**
* add code_challenge and code_challenge_method
* - throw if either of them are not passed
* @param codeChallenge
* @param codeChallengeMethod
*/
addCodeChallengeParams(
codeChallenge: string,
codeChallengeMethod: string
): void {
RequestValidator.validateCodeChallengeParams(
codeChallenge,
codeChallengeMethod
);
if (codeChallenge && codeChallengeMethod) {
this.parameters.set(
AADServerParamKeys.CODE_CHALLENGE,
encodeURIComponent(codeChallenge)
);
this.parameters.set(
AADServerParamKeys.CODE_CHALLENGE_METHOD,
encodeURIComponent(codeChallengeMethod)
);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.pkceParamsMissing
);
}
}
/**
* add the `authorization_code` passed by the user to exchange for a token
* @param code
*/
addAuthorizationCode(code: string): void {
this.parameters.set(AADServerParamKeys.CODE, encodeURIComponent(code));
}
/**
* add the `authorization_code` passed by the user to exchange for a token
* @param code
*/
addDeviceCode(code: string): void {
this.parameters.set(
AADServerParamKeys.DEVICE_CODE,
encodeURIComponent(code)
);
}
/**
* add the `refreshToken` passed by the user
* @param refreshToken
*/
addRefreshToken(refreshToken: string): void {
this.parameters.set(
AADServerParamKeys.REFRESH_TOKEN,
encodeURIComponent(refreshToken)
);
}
/**
* add the `code_verifier` passed by the user to exchange for a token
* @param codeVerifier
*/
addCodeVerifier(codeVerifier: string): void {
this.parameters.set(
AADServerParamKeys.CODE_VERIFIER,
encodeURIComponent(codeVerifier)
);
}
/**
* add client_secret
* @param clientSecret
*/
addClientSecret(clientSecret: string): void {
this.parameters.set(
AADServerParamKeys.CLIENT_SECRET,
encodeURIComponent(clientSecret)
);
}
/**
* add clientAssertion for confidential client flows
* @param clientAssertion
*/
addClientAssertion(clientAssertion: string): void {
if (clientAssertion) {
this.parameters.set(
AADServerParamKeys.CLIENT_ASSERTION,
encodeURIComponent(clientAssertion)
);
}
}
/**
* add clientAssertionType for confidential client flows
* @param clientAssertionType
*/
addClientAssertionType(clientAssertionType: string): void {
if (clientAssertionType) {
this.parameters.set(
AADServerParamKeys.CLIENT_ASSERTION_TYPE,
encodeURIComponent(clientAssertionType)
);
}
}
/**
* add OBO assertion for confidential client flows
* @param clientAssertion
*/
addOboAssertion(oboAssertion: string): void {
this.parameters.set(
AADServerParamKeys.OBO_ASSERTION,
encodeURIComponent(oboAssertion)
);
}
/**
* add grant type
* @param grantType
*/
addRequestTokenUse(tokenUse: string): void {
this.parameters.set(
AADServerParamKeys.REQUESTED_TOKEN_USE,
encodeURIComponent(tokenUse)
);
}
/**
* add grant type
* @param grantType
*/
addGrantType(grantType: string): void {
this.parameters.set(
AADServerParamKeys.GRANT_TYPE,
encodeURIComponent(grantType)
);
}
/**
* add client info
*
*/
addClientInfo(): void {
this.parameters.set(CLIENT_INFO, "1");
}
/**
* add extraQueryParams
* @param eQParams
*/
addExtraQueryParameters(eQParams: StringDict): void {
Object.entries(eQParams).forEach(([key, value]) => {
if (!this.parameters.has(key) && value) {
this.parameters.set(key, value);
}
});
}
addClientCapabilitiesToClaims(
claims?: string,
clientCapabilities?: Array<string>
): string {
let mergedClaims: object;
// Parse provided claims into JSON object or initialize empty object
if (!claims) {
mergedClaims = {};
} else {
try {
mergedClaims = JSON.parse(claims);
} catch (e) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.invalidClaims
);
}
}
if (clientCapabilities && clientCapabilities.length > 0) {
if (!mergedClaims.hasOwnProperty(ClaimsRequestKeys.ACCESS_TOKEN)) {
// Add access_token key to claims object
mergedClaims[ClaimsRequestKeys.ACCESS_TOKEN] = {};
}
// Add xms_cc claim with provided clientCapabilities to access_token key
mergedClaims[ClaimsRequestKeys.ACCESS_TOKEN][
ClaimsRequestKeys.XMS_CC
] = {
values: clientCapabilities,
};
}
return JSON.stringify(mergedClaims);
}
/**
* adds `username` for Password Grant flow
* @param username
*/
addUsername(username: string): void {
this.parameters.set(
PasswordGrantConstants.username,
encodeURIComponent(username)
);
}
/**
* adds `password` for Password Grant flow
* @param password
*/
addPassword(password: string): void {
this.parameters.set(
PasswordGrantConstants.password,
encodeURIComponent(password)
);
}
/**
* add pop_jwk to query params
* @param cnfString
*/
addPopToken(cnfString: string): void {
if (cnfString) {
this.parameters.set(
AADServerParamKeys.TOKEN_TYPE,
AuthenticationScheme.POP
);
this.parameters.set(
AADServerParamKeys.REQ_CNF,
encodeURIComponent(cnfString)
);
}
}
/**
* add SSH JWK and key ID to query params
*/
addSshJwk(sshJwkString: string): void {
if (sshJwkString) {
this.parameters.set(
AADServerParamKeys.TOKEN_TYPE,
AuthenticationScheme.SSH
);
this.parameters.set(
AADServerParamKeys.REQ_CNF,
encodeURIComponent(sshJwkString)
);
}
}
/**
* add server telemetry fields
* @param serverTelemetryManager
*/
addServerTelemetry(serverTelemetryManager: ServerTelemetryManager): void {
this.parameters.set(
AADServerParamKeys.X_CLIENT_CURR_TELEM,
serverTelemetryManager.generateCurrentRequestHeaderValue()
);
this.parameters.set(
AADServerParamKeys.X_CLIENT_LAST_TELEM,
serverTelemetryManager.generateLastRequestHeaderValue()
);
}
/**
* Adds parameter that indicates to the server that throttling is supported
*/
addThrottling(): void {
this.parameters.set(
AADServerParamKeys.X_MS_LIB_CAPABILITY,
ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE
);
}
/**
* Adds logout_hint parameter for "silent" logout which prevent server account picker
*/
addLogoutHint(logoutHint: string): void {
this.parameters.set(
AADServerParamKeys.LOGOUT_HINT,
encodeURIComponent(logoutHint)
);
}
addBrokerParameters(params: {
brokerClientId: string;
brokerRedirectUri: string;
}): void {
const brokerParams: StringDict = {};
brokerParams[AADServerParamKeys.BROKER_CLIENT_ID] =
params.brokerClientId;
brokerParams[AADServerParamKeys.BROKER_REDIRECT_URI] =
params.brokerRedirectUri;
this.addExtraQueryParameters(brokerParams);
}
/**
* Utility to create a URL from the params map
*/
createQueryString(): string {
const queryParameterArray: Array<string> = new Array<string>();
this.parameters.forEach((value, key) => {
queryParameterArray.push(`${key}=${value}`);
});
instrumentBrokerParams(
this.parameters,
this.correlationId,
this.performanceClient
);
return queryParameterArray.join("&");
}
}
+90
View File
@@ -0,0 +1,90 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import { PromptValue, CodeChallengeMethodValues } from "../utils/Constants.js";
/**
* Validates server consumable params from the "request" objects
*/
export class RequestValidator {
/**
* Utility to check if the `redirectUri` in the request is a non-null value
* @param redirectUri
*/
static validateRedirectUri(redirectUri: string): void {
if (!redirectUri) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.redirectUriEmpty
);
}
}
/**
* Utility to validate prompt sent by the user in the request
* @param prompt
*/
static validatePrompt(prompt: string): void {
const promptValues = [];
for (const value in PromptValue) {
promptValues.push(PromptValue[value]);
}
if (promptValues.indexOf(prompt) < 0) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.invalidPromptValue
);
}
}
static validateClaims(claims: string): void {
try {
JSON.parse(claims);
} catch (e) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.invalidClaims
);
}
}
/**
* Utility to validate code_challenge and code_challenge_method
* @param codeChallenge
* @param codeChallengeMethod
*/
static validateCodeChallengeParams(
codeChallenge: string,
codeChallengeMethod: string
): void {
if (!codeChallenge || !codeChallengeMethod) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.pkceParamsMissing
);
} else {
this.validateCodeChallengeMethod(codeChallengeMethod);
}
}
/**
* Utility to validate code_challenge_method
* @param codeChallengeMethod
*/
static validateCodeChallengeMethod(codeChallengeMethod: string): void {
if (
[
CodeChallengeMethodValues.PLAIN,
CodeChallengeMethodValues.S256,
].indexOf(codeChallengeMethod) < 0
) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.invalidCodeChallengeMethod
);
}
}
}
+236
View File
@@ -0,0 +1,236 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import { StringUtils } from "../utils/StringUtils.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { Constants, OIDC_SCOPES } from "../utils/Constants.js";
/**
* The ScopeSet class creates a set of scopes. Scopes are case-insensitive, unique values, so the Set object in JS makes
* the most sense to implement for this class. All scopes are trimmed and converted to lower case strings in intersection and union functions
* to ensure uniqueness of strings.
*/
export class ScopeSet {
// Scopes as a Set of strings
private scopes: Set<string>;
constructor(inputScopes: Array<string>) {
// Filter empty string and null/undefined array items
const scopeArr = inputScopes
? StringUtils.trimArrayEntries([...inputScopes])
: [];
const filteredInput = scopeArr
? StringUtils.removeEmptyStringsFromArray(scopeArr)
: [];
// Check if scopes array has at least one member
if (!filteredInput || !filteredInput.length) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.emptyInputScopesError
);
}
this.scopes = new Set<string>(); // Iterator in constructor not supported by IE11
filteredInput.forEach((scope) => this.scopes.add(scope));
}
/**
* Factory method to create ScopeSet from space-delimited string
* @param inputScopeString
* @param appClientId
* @param scopesRequired
*/
static fromString(inputScopeString: string): ScopeSet {
const scopeString = inputScopeString || Constants.EMPTY_STRING;
const inputScopes: Array<string> = scopeString.split(" ");
return new ScopeSet(inputScopes);
}
/**
* Creates the set of scopes to search for in cache lookups
* @param inputScopeString
* @returns
*/
static createSearchScopes(inputScopeString: Array<string>): ScopeSet {
const scopeSet = new ScopeSet(inputScopeString);
if (!scopeSet.containsOnlyOIDCScopes()) {
scopeSet.removeOIDCScopes();
} else {
scopeSet.removeScope(Constants.OFFLINE_ACCESS_SCOPE);
}
return scopeSet;
}
/**
* Check if a given scope is present in this set of scopes.
* @param scope
*/
containsScope(scope: string): boolean {
const lowerCaseScopes = this.printScopesLowerCase().split(" ");
const lowerCaseScopesSet = new ScopeSet(lowerCaseScopes);
// compare lowercase scopes
return scope
? lowerCaseScopesSet.scopes.has(scope.toLowerCase())
: false;
}
/**
* Check if a set of scopes is present in this set of scopes.
* @param scopeSet
*/
containsScopeSet(scopeSet: ScopeSet): boolean {
if (!scopeSet || scopeSet.scopes.size <= 0) {
return false;
}
return (
this.scopes.size >= scopeSet.scopes.size &&
scopeSet.asArray().every((scope) => this.containsScope(scope))
);
}
/**
* Check if set of scopes contains only the defaults
*/
containsOnlyOIDCScopes(): boolean {
let defaultScopeCount = 0;
OIDC_SCOPES.forEach((defaultScope: string) => {
if (this.containsScope(defaultScope)) {
defaultScopeCount += 1;
}
});
return this.scopes.size === defaultScopeCount;
}
/**
* Appends single scope if passed
* @param newScope
*/
appendScope(newScope: string): void {
if (newScope) {
this.scopes.add(newScope.trim());
}
}
/**
* Appends multiple scopes if passed
* @param newScopes
*/
appendScopes(newScopes: Array<string>): void {
try {
newScopes.forEach((newScope) => this.appendScope(newScope));
} catch (e) {
throw createClientAuthError(
ClientAuthErrorCodes.cannotAppendScopeSet
);
}
}
/**
* Removes element from set of scopes.
* @param scope
*/
removeScope(scope: string): void {
if (!scope) {
throw createClientAuthError(
ClientAuthErrorCodes.cannotRemoveEmptyScope
);
}
this.scopes.delete(scope.trim());
}
/**
* Removes default scopes from set of scopes
* Primarily used to prevent cache misses if the default scopes are not returned from the server
*/
removeOIDCScopes(): void {
OIDC_SCOPES.forEach((defaultScope: string) => {
this.scopes.delete(defaultScope);
});
}
/**
* Combines an array of scopes with the current set of scopes.
* @param otherScopes
*/
unionScopeSets(otherScopes: ScopeSet): Set<string> {
if (!otherScopes) {
throw createClientAuthError(
ClientAuthErrorCodes.emptyInputScopeSet
);
}
const unionScopes = new Set<string>(); // Iterator in constructor not supported in IE11
otherScopes.scopes.forEach((scope) =>
unionScopes.add(scope.toLowerCase())
);
this.scopes.forEach((scope) => unionScopes.add(scope.toLowerCase()));
return unionScopes;
}
/**
* Check if scopes intersect between this set and another.
* @param otherScopes
*/
intersectingScopeSets(otherScopes: ScopeSet): boolean {
if (!otherScopes) {
throw createClientAuthError(
ClientAuthErrorCodes.emptyInputScopeSet
);
}
// Do not allow OIDC scopes to be the only intersecting scopes
if (!otherScopes.containsOnlyOIDCScopes()) {
otherScopes.removeOIDCScopes();
}
const unionScopes = this.unionScopeSets(otherScopes);
const sizeOtherScopes = otherScopes.getScopeCount();
const sizeThisScopes = this.getScopeCount();
const sizeUnionScopes = unionScopes.size;
return sizeUnionScopes < sizeThisScopes + sizeOtherScopes;
}
/**
* Returns size of set of scopes.
*/
getScopeCount(): number {
return this.scopes.size;
}
/**
* Returns the scopes as an array of string values
*/
asArray(): Array<string> {
const array: Array<string> = [];
this.scopes.forEach((val) => array.push(val));
return array;
}
/**
* Prints scopes into a space-delimited string
*/
printScopes(): string {
if (this.scopes) {
const scopeArr = this.asArray();
return scopeArr.join(" ");
}
return Constants.EMPTY_STRING;
}
/**
* Prints scopes into a space-delimited lower-case string (used for caching)
*/
printScopesLowerCase(): string {
return this.printScopes().toLowerCase();
}
}
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Controls whether tokens should be stored in the cache or not. If set to false, tokens may still be acquired and returned but will not be cached for later retrieval.
*/
export type StoreInCache = {
/* Indicates whether or not the acquired accessToken will be stored in the cache */
accessToken?: boolean;
/* Indicates whether or not the acquired idToken will be stored in the cache */
idToken?: boolean;
/* Indicates whether or not the acquired refreshToken will be stored in the cache */
refreshToken?: boolean;
};
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountInfo } from "../account/AccountInfo.js";
/**
* Result returned from the authority's token endpoint.
* - uniqueId - `oid` or `sub` claim from ID token
* - tenantId - `tid` claim from ID token
* - scopes - Scopes that are validated for the respective token
* - account - An account object representation of the currently signed-in user
* - idToken - Id token received as part of the response
* - idTokenClaims - MSAL-relevant ID token claims
* - accessToken - Access token or SSH certificate received as part of the response
* - fromCache - Boolean denoting whether token came from cache
* - expiresOn - Javascript Date object representing relative expiration of access token
* - extExpiresOn - Javascript Date object representing extended relative expiration of access token in case of server outage
* - refreshOn - Javascript Date object representing relative time until an access token must be refreshed
* - state - Value passed in by user in request
* - familyId - Family ID identifier, usually only used for refresh tokens
* - requestId - Request ID returned as part of the response
*/
export type AuthenticationResult = {
authority: string;
uniqueId: string;
tenantId: string;
scopes: Array<string>;
account: AccountInfo | null;
idToken: string;
idTokenClaims: object;
accessToken: string;
fromCache: boolean;
expiresOn: Date | null;
extExpiresOn?: Date;
refreshOn?: Date;
tokenType: string;
correlationId: string;
requestId?: string;
state?: string;
familyId?: string;
cloudGraphHostName?: string;
msGraphHost?: string;
code?: string;
fromNativeBroker?: boolean;
};
@@ -0,0 +1,18 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Response returned after processing the code response query string or fragment.
*/
export type AuthorizationCodePayload = {
code: string;
cloud_instance_name?: string;
cloud_instance_host_name?: string;
cloud_graph_host_name?: string;
msgraph_host?: string;
state?: string;
nonce?: string;
client_info?: string;
};
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* DeviceCode returned by the security token service device code endpoint containing information necessary for device code flow.
* - userCode: code which user needs to provide when authenticating at the verification URI
* - deviceCode: code which should be included in the request for the access token
* - verificationUri: URI where user can authenticate
* - expiresIn: expiration time of the device code in seconds
* - interval: interval at which the STS should be polled at
* - message: message which should be displayed to the user
*/
export type DeviceCodeResponse = {
userCode: string;
deviceCode: string;
verificationUri: string;
expiresIn: number;
interval: number;
message: string;
};
export type ServerDeviceCodeResponse = {
user_code: string;
device_code: string;
verification_uri: string;
expires_in: number;
interval: number;
message: string;
};
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ServerAuthorizationTokenResponse } from "./ServerAuthorizationTokenResponse.js";
/**
* Response object used for loading external tokens to cache.
* - token_type: Indicates the token type value. The only type that Azure AD supports is Bearer.
* - scope: The scopes that the access_token is valid for.
* - expires_in: How long the access token is valid (in seconds).
* - id_token: A JSON Web Token (JWT). The app can decode the segments of this token to request information about the user who signed in.
* - refresh_token: An OAuth 2.0 refresh token. The app can use this token acquire additional access tokens after the current access token expires.
* - access_token: The requested access token. The app can use this token to authenticate to the secured resource, such as a web API.
* - client_info: Client info object
*/
export type ExternalTokenResponse = Pick<
ServerAuthorizationTokenResponse,
| "token_type"
| "scope"
| "expires_in"
| "ext_expires_in"
| "id_token"
| "refresh_token"
| "refresh_token_expires_in"
| "foci"
> & {
access_token?: string;
client_info?: string;
};
+9
View File
@@ -0,0 +1,9 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type IMDSBadResponse = {
error: string;
"newest-versions": Array<string>;
};

Some files were not shown because too many files have changed in this diff Show More