Estructura inicial del proyecto

This commit is contained in:
2026-06-02 16:57:08 +00:00
commit 8b306b9afc
9864 changed files with 1435687 additions and 0 deletions

View File

@ -0,0 +1,183 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TokenClaims } from "./TokenClaims.js";
export type DataBoundary = "EU" | "None";
/**
* 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
* - upn - The user's UPN used to populate the account username in cases where preferred_username is not present in the ID token claims.
* - 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
* - dataBoundary - Data boundary extracted from clientInfo
*/
export type AccountInfo = {
homeAccountId: string;
environment: string;
tenantId: string;
username: string;
localAccountId: string;
loginHint?: string;
name?: string;
upn?: string;
idToken?: string;
idTokenClaims?: TokenClaims & {
[key: string]:
| string
| number
| string[]
| object
| undefined
| unknown;
};
nativeAccountId?: string;
authorityType?: string;
tenantProfiles?: Map<string, TenantProfile>;
dataBoundary?: DataBoundary;
};
/**
* Account details that vary across tenants for the same user
*/
export type TenantProfile = Pick<
AccountInfo,
"tenantId" | "localAccountId" | "name" | "username" | "loginHint" | "upn"
> & {
/**
* - 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,
preferred_username,
upn,
login_hint,
} = 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,
username: preferred_username || upn || "",
loginHint: login_hint,
isHomeTenant: tenantIdMatchesHomeTenant(tenantId, homeAccountId),
upn: upn,
};
} else {
return {
tenantId,
localAccountId,
username: "",
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 which is a utility property of tenant profile but not required in 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;
}

View File

@ -0,0 +1,93 @@
/*
* 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);
}
}
/**
* Check if the signin_state claim contains "kmsi"
* @param idTokenClaims
* @returns
*/
export function isKmsi(idTokenClaims: TokenClaims): boolean {
if (!idTokenClaims.signin_state) {
return false;
}
/**
* Signin_state claim known values:
* dvc_mngd - device is managed
* dvc_dmjd - device is domain joined
* kmsi - user opted to "keep me signed in"
* inknownntwk - Request made inside a known network. Don't use this, use CAE instead.
*/
const kmsiClaims = ["kmsi", "dvc_dmjd"]; // There are some cases where kmsi may not be returned but persistent storage is still OK - allow dvc_dmjd as well
return idTokenClaims.signin_state.some((value) =>
kmsiClaims.includes(value.trim().toLowerCase())
);
}
/**
* 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);
}
}

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];

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;
};

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
createClientAuthError,
ClientAuthErrorCodes,
} from "../error/ClientAuthError.js";
import * as Constants from "../utils/Constants.js";
/**
* Client info object which consists of:
* uid: user id
* utid: tenant id
* xms_tdbr: optional, only for non-US tenants
*/
export type ClientInfo = {
uid: string;
utid: string;
xms_tdbr?: 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(
Constants.CLIENT_INFO_SEPARATOR,
2
);
return {
uid: clientInfoParts[0],
utid: clientInfoParts.length < 2 ? "" : clientInfoParts[1],
};
}

View File

@ -0,0 +1,102 @@
/*
* 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;
/**
* Contains KMSI (Keep Me Signed In) status among other things
*/
signin_state?: Array<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

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 * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.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 cacheManager
* @param authorityOptions
* @param logger
* @param correlationId
* @param performanceClient
* @internal
*/
export async function createDiscoveredInstance(
authorityUri: string,
networkClient: INetworkModule,
cacheManager: ICacheManager,
authorityOptions: AuthorityOptions,
logger: Logger,
correlationId: string,
performanceClient: IPerformanceClient
): Promise<Authority> {
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
);
}
}

View File

@ -0,0 +1,242 @@
/*
* 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;
};
// Build endpoint metadata dynamically to avoid string duplication
const endpointHosts: Array<{ host: string; issuerHost?: string }> = [
{ host: "login.microsoftonline.com" },
{
host: "login.chinacloudapi.cn",
issuerHost: "login.partner.microsoftonline.cn", // Issuer differs
},
{ host: "login.microsoftonline.us" },
{ host: "login.sovcloud-identity.fr" },
{ host: "login.sovcloud-identity.de" },
{ host: "login.sovcloud-identity.sg" },
];
function buildOpenIdConfig(
host: string,
issuerHost: string
): OpenIdConfigResponse {
return {
token_endpoint: `https://${host}/{tenantid}/oauth2/v2.0/token`,
jwks_uri: `https://${host}/{tenantid}/discovery/v2.0/keys`,
issuer: `https://${issuerHost}/{tenantid}/v2.0`,
authorization_endpoint: `https://${host}/{tenantid}/oauth2/v2.0/authorize`,
end_session_endpoint: `https://${host}/{tenantid}/oauth2/v2.0/logout`,
};
}
const dynamicEndpointMetadata: RawMetadata["endpointMetadata"] =
endpointHosts.reduce((acc, { host, issuerHost }) => {
acc[host] = buildOpenIdConfig(host, issuerHost || host);
return acc;
}, {} as Record<string, OpenIdConfigResponse>);
export const rawMetdataJSON: RawMetadata = {
endpointMetadata: dynamicEndpointMetadata,
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"],
},
{
preferred_network: "login.sovcloud-identity.fr",
preferred_cache: "login.sovcloud-identity.fr",
aliases: ["login.sovcloud-identity.fr"],
},
{
preferred_network: "login.sovcloud-identity.de",
preferred_cache: "login.sovcloud-identity.de",
aliases: ["login.sovcloud-identity.de"],
},
{
preferred_network: "login.sovcloud-identity.sg",
preferred_cache: "login.sovcloud-identity.sg",
aliases: ["login.sovcloud-identity.sg"],
},
{
preferred_network: "login.windows-ppe.net",
preferred_cache: "login.windows-ppe.net",
aliases: [
"login.windows-ppe.net",
"sts.windows-ppe.net",
"login.microsoft-ppe.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,
correlationId: string
): string[] {
let staticAliases: string[] | undefined;
const canonicalAuthority = staticAuthorityOptions.canonicalAuthority;
if (canonicalAuthority) {
const authorityHost = new UrlString(
canonicalAuthority
).getUrlComponents().HostNameAndPort;
staticAliases =
getAliasesFromMetadata(
logger,
correlationId,
authorityHost,
staticAuthorityOptions.cloudDiscoveryMetadata?.metadata,
AuthorityMetadataSource.CONFIG
) ||
getAliasesFromMetadata(
logger,
correlationId,
authorityHost,
InstanceDiscoveryMetadata.metadata,
AuthorityMetadataSource.HARDCODED_VALUES
) ||
staticAuthorityOptions.knownAuthorities;
}
return staticAliases || [];
}
/**
* Returns aliases for from the raw cloud discovery metadata passed in
* @param authorityHost
* @param rawCloudDiscoveryMetadata
* @returns
*/
export function getAliasesFromMetadata(
logger: Logger,
correlationId: string,
authorityHost?: string,
cloudDiscoveryMetadata?: CloudDiscoveryMetadata[],
source?: AuthorityMetadataSource
): string[] | null {
logger.trace(
`getAliasesFromMetadata called with source: '${source}'`,
correlationId
);
if (authorityHost && cloudDiscoveryMetadata) {
const metadata = getCloudDiscoveryMetadataFromNetworkResponse(
cloudDiscoveryMetadata,
authorityHost
);
if (metadata) {
logger.trace(
`getAliasesFromMetadata: found cloud discovery metadata in '${source}', returning aliases`,
correlationId
);
return metadata.aliases;
} else {
logger.trace(
`getAliasesFromMetadata: did not find cloud discovery metadata in '${source}'`,
correlationId
);
}
}
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;
}

View File

@ -0,0 +1,48 @@
/*
* 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;
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];

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];

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;

View File

@ -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;
};

View File

@ -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>;
};

View File

@ -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")
);
}

View File

@ -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")
);
}

View File

@ -0,0 +1,10 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type ImdsOptions = {
headers?: {
Metadata: string;
};
};

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ResponseMode } from "../utils/Constants.js";
/**
* Options for the OIDC protocol mode.
*/
export type OIDCOptions = {
responseMode?: ResponseMode;
defaultScopes?: Array<string>;
};

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")
);
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Protocol modes supported by MSAL.
*/
export const ProtocolMode = {
/**
* Auth Code + PKCE with Entra ID (formerly AAD) specific optimizations and features
*/
AAD: "AAD",
/**
* Auth Code + PKCE without Entra ID specific optimizations and features. For use only with non-Microsoft owned authorities.
* Support is limited for this mode.
*/
OIDC: "OIDC",
/**
* Encrypted Authorize Response (EAR) with Entra ID specific optimizations and features
*/
EAR: "EAR",
} as const;
export type ProtocolMode = (typeof ProtocolMode)[keyof typeof ProtocolMode];

View File

@ -0,0 +1,178 @@
/*
* 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 * as Constants from "../utils/Constants.js";
import { RegionDiscoveryMetadata } from "./RegionDiscoveryMetadata.js";
import { ImdsOptions } from "./ImdsOptions.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.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;
// CorrelationId
protected correlationId: string;
// 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> {
// 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 === Constants.HTTP_SUCCESS
) {
autodetectedRegionName = localIMDSVersionResponse.body;
regionDiscoveryMetadata.region_source =
Constants.RegionDiscoverySources.IMDS;
}
// If the response using the local IMDS version failed, try to fetch the current version of IMDS and retry.
if (
localIMDSVersionResponse.status ===
Constants.HTTP_BAD_REQUEST
) {
const currentIMDSVersion = await invokeAsync(
this.getCurrentVersion.bind(this),
PerformanceEvents.RegionDiscoveryGetCurrentVersion,
this.logger,
this.performanceClient,
this.correlationId
)(options);
if (!currentIMDSVersion) {
regionDiscoveryMetadata.region_source =
Constants.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 ===
Constants.HTTP_SUCCESS
) {
autodetectedRegionName =
currentIMDSVersionResponse.body;
regionDiscoveryMetadata.region_source =
Constants.RegionDiscoverySources.IMDS;
}
}
} catch (e) {
regionDiscoveryMetadata.region_source =
Constants.RegionDiscoverySources.FAILED_AUTO_DETECTION;
return null;
}
} else {
regionDiscoveryMetadata.region_source =
Constants.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 =
Constants.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>> {
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> {
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 === Constants.HTTP_BAD_REQUEST &&
response.body &&
response.body["newest-versions"] &&
response.body["newest-versions"].length > 0
) {
return response.body["newest-versions"][0];
}
return null;
} catch (e) {
return null;
}
}
}

View File

@ -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;
};

View File

@ -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

View File

@ -0,0 +1,29 @@
/*
* 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;
/** Matches the resource passed into the request for MCP flows */
resource?: string;
};

View File

@ -0,0 +1,52 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TenantProfile, DataBoundary } from "../../account/AccountInfo.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 type AccountEntity = {
homeAccountId: string;
environment: string;
realm: string;
localAccountId: string;
username: string;
authorityType: string;
loginHint?: string;
clientInfo?: string;
name?: string;
lastModificationTime?: string;
lastModificationApp?: string;
cloudGraphHostName?: string;
msGraphHost?: string;
nativeAccountId?: string;
tenantProfiles?: Array<TenantProfile>;
/** Timestamp when the entry was last updated */
lastUpdatedAt: string;
dataBoundary?: DataBoundary;
/** API identifier for telemetry to indicate which API cached this account */
cachedByApiId?: number;
};

View File

@ -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;
};

View File

@ -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;
};

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;
};

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;
/** Timestamp when the entry was last updated */
lastUpdatedAt: string;
};

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;
};

View File

@ -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;
};

View File

@ -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;
};

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;
};

View File

@ -0,0 +1,289 @@
/*
* 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
* @param correlationId
*/
getAccount(accountKey: string, correlationId: string): AccountEntity | null;
/**
* set account entity in the platform cache
* @param account
* @param correlationId
*/
setAccount(
account: AccountEntity,
correlationId: string,
kmsi: boolean,
apiId: number
): Promise<void>;
/**
* fetch the idToken entity from the platform cache
* @param idTokenKey
* @param correlationId
*/
getIdTokenCredential(
idTokenKey: string,
correlationId: string
): IdTokenEntity | null;
/**
* set idToken entity to the platform cache
* @param idToken
* @param correlationId
*/
setIdTokenCredential(
idToken: IdTokenEntity,
correlationId: string,
kmsi: boolean
): Promise<void>;
/**
* fetch the idToken entity from the platform cache
* @param accessTokenKey
* @param correlationId
*/
getAccessTokenCredential(
accessTokenKey: string,
correlationId: string
): AccessTokenEntity | null;
/**
* set idToken entity to the platform cache
* @param accessToken
* @param correlationId
*/
setAccessTokenCredential(
accessToken: AccessTokenEntity,
correlationId: string,
kmsi: boolean
): Promise<void>;
/**
* fetch the idToken entity from the platform cache
* @param refreshTokenKey
* @param correlationId
*/
getRefreshTokenCredential(
refreshTokenKey: string,
correlationId: string
): RefreshTokenEntity | null;
/**
* set idToken entity to the platform cache
* @param refreshToken
* @param correlationId
*/
setRefreshTokenCredential(
refreshToken: RefreshTokenEntity,
correlationId: string,
kmsi: boolean
): Promise<void>;
/**
* fetch appMetadata entity from the platform cache
* @param appMetadataKey
* @param correlationId
*/
getAppMetadata(
appMetadataKey: string,
correlationId: string
): AppMetadataEntity | null;
/**
* set appMetadata entity to the platform cache
* @param appMetadata
* @param correlationId
*/
setAppMetadata(appMetadata: AppMetadataEntity, correlationId: string): void;
/**
* fetch server telemetry entity from the platform cache
* @param serverTelemetryKey
* @param correlationId
*/
getServerTelemetry(
serverTelemetryKey: string,
correlationId: string
): ServerTelemetryEntity | null;
/**
* set server telemetry entity to the platform cache
* @param serverTelemetryKey
* @param serverTelemetry
* @param correlationId
*/
setServerTelemetry(
serverTelemetryKey: string,
serverTelemetry: ServerTelemetryEntity,
correlationId: string
): void;
/**
* fetch cloud discovery metadata entity from the platform cache
* @param key
* @param correlationId
*/
getAuthorityMetadata(
key: string,
correlationId: string
): AuthorityMetadataEntity | null;
/**
* Get cache keys for authority metadata
* @param correlationId
*/
getAuthorityMetadataKeys(correlationId: string): Array<string>;
/**
* set cloud discovery metadata entity to the platform cache
* @param key
* @param value
* @param correlationId
*/
setAuthorityMetadata(
key: string,
value: AuthorityMetadataEntity,
correlationId: string
): void;
/**
* Provide an alias to find a matching AuthorityMetadataEntity in cache
* @param host
* @param correlationId
*/
getAuthorityMetadataByAlias(
host: string,
correlationId: string
): AuthorityMetadataEntity | null;
/**
* given an authority generates the cache key for authorityMetadata
* @param authority
* @param correlationId
*/
generateAuthorityMetadataCacheKey(
authority: string,
correlationId: string
): string;
/**
* fetch throttling entity from the platform cache
* @param throttlingCacheKey
* @param correlationId
*/
getThrottlingCache(
throttlingCacheKey: string,
correlationId: string
): ThrottlingEntity | null;
/**
* set throttling entity to the platform cache
* @param throttlingCacheKey
* @param throttlingCache
* @param correlationId
*/
setThrottlingCache(
throttlingCacheKey: string,
throttlingCache: ThrottlingEntity,
correlationId: string
): void;
/**
* Returns all accounts in cache
* @param filter
* @param correlationId
*/
getAllAccounts(filter: AccountFilter, correlationId: string): AccountInfo[];
/**
* saves a cache record
* @param cacheRecord
* @param correlationId
* @param storeInCache
*/
saveCacheRecord(
cacheRecord: CacheRecord,
correlationId: string,
kmsi: boolean,
apiId: number,
storeInCache?: StoreInCache
): Promise<void>;
/**
* retrieve accounts matching all provided filters; if no filter is set, get all accounts
* @param filter
* @param correlationId
*/
getAccountsFilteredBy(
filter: AccountFilter,
correlationId: string
): AccountEntity[];
/**
* Get AccountInfo object based on provided filters
* @param filter
* @param correlationId
*/
getAccountInfoFilteredBy(
filter: AccountFilter,
correlationId: string
): AccountInfo | null;
/**
* Removes all accounts and related tokens from cache.
* @param correlationId
*/
removeAllAccounts(correlationId: string): void;
/**
* returns a boolean if the given account is removed
* @param account
* @param correlationId
*/
removeAccount(account: AccountInfo, correlationId: string): void;
/**
* returns a boolean if the given account is removed
* @param account
* @param correlationId
*/
removeAccountContext(account: AccountInfo, correlationId: string): void;
/**
* @param key
* @param correlationId
*/
removeIdToken(key: string, correlationId: string): void;
/**
* @param key
* @param correlationId
*/
removeAccessToken(key: string, correlationId: string): void;
/**
* @param key
* @param correlationId
*/
removeRefreshToken(key: string, correlationId: string): void;
}

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>;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,292 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as Constants 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,
DataBoundary,
} 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";
import { AccountEntity } from "../entities/AccountEntity.js";
/**
* Generate Account Id key component as per the schema: <home_account_id>-<environment>
*/
export function generateAccountId(accountEntity: AccountEntity): string {
const accountId: Array<string> = [
accountEntity.homeAccountId,
accountEntity.environment,
];
return accountId.join(Constants.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Returns the AccountInfo interface for this account.
*/
export function getAccountInfo(accountEntity: AccountEntity): AccountInfo {
const tenantProfiles = accountEntity.tenantProfiles || [];
// Ensure at least the home tenant profile exists
if (
tenantProfiles.length === 0 &&
accountEntity.realm &&
accountEntity.localAccountId
) {
tenantProfiles.push(
buildTenantProfile(
accountEntity.homeAccountId,
accountEntity.localAccountId,
accountEntity.realm
)
);
}
return {
homeAccountId: accountEntity.homeAccountId,
environment: accountEntity.environment,
tenantId: accountEntity.realm,
username: accountEntity.username,
localAccountId: accountEntity.localAccountId,
loginHint: accountEntity.loginHint,
name: accountEntity.name,
nativeAccountId: accountEntity.nativeAccountId,
authorityType: accountEntity.authorityType,
// Deserialize tenant profiles array into a Map
tenantProfiles: new Map(
tenantProfiles.map((tenantProfile) => {
return [tenantProfile.tenantId, tenantProfile];
})
),
dataBoundary: accountEntity.dataBoundary,
};
}
/**
* Returns true if the account entity is in single tenant format (outdated), false otherwise
*/
export function isSingleTenant(accountEntity: AccountEntity): boolean {
return !accountEntity.tenantProfiles;
}
/**
* Build Account cache from IdToken, clientInfo and authority/policy. Associated with AAD.
* @param accountDetails
*/
export function createAccountEntity(
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 {
let authorityType;
if (authority.authorityType === AuthorityType.Adfs) {
authorityType = Constants.CACHE_ACCOUNT_TYPE_ADFS;
} else if (authority.protocolMode === ProtocolMode.OIDC) {
authorityType = Constants.CACHE_ACCOUNT_TYPE_GENERIC;
} else {
authorityType = Constants.CACHE_ACCOUNT_TYPE_MSSTS;
}
let clientInfo: ClientInfo | undefined;
let dataBoundary: DataBoundary | undefined;
if (accountDetails.clientInfo && base64Decode) {
clientInfo = buildClientInfo(accountDetails.clientInfo, base64Decode);
if (clientInfo.xms_tdbr) {
dataBoundary = clientInfo.xms_tdbr === "EU" ? "EU" : "None";
}
}
const env =
accountDetails.environment ||
(authority && authority.getPreferredCache());
if (!env) {
throw createClientAuthError(
ClientAuthErrorCodes.invalidCacheEnvironment
);
}
/*
* 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;
const username = preferredUsername || email || "";
const loginHint = accountDetails.idTokenClaims?.login_hint;
const realm =
clientInfo?.utid ||
getTenantIdFromIdTokenClaims(accountDetails.idTokenClaims) ||
""; // non-AAD scenarios can have empty realm
// How do you account for MSA CID here?
const localAccountId =
clientInfo?.uid ||
accountDetails.idTokenClaims?.oid ||
accountDetails.idTokenClaims?.sub ||
"";
let tenantProfiles;
if (accountDetails.tenantProfiles) {
tenantProfiles = accountDetails.tenantProfiles;
} else {
const tenantProfile = buildTenantProfile(
accountDetails.homeAccountId,
localAccountId,
realm,
accountDetails.idTokenClaims
);
tenantProfiles = [tenantProfile];
}
return {
homeAccountId: accountDetails.homeAccountId,
environment: env,
realm: realm,
localAccountId: localAccountId,
username: username,
authorityType: authorityType,
loginHint: loginHint,
clientInfo: accountDetails.clientInfo,
name: accountDetails.idTokenClaims?.name || "",
lastModificationTime: undefined,
lastModificationApp: undefined,
cloudGraphHostName: accountDetails.cloudGraphHostName,
msGraphHost: accountDetails.msGraphHost,
nativeAccountId: accountDetails.nativeAccountId,
tenantProfiles: tenantProfiles,
dataBoundary,
} as AccountEntity;
}
/**
* Creates an AccountEntity object from AccountInfo
* @param accountInfo
* @param cloudGraphHostName
* @param msGraphHost
* @returns
*/
export function createAccountEntityFromAccountInfo(
accountInfo: AccountInfo,
cloudGraphHostName?: string,
msGraphHost?: string
): AccountEntity {
// Serialize tenant profiles map into an array
const tenantProfiles = Array.from(
accountInfo.tenantProfiles?.values() || []
);
// Ensure at least the home tenant profile exists
if (
tenantProfiles.length === 0 &&
accountInfo.tenantId &&
accountInfo.localAccountId
) {
tenantProfiles.push(
buildTenantProfile(
accountInfo.homeAccountId,
accountInfo.localAccountId,
accountInfo.tenantId,
accountInfo.idTokenClaims
)
);
}
return {
authorityType:
accountInfo.authorityType || Constants.CACHE_ACCOUNT_TYPE_GENERIC,
homeAccountId: accountInfo.homeAccountId,
localAccountId: accountInfo.localAccountId,
nativeAccountId: accountInfo.nativeAccountId,
realm: accountInfo.tenantId,
environment: accountInfo.environment,
username: accountInfo.username,
loginHint: accountInfo.loginHint,
name: accountInfo.name,
cloudGraphHostName: cloudGraphHostName,
msGraphHost: msGraphHost,
tenantProfiles: tenantProfiles,
dataBoundary: accountInfo.dataBoundary,
} as AccountEntity;
}
/**
* Generate HomeAccountId from server response
* @param serverClientInfo
* @param authType
*/
export function generateHomeAccountId(
serverClientInfo: string,
authType: AuthorityType,
logger: Logger,
cryptoObj: ICrypto,
correlationId: string,
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", correlationId);
}
// default to "sub" claim
return idTokenClaims?.sub || "";
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isAccountEntity(entity: object): entity is AccountEntity {
if (!entity) {
return false;
}
return (
entity.hasOwnProperty("homeAccountId") &&
entity.hasOwnProperty("environment") &&
entity.hasOwnProperty("realm") &&
entity.hasOwnProperty("localAccountId") &&
entity.hasOwnProperty("username") &&
entity.hasOwnProperty("authorityType")
);
}

View File

@ -0,0 +1,377 @@
/*
* 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 * as Constants 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";
/**
* 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: Constants.CredentialType.ID_TOKEN,
homeAccountId: homeAccountId,
environment: environment,
clientId: clientId,
secret: idToken,
realm: tenantId,
lastUpdatedAt: Date.now().toString(), // Set the last updated time to now
};
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?: Constants.AuthenticationScheme,
userAssertionHash?: string,
keyId?: string
): AccessTokenEntity {
const atEntity: AccessTokenEntity = {
homeAccountId: homeAccountId,
credentialType: Constants.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 || Constants.AuthenticationScheme.BEARER,
lastUpdatedAt: Date.now().toString(), // Set the last updated time to now
};
if (userAssertionHash) {
atEntity.userAssertionHash = userAssertionHash;
}
if (refreshOn) {
atEntity.refreshOn = refreshOn.toString();
}
/*
* Create Access Token With Auth Scheme instead of regular access token
* Cast to lower to handle "bearer" from ADFS
*/
if (
atEntity.tokenType?.toLowerCase() !==
Constants.AuthenticationScheme.BEARER.toLowerCase()
) {
atEntity.credentialType =
Constants.CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME;
switch (atEntity.tokenType) {
case Constants.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 Constants.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: Constants.CredentialType.REFRESH_TOKEN,
homeAccountId: homeAccountId,
environment: environment,
clientId: clientId,
secret: refreshToken,
lastUpdatedAt: Date.now().toString(),
};
if (userAssertionHash) {
rtEntity.userAssertionHash = userAssertionHash;
}
if (familyId) {
rtEntity.familyId = familyId;
}
if (expiresOn) {
rtEntity.expiresOn = expiresOn.toString();
}
return rtEntity;
}
export function isCredentialEntity(entity: object): entity is CredentialEntity {
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
): entity is AccessTokenEntity {
if (!entity) {
return false;
}
return (
isCredentialEntity(entity) &&
entity.hasOwnProperty("realm") &&
entity.hasOwnProperty("target") &&
(entity["credentialType"] === Constants.CredentialType.ACCESS_TOKEN ||
entity["credentialType"] ===
Constants.CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME)
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isIdTokenEntity(entity: object): entity is IdTokenEntity {
if (!entity) {
return false;
}
return (
isCredentialEntity(entity) &&
entity.hasOwnProperty("realm") &&
entity["credentialType"] === Constants.CredentialType.ID_TOKEN
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isRefreshTokenEntity(
entity: object
): entity is RefreshTokenEntity {
if (!entity) {
return false;
}
return (
isCredentialEntity(entity) &&
entity["credentialType"] === Constants.CredentialType.REFRESH_TOKEN
);
}
/**
* 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(Constants.SERVER_TELEM_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(Constants.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> = [
Constants.APP_METADATA,
environment,
clientId,
];
return appMetaDataKeyArray
.join(Constants.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(Constants.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(Constants.AUTHORITY_METADATA_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() +
Constants.AUTHORITY_METADATA_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();
}

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"
| "upn"
>;
/**
* 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;
};
/**
* AppMetadata: appmetadata-<environment>-<client_id>
*/
export type AppMetadataFilter = {
environment?: string;
clientId?: string;
};
export type TokenKeys = {
idToken: string[];
accessToken: string[];
refreshToken: string[];
};

View File

@ -0,0 +1,606 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CommonAuthorizationCodeRequest } from "../request/CommonAuthorizationCodeRequest.js";
import { Authority } from "../authority/Authority.js";
import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js";
import * as UrlUtils from "../utils/UrlUtils.js";
import * as Constants from "../utils/Constants.js";
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
import {
buildClientConfiguration,
ClientConfiguration,
CommonClientConfiguration,
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 {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { UrlString } from "../url/UrlString.js";
import { CommonEndSessionRequest } from "../request/CommonEndSessionRequest.js";
import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js";
import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import {
buildClientInfoFromHomeAccountId,
buildClientInfo,
} from "../account/ClientInfo.js";
import { CcsCredentialType, CcsCredential } from "../account/CcsCredential.js";
import {
createClientConfigurationError,
ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import { ClientAssertion } from "../account/ClientCredentials.js";
import { getClientAssertion } from "../utils/ClientAssertionUtils.js";
import { getRequestThumbprint } from "../network/RequestThumbprint.js";
import {
createTokenQueryParameters,
createTokenRequestHeaders,
executePostToTokenEndpoint,
} from "../protocol/Token.js";
import { createDiscoveredInstance } from "../authority/AuthorityFactory.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { Logger } from "../logger/Logger.js";
import { ICrypto } from "../crypto/ICrypto.js";
import { CacheManager } from "../cache/CacheManager.js";
import { INetworkModule } from "../network/INetworkModule.js";
import { version, name } from "../packageMetadata.js";
/**
* Oauth2.0 Authorization Code client
* @internal
*/
export class AuthorizationCodeClient {
// Flag to indicate if client is for hybrid spa auth code redemption
protected includeRedirectUri: boolean = true;
private oidcDefaultScopes;
// 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;
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;
this.oidcDefaultScopes =
this.config.authOptions.authority.options.OIDCOptions?.defaultScopes;
}
/**
* 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,
apiId: number,
authCodePayload?: AuthorizationCodePayload
): Promise<AuthenticationResult> {
if (!request.code) {
throw createClientAuthError(
ClientAuthErrorCodes.requestCannotBeMade
);
}
// Check for new cloud instance
if (authCodePayload && authCodePayload.cloud_instance_host_name) {
await invokeAsync(
this.updateTokenEndpointAuthority.bind(this),
PerformanceEvents.UpdateTokenEndpointAuthority,
this.logger,
this.performanceClient,
request.correlationId
)(authCodePayload.cloud_instance_host_name, request.correlationId);
}
const reqTimestamp = TimeUtils.nowSeconds();
const response = await invokeAsync(
this.executeTokenRequest.bind(this),
PerformanceEvents.AuthClientExecuteTokenRequest,
this.logger,
this.performanceClient,
request.correlationId
)(this.authority, request, this.serverTelemetryManager);
// Retrieve requestId from response headers
const requestId =
response.headers?.[Constants.HeaderNames.X_MS_REQUEST_ID];
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.performanceClient,
this.config.serializableCache,
this.config.persistencePlugin
);
// Validate response. This function throws a server error if an error is returned by the server.
responseHandler.validateTokenResponse(
response.body,
request.correlationId
);
return invokeAsync(
responseHandler.handleServerTokenResponse.bind(responseHandler),
PerformanceEvents.HandleServerTokenResponse,
this.logger,
this.performanceClient,
request.correlationId
)(
response.body,
this.authority,
reqTimestamp,
request,
apiId,
authCodePayload,
undefined,
undefined,
undefined,
requestId
);
}
/**
* 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,
serverTelemetryManager: ServerTelemetryManager | null
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
const queryParametersString = createTokenQueryParameters(
request,
this.config.authOptions.clientId,
this.config.authOptions.redirectUri,
this.performanceClient
);
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}${Constants.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
type: CcsCredentialType.HOME_ACCOUNT_ID,
};
} catch (e) {
this.logger.verbose(
`Could not parse client info for CCS Header: '${e}'`,
request.correlationId
);
}
}
const headers: Record<string, string> = createTokenRequestHeaders(
this.logger,
this.config.systemOptions.preventCorsPreflight,
ccsCredential || request.ccsCredential
);
const thumbprint = getRequestThumbprint(
this.config.authOptions.clientId,
request
);
return invokeAsync(
executePostToTokenEndpoint,
PerformanceEvents.AuthorizationCodeClientExecutePostToTokenEndpoint,
this.logger,
this.performanceClient,
request.correlationId
)(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId,
this.cacheManager,
this.networkClient,
this.logger,
this.performanceClient,
serverTelemetryManager
);
}
/**
* Generates a map for all the params to be sent to the service
* @param request
*/
private async createTokenRequestBody(
request: CommonAuthorizationCodeRequest
): Promise<string> {
const parameters = new Map<string, string>();
RequestParameterBuilder.addClientId(
parameters,
request.embeddedClientId ||
request.extraParameters?.[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
if (!request.redirectUri) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.redirectUriEmpty
);
}
} else {
// Validate and include redirect uri
RequestParameterBuilder.addRedirectUri(
parameters,
request.redirectUri
);
}
// Add scope array, parameter builder will add default scopes and dedupe
RequestParameterBuilder.addScopes(
parameters,
request.scopes,
true,
this.oidcDefaultScopes
);
RequestParameterBuilder.addResource(parameters, request.resource);
// add code: user set, not validated
RequestParameterBuilder.addAuthorizationCode(parameters, request.code);
// Add library metadata
RequestParameterBuilder.addLibraryInfo(
parameters,
this.config.libraryInfo
);
RequestParameterBuilder.addApplicationTelemetry(
parameters,
this.config.telemetry.application
);
RequestParameterBuilder.addThrottling(parameters);
if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) {
RequestParameterBuilder.addServerTelemetry(
parameters,
this.serverTelemetryManager
);
}
// add code_verifier if passed
if (request.codeVerifier) {
RequestParameterBuilder.addCodeVerifier(
parameters,
request.codeVerifier
);
}
if (this.config.clientCredentials.clientSecret) {
RequestParameterBuilder.addClientSecret(
parameters,
this.config.clientCredentials.clientSecret
);
}
if (this.config.clientCredentials.clientAssertion) {
const clientAssertion: ClientAssertion =
this.config.clientCredentials.clientAssertion;
RequestParameterBuilder.addClientAssertion(
parameters,
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
RequestParameterBuilder.addClientAssertionType(
parameters,
clientAssertion.assertionType
);
}
RequestParameterBuilder.addGrantType(
parameters,
Constants.GrantType.AUTHORIZATION_CODE_GRANT
);
RequestParameterBuilder.addClientInfo(parameters);
if (
request.authenticationScheme === Constants.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)
RequestParameterBuilder.addPopToken(parameters, reqCnfData);
} else if (
request.authenticationScheme === Constants.AuthenticationScheme.SSH
) {
if (request.sshJwk) {
RequestParameterBuilder.addSshJwk(parameters, request.sshJwk);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshJwk
);
}
}
let ccsCred: CcsCredential | undefined = undefined;
if (request.clientInfo) {
try {
const clientInfo = buildClientInfo(
request.clientInfo,
this.cryptoUtils.base64Decode
);
ccsCred = {
credential: `${clientInfo.uid}${Constants.CLIENT_INFO_SEPARATOR}${clientInfo.utid}`,
type: CcsCredentialType.HOME_ACCOUNT_ID,
};
} catch (e) {
this.logger.verbose(
`Could not parse client info for CCS Header: '${e}'`,
request.correlationId
);
}
} 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
);
RequestParameterBuilder.addCcsOid(
parameters,
clientInfo
);
} catch (e) {
this.logger.verbose(
`Could not parse home account ID for CCS Header: '${e}'`,
request.correlationId
);
}
break;
case CcsCredentialType.UPN:
RequestParameterBuilder.addCcsUpn(
parameters,
ccsCred.credential
);
break;
}
}
if (request.embeddedClientId) {
RequestParameterBuilder.addBrokerParameters(
parameters,
this.config.authOptions.clientId,
this.config.authOptions.redirectUri
);
}
if (request.extraParameters) {
RequestParameterBuilder.addExtraParameters(
parameters,
request.extraParameters
);
}
// Add hybrid spa parameters if not already provided
if (
request.enableSpaAuthorizationCode &&
(!request.extraParameters ||
!request.extraParameters[AADServerParamKeys.RETURN_SPA_CODE])
) {
RequestParameterBuilder.addExtraParameters(parameters, {
[AADServerParamKeys.RETURN_SPA_CODE]: "1",
});
}
RequestParameterBuilder.instrumentBrokerParams(
parameters,
request.correlationId,
this.performanceClient
);
RequestParameterBuilder.addClaims(
parameters,
request.claims,
this.config.authOptions.clientCapabilities,
request.skipBrokerClaims
);
return UrlUtils.mapToQueryString(parameters);
}
/**
* This API validates the `EndSessionRequest` and creates a URL
* @param request
*/
private createLogoutUrlQueryString(
request: CommonEndSessionRequest
): string {
const parameters = new Map<string, string>();
if (request.postLogoutRedirectUri) {
RequestParameterBuilder.addPostLogoutRedirectUri(
parameters,
request.postLogoutRedirectUri
);
}
if (request.correlationId) {
RequestParameterBuilder.addCorrelationId(
parameters,
request.correlationId
);
}
if (request.idTokenHint) {
RequestParameterBuilder.addIdTokenHint(
parameters,
request.idTokenHint
);
}
if (request.state) {
RequestParameterBuilder.addState(parameters, request.state);
}
if (request.logoutHint) {
RequestParameterBuilder.addLogoutHint(
parameters,
request.logoutHint
);
}
if (request.extraQueryParameters) {
RequestParameterBuilder.addExtraParameters(
parameters,
request.extraQueryParameters
);
}
if (this.config.authOptions.instanceAware) {
RequestParameterBuilder.addInstanceAware(parameters);
}
return UrlUtils.mapToQueryString(parameters);
}
/**
* Updates the authority to the cloud instance provided in the authorization response
* @param cloudInstanceHostName - cloud instance host name from authorization code payload
* @param correlationId - request correlation id
*/
private async updateTokenEndpointAuthority(
cloudInstanceHostName: string,
correlationId: string
): Promise<void> {
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;
}
}

View File

@ -0,0 +1,564 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
buildClientConfiguration,
ClientConfiguration,
CommonClientConfiguration,
isOidcProtocolMode,
} from "../config/ClientConfiguration.js";
import { CommonRefreshTokenRequest } from "../request/CommonRefreshTokenRequest.js";
import { Authority } from "../authority/Authority.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js";
import * as UrlUtils from "../utils/UrlUtils.js";
import * as Constants 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 { 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 * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import { invoke, invokeAsync } from "../utils/FunctionWrappers.js";
import { ClientAssertion } from "../account/ClientCredentials.js";
import { getClientAssertion } from "../utils/ClientAssertionUtils.js";
import { getRequestThumbprint } from "../network/RequestThumbprint.js";
import {
createTokenQueryParameters,
createTokenRequestHeaders,
executePostToTokenEndpoint,
} from "../protocol/Token.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { INetworkModule } from "../network/INetworkModule.js";
import { CacheManager } from "../cache/CacheManager.js";
import { ICrypto } from "../crypto/ICrypto.js";
import { Logger } from "../logger/Logger.js";
import { version, name } from "../packageMetadata.js";
const DEFAULT_REFRESH_TOKEN_EXPIRATION_OFFSET_SECONDS = 300; // 5 Minutes
/**
* OAuth2.0 refresh token client
* @internal
*/
export class RefreshTokenClient {
// 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;
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;
}
public async acquireToken(
request: CommonRefreshTokenRequest,
apiId: number
): Promise<AuthenticationResult> {
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?.[Constants.HeaderNames.X_MS_REQUEST_ID];
const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
this.cacheManager,
this.cryptoUtils,
this.logger,
this.performanceClient,
this.config.serializableCache,
this.config.persistencePlugin
);
responseHandler.validateTokenResponse(
response.body,
request.correlationId
);
return invokeAsync(
responseHandler.handleServerTokenResponse.bind(responseHandler),
PerformanceEvents.HandleServerTokenResponse,
this.logger,
this.performanceClient,
request.correlationId
)(
response.body,
this.authority,
reqTimestamp,
request,
apiId,
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,
apiId: number
): Promise<AuthenticationResult> {
// Cannot renew token if no request object is given.
if (!request) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.tokenRequestEmpty
);
}
// 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,
request.correlationId
);
// 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, apiId);
} catch (e) {
const noFamilyRTInCache =
e instanceof InteractionRequiredAuthError &&
e.errorCode ===
InteractionRequiredAuthErrorCodes.noTokensFound;
const clientMismatchErrorWithFamilyRT =
e instanceof ServerError &&
e.errorCode === Constants.INVALID_GRANT_ERROR &&
e.subError === Constants.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, apiId);
// 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, apiId);
}
/**
* 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,
apiId: number
) {
// 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, request.correlationId, undefined);
if (!refreshToken) {
throw createInteractionRequiredAuthError(
InteractionRequiredAuthErrorCodes.noTokensFound
);
}
if (refreshToken.expiresOn) {
const offset =
request.refreshTokenExpirationOffsetSeconds ||
DEFAULT_REFRESH_TOKEN_EXPIRATION_OFFSET_SECONDS;
this.performanceClient?.addFields(
{
cacheRtExpiresOnSeconds: Number(refreshToken.expiresOn),
rtOffsetSeconds: offset,
},
request.correlationId
);
if (TimeUtils.isTokenExpired(refreshToken.expiresOn, offset)) {
throw createInteractionRequiredAuthError(
InteractionRequiredAuthErrorCodes.refreshTokenExpired
);
}
}
// attach cached RT size to the current measurement
const refreshTokenRequest: CommonRefreshTokenRequest = {
...request,
refreshToken: refreshToken.secret,
authenticationScheme:
request.authenticationScheme ||
Constants.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, apiId);
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
if (e.subError === InteractionRequiredAuthErrorCodes.badToken) {
// Remove bad refresh token from cache
this.logger.verbose(
"acquireTokenWithRefreshToken: bad refresh token, removing from cache",
request.correlationId
);
const badRefreshTokenKey =
this.cacheManager.generateCredentialKey(refreshToken);
this.cacheManager.removeRefreshToken(
badRefreshTokenKey,
request.correlationId
);
}
}
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>> {
const queryParametersString = createTokenQueryParameters(
request,
this.config.authOptions.clientId,
this.config.authOptions.redirectUri,
this.performanceClient
);
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> = createTokenRequestHeaders(
this.logger,
this.config.systemOptions.preventCorsPreflight,
request.ccsCredential
);
const thumbprint = getRequestThumbprint(
this.config.authOptions.clientId,
request
);
return invokeAsync(
executePostToTokenEndpoint,
PerformanceEvents.RefreshTokenClientExecutePostToTokenEndpoint,
this.logger,
this.performanceClient,
request.correlationId
)(
endpoint,
requestBody,
headers,
thumbprint,
request.correlationId,
this.cacheManager,
this.networkClient,
this.logger,
this.performanceClient,
this.serverTelemetryManager
);
}
/**
* Helper function to create the token request body
* @param request
*/
private async createTokenRequestBody(
request: CommonRefreshTokenRequest
): Promise<string> {
const parameters = new Map<string, string>();
RequestParameterBuilder.addClientId(
parameters,
request.embeddedClientId ||
request.extraParameters?.[AADServerParamKeys.CLIENT_ID] ||
this.config.authOptions.clientId
);
if (request.redirectUri) {
RequestParameterBuilder.addRedirectUri(
parameters,
request.redirectUri
);
}
RequestParameterBuilder.addScopes(
parameters,
request.scopes,
true,
this.config.authOptions.authority.options.OIDCOptions?.defaultScopes
);
RequestParameterBuilder.addGrantType(
parameters,
Constants.GrantType.REFRESH_TOKEN_GRANT
);
RequestParameterBuilder.addClientInfo(parameters);
RequestParameterBuilder.addLibraryInfo(
parameters,
this.config.libraryInfo
);
RequestParameterBuilder.addApplicationTelemetry(
parameters,
this.config.telemetry.application
);
RequestParameterBuilder.addThrottling(parameters);
if (this.serverTelemetryManager && !isOidcProtocolMode(this.config)) {
RequestParameterBuilder.addServerTelemetry(
parameters,
this.serverTelemetryManager
);
}
RequestParameterBuilder.addRefreshToken(
parameters,
request.refreshToken
);
if (this.config.clientCredentials.clientSecret) {
RequestParameterBuilder.addClientSecret(
parameters,
this.config.clientCredentials.clientSecret
);
}
if (this.config.clientCredentials.clientAssertion) {
const clientAssertion: ClientAssertion =
this.config.clientCredentials.clientAssertion;
RequestParameterBuilder.addClientAssertion(
parameters,
await getClientAssertion(
clientAssertion.assertion,
this.config.authOptions.clientId,
request.resourceRequestUri
)
);
RequestParameterBuilder.addClientAssertionType(
parameters,
clientAssertion.assertionType
);
}
if (
request.authenticationScheme === Constants.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)
RequestParameterBuilder.addPopToken(parameters, reqCnfData);
} else if (
request.authenticationScheme === Constants.AuthenticationScheme.SSH
) {
if (request.sshJwk) {
RequestParameterBuilder.addSshJwk(parameters, request.sshJwk);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshJwk
);
}
}
if (
this.config.systemOptions.preventCorsPreflight &&
request.ccsCredential
) {
switch (request.ccsCredential.type) {
case CcsCredentialType.HOME_ACCOUNT_ID:
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.ccsCredential.credential
);
RequestParameterBuilder.addCcsOid(
parameters,
clientInfo
);
} catch (e) {
this.logger.verbose(
`Could not parse home account ID for CCS Header: '${e}'`,
request.correlationId
);
}
break;
case CcsCredentialType.UPN:
RequestParameterBuilder.addCcsUpn(
parameters,
request.ccsCredential.credential
);
break;
}
}
if (request.embeddedClientId) {
RequestParameterBuilder.addBrokerParameters(
parameters,
this.config.authOptions.clientId,
this.config.authOptions.redirectUri
);
}
if (request.extraParameters) {
RequestParameterBuilder.addExtraParameters(parameters, {
...request.extraParameters,
});
}
RequestParameterBuilder.instrumentBrokerParams(
parameters,
request.correlationId,
this.performanceClient
);
RequestParameterBuilder.addClaims(
parameters,
request.claims,
this.config.authOptions.clientCapabilities,
request.skipBrokerClaims
);
return UrlUtils.mapToQueryString(parameters);
}
}

View File

@ -0,0 +1,272 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
buildClientConfiguration,
ClientConfiguration,
CommonClientConfiguration,
} 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 * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import {
Authority,
getTenantFromAuthorityString,
} from "../authority/Authority.js";
import { Logger } from "../logger/Logger.js";
import { ICrypto } from "../crypto/ICrypto.js";
import { CacheManager } from "../cache/CacheManager.js";
import { INetworkModule } from "../network/INetworkModule.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { version, name } from "../packageMetadata.js";
/** @internal */
export class SilentFlowClient {
// 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;
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;
}
/**
* Retrieves token from cache or throws an error if it must be refreshed.
* @param request
*/
async acquireCachedToken(
request: CommonSilentFlowRequest
): Promise<[AuthenticationResult, CacheOutcome]> {
let lastCacheOutcome: CacheOutcome = CacheOutcome.NOT_APPLICABLE;
if (request.forceRefresh || !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
);
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 (request.resource) {
// cached access token must have a resource that matches the request resource for MCP scenarios
if (cachedAccessToken.resource !== request.resource) {
this.setCacheOutcome(
CacheOutcome.NO_CACHED_ACCESS_TOKEN,
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.getAccount(
this.cacheManager.generateAccountKey(request.account),
request.correlationId
),
accessToken: cachedAccessToken,
idToken: this.cacheManager.getIdToken(
request.account,
request.correlationId,
tokenKeys,
requestTenantId
),
refreshToken: null,
appMetadata: this.cacheManager.readAppMetadataFromCache(
environment,
request.correlationId
),
};
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}'`,
correlationId
);
}
}
/**
* Helper function to build response object from the CacheRecord
* @param cacheRecord
*/
private async generateResultFromCacheRecord(
cacheRecord: CacheRecord,
request: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
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,
this.performanceClient,
idTokenClaims
);
}
}

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;
};

View File

@ -0,0 +1,277 @@
/*
* 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 {
DEFAULT_COMMON_TENANT,
DEFAULT_TOKEN_RENEWAL_OFFSET_SEC,
SKU,
} from "../utils/Constants.js";
import { version } from "../packageMetadata.js";
import type { 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";
import { StubPerformanceClient } from "../telemetry/performance/StubPerformanceClient.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;
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>;
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.
* - 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.
* - isMcp - A flag of whether the application is an MCP application, which requires a resource parameter to be sent in token requests.
* @internal
*/
export type AuthOptions = {
clientId: string;
authority: Authority;
redirectUri: string;
clientCapabilities?: Array<string>;
azureCloudOptions?: AzureCloudOptions;
instanceAware?: boolean;
isMcp?: 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
* - protocolMode - Enum that represents the protocol that msal follows. Used for configuring proper endpoints.
*/
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;
};
/**
* 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: "",
};
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: SKU,
version: version,
cpu: "",
os: "",
};
const DEFAULT_CLIENT_CREDENTIALS: ClientCredentials = {
clientSecret: "",
clientAssertion: undefined,
};
const DEFAULT_AZURE_CLOUD_OPTIONS: AzureCloudOptions = {
azureCloudInstance: AzureCloudInstance.None,
tenant: `${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,
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,
storageInterface:
storageImplementation ||
new DefaultStorageClass(
userAuthOptions.clientId,
DEFAULT_CRYPTO_IMPLEMENTATION,
new Logger(loggerOptions),
new StubPerformanceClient()
),
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,
instanceAware: false,
isMcp: 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
);
}

View File

@ -0,0 +1,65 @@
/*
* 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";
export const INSTANCE_AWARE = "instance_aware";
export const EAR_JWK = "ear_jwk";
export const EAR_JWE_CRYPTO = "ear_jwe_crypto";
export const RESOURCE = "resource";
export const CLI_DATA = "clidata";

View File

@ -0,0 +1,128 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import type { BaseAuthRequest } from "../request/BaseAuthRequest.js";
import type { 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
* @param correlationId
*/
removeTokenBindingKey(kid: string, correlationId: string): Promise<void>;
/**
* Removes all cryptographic keys from IndexedDB storage
* @param correlationId
*/
clearKeystore(correlationId: string): 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<void> {
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);
},
};

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;
}

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);
}
}

View File

@ -0,0 +1,150 @@
/*
* 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 * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.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> {
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> {
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
);
}
}

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;
};

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as AuthErrorCodes from "./AuthErrorCodes.js";
import type { PlatformBrokerError } from "./PlatformBrokerError.js";
export { AuthErrorCodes };
export function getDefaultErrorMessage(code: string): string {
return `See https://aka.ms/msal.js.errors#${code} for details`;
}
/**
* 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;
/**
* Default PlatformBrokerError from MsalNodeRuntime when broker is enabled
*/
platformBrokerError?: PlatformBrokerError;
constructor(errorCode?: string, errorMessage?: string, suberror?: string) {
const message =
errorMessage ||
(errorCode ? getDefaultErrorMessage(errorCode) : "");
const errorString = message ? `${errorCode}: ${message}` : errorCode;
super(errorString);
Object.setPrototypeOf(this, AuthError.prototype);
this.errorCode = errorCode || "";
this.errorMessage = message || "";
this.subError = suberror || "";
this.name = "AuthError";
}
setCorrelationId(correlationId: string): void {
this.correlationId = correlationId;
}
}
export function createAuthError(
code: string,
additionalMessage?: string
): AuthError {
return new AuthError(
code,
additionalMessage || getDefaultErrorMessage(code)
);
}

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";

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as CacheErrorCodes from "./CacheErrorCodes.js";
import { getDefaultErrorMessage } from "./AuthError.js";
export { CacheErrorCodes };
/**
* 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 || getDefaultErrorMessage(errorCode);
super(message);
Object.setPrototypeOf(this, CacheError.prototype);
this.name = "CacheError";
this.errorCode = errorCode;
this.errorMessage = message;
}
}
/**
* Helper function to wrap browser errors in a CacheError object
* @param e
* @returns
*/
export function createCacheError(e: unknown): CacheError {
if (!(e instanceof Error)) {
return new CacheError(CacheErrorCodes.cacheErrorUnknown);
}
if (
e.name === "QuotaExceededError" ||
e.name === "NS_ERROR_DOM_QUOTA_REACHED" ||
e.message.includes("exceeded the quota")
) {
return new CacheError(CacheErrorCodes.cacheQuotaExceeded);
} else {
return new CacheError(e.name, e.message);
}
}

View File

@ -0,0 +1,7 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const cacheQuotaExceeded = "cache_quota_exceeded";
export const cacheErrorUnknown = "cache_error_unknown";

View File

@ -0,0 +1,31 @@
/*
* 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.
*/
/**
* 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);
this.name = "ClientAuthError";
Object.setPrototypeOf(this, ClientAuthError.prototype);
}
}
export function createClientAuthError(
errorCode: string,
additionalMessage?: string
): ClientAuthError {
return new ClientAuthError(errorCode, additionalMessage);
}

View File

@ -0,0 +1,47 @@
/*
* 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 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 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 tokenRefreshRequired = "token_refresh_required";
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 methodNotImplemented = "method_not_implemented";
export const nestedAppAuthBridgeDisabled = "nested_app_auth_bridge_disabled";
export const platformBrokerError = "platform_broker_error";
export const resourceParameterRequired = "resource_parameter_required";
export const misplacedResourceParam = "misplaced_resource_parameter";

View File

@ -0,0 +1,25 @@
/*
* 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 };
/**
* Error thrown when there is an error in configuration of the MSAL.js library.
*/
export class ClientConfigurationError extends AuthError {
constructor(errorCode: string) {
super(errorCode);
this.name = "ClientConfigurationError";
Object.setPrototypeOf(this, ClientConfigurationError.prototype);
}
}
export function createClientConfigurationError(
errorCode: string
): ClientConfigurationError {
return new ClientConfigurationError(errorCode);
}

View File

@ -0,0 +1,31 @@
/*
* 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 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";
export const invalidRequestMethodForEAR = "invalid_request_method_for_EAR";
export const invalidPlatformBrokerConfiguration =
"invalid_platform_broker_configuration";
export const issuerValidationFailed = "issuer_validation_failed";

View File

@ -0,0 +1,123 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
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,
InteractionRequiredAuthErrorCodes.uxNotAllowed,
InteractionRequiredAuthErrorCodes.interruptedUser,
];
export const InteractionRequiredAuthSubErrorMessage = [
"message_only",
"additional_action",
"basic_action",
"user_password_expired",
"consent_required",
"bad_token",
"ux_not_allowed",
"interrupted_user",
];
/**
* 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 || "";
this.traceId = traceId || "";
this.correlationId = correlationId || "";
this.claims = claims || "";
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,
errorMessage?: string
): InteractionRequiredAuthError {
return new InteractionRequiredAuthError(errorCode, errorMessage);
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* MSAL-defined interaction required error code indicating no tokens are found in cache.
* @public
*/
export const noTokensFound = "no_tokens_found";
/**
* MSAL-defined error code indicating a native account is unavailable on the platform.
* @public
*/
export const nativeAccountUnavailable = "native_account_unavailable";
/**
* MSAL-defined error code indicating the refresh token has expired and user interaction is needed.
* @public
*/
export const refreshTokenExpired = "refresh_token_expired";
/**
* MSAL-defined error code indicating UI/UX is not allowed (e.g., blocked by policy), requiring alternate interaction.
* @public
*/
export const uxNotAllowed = "ux_not_allowed";
/**
* Server-originated error code indicating interaction is required to complete the request.
* @public
*/
export const interactionRequired = "interaction_required";
/**
* Server-originated error code indicating user consent is required.
* @public
*/
export const consentRequired = "consent_required";
/**
* Server-originated error code indicating user login is required.
* @public
*/
export const loginRequired = "login_required";
/**
* Server-originated error code indicating the token is invalid or corrupted.
* @public
*/
export const badToken = "bad_token";
/**
* Server-originated error code indicating the user is in an interrupted state and interaction is required.
* @public
*/
export const interruptedUser = "interrupted_user";

View File

@ -0,0 +1,25 @@
/*
* 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 };
/**
* 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);
}

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";

View File

@ -0,0 +1,46 @@
/*
* 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>,
additionalError?: Error
): NetworkError {
error.errorMessage = `${error.errorMessage}, additionalErrorInfo: error.name:${additionalError?.name}, error.message:${additionalError?.message}`;
return new NetworkError(error, httpStatus, responseHeaders);
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "./AuthError.js";
/**
* Converts a numeric tag from MSAL Runtime to a 5-character string representation.
* Tags are encoded as 30-bit values (6 bits per character) using a custom symbol space.
* @param tag - The numeric tag to convert
* @returns The string representation of the tag
*/
function tagToString(tag: number): string {
if (tag === 0) {
return "UNTAG";
}
const tagSymbolSpace =
"abcdefghijklmnopqrstuvwxyz0123456789****************************";
let tagBuffer = "*****";
const chars = [
tagSymbolSpace[(tag >> 24) & 0x3f],
tagSymbolSpace[(tag >> 18) & 0x3f],
tagSymbolSpace[(tag >> 12) & 0x3f],
tagSymbolSpace[(tag >> 6) & 0x3f],
tagSymbolSpace[(tag >> 0) & 0x3f],
];
tagBuffer = chars.join("");
return tagBuffer;
}
/**
* Error class for MSAL Runtime errors that preserves detailed broker information
*/
export class PlatformBrokerError extends AuthError {
/**
* Numeric error code from MSAL Runtime
*/
public statusCode: number;
/**
* Error tag from MSAL Runtime
*/
public tag: string;
constructor(
errorStatus: string,
errorContext: string,
errorCode: number,
errorTag: number
) {
const tagString = tagToString(errorTag);
const enhancedErrorContext = errorContext
? `${errorContext} (Error Code: ${errorCode}, Tag: ${tagString})`
: `(Error Code: ${errorCode}, Tag: ${tagString})`;
super(errorStatus, enhancedErrorContext);
this.name = "PlatformBrokerError";
this.statusCode = errorCode;
this.tag = tagString;
Object.setPrototypeOf(this, PlatformBrokerError.prototype);
}
}

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);
}
}

View File

@ -0,0 +1,25 @@
/*
* 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,
} from "./telemetry/performance/IPerformanceClient.js";
export {
IntFields,
PerformanceEvent,
PerformanceEventStatus,
SubMeasurement,
} from "./telemetry/performance/PerformanceEvent.js";
export * as PerformanceEvents from "./telemetry/performance/PerformanceEvents.js";
export { IPerformanceMeasurement } from "./telemetry/performance/IPerformanceMeasurement.js";
export { PerformanceClient } from "./telemetry/performance/PerformanceClient.js";
export { StubPerformanceClient } from "./telemetry/performance/StubPerformanceClient.js";
export { PopTokenGenerator } from "./crypto/PopTokenGenerator.js";

View File

@ -0,0 +1,173 @@
/*
* 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";
import * as AccountEntityUtils from "./cache/utils/AccountEntityUtils.js";
export { AuthToken };
export { AuthorityFactory };
export { CacheHelpers };
export { TimeUtils };
export { UrlUtils };
export { AADServerParamKeys };
export { AccountEntityUtils };
export { AuthorizationCodeClient } from "./client/AuthorizationCodeClient.js";
export { RefreshTokenClient } from "./client/RefreshTokenClient.js";
export { SilentFlowClient } from "./client/SilentFlowClient.js";
export {
AuthOptions,
SystemOptions,
LoggerOptions,
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,
getRequestThumbprint,
} 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 * as AuthorizeProtocol from "./protocol/Authorize.js";
export * as TokenProtocol from "./protocol/Token.js";
export {
BaseAuthRequest,
enforceResourceParameter,
} 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 * as 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 { AuthorizeResponse } from "./response/AuthorizeResponse.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,
createInteractionRequiredAuthError,
} from "./error/InteractionRequiredAuthError.js";
export {
AuthError,
AuthErrorCodes,
createAuthError,
} from "./error/AuthError.js";
export { PlatformBrokerError } from "./error/PlatformBrokerError.js";
export { ServerError } from "./error/ServerError.js";
export { NetworkError, createNetworkError } from "./error/NetworkError.js";
export {
CacheError,
CacheErrorCodes,
createCacheError,
} from "./error/CacheError.js";
export {
ClientAuthError,
ClientAuthErrorCodes,
createClientAuthError,
} from "./error/ClientAuthError.js";
export {
ClientConfigurationError,
ClientConfigurationErrorCodes,
createClientConfigurationError,
} from "./error/ClientConfigurationError.js";
export * as Constants from "./utils/Constants.js";
export { StringUtils } from "./utils/StringUtils.js";
export { StringDict } from "./utils/MsalTypes.js";
export { RequestStateObject, LibraryStateObject } from "./utils/StateTypes.js";
export * as ProtocolUtils 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";

View File

@ -0,0 +1,36 @@
/*
* 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 { 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";
export { StubPerformanceClient } from "./telemetry/performance/StubPerformanceClient.js";
export {
buildClientConfiguration,
CommonClientConfiguration,
} from "./config/ClientConfiguration.js";

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";

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
backend/node_modules/@azure/msal-common/src/index.ts generated vendored Normal file
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";

View File

@ -0,0 +1,407 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import type { LoggerOptions } from "../config/ClientConfiguration.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;
}
/**
* Represents a single logged message with metadata
*/
export interface LoggedMessage {
hash: string;
level: LogLevel;
containsPii: boolean;
milliseconds: number;
}
/**
* LRU cache node for correlation ID management
*/
interface CorrelationLogData {
logs: LoggedMessage[];
firstEventTime: number;
}
// Shared cache state for better minification - using Map's insertion order for LRU
const CACHE_CAPACITY = 50;
const MAX_LOGS_PER_CORRELATION = 500;
const correlationCache = new Map<string, CorrelationLogData>();
/**
* Mark correlation ID as recently used by moving it to end of Map
* @param correlationId
* @param {CorrelationLogData} data
*/
function markAsRecentlyUsed(
correlationId: string,
data: CorrelationLogData
): void {
correlationCache.delete(correlationId);
correlationCache.set(correlationId, data);
}
/**
* Add log message to cache for specific correlation ID
* @param correlationId
* @param {LoggedMessage} loggedMessage
*/
function addLogToCache(
correlationId: string,
loggedMessage: LoggedMessage
): void {
const currentTime = Date.now();
let data = correlationCache.get(correlationId);
if (data) {
// Mark as recently used
markAsRecentlyUsed(correlationId, data);
} else {
// Create new entry
data = { logs: [], firstEventTime: currentTime };
correlationCache.set(correlationId, data);
// Remove LRU (first entry) if capacity exceeded
if (correlationCache.size > CACHE_CAPACITY) {
const firstKey = correlationCache.keys().next().value;
if (firstKey) {
correlationCache.delete(firstKey);
}
}
}
// Add log to the data, maintaining max logs per correlation
data.logs.push({
...loggedMessage,
milliseconds: currentTime - data.firstEventTime,
});
if (data.logs.length > MAX_LOGS_PER_CORRELATION) {
data.logs.shift(); // Remove oldest log
}
}
/**
* Get all logs for specific correlation ID
* @param correlationId
*/
export function getLogsFromCache(correlationId: string): LoggedMessage[] {
const data = correlationCache.get(correlationId);
if (data) {
markAsRecentlyUsed(correlationId, data);
return [...data.logs]; // Return copy
}
return [];
}
/**
* Get logs for correlation ID and flush them from cache
* Attaches logs with empty correlation id to the requested correlation logs
* @param correlationId
*/
export function getAndFlushLogsFromCache(
correlationId: string
): LoggedMessage[] {
const res: LoggedMessage[] = [];
for (const id of ["", correlationId]) {
const data = correlationCache.get(id);
res.push(...(data?.logs ?? []));
correlationCache.delete(id); // Remove the correlation ID completely from cache
}
return res;
}
/**
* Get all correlation IDs that have logs
*/
export function getCachedCorrelationIds(): string[] {
return Array.from(correlationCache.keys());
}
/**
* Checks if a string is already a hashed logging string (6 alphanumeric characters)
*/
function isHashedString(str: string): boolean {
if (str.length !== 6) {
return false;
}
for (let i = 0; i < str.length; i++) {
const char = str[i];
const isAlphaNumeric =
(char >= "a" && char <= "z") ||
(char >= "A" && char <= "Z") ||
(char >= "0" && char <= "9");
if (!isAlphaNumeric) {
return false;
}
}
return true;
}
/**
* Class which facilitates logging of messages to a specific place.
*/
export class Logger {
// 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.packageName = packageName || "";
this.packageVersion = packageVersion || "";
}
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): Logger {
return new Logger(
{
loggerCallback: this.localCallback,
piiLoggingEnabled: this.piiLoggingEnabled,
logLevel: this.level,
},
packageName,
packageVersion
);
}
/**
* Log message with required options.
*/
private logMessage(
logMessage: string,
options: LoggerMessageOptions
): void {
const correlationId = options.correlationId;
const isHashedInput = isHashedString(logMessage);
if (isHashedInput) {
const loggedMessage: LoggedMessage = {
hash: logMessage,
level: options.logLevel,
containsPii: options.containsPii || false,
milliseconds: 0, // Will be calculated in addLogToCache
};
addLogToCache(correlationId, loggedMessage);
}
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}] : [${correlationId}]`;
const log = `${logHeader} : ${this.packageName}@${
this.packageVersion
} : ${LogLevel[options.logLevel]} - ${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,
});
}
/**
* Logs error messages with PII.
*/
errorPii(message: string, correlationId: string): void {
this.logMessage(message, {
logLevel: LogLevel.Error,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Logs warning messages.
*/
warning(message: string, correlationId: string): void {
this.logMessage(message, {
logLevel: LogLevel.Warning,
containsPii: false,
correlationId: correlationId,
});
}
/**
* Logs warning messages with PII.
*/
warningPii(message: string, correlationId: string): void {
this.logMessage(message, {
logLevel: LogLevel.Warning,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Logs info messages.
*/
info(message: string, correlationId: string): void {
this.logMessage(message, {
logLevel: LogLevel.Info,
containsPii: false,
correlationId: correlationId,
});
}
/**
* Logs info messages with PII.
*/
infoPii(message: string, correlationId: string): void {
this.logMessage(message, {
logLevel: LogLevel.Info,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Logs verbose messages.
*/
verbose(message: string, correlationId: string): void {
this.logMessage(message, {
logLevel: LogLevel.Verbose,
containsPii: false,
correlationId: correlationId,
});
}
/**
* Logs verbose messages with PII.
*/
verbosePii(message: string, correlationId: string): void {
this.logMessage(message, {
logLevel: LogLevel.Verbose,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Logs trace messages.
*/
trace(message: string, correlationId: string): void {
this.logMessage(message, {
logLevel: LogLevel.Trace,
containsPii: false,
correlationId: correlationId,
});
}
/**
* Logs trace messages with PII.
*/
tracePii(message: string, correlationId: string): void {
this.logMessage(message, {
logLevel: LogLevel.Trace,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Returns whether PII Logging is enabled or not.
*/
isPiiLoggingEnabled(): boolean {
return this.piiLoggingEnabled || false;
}
}

View File

@ -0,0 +1,59 @@
/*
* 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 options - Headers and/or body to include on the request
* @param timeout
*/
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 options - Headers and/or body to include on the request
*/
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)
);
},
};

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;
};

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ShrOptions } from "../crypto/SignedHttpRequest.js";
import { BaseAuthRequest } from "../request/BaseAuthRequest.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;
embeddedClientId?: string;
};
export function getRequestThumbprint(
clientId: string,
request: BaseAuthRequest,
homeAccountId?: string
): RequestThumbprint {
return {
clientId: clientId,
authority: request.authority,
scopes: request.scopes,
homeAccountIdentifier: homeAccountId,
claims: request.claims,
authenticationScheme: request.authenticationScheme,
resourceRequestMethod: request.resourceRequestMethod,
resourceRequestUri: request.resourceRequestUri,
shrClaims: request.shrClaims,
sshKid: request.sshKid,
embeddedClientId:
request.embeddedClientId || request.extraParameters?.clientId,
};
}

View File

@ -0,0 +1,151 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { NetworkResponse } from "./NetworkResponse.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import * as Constants from "../utils/Constants.js";
import { CacheManager } from "../cache/CacheManager.js";
import { ServerError } from "../error/ServerError.js";
import {
getRequestThumbprint,
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 `${Constants.THROTTLING_PREFIX}.${JSON.stringify(thumbprint)}`;
}
/**
* Performs necessary throttling checks before a network request.
* @param cacheManager
* @param thumbprint
*/
static preProcess(
cacheManager: CacheManager,
thumbprint: RequestThumbprint,
correlationId: string
): void {
const key = ThrottlingUtils.generateThrottlingStorageKey(thumbprint);
const value = cacheManager.getThrottlingCache(key, correlationId);
if (value) {
if (value.throttleTime < Date.now()) {
cacheManager.removeItem(key, correlationId);
return;
}
throw new ServerError(
value.errorCodes?.join(" ") || "",
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>,
correlationId: string
): void {
if (
ThrottlingUtils.checkResponseStatus(response) ||
ThrottlingUtils.checkResponseForRetryAfter(response)
) {
const thumbprintValue: ThrottlingEntity = {
throttleTime: ThrottlingUtils.calculateThrottleTime(
parseInt(
response.headers[Constants.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,
correlationId
);
}
}
/**
* 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(
Constants.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 || Constants.DEFAULT_THROTTLE_TIME_SECONDS),
currentSeconds + Constants.DEFAULT_MAX_THROTTLE_TIME_SECONDS
) * 1000
);
}
static removeThrottle(
cacheManager: CacheManager,
clientId: string,
request: BaseAuthRequest,
homeAccountIdentifier?: string
): void {
const thumbprint = getRequestThumbprint(
clientId,
request,
homeAccountIdentifier
);
const key = this.generateThrottlingStorageKey(thumbprint);
cacheManager.removeItem(key, request.correlationId);
}
}

View File

@ -0,0 +1,3 @@
/* eslint-disable header/header */
export const name = "@azure/msal-common";
export const version = "16.6.2";

View File

@ -0,0 +1,419 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CommonAuthorizationUrlRequest } from "../request/CommonAuthorizationUrlRequest.js";
import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
import { AuthOptions } from "../config/ClientConfiguration.js";
import { PromptValue } from "../utils/Constants.js";
import { AccountInfo } from "../account/AccountInfo.js";
import { Logger } from "../logger/Logger.js";
import { buildClientInfoFromHomeAccountId } from "../account/ClientInfo.js";
import { Authority } from "../authority/Authority.js";
import { mapToQueryString } from "../utils/UrlUtils.js";
import { UrlString } from "../url/UrlString.js";
import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js";
import { AuthorizeResponse } from "../response/AuthorizeResponse.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import {
InteractionRequiredAuthError,
isInteractionRequiredError,
} from "../error/InteractionRequiredAuthError.js";
import { ServerError } from "../error/ServerError.js";
/**
* Returns map of parameters that are applicable to all calls to /authorize whether using PKCE or EAR
* @param config
* @param request
* @param logger
* @param performanceClient
* @returns
*/
export function getStandardAuthorizeRequestParameters(
authOptions: AuthOptions,
request: CommonAuthorizationUrlRequest,
logger: Logger,
performanceClient?: IPerformanceClient
): Map<string, string> {
// generate the correlationId if not set by the user and add
const correlationId = request.correlationId;
const parameters = new Map<string, string>();
RequestParameterBuilder.addClientId(
parameters,
request.embeddedClientId ||
request.extraQueryParameters?.[AADServerParamKeys.CLIENT_ID] ||
authOptions.clientId
);
const requestScopes = [
...(request.scopes || []),
...(request.extraScopesToConsent || []),
];
RequestParameterBuilder.addScopes(
parameters,
requestScopes,
true,
authOptions.authority.options.OIDCOptions?.defaultScopes
);
RequestParameterBuilder.addResource(parameters, request.resource);
RequestParameterBuilder.addRedirectUri(parameters, request.redirectUri);
RequestParameterBuilder.addCorrelationId(parameters, correlationId);
// add response_mode. If not passed in it defaults to query.
RequestParameterBuilder.addResponseMode(parameters, request.responseMode);
// add client_info=1
RequestParameterBuilder.addClientInfo(parameters);
// add clidata=1
RequestParameterBuilder.addCliData(parameters);
if (request.prompt) {
RequestParameterBuilder.addPrompt(parameters, request.prompt);
performanceClient?.addFields({ prompt: request.prompt }, correlationId);
}
if (request.domainHint) {
RequestParameterBuilder.addDomainHint(parameters, request.domainHint);
performanceClient?.addFields(
{ domainHintFromRequest: true },
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
logger.verbose(
"createAuthCodeUrlQueryString: Prompt is none, adding sid from request",
request.correlationId
);
RequestParameterBuilder.addSid(parameters, request.sid);
performanceClient?.addFields(
{ sidFromRequest: true },
correlationId
);
} else if (request.account) {
const accountSid = extractAccountSid(request.account);
let accountLoginHintClaim = extractLoginHint(request.account);
if (accountLoginHintClaim && request.domainHint) {
logger.warning(
`AuthorizationCodeClient.createAuthCodeUrlQueryString: "domainHint" param is set, skipping opaque "login_hint" claim. Please consider not passing domainHint`,
request.correlationId
);
accountLoginHintClaim = null;
}
// If login_hint claim is present, use it over sid/username
if (accountLoginHintClaim) {
logger.verbose(
"createAuthCodeUrlQueryString: login_hint claim present on account",
request.correlationId
);
RequestParameterBuilder.addLoginHint(
parameters,
accountLoginHintClaim
);
performanceClient?.addFields(
{ loginHintFromClaim: true },
correlationId
);
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.account.homeAccountId
);
RequestParameterBuilder.addCcsOid(parameters, clientInfo);
} catch (e) {
logger.verbose(
"createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header",
request.correlationId
);
}
} 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
*/
logger.verbose(
"createAuthCodeUrlQueryString: Prompt is none, adding sid from account",
request.correlationId
);
RequestParameterBuilder.addSid(parameters, accountSid);
performanceClient?.addFields(
{ sidFromClaim: true },
correlationId
);
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.account.homeAccountId
);
RequestParameterBuilder.addCcsOid(parameters, clientInfo);
} catch (e) {
logger.verbose(
"createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header",
request.correlationId
);
}
} else if (request.loginHint) {
logger.verbose(
"createAuthCodeUrlQueryString: Adding login_hint from request",
request.correlationId
);
RequestParameterBuilder.addLoginHint(
parameters,
request.loginHint
);
RequestParameterBuilder.addCcsUpn(
parameters,
request.loginHint
);
performanceClient?.addFields(
{ loginHintFromRequest: true },
correlationId
);
} else if (request.account.username) {
// Fallback to account username if provided
logger.verbose(
"createAuthCodeUrlQueryString: Adding login_hint from account",
request.correlationId
);
RequestParameterBuilder.addLoginHint(
parameters,
request.account.username
);
performanceClient?.addFields(
{ loginHintFromUpn: true },
correlationId
);
try {
const clientInfo = buildClientInfoFromHomeAccountId(
request.account.homeAccountId
);
RequestParameterBuilder.addCcsOid(parameters, clientInfo);
} catch (e) {
logger.verbose(
"createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header",
request.correlationId
);
}
}
} else if (request.loginHint) {
logger.verbose(
"createAuthCodeUrlQueryString: No account, adding login_hint from request",
request.correlationId
);
RequestParameterBuilder.addLoginHint(parameters, request.loginHint);
RequestParameterBuilder.addCcsUpn(parameters, request.loginHint);
performanceClient?.addFields(
{ loginHintFromRequest: true },
correlationId
);
}
} else {
logger.verbose(
"createAuthCodeUrlQueryString: Prompt is select_account, ignoring account hints",
request.correlationId
);
}
if (request.nonce) {
RequestParameterBuilder.addNonce(parameters, request.nonce);
}
if (request.state) {
RequestParameterBuilder.addState(parameters, request.state);
}
if (request.embeddedClientId) {
RequestParameterBuilder.addBrokerParameters(
parameters,
authOptions.clientId,
authOptions.redirectUri
);
}
RequestParameterBuilder.addClaims(
parameters,
request.claims,
authOptions.clientCapabilities,
request.skipBrokerClaims
);
// If extraQueryParameters includes instance_aware its value will be added when extraQueryParameters are added
if (
authOptions.instanceAware &&
(!request.extraQueryParameters ||
!Object.keys(request.extraQueryParameters).includes(
AADServerParamKeys.INSTANCE_AWARE
))
) {
RequestParameterBuilder.addInstanceAware(parameters);
}
return parameters;
}
/**
* Returns authorize endpoint with given request parameters in the query string
* @param authority
* @param requestParameters
* @returns
*/
export function getAuthorizeUrl(
authority: Authority,
requestParameters: Map<string, string>
): string {
const queryString = mapToQueryString(requestParameters);
return UrlString.appendQueryString(
authority.authorizationEndpoint,
queryString
);
}
/**
* 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 serverParams
* @param cachedState
*/
export function getAuthorizationCodePayload(
serverParams: AuthorizeResponse,
cachedState: string
): AuthorizationCodePayload {
// Get code response
validateAuthorizationResponse(serverParams, cachedState);
// throw when there is no auth code in the response
if (!serverParams.code) {
throw createClientAuthError(
ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse
);
}
return serverParams as AuthorizationCodePayload;
}
/**
* Function which validates server authorization code response.
* @param serverResponseHash
* @param requestState
*/
export function validateAuthorizationResponse(
serverResponse: AuthorizeResponse,
requestState: string
): void {
if (!serverResponse.state || !requestState) {
throw serverResponse.state
? createClientAuthError(
ClientAuthErrorCodes.stateNotFound,
"Cached State"
)
: createClientAuthError(
ClientAuthErrorCodes.stateNotFound,
"Server State"
);
}
let decodedServerResponseState: string;
let decodedRequestState: string;
try {
decodedServerResponseState = decodeURIComponent(serverResponse.state);
} catch (e) {
throw createClientAuthError(
ClientAuthErrorCodes.invalidState,
serverResponse.state
);
}
try {
decodedRequestState = decodeURIComponent(requestState);
} catch (e) {
throw createClientAuthError(
ClientAuthErrorCodes.invalidState,
serverResponse.state
);
}
if (decodedServerResponseState !== decodedRequestState) {
throw createClientAuthError(ClientAuthErrorCodes.stateMismatch);
}
// Check for error
if (
serverResponse.error ||
serverResponse.error_description ||
serverResponse.suberror
) {
const serverErrorNo = parseServerErrorNo(serverResponse);
if (
isInteractionRequiredError(
serverResponse.error,
serverResponse.error_description,
serverResponse.suberror
)
) {
throw new InteractionRequiredAuthError(
serverResponse.error || "",
serverResponse.error_description,
serverResponse.suberror,
serverResponse.timestamp || "",
serverResponse.trace_id || "",
serverResponse.correlation_id || "",
serverResponse.claims || "",
serverErrorNo
);
}
throw new ServerError(
serverResponse.error || "",
serverResponse.error_description,
serverResponse.suberror,
serverErrorNo
);
}
}
/**
* Get server error No from the error_uri
* @param serverResponse
* @returns
*/
function parseServerErrorNo(
serverResponse: AuthorizeResponse
): string | undefined {
const errorCodePrefix = "code=";
const errorCodePrefixIndex =
serverResponse.error_uri?.lastIndexOf(errorCodePrefix);
return errorCodePrefixIndex && errorCodePrefixIndex >= 0
? serverResponse.error_uri?.substring(
errorCodePrefixIndex + errorCodePrefix.length
)
: undefined;
}
/**
* Helper to get sid from account. Returns null if idTokenClaims are not present or sid is not present.
* @param account
*/
function extractAccountSid(account: AccountInfo): string | null {
return account.idTokenClaims?.sid || null;
}
function extractLoginHint(account: AccountInfo): string | null {
return account.loginHint || account.idTokenClaims?.login_hint || null;
}

View File

@ -0,0 +1,230 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CcsCredential, CcsCredentialType } from "../account/CcsCredential.js";
import { buildClientInfoFromHomeAccountId } from "../account/ClientInfo.js";
import { Logger } from "../logger/Logger.js";
import { BaseAuthRequest } from "../request/BaseAuthRequest.js";
import { HeaderNames, URL_FORM_CONTENT_TYPE } from "../utils/Constants.js";
import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js";
import * as UrlUtils from "../utils/UrlUtils.js";
import { IPerformanceClient } from "../exports-browser-only.js";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse.js";
import { RequestThumbprint } from "../network/RequestThumbprint.js";
import {
INetworkModule,
NetworkRequestOptions,
} from "../network/INetworkModule.js";
import { NetworkResponse } from "../network/NetworkResponse.js";
import { ThrottlingUtils } from "../network/ThrottlingUtils.js";
import { NetworkError } from "../error/NetworkError.js";
import { AuthError } from "../error/AuthError.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import { invokeAsync } from "../utils/FunctionWrappers.js";
import * as PerformanceEvents from "../telemetry/performance/PerformanceEvents.js";
import { CacheManager } from "../cache/CacheManager.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
/**
* Creates default headers for requests to token endpoint
*/
export function createTokenRequestHeaders(
logger: Logger,
preventCorsPreflight: boolean,
ccsCred?: CcsCredential
): Record<string, string> {
const headers: Record<string, string> = {};
headers[HeaderNames.CONTENT_TYPE] = URL_FORM_CONTENT_TYPE;
if (!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) {
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;
}
/**
* Creates query string for the /token request
* @param request
*/
export function createTokenQueryParameters(
request: BaseAuthRequest,
clientId: string,
redirectUri: string,
performanceClient: IPerformanceClient
): string {
const parameters = new Map<string, string>();
if (request.embeddedClientId) {
RequestParameterBuilder.addBrokerParameters(
parameters,
clientId,
redirectUri
);
}
if (request.extraQueryParameters) {
RequestParameterBuilder.addExtraParameters(
parameters,
request.extraQueryParameters
);
}
RequestParameterBuilder.addCorrelationId(parameters, request.correlationId);
RequestParameterBuilder.instrumentBrokerParams(
parameters,
request.correlationId,
performanceClient
);
return UrlUtils.mapToQueryString(parameters);
}
/**
* Http post to token endpoint
* @param tokenEndpoint
* @param queryString
* @param headers
* @param thumbprint
*/
export async function executePostToTokenEndpoint(
tokenEndpoint: string,
queryString: string,
headers: Record<string, string>,
thumbprint: RequestThumbprint,
correlationId: string,
cacheManager: CacheManager,
networkClient: INetworkModule,
logger: Logger,
performanceClient: IPerformanceClient,
serverTelemetryManager: ServerTelemetryManager | null
): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
const response = await sendPostRequest<ServerAuthorizationTokenResponse>(
thumbprint,
tokenEndpoint,
{ body: queryString, headers: headers },
correlationId,
cacheManager,
networkClient,
logger,
performanceClient
);
if (
serverTelemetryManager &&
response.status < 500 &&
response.status !== 429
) {
// Telemetry data successfully logged by server, clear Telemetry cache
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
* @param cacheManager - Cache manager instance
* @param networkClient - Network module instance
* @param logger - Logger instance
* @param performanceClient - Performance client instance
*/
export async function sendPostRequest<
T extends ServerAuthorizationTokenResponse
>(
thumbprint: RequestThumbprint,
tokenEndpoint: string,
options: NetworkRequestOptions,
correlationId: string,
cacheManager: CacheManager,
networkClient: INetworkModule,
logger: Logger,
performanceClient: IPerformanceClient
): Promise<NetworkResponse<T>> {
ThrottlingUtils.preProcess(cacheManager, thumbprint, correlationId);
let response;
try {
response = await invokeAsync(
networkClient.sendPostRequestAsync.bind(networkClient)<T>,
PerformanceEvents.NetworkClientSendPostRequestAsync,
logger,
performanceClient,
correlationId
)(tokenEndpoint, options);
const responseHeaders = response.headers || {};
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) {
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(
cacheManager,
thumbprint,
response,
correlationId
);
return response;
}

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 { 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, ""));
});
return challengeMap;
}
}

View File

@ -0,0 +1,150 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthenticationScheme, HttpMethod } from "../utils/Constants.js";
import type { AzureCloudOptions } from "../config/ClientConfiguration.js";
import { StringDict } from "../utils/MsalTypes.js";
import { StoreInCache } from "./StoreInCache.js";
import { ShrOptions } from "../crypto/SignedHttpRequest.js";
import { createClientAuthError } from "../error/ClientAuthError.js";
import * as ClientAuthErrorCodes from "../error/ClientAuthErrorCodes.js";
/**
* BaseAuthRequest
*/
export type BaseAuthRequest = {
/**
* 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.
*/
authority: string;
/**
* Unique GUID set per request to trace a request end-to-end for telemetry purposes.
*/
correlationId: string;
/**
* Array of scopes the application is requesting access to.
*/
scopes: Array<string>;
/**
* The type of token retrieved. Defaults to "Bearer". Can also be type "pop" or "SSH".
*/
authenticationScheme?: AuthenticationScheme;
/**
* A stringified claims request which will be added to all /authorize and /token calls
*/
claims?: string;
/**
* A stringified claims object which will be added to a Signed HTTP Request
*/
shrClaims?: string;
/**
* A server-generated timestamp that has been encrypted and base64URL encoded, which will be added to a Signed HTTP Request.
*/
shrNonce?: string;
/**
* An object containing options for the Signed HTTP Request
*/
shrOptions?: ShrOptions;
/**
* HTTP Request type used to request data from the resource (i.e. "GET", "POST", etc.). Used for proof-of-possession flows.
*/
resourceRequestMethod?: string;
/**
* URI that token will be used for. Used for proof-of-possession flows.
*/
resourceRequestUri?: string;
/**
* A stringified JSON Web Key representing a public key that can be signed by an SSH certificate.
*/
sshJwk?: string;
/**
* Key ID that uniquely identifies the SSH public key mentioned above.
*/
sshKid?: string;
/**
* Convenience string enums for users to provide public/sovereign cloud ids
*/
azureCloudOptions?: AzureCloudOptions;
/**
* Maximum allowed age, in milliseconds, of the user's authentication before a new sign-in is required.
*/
maxAge?: number;
/**
* Object containing boolean values indicating whether to store tokens in the cache or not (default is true)
*/
storeInCache?: StoreInCache;
/**
* Scenario id to track custom user prompts
*/
scenarioId?: string;
/**
* Key ID to identify the public key for PoP token request
*/
popKid?: string;
/**
* 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.
*/
embeddedClientId?: string;
/**
* HTTP method to use for the /authorize request. Defaults to GET, but can be set to POST if the request requires body parameters.
*/
httpMethod?: HttpMethod;
/**
* Resource parameter to be sent with the request. Used for MCP flows.
*/
resource?: string;
/**
* When true and a brokered flow is in effect—i.e., when a broker client id (brk_client_id), typically derived from embeddedClientId or other broker parameters, is included in the request—clientCapabilities from configuration will be excluded from claims. Has no effect when brk_client_id is not present (non-brokered flows).
*/
skipBrokerClaims?: boolean;
/**
* String to string map of custom query parameters added to outgoing token service requests. Unless the parameter is only supported on query strings use extraParameters instead
*/
extraQueryParameters?: StringDict;
/**
* String to string map of custom parameters added to outgoing token service requests.
*/
extraParameters?: StringDict;
};
/**
* Helper to enforce resource parameter presence in token requests when isMcp is set in the configuration.
* If resource parameter is set in both the request and in extraQueryParameters or extraParameters, an error will be thrown.
* This is used for MCP flows.
* @param isMcp - Flag indicating if application is an MCP app, from configuration
* @param request - Auth request
*/
export function enforceResourceParameter(
isMcp: boolean,
request: Partial<BaseAuthRequest>
): void {
if (!isMcp) {
return;
}
if (
request.resource &&
(containsResourceParam(request.extraParameters) ||
containsResourceParam(request.extraQueryParameters))
) {
throw createClientAuthError(
ClientAuthErrorCodes.misplacedResourceParam
);
}
if (!request.resource) {
throw createClientAuthError(
ClientAuthErrorCodes.resourceParameterRequired
);
}
}
function containsResourceParam(params?: StringDict): boolean {
if (!params) {
return false;
}
return Object.prototype.hasOwnProperty.call(params, "resource");
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseAuthRequest } from "./BaseAuthRequest.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)
*/
export type CommonAuthorizationCodeRequest = BaseAuthRequest & {
/**
* The authorization_code that the user acquired in the first leg of the flow.
*/
code: string;
/**
* 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
*/
redirectUri: string;
/**
* 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
*/
codeVerifier?: string;
/**
* Enables the acquisition of a SPA authorization code (confidential clients only).
*/
enableSpaAuthorizationCode?: boolean;
/**
* Encoded client_info returned with the authorization code, used to bind the token response to a specific account.
*/
clientInfo?: string;
/**
* Credential used to populate the CCS (Cache Credential Service) header.
*/
ccsCredential?: CcsCredential;
};

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ResponseMode } from "../utils/Constants.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)
*/
export type CommonAuthorizationUrlRequest = BaseAuthRequest & {
/**
* 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.
*/
redirectUri: string;
/**
* 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.
*/
responseMode: ResponseMode;
/**
* 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.
*/
account?: AccountInfo;
/**
* JSON Web Key used when constructing Encrypted Authorize Response (EAR) parameters.
*/
earJwk?: string;
/**
* 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
*/
codeChallenge?: string;
/**
* 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
*/
codeChallengeMethod?: string;
/**
* 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.
*/
domainHint?: string;
/**
* Scopes for a different resource when the user needs consent upfront.
*/
extraScopesToConsent?: Array<string>;
/**
* 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. Can also be the string value extracted from the login_hint claim on an idToken obtained previously to provide SSO.
*/
loginHint?: string;
/**
* 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.
*/
nonce: string;
/**
* 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.
*/
prompt?: string;
/**
* Session ID, unique identifier for the session. Available as an optional claim on ID tokens. Use login_hint optional claim provided on loginHint paramter instead, when available.
*/
sid?: string;
/**
* 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. For security and privacy reasons, we do not recommend putting URLs or other sensitive data directly in the state parameter. Instead, use a key or identifier that corresponds to data stored in browser storage (e.g., localStorage, sessionStorage), allowing your app to securely reference the necessary data after authentication.
*/
state: string;
/**
* Indicates whether this authorization request is being initiated by a platform authentication broker instead of a standard web flow.
*/
platformBroker?: boolean;
};

View File

@ -0,0 +1,41 @@
/*
* 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
*/
export type CommonEndSessionRequest = {
/**
* Unique GUID set per request to trace a request end-to-end for telemetry purposes.
*/
correlationId: string;
/**
* Account object that will be logged out of. All tokens tied to this account will be cleared.
*/
account?: AccountInfo | null;
/**
* URI to navigate to after logout page.
*/
postLogoutRedirectUri?: string | null;
/**
* ID Token used by B2C to validate logout if required by the policy
*/
idTokenHint?: string;
/**
* A value included in the request to the logout endpoint which will be returned in the query string upon post logout redirection. For security and privacy reasons, we do not recommend putting URLs or other sensitive data directly in the state parameter. Instead, use a key or identifier that corresponds to data stored in browser storage (e.g., localStorage, sessionStorage), allowing your app to securely reference the necessary data after logout.
*/
state?: string;
/**
* A string that specifies the account that is being logged out in order to skip the server account picker on logout
*/
logoutHint?: string;
/**
* String to string map of custom query parameters added to the /authorize call
*/
extraQueryParameters?: StringDict;
};

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseAuthRequest } from "./BaseAuthRequest.js";
import { CcsCredential } from "../account/CcsCredential.js";
/**
* CommonRefreshTokenRequest
*/
export type CommonRefreshTokenRequest = BaseAuthRequest & {
/**
* A refresh token returned from a previous request to the Identity provider.
*/
refreshToken: string;
/**
* Credential used to populate the CCS (Cache Credential Service) header.
*/
ccsCredential?: CcsCredential;
/**
* Force MSAL to cache a refresh token flow response when there is no account in the cache. Used for migration scenarios.
*/
forceCache?: boolean;
/**
* Redirect URI to send with the refresh token request.
*/
redirectUri?: string;
};

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountInfo } from "../account/AccountInfo.js";
import { BaseAuthRequest } from "./BaseAuthRequest.js";
/**
* SilentFlow parameters passed by the user to retrieve credentials silently
*/
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;
/**
* 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;
};

View File

@ -0,0 +1,26 @@
/*
* 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;
resource?: string;
extraParameters?: StringDict;
extraScopesToConsent?: Array<string>;
loginHint?: string;
prompt?: string;
};

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;
};

View File

@ -0,0 +1,668 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import * as Constants 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 {
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";
import { StringUtils } from "../utils/StringUtils.js";
export function instrumentBrokerParams(
parameters: Map<string, string>,
correlationId?: string,
performanceClient?: IPerformanceClient
): void {
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
);
}
}
/**
* Add the given response_type
* @param parameters
* @param responseType
*/
export function addResponseType(
parameters: Map<string, string>,
responseType: Constants.OAuthResponseType
): void {
parameters.set(AADServerParamKeys.RESPONSE_TYPE, responseType);
}
/**
* add response_mode. defaults to query.
* @param responseMode
*/
export function addResponseMode(
parameters: Map<string, string>,
responseMode?: Constants.ResponseMode
): void {
parameters.set(
AADServerParamKeys.RESPONSE_MODE,
responseMode ? responseMode : Constants.ResponseMode.QUERY
);
}
/**
* Add flag to indicate STS should attempt to use WAM if available
*/
export function addNativeBroker(parameters: Map<string, string>): void {
parameters.set(AADServerParamKeys.NATIVE_BROKER, "1");
}
/**
* add scopes. set addOidcScopes to false to prevent default scopes in non-user scenarios
* @param scopeSet
* @param addOidcScopes
*/
export function addScopes(
parameters: Map<string, string>,
scopes: string[],
addOidcScopes: boolean = true,
defaultScopes: Array<string> = Constants.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);
parameters.set(AADServerParamKeys.SCOPE, scopeSet.printScopes());
}
/**
* add clientId
* @param clientId
*/
export function addClientId(
parameters: Map<string, string>,
clientId: string
): void {
parameters.set(AADServerParamKeys.CLIENT_ID, clientId);
}
/**
* add redirect_uri
* @param redirectUri
*/
export function addRedirectUri(
parameters: Map<string, string>,
redirectUri: string
): void {
parameters.set(AADServerParamKeys.REDIRECT_URI, redirectUri);
}
/**
* add post logout redirectUri
* @param redirectUri
*/
export function addPostLogoutRedirectUri(
parameters: Map<string, string>,
redirectUri: string
): void {
parameters.set(AADServerParamKeys.POST_LOGOUT_URI, redirectUri);
}
/**
* add id_token_hint to logout request
* @param idTokenHint
*/
export function addIdTokenHint(
parameters: Map<string, string>,
idTokenHint: string
): void {
parameters.set(AADServerParamKeys.ID_TOKEN_HINT, idTokenHint);
}
/**
* add domain_hint
* @param domainHint
*/
export function addDomainHint(
parameters: Map<string, string>,
domainHint: string
): void {
parameters.set(AADServerParamKeys.DOMAIN_HINT, domainHint);
}
/**
* add login_hint
* @param loginHint
*/
export function addLoginHint(
parameters: Map<string, string>,
loginHint: string
): void {
parameters.set(AADServerParamKeys.LOGIN_HINT, loginHint);
}
/**
* Adds the CCS (Cache Credential Service) query parameter for login_hint
* @param loginHint
*/
export function addCcsUpn(
parameters: Map<string, string>,
loginHint: string
): void {
parameters.set(Constants.HeaderNames.CCS_HEADER, `UPN:${loginHint}`);
}
/**
* Adds the CCS (Cache Credential Service) query parameter for account object
* @param loginHint
*/
export function addCcsOid(
parameters: Map<string, string>,
clientInfo: ClientInfo
): void {
parameters.set(
Constants.HeaderNames.CCS_HEADER,
`Oid:${clientInfo.uid}@${clientInfo.utid}`
);
}
/**
* add sid
* @param sid
*/
export function addSid(parameters: Map<string, string>, sid: string): void {
parameters.set(AADServerParamKeys.SID, sid);
}
/**
* Adds claims to request parameters, conditionally excluding clientCapabilities
* when skipBrokerClaims is true and a brokered flow is in effect.
* @param parameters - The request parameters map
* @param claims - The claims string from the request
* @param clientCapabilities - The client capabilities from configuration
* @param skipBrokerClaims - When true and BROKER_CLIENT_ID is present, excludes clientCapabilities from claims
*/
export function addClaims(
parameters: Map<string, string>,
claims?: string,
clientCapabilities?: Array<string>,
skipBrokerClaims?: boolean
): void {
// Skip clientCapabilities if skipBrokerClaims is set to true and this is a brokered authentication flow
const configClaims =
skipBrokerClaims && parameters.has(AADServerParamKeys.BROKER_CLIENT_ID)
? undefined
: clientCapabilities;
if (
!StringUtils.isEmptyObj(claims) ||
(configClaims && configClaims.length > 0)
) {
const mergedClaims = addClientCapabilitiesToClaims(
claims,
configClaims
);
try {
JSON.parse(mergedClaims);
} catch (e) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.invalidClaims
);
}
parameters.set(AADServerParamKeys.CLAIMS, mergedClaims);
}
}
/**
* add correlationId
* @param correlationId
*/
export function addCorrelationId(
parameters: Map<string, string>,
correlationId: string
): void {
parameters.set(AADServerParamKeys.CLIENT_REQUEST_ID, correlationId);
}
/**
* add library info query params
* @param libraryInfo
*/
export function addLibraryInfo(
parameters: Map<string, string>,
libraryInfo: LibraryInfo
): void {
// Telemetry Info
parameters.set(AADServerParamKeys.X_CLIENT_SKU, libraryInfo.sku);
parameters.set(AADServerParamKeys.X_CLIENT_VER, libraryInfo.version);
if (libraryInfo.os) {
parameters.set(AADServerParamKeys.X_CLIENT_OS, libraryInfo.os);
}
if (libraryInfo.cpu) {
parameters.set(AADServerParamKeys.X_CLIENT_CPU, libraryInfo.cpu);
}
}
/**
* Add client telemetry parameters
* @param appTelemetry
*/
export function addApplicationTelemetry(
parameters: Map<string, string>,
appTelemetry: ApplicationTelemetry
): void {
if (appTelemetry?.appName) {
parameters.set(AADServerParamKeys.X_APP_NAME, appTelemetry.appName);
}
if (appTelemetry?.appVersion) {
parameters.set(AADServerParamKeys.X_APP_VER, appTelemetry.appVersion);
}
}
/**
* add prompt
* @param prompt
*/
export function addPrompt(
parameters: Map<string, string>,
prompt: string
): void {
parameters.set(AADServerParamKeys.PROMPT, prompt);
}
/**
* add state
* @param state
*/
export function addState(parameters: Map<string, string>, state: string): void {
if (state) {
parameters.set(AADServerParamKeys.STATE, state);
}
}
/**
* add nonce
* @param nonce
*/
export function addNonce(parameters: Map<string, string>, nonce: string): void {
parameters.set(AADServerParamKeys.NONCE, nonce);
}
/**
* add code_challenge and code_challenge_method
* - throw if either of them are not passed
* @param codeChallenge
* @param codeChallengeMethod
*/
export function addCodeChallengeParams(
parameters: Map<string, string>,
codeChallenge?: string,
codeChallengeMethod?: string
): void {
if (codeChallenge && codeChallengeMethod) {
parameters.set(AADServerParamKeys.CODE_CHALLENGE, codeChallenge);
parameters.set(
AADServerParamKeys.CODE_CHALLENGE_METHOD,
codeChallengeMethod
);
} else {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.pkceParamsMissing
);
}
}
/**
* add the `authorization_code` passed by the user to exchange for a token
* @param code
*/
export function addAuthorizationCode(
parameters: Map<string, string>,
code: string
): void {
parameters.set(AADServerParamKeys.CODE, code);
}
/**
* add the `authorization_code` passed by the user to exchange for a token
* @param code
*/
export function addDeviceCode(
parameters: Map<string, string>,
code: string
): void {
parameters.set(AADServerParamKeys.DEVICE_CODE, code);
}
/**
* add the `refreshToken` passed by the user
* @param refreshToken
*/
export function addRefreshToken(
parameters: Map<string, string>,
refreshToken: string
): void {
parameters.set(AADServerParamKeys.REFRESH_TOKEN, refreshToken);
}
/**
* add the `code_verifier` passed by the user to exchange for a token
* @param codeVerifier
*/
export function addCodeVerifier(
parameters: Map<string, string>,
codeVerifier: string
): void {
parameters.set(AADServerParamKeys.CODE_VERIFIER, codeVerifier);
}
/**
* add client_secret
* @param clientSecret
*/
export function addClientSecret(
parameters: Map<string, string>,
clientSecret: string
): void {
parameters.set(AADServerParamKeys.CLIENT_SECRET, clientSecret);
}
/**
* add clientAssertion for confidential client flows
* @param clientAssertion
*/
export function addClientAssertion(
parameters: Map<string, string>,
clientAssertion: string
): void {
if (clientAssertion) {
parameters.set(AADServerParamKeys.CLIENT_ASSERTION, clientAssertion);
}
}
/**
* add clientAssertionType for confidential client flows
* @param clientAssertionType
*/
export function addClientAssertionType(
parameters: Map<string, string>,
clientAssertionType: string
): void {
if (clientAssertionType) {
parameters.set(
AADServerParamKeys.CLIENT_ASSERTION_TYPE,
clientAssertionType
);
}
}
/**
* add OBO assertion for confidential client flows
* @param clientAssertion
*/
export function addOboAssertion(
parameters: Map<string, string>,
oboAssertion: string
): void {
parameters.set(AADServerParamKeys.OBO_ASSERTION, oboAssertion);
}
/**
* add grant type
* @param grantType
*/
export function addRequestTokenUse(
parameters: Map<string, string>,
tokenUse: string
): void {
parameters.set(AADServerParamKeys.REQUESTED_TOKEN_USE, tokenUse);
}
/**
* add grant type
* @param grantType
*/
export function addGrantType(
parameters: Map<string, string>,
grantType: string
): void {
parameters.set(AADServerParamKeys.GRANT_TYPE, grantType);
}
/**
* add client info
*
*/
export function addClientInfo(parameters: Map<string, string>): void {
parameters.set(Constants.CLIENT_INFO, "1");
}
/**
* add clidata=1 to request to indicate client data support
*/
export function addCliData(parameters: Map<string, string>): void {
parameters.set(AADServerParamKeys.CLI_DATA, "1");
}
export function addInstanceAware(parameters: Map<string, string>): void {
if (!parameters.has(AADServerParamKeys.INSTANCE_AWARE)) {
parameters.set(AADServerParamKeys.INSTANCE_AWARE, "true");
}
}
/**
* Add extraParameters
* @param extraParams - String dictionary containing extra parameters to be added.
*/
export function addExtraParameters(
parameters: Map<string, string>,
extraParams: StringDict
): void {
Object.entries(extraParams).forEach(([key, value]) => {
if (!parameters.has(key) && value) {
parameters.set(key, value);
}
});
}
export function 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(
Constants.ClaimsRequestKeys.ACCESS_TOKEN
)
) {
// Add access_token key to claims object
mergedClaims[Constants.ClaimsRequestKeys.ACCESS_TOKEN] = {};
}
// Add xms_cc claim with provided clientCapabilities to access_token key
mergedClaims[Constants.ClaimsRequestKeys.ACCESS_TOKEN][
Constants.ClaimsRequestKeys.XMS_CC
] = {
values: clientCapabilities,
};
}
return JSON.stringify(mergedClaims);
}
/**
* adds `username` for Password Grant flow
* @param username
*/
export function addUsername(
parameters: Map<string, string>,
username: string
): void {
parameters.set(Constants.PasswordGrantConstants.username, username);
}
/**
* adds `password` for Password Grant flow
* @param password
*/
export function addPassword(
parameters: Map<string, string>,
password: string
): void {
parameters.set(Constants.PasswordGrantConstants.password, password);
}
/**
* add pop_jwk to query params
* @param cnfString
*/
export function addPopToken(
parameters: Map<string, string>,
cnfString: string
): void {
if (cnfString) {
parameters.set(
AADServerParamKeys.TOKEN_TYPE,
Constants.AuthenticationScheme.POP
);
parameters.set(AADServerParamKeys.REQ_CNF, cnfString);
}
}
/**
* add SSH JWK and key ID to query params
*/
export function addSshJwk(
parameters: Map<string, string>,
sshJwkString: string
): void {
if (sshJwkString) {
parameters.set(
AADServerParamKeys.TOKEN_TYPE,
Constants.AuthenticationScheme.SSH
);
parameters.set(AADServerParamKeys.REQ_CNF, sshJwkString);
}
}
/**
* add server telemetry fields
* @param serverTelemetryManager
*/
export function addServerTelemetry(
parameters: Map<string, string>,
serverTelemetryManager: ServerTelemetryManager
): void {
parameters.set(
AADServerParamKeys.X_CLIENT_CURR_TELEM,
serverTelemetryManager.generateCurrentRequestHeaderValue()
);
parameters.set(
AADServerParamKeys.X_CLIENT_LAST_TELEM,
serverTelemetryManager.generateLastRequestHeaderValue()
);
}
/**
* Adds parameter that indicates to the server that throttling is supported
*/
export function addThrottling(parameters: Map<string, string>): void {
parameters.set(
AADServerParamKeys.X_MS_LIB_CAPABILITY,
Constants.X_MS_LIB_CAPABILITY_VALUE
);
}
/**
* Adds logout_hint parameter for "silent" logout which prevent server account picker
*/
export function addLogoutHint(
parameters: Map<string, string>,
logoutHint: string
): void {
parameters.set(AADServerParamKeys.LOGOUT_HINT, logoutHint);
}
export function addBrokerParameters(
parameters: Map<string, string>,
brokerClientId: string,
brokerRedirectUri: string
): void {
if (!parameters.has(AADServerParamKeys.BROKER_CLIENT_ID)) {
parameters.set(AADServerParamKeys.BROKER_CLIENT_ID, brokerClientId);
}
if (!parameters.has(AADServerParamKeys.BROKER_REDIRECT_URI)) {
parameters.set(
AADServerParamKeys.BROKER_REDIRECT_URI,
brokerRedirectUri
);
}
}
/**
* Add EAR (Encrypted Authorize Response) request parameters
* @param parameters
* @param jwk
*/
export function addEARParameters(
parameters: Map<string, string>,
jwk: string
): void {
parameters.set(AADServerParamKeys.EAR_JWK, encodeURIComponent(jwk));
// ear_jwe_crypto will always have value: {"alg":"dir","enc":"A256GCM"} so we can hardcode this
const jweCryptoB64Encoded = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0";
parameters.set(AADServerParamKeys.EAR_JWE_CRYPTO, jweCryptoB64Encoded);
}
export function addResource(
parameters: Map<string, string>,
resource?: string
): void {
if (resource) {
parameters.set(AADServerParamKeys.RESOURCE, resource);
}
}

View File

@ -0,0 +1,246 @@
/*
* 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 {
OIDC_SCOPES,
OIDC_DEFAULT_SCOPES,
OFFLINE_ACCESS_SCOPE,
} 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 || "";
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 {
// Handle empty scopes by using default OIDC scopes for cache lookup
const scopesToUse =
inputScopeString && inputScopeString.length > 0
? inputScopeString
: [...OIDC_DEFAULT_SCOPES];
const scopeSet = new ScopeSet(scopesToUse);
if (!scopeSet.containsOnlyOIDCScopes()) {
scopeSet.removeOIDCScopes();
} else {
scopeSet.removeScope(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 "";
}
/**
* Prints scopes into a space-delimited lower-case string (used for caching)
*/
printScopesLowerCase(): string {
return this.printScopes().toLowerCase();
}
}

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;
};

View File

@ -0,0 +1,48 @@
/*
* 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;
fromPlatformBroker?: boolean;
resource?: string;
};

View File

@ -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;
};

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Response properties that may be returned by the /authorize endpoint
*/
export type AuthorizeResponse = {
/**
* Authorization Code to be exchanged for tokens
*/
code?: string;
/**
* Encrypted Authorize Response (EAR) JWE
*/
ear_jwe?: string;
/**
* Client info object containing UserId and TenantId
*/
client_info?: string;
/**
* State string, should match what was sent on request
*/
state?: string;
/**
* Cloud instance returned when application is instance aware
*/
cloud_instance_name?: string;
/**
* Cloud instance hostname returned when application is instance aware
*/
cloud_instance_host_name?: string;
/**
* AAD Graph hostname returned when application is instance aware
* https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-graph-api
*/
cloud_graph_host_name?: string;
/**
* Microsoft Graph hostname returned when application is instance aware
* https://docs.microsoft.com/en-us/graph/overview
*/
msgraph_host?: string;
/**
* Server error code
*/
error?: string;
/**
* Server error URI
*/
error_uri?: string;
/**
* Server error description
*/
error_description?: string;
/**
* Server Sub-Error
*/
suberror?: string;
/**
* Timestamp of request
*/
timestamp?: string;
/**
* Trace Id used to look up request in logs
*/
trace_id?: string;
/**
* Correlation ID use to look up request in logs
*/
correlation_id?: string;
/**
* Claims
*/
claims?: string;
/**
* AccountId for the user, returned when platform broker is available to use
*/
accountId?: string;
/**
* Client data returned by the server when clidata=1 is sent on the request.
* On the wire, this is sent as a URL-encoded string in the format:
* account_type|error|sub_error|cloud_instance|caller_data_boundary
* In this parsed AuthorizeResponse object, the value is already URL-decoded.
*/
clientdata?: string;
};

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;
};

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;
};

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>;
};

View File

@ -0,0 +1,716 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccountInfo,
buildTenantProfile,
updateAccountTenantProfileData,
} from "../account/AccountInfo.js";
import {
checkMaxAge,
extractTokenClaims,
isKmsi,
} from "../account/AuthToken.js";
import {
TokenClaims,
getTenantIdFromIdTokenClaims,
} from "../account/TokenClaims.js";
import { Authority } from "../authority/Authority.js";
import { CacheManager } from "../cache/CacheManager.js";
import { AccessTokenEntity } from "../cache/entities/AccessTokenEntity.js";
import { AccountEntity } from "../cache/entities/AccountEntity.js";
import { AppMetadataEntity } from "../cache/entities/AppMetadataEntity.js";
import { CacheRecord } from "../cache/entities/CacheRecord.js";
import { IdTokenEntity } from "../cache/entities/IdTokenEntity.js";
import { RefreshTokenEntity } from "../cache/entities/RefreshTokenEntity.js";
import { ICachePlugin } from "../cache/interface/ICachePlugin.js";
import { ISerializableTokenCache } from "../cache/interface/ISerializableTokenCache.js";
import { TokenCacheContext } from "../cache/persistence/TokenCacheContext.js";
import * as AccountEntityUtils from "../cache/utils/AccountEntityUtils.js";
import * as CacheHelpers from "../cache/utils/CacheHelpers.js";
import { ICrypto } from "../crypto/ICrypto.js";
import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../error/ClientAuthError.js";
import {
InteractionRequiredAuthError,
isInteractionRequiredError,
} from "../error/InteractionRequiredAuthError.js";
import { ServerError } from "../error/ServerError.js";
import { Logger } from "../logger/Logger.js";
import { BaseAuthRequest } from "../request/BaseAuthRequest.js";
import { ScopeSet } from "../request/ScopeSet.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";
import * as Constants from "../utils/Constants.js";
import * as ProtocolUtils from "../utils/ProtocolUtils.js";
import { RequestStateObject } from "../utils/StateTypes.js";
import * as TimeUtils from "../utils/TimeUtils.js";
import { AuthenticationResult } from "./AuthenticationResult.js";
import { AuthorizationCodePayload } from "./AuthorizationCodePayload.js";
import { ServerAuthorizationTokenResponse } from "./ServerAuthorizationTokenResponse.js";
/**
* Class that handles response parsing.
* @internal
*/
export class ResponseHandler {
private clientId: string;
private cacheStorage: CacheManager;
private cryptoObj: ICrypto;
private logger: Logger;
private homeAccountIdentifier: string;
private performanceClient: IPerformanceClient;
private serializableCache: ISerializableTokenCache | null;
private persistencePlugin: ICachePlugin | null;
constructor(
clientId: string,
cacheStorage: CacheManager,
cryptoObj: ICrypto,
logger: Logger,
performanceClient: IPerformanceClient,
serializableCache: ISerializableTokenCache | null,
persistencePlugin: ICachePlugin | null
) {
this.clientId = clientId;
this.cacheStorage = cacheStorage;
this.cryptoObj = cryptoObj;
this.logger = logger;
this.performanceClient = performanceClient;
this.serializableCache = serializableCache;
this.persistencePlugin = persistencePlugin;
}
/**
* Function which validates server authorization token response.
* @param serverResponse
* @param correlationId
* @param refreshAccessToken
*/
validateTokenResponse(
serverResponse: ServerAuthorizationTokenResponse,
correlationId: string,
refreshAccessToken?: boolean
): void {
// Check for error
if (
serverResponse.error ||
serverResponse.error_description ||
serverResponse.suberror
) {
const errString = `Error(s): ${
serverResponse.error_codes || Constants.NOT_AVAILABLE
} - Timestamp: ${
serverResponse.timestamp || Constants.NOT_AVAILABLE
} - Description: ${
serverResponse.error_description || Constants.NOT_AVAILABLE
} - Correlation ID: ${
serverResponse.correlation_id || Constants.NOT_AVAILABLE
} - Trace ID: ${
serverResponse.trace_id || Constants.NOT_AVAILABLE
}`;
const serverErrorNo = serverResponse.error_codes?.length
? serverResponse.error_codes[0]
: undefined;
const serverError = new ServerError(
serverResponse.error,
errString,
serverResponse.suberror,
serverErrorNo,
serverResponse.status
);
// check if 500 error
if (
refreshAccessToken &&
serverResponse.status &&
serverResponse.status >=
Constants.HTTP_SERVER_ERROR_RANGE_START &&
serverResponse.status <= Constants.HTTP_SERVER_ERROR_RANGE_END
) {
this.logger.warning(
`executeTokenRequest:validateTokenResponse - AAD is currently unavailable and the access token is unable to be refreshed.\n${serverError}`,
correlationId
);
// don't throw an exception, but alert the user via a log that the token was unable to be refreshed
return;
// check if 400 error
} else if (
refreshAccessToken &&
serverResponse.status &&
serverResponse.status >=
Constants.HTTP_CLIENT_ERROR_RANGE_START &&
serverResponse.status <= Constants.HTTP_CLIENT_ERROR_RANGE_END
) {
this.logger.warning(
`executeTokenRequest:validateTokenResponse - AAD is currently available but is unable to refresh the access token.\n${serverError}`,
correlationId
);
// don't throw an exception, but alert the user via a log that the token was unable to be refreshed
return;
}
if (
isInteractionRequiredError(
serverResponse.error,
serverResponse.error_description,
serverResponse.suberror
)
) {
throw new InteractionRequiredAuthError(
serverResponse.error,
serverResponse.error_description,
serverResponse.suberror,
serverResponse.timestamp || "",
serverResponse.trace_id || "",
serverResponse.correlation_id || "",
serverResponse.claims || "",
serverErrorNo
);
}
throw serverError;
}
}
/**
* Returns a constructed token response based on given string. Also manages the cache updates and cleanups.
* @param serverTokenResponse
* @param authority
*/
async handleServerTokenResponse(
serverTokenResponse: ServerAuthorizationTokenResponse,
authority: Authority,
reqTimestamp: number,
request: BaseAuthRequest,
apiId: number,
authCodePayload?: AuthorizationCodePayload,
userAssertionHash?: string,
handlingRefreshTokenResponse?: boolean,
forceCacheRefreshTokenResponse?: boolean,
serverRequestId?: string
): Promise<AuthenticationResult> {
// create an idToken object (not entity)
let idTokenClaims: TokenClaims | undefined;
if (serverTokenResponse.id_token) {
idTokenClaims = extractTokenClaims(
serverTokenResponse.id_token || "",
this.cryptoObj.base64Decode
);
// token nonce check (TODO: Add a warning if no nonce is given?)
if (authCodePayload && authCodePayload.nonce) {
if (idTokenClaims.nonce !== authCodePayload.nonce) {
throw createClientAuthError(
ClientAuthErrorCodes.nonceMismatch
);
}
}
// 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);
}
}
// generate homeAccountId
this.homeAccountIdentifier = AccountEntityUtils.generateHomeAccountId(
serverTokenResponse.client_info || "",
authority.authorityType,
this.logger,
this.cryptoObj,
request.correlationId,
idTokenClaims
);
// save the response tokens
let requestStateObj: RequestStateObject | undefined;
if (!!authCodePayload && !!authCodePayload.state) {
requestStateObj = ProtocolUtils.parseRequestState(
this.cryptoObj.base64Decode,
authCodePayload.state
);
}
// Add keyId from request to serverTokenResponse if defined
serverTokenResponse.key_id =
serverTokenResponse.key_id || request.sshKid || undefined;
const cacheRecord = this.generateCacheRecord(
serverTokenResponse,
authority,
reqTimestamp,
request,
idTokenClaims,
userAssertionHash,
authCodePayload
);
let cacheContext;
try {
if (this.persistencePlugin && this.serializableCache) {
this.logger.verbose(
"Persistence enabled, calling beforeCacheAccess",
request.correlationId
);
cacheContext = new TokenCacheContext(
this.serializableCache,
true
);
await this.persistencePlugin.beforeCacheAccess(cacheContext);
}
/*
* When saving a refreshed tokens to the cache, it is expected that the account that was used is present in the cache.
* If not present, we should return null, as it's the case that another application called removeAccount in between
* the calls to getAllAccounts and acquireTokenSilent. We should not overwrite that removal, unless explicitly flagged by
* the developer, as in the case of refresh token flow used in ADAL Node to MSAL Node migration.
*/
if (
handlingRefreshTokenResponse &&
!forceCacheRefreshTokenResponse &&
cacheRecord.account
) {
const cachedAccounts = this.cacheStorage.getAllAccounts(
{
homeAccountId: cacheRecord.account.homeAccountId,
environment: cacheRecord.account.environment,
},
request.correlationId
);
if (cachedAccounts.length < 1) {
this.logger.warning(
"Account used to refresh tokens not in persistence, refreshed tokens will not be stored in the cache",
request.correlationId
);
this.performanceClient?.addFields(
{
acntLoggedOut: true,
},
request.correlationId
);
return await ResponseHandler.generateAuthenticationResult(
this.cryptoObj,
authority,
cacheRecord,
false,
request,
this.performanceClient,
idTokenClaims,
requestStateObj,
undefined,
serverRequestId
);
}
}
await this.cacheStorage.saveCacheRecord(
cacheRecord,
request.correlationId,
isKmsi(idTokenClaims || {}),
apiId,
request.storeInCache
);
} finally {
if (
this.persistencePlugin &&
this.serializableCache &&
cacheContext
) {
this.logger.verbose(
"Persistence enabled, calling afterCacheAccess",
request.correlationId
);
await this.persistencePlugin.afterCacheAccess(cacheContext);
}
}
return ResponseHandler.generateAuthenticationResult(
this.cryptoObj,
authority,
cacheRecord,
false,
request,
this.performanceClient,
idTokenClaims,
requestStateObj,
serverTokenResponse,
serverRequestId
);
}
/**
* Generates CacheRecord
* @param serverTokenResponse
* @param idTokenObj
* @param authority
*/
private generateCacheRecord(
serverTokenResponse: ServerAuthorizationTokenResponse,
authority: Authority,
reqTimestamp: number,
request: BaseAuthRequest,
idTokenClaims?: TokenClaims,
userAssertionHash?: string,
authCodePayload?: AuthorizationCodePayload
): CacheRecord {
const env = authority.getPreferredCache();
if (!env) {
throw createClientAuthError(
ClientAuthErrorCodes.invalidCacheEnvironment
);
}
const claimsTenantId = getTenantIdFromIdTokenClaims(idTokenClaims);
// IdToken: non AAD scenarios can have empty realm
let cachedIdToken: IdTokenEntity | undefined;
let cachedAccount: AccountEntity | undefined;
if (serverTokenResponse.id_token && !!idTokenClaims) {
cachedIdToken = CacheHelpers.createIdTokenEntity(
this.homeAccountIdentifier,
env,
serverTokenResponse.id_token,
this.clientId,
claimsTenantId || ""
);
cachedAccount = buildAccountToCache(
this.cacheStorage,
authority,
this.homeAccountIdentifier,
this.cryptoObj.base64Decode,
request.correlationId,
idTokenClaims,
serverTokenResponse.client_info,
env,
claimsTenantId,
authCodePayload,
undefined, // nativeAccountId
this.logger,
this.performanceClient
);
}
// AccessToken
let cachedAccessToken: AccessTokenEntity | null = null;
if (serverTokenResponse.access_token) {
// If scopes not returned in server response, use request scopes
const responseScopes = serverTokenResponse.scope
? ScopeSet.fromString(serverTokenResponse.scope)
: new ScopeSet(request.scopes || []);
/*
* Use timestamp calculated before request
* Server may return timestamps as strings, parse to numbers if so.
*/
const expiresIn: number =
(typeof serverTokenResponse.expires_in === "string"
? parseInt(serverTokenResponse.expires_in, 10)
: serverTokenResponse.expires_in) || 0;
const extExpiresIn: number =
(typeof serverTokenResponse.ext_expires_in === "string"
? parseInt(serverTokenResponse.ext_expires_in, 10)
: serverTokenResponse.ext_expires_in) || 0;
const refreshIn: number | undefined =
(typeof serverTokenResponse.refresh_in === "string"
? parseInt(serverTokenResponse.refresh_in, 10)
: serverTokenResponse.refresh_in) || undefined;
const tokenExpirationSeconds = reqTimestamp + expiresIn;
const extendedTokenExpirationSeconds =
tokenExpirationSeconds + extExpiresIn;
const refreshOnSeconds =
refreshIn && refreshIn > 0
? reqTimestamp + refreshIn
: undefined;
// non AAD scenarios can have empty realm
cachedAccessToken = CacheHelpers.createAccessTokenEntity(
this.homeAccountIdentifier,
env,
serverTokenResponse.access_token,
this.clientId,
claimsTenantId || authority.tenant || "",
responseScopes.printScopes(),
tokenExpirationSeconds,
extendedTokenExpirationSeconds,
this.cryptoObj.base64Decode,
refreshOnSeconds,
serverTokenResponse.token_type,
userAssertionHash,
serverTokenResponse.key_id
);
// Set resource (to be used for MCP scenarios)
const resource = request.resource || null;
if (resource) {
cachedAccessToken.resource = resource;
}
}
// refreshToken
let cachedRefreshToken: RefreshTokenEntity | null = null;
if (serverTokenResponse.refresh_token) {
let rtExpiresOn: number | undefined;
if (serverTokenResponse.refresh_token_expires_in) {
const rtExpiresIn: number =
typeof serverTokenResponse.refresh_token_expires_in ===
"string"
? parseInt(
serverTokenResponse.refresh_token_expires_in,
10
)
: serverTokenResponse.refresh_token_expires_in;
rtExpiresOn = reqTimestamp + rtExpiresIn;
this.performanceClient?.addFields(
{ ntwkRtExpiresOnSeconds: rtExpiresOn },
request.correlationId
);
}
cachedRefreshToken = CacheHelpers.createRefreshTokenEntity(
this.homeAccountIdentifier,
env,
serverTokenResponse.refresh_token,
this.clientId,
serverTokenResponse.foci,
userAssertionHash,
rtExpiresOn
);
}
// appMetadata
let cachedAppMetadata: AppMetadataEntity | null = null;
if (serverTokenResponse.foci) {
cachedAppMetadata = {
clientId: this.clientId,
environment: env,
familyId: serverTokenResponse.foci,
};
}
return {
account: cachedAccount,
idToken: cachedIdToken,
accessToken: cachedAccessToken,
refreshToken: cachedRefreshToken,
appMetadata: cachedAppMetadata,
};
}
/**
* Creates an @AuthenticationResult from @CacheRecord , @IdToken , and a boolean that states whether or not the result is from cache.
*
* Optionally takes a state string that is set as-is in the response.
*
* @param cacheRecord
* @param idTokenObj
* @param fromTokenCache
* @param stateString
*/
static async generateAuthenticationResult(
cryptoObj: ICrypto,
authority: Authority,
cacheRecord: CacheRecord,
fromTokenCache: boolean,
request: BaseAuthRequest,
performanceClient: IPerformanceClient,
idTokenClaims?: TokenClaims,
requestState?: RequestStateObject,
serverTokenResponse?: ServerAuthorizationTokenResponse,
requestId?: string
): Promise<AuthenticationResult> {
let accessToken: string = "";
let responseScopes: Array<string> = [];
let expiresOn: Date | null = null;
let extExpiresOn: Date | undefined;
let refreshOn: Date | undefined;
let familyId: string = "";
if (cacheRecord.accessToken) {
/*
* if the request object has `popKid` property, `signPopToken` will be set to false and
* the token will be returned unsigned
*/
if (
cacheRecord.accessToken.tokenType ===
Constants.AuthenticationScheme.POP &&
!request.popKid
) {
const popTokenGenerator: PopTokenGenerator =
new PopTokenGenerator(cryptoObj, performanceClient);
const { secret, keyId } = cacheRecord.accessToken;
if (!keyId) {
throw createClientAuthError(
ClientAuthErrorCodes.keyIdMissing
);
}
accessToken = await popTokenGenerator.signPopToken(
secret,
keyId,
request
);
} else {
accessToken = cacheRecord.accessToken.secret;
}
responseScopes = ScopeSet.fromString(
cacheRecord.accessToken.target
).asArray();
// Access token expiresOn cached in seconds, converting to Date for AuthenticationResult
expiresOn = TimeUtils.toDateFromSeconds(
cacheRecord.accessToken.expiresOn
);
extExpiresOn = TimeUtils.toDateFromSeconds(
cacheRecord.accessToken.extendedExpiresOn
);
if (cacheRecord.accessToken.refreshOn) {
refreshOn = TimeUtils.toDateFromSeconds(
cacheRecord.accessToken.refreshOn
);
}
}
if (cacheRecord.appMetadata) {
familyId =
cacheRecord.appMetadata.familyId === Constants.THE_FAMILY_ID
? Constants.THE_FAMILY_ID
: "";
}
const uid = idTokenClaims?.oid || idTokenClaims?.sub || "";
const tid = idTokenClaims?.tid || "";
// for hybrid + native bridge enablement, send back the native account Id
if (serverTokenResponse?.spa_accountid && !!cacheRecord.account) {
cacheRecord.account.nativeAccountId =
serverTokenResponse?.spa_accountid;
}
const accountInfo: AccountInfo | null = cacheRecord.account
? updateAccountTenantProfileData(
AccountEntityUtils.getAccountInfo(cacheRecord.account),
undefined, // tenantProfile optional
idTokenClaims,
cacheRecord.idToken?.secret
)
: null;
return {
authority: authority.canonicalAuthority,
uniqueId: uid,
tenantId: tid,
scopes: responseScopes,
account: accountInfo,
idToken: cacheRecord?.idToken?.secret || "",
idTokenClaims: idTokenClaims || {},
accessToken: accessToken,
fromCache: fromTokenCache,
expiresOn: expiresOn,
extExpiresOn: extExpiresOn,
refreshOn: refreshOn,
correlationId: request.correlationId,
requestId: requestId || "",
familyId: familyId,
tokenType: cacheRecord.accessToken?.tokenType || "",
state: requestState ? requestState.userRequestState : "",
cloudGraphHostName: cacheRecord.account?.cloudGraphHostName || "",
msGraphHost: cacheRecord.account?.msGraphHost || "",
code: serverTokenResponse?.spa_code,
fromPlatformBroker: false,
};
}
}
export function buildAccountToCache(
cacheStorage: CacheManager,
authority: Authority,
homeAccountId: string,
base64Decode: (input: string) => string,
correlationId: string,
idTokenClaims?: TokenClaims,
clientInfo?: string,
environment?: string,
claimsTenantId?: string | null,
authCodePayload?: AuthorizationCodePayload,
nativeAccountId?: string,
logger?: Logger,
performanceClient?: IPerformanceClient
): AccountEntity {
logger?.verbose("setCachedAccount called", correlationId);
/*
* Check if base account is already cached. Filter by homeAccountId (identifies
* the user's home identity) and environment (identifies the cloud) — the two
* tenant-agnostic properties that uniquely locate a base AccountEntity.
*/
const accountEnvironment = environment || authority.getPreferredCache();
const matchedAccounts = cacheStorage.getAccountsFilteredBy(
{ homeAccountId, environment: accountEnvironment },
correlationId
);
performanceClient?.addFields(
{ cacheMatchedAccounts: matchedAccounts.length },
correlationId
);
if (matchedAccounts.length > 1) {
/*
* Base accounts are expected to be unique for a given homeAccountId in normal cache usage.
* If multiple matches exist, ignore the cache hit rather than arbitrarily choosing one.
*/
logger?.warning(
"Multiple base accounts matched homeAccountId. Ignoring cached account and creating a new base account.",
correlationId
);
}
const cachedAccount =
matchedAccounts.length === 1 ? matchedAccounts[0] : null;
const baseAccount =
cachedAccount ||
AccountEntityUtils.createAccountEntity(
{
homeAccountId,
idTokenClaims,
clientInfo,
environment,
cloudGraphHostName: authCodePayload?.cloud_graph_host_name,
msGraphHost: authCodePayload?.msgraph_host,
nativeAccountId: nativeAccountId,
},
authority,
base64Decode
);
const tenantProfiles = baseAccount.tenantProfiles || [];
const tenantId = claimsTenantId || baseAccount.realm;
if (
tenantId &&
!tenantProfiles.find((tenantProfile) => {
return tenantProfile.tenantId === tenantId;
})
) {
const newTenantProfile = buildTenantProfile(
homeAccountId,
baseAccount.localAccountId,
tenantId,
idTokenClaims
);
tenantProfiles.push(newTenantProfile);
}
baseAccount.tenantProfiles = tenantProfiles;
return baseAccount;
}

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