Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CredentialEntity } from "./CredentialEntity.js";
import { AuthenticationScheme } from "../../utils/Constants.js";
/**
* Access token cache type
*/
export type AccessTokenEntity = CredentialEntity & {
/** Full tenant or organizational identifier that the account belongs to */
realm: string;
/** Permissions that are included in the token, or for refresh tokens, the resource identifier. */
target: string;
/** Absolute device time when entry was created in the cache. */
cachedAt: string;
/** Token expiry time, calculated based on current UTC time in seconds. Represented as a string. */
expiresOn: string;
/** Additional extended expiry time until when token is valid in case of server-side outage. Represented as string in UTC seconds. */
extendedExpiresOn?: string;
/** Used for proactive refresh */
refreshOn?: string;
/** Matches the authentication scheme for which the token was issued (i.e. Bearer or pop) */
tokenType?: AuthenticationScheme;
/** Stringified claims object */
requestedClaims?: string;
/** Matches the SHA 256 hash of the claims object included in the token request */
requestedClaimsHash?: string;
};
+360
View File
@@ -0,0 +1,360 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CacheAccountType, Separators } from "../../utils/Constants.js";
import { Authority } from "../../authority/Authority.js";
import { ICrypto } from "../../crypto/ICrypto.js";
import { ClientInfo, buildClientInfo } from "../../account/ClientInfo.js";
import {
AccountInfo,
TenantProfile,
buildTenantProfile,
} from "../../account/AccountInfo.js";
import {
createClientAuthError,
ClientAuthErrorCodes,
} from "../../error/ClientAuthError.js";
import { AuthorityType } from "../../authority/AuthorityType.js";
import { Logger } from "../../logger/Logger.js";
import {
TokenClaims,
getTenantIdFromIdTokenClaims,
} from "../../account/TokenClaims.js";
import { ProtocolMode } from "../../authority/ProtocolMode.js";
/**
* Type that defines required and optional parameters for an Account field (based on universal cache schema implemented by all MSALs).
*
* Key : Value Schema
*
* Key: <home_account_id>-<environment>-<realm*>
*
* Value Schema:
* {
* homeAccountId: home account identifier for the auth scheme,
* environment: entity that issued the token, represented as a full host
* realm: Full tenant or organizational identifier that the account belongs to
* localAccountId: Original tenant-specific accountID, usually used for legacy cases
* username: primary username that represents the user, usually corresponds to preferred_username in the v2 endpt
* authorityType: Accounts authority type as a string
* name: Full name for the account, including given name and family name,
* lastModificationTime: last time this entity was modified in the cache
* lastModificationApp:
* nativeAccountId: Account identifier on the native device
* tenantProfiles: Array of tenant profile objects for each tenant that the account has authenticated with in the browser
* }
* @internal
*/
export class AccountEntity {
homeAccountId: string;
environment: string;
realm: string;
localAccountId: string;
username: string;
authorityType: string;
clientInfo?: string;
name?: string;
lastModificationTime?: string;
lastModificationApp?: string;
cloudGraphHostName?: string;
msGraphHost?: string;
nativeAccountId?: string;
tenantProfiles?: Array<TenantProfile>;
/**
* Generate Account Id key component as per the schema: <home_account_id>-<environment>
*/
generateAccountId(): string {
const accountId: Array<string> = [this.homeAccountId, this.environment];
return accountId.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Generate Account Cache Key as per the schema: <home_account_id>-<environment>-<realm*>
*/
generateAccountKey(): string {
return AccountEntity.generateAccountCacheKey({
homeAccountId: this.homeAccountId,
environment: this.environment,
tenantId: this.realm,
username: this.username,
localAccountId: this.localAccountId,
});
}
/**
* Returns the AccountInfo interface for this account.
*/
getAccountInfo(): AccountInfo {
return {
homeAccountId: this.homeAccountId,
environment: this.environment,
tenantId: this.realm,
username: this.username,
localAccountId: this.localAccountId,
name: this.name,
nativeAccountId: this.nativeAccountId,
authorityType: this.authorityType,
// Deserialize tenant profiles array into a Map
tenantProfiles: new Map(
(this.tenantProfiles || []).map((tenantProfile) => {
return [tenantProfile.tenantId, tenantProfile];
})
),
};
}
/**
* Returns true if the account entity is in single tenant format (outdated), false otherwise
*/
isSingleTenant(): boolean {
return !this.tenantProfiles;
}
/**
* Generates account key from interface
* @param accountInterface
*/
static generateAccountCacheKey(accountInterface: AccountInfo): string {
const homeTenantId = accountInterface.homeAccountId.split(".")[1];
const accountKey = [
accountInterface.homeAccountId,
accountInterface.environment || "",
homeTenantId || accountInterface.tenantId || "",
];
return accountKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Build Account cache from IdToken, clientInfo and authority/policy. Associated with AAD.
* @param accountDetails
*/
static createAccount(
accountDetails: {
homeAccountId: string;
idTokenClaims?: TokenClaims;
clientInfo?: string;
cloudGraphHostName?: string;
msGraphHost?: string;
environment?: string;
nativeAccountId?: string;
tenantProfiles?: Array<TenantProfile>;
},
authority: Authority,
base64Decode?: (input: string) => string
): AccountEntity {
const account: AccountEntity = new AccountEntity();
if (authority.authorityType === AuthorityType.Adfs) {
account.authorityType = CacheAccountType.ADFS_ACCOUNT_TYPE;
} else if (authority.protocolMode === ProtocolMode.AAD) {
account.authorityType = CacheAccountType.MSSTS_ACCOUNT_TYPE;
} else {
account.authorityType = CacheAccountType.GENERIC_ACCOUNT_TYPE;
}
let clientInfo: ClientInfo | undefined;
if (accountDetails.clientInfo && base64Decode) {
clientInfo = buildClientInfo(
accountDetails.clientInfo,
base64Decode
);
}
account.clientInfo = accountDetails.clientInfo;
account.homeAccountId = accountDetails.homeAccountId;
account.nativeAccountId = accountDetails.nativeAccountId;
const env =
accountDetails.environment ||
(authority && authority.getPreferredCache());
if (!env) {
throw createClientAuthError(
ClientAuthErrorCodes.invalidCacheEnvironment
);
}
account.environment = env;
// non AAD scenarios can have empty realm
account.realm =
clientInfo?.utid ||
getTenantIdFromIdTokenClaims(accountDetails.idTokenClaims) ||
"";
// How do you account for MSA CID here?
account.localAccountId =
clientInfo?.uid ||
accountDetails.idTokenClaims?.oid ||
accountDetails.idTokenClaims?.sub ||
"";
/*
* In B2C scenarios the emails claim is used instead of preferred_username and it is an array.
* In most cases it will contain a single email. This field should not be relied upon if a custom
* policy is configured to return more than 1 email.
*/
const preferredUsername =
accountDetails.idTokenClaims?.preferred_username ||
accountDetails.idTokenClaims?.upn;
const email = accountDetails.idTokenClaims?.emails
? accountDetails.idTokenClaims.emails[0]
: null;
account.username = preferredUsername || email || "";
account.name = accountDetails.idTokenClaims?.name || "";
account.cloudGraphHostName = accountDetails.cloudGraphHostName;
account.msGraphHost = accountDetails.msGraphHost;
if (accountDetails.tenantProfiles) {
account.tenantProfiles = accountDetails.tenantProfiles;
} else {
const tenantProfile = buildTenantProfile(
accountDetails.homeAccountId,
account.localAccountId,
account.realm,
accountDetails.idTokenClaims
);
account.tenantProfiles = [tenantProfile];
}
return account;
}
/**
* Creates an AccountEntity object from AccountInfo
* @param accountInfo
* @param cloudGraphHostName
* @param msGraphHost
* @returns
*/
static createFromAccountInfo(
accountInfo: AccountInfo,
cloudGraphHostName?: string,
msGraphHost?: string
): AccountEntity {
const account: AccountEntity = new AccountEntity();
account.authorityType =
accountInfo.authorityType || CacheAccountType.GENERIC_ACCOUNT_TYPE;
account.homeAccountId = accountInfo.homeAccountId;
account.localAccountId = accountInfo.localAccountId;
account.nativeAccountId = accountInfo.nativeAccountId;
account.realm = accountInfo.tenantId;
account.environment = accountInfo.environment;
account.username = accountInfo.username;
account.name = accountInfo.name;
account.cloudGraphHostName = cloudGraphHostName;
account.msGraphHost = msGraphHost;
// Serialize tenant profiles map into an array
account.tenantProfiles = Array.from(
accountInfo.tenantProfiles?.values() || []
);
return account;
}
/**
* Generate HomeAccountId from server response
* @param serverClientInfo
* @param authType
*/
static generateHomeAccountId(
serverClientInfo: string,
authType: AuthorityType,
logger: Logger,
cryptoObj: ICrypto,
idTokenClaims?: TokenClaims
): string {
// since ADFS/DSTS do not have tid and does not set client_info
if (
!(
authType === AuthorityType.Adfs ||
authType === AuthorityType.Dsts
)
) {
// for cases where there is clientInfo
if (serverClientInfo) {
try {
const clientInfo = buildClientInfo(
serverClientInfo,
cryptoObj.base64Decode
);
if (clientInfo.uid && clientInfo.utid) {
return `${clientInfo.uid}.${clientInfo.utid}`;
}
} catch (e) {}
}
logger.warning("No client info in response");
}
// default to "sub" claim
return idTokenClaims?.sub || "";
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
static isAccountEntity(entity: object): boolean {
if (!entity) {
return false;
}
return (
entity.hasOwnProperty("homeAccountId") &&
entity.hasOwnProperty("environment") &&
entity.hasOwnProperty("realm") &&
entity.hasOwnProperty("localAccountId") &&
entity.hasOwnProperty("username") &&
entity.hasOwnProperty("authorityType")
);
}
/**
* Helper function to determine whether 2 accountInfo objects represent the same account
* @param accountA
* @param accountB
* @param compareClaims - If set to true idTokenClaims will also be compared to determine account equality
*/
static accountInfoIsEqual(
accountA: AccountInfo | null,
accountB: AccountInfo | null,
compareClaims?: boolean
): boolean {
if (!accountA || !accountB) {
return false;
}
let claimsMatch = true; // default to true so as to not fail comparison below if compareClaims: false
if (compareClaims) {
const accountAClaims = (accountA.idTokenClaims ||
{}) as TokenClaims;
const accountBClaims = (accountB.idTokenClaims ||
{}) as TokenClaims;
// issued at timestamp and nonce are expected to change each time a new id token is acquired
claimsMatch =
accountAClaims.iat === accountBClaims.iat &&
accountAClaims.nonce === accountBClaims.nonce;
}
return (
accountA.homeAccountId === accountB.homeAccountId &&
accountA.localAccountId === accountB.localAccountId &&
accountA.username === accountB.username &&
accountA.tenantId === accountB.tenantId &&
accountA.environment === accountB.environment &&
accountA.nativeAccountId === accountB.nativeAccountId &&
claimsMatch
);
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* App Metadata Cache Type
*/
export type AppMetadataEntity = {
/** clientId of the application */
clientId: string;
/** entity that issued the token, represented as a full host */
environment: string;
/** Family identifier, '1' represents Microsoft Family */
familyId?: string;
};
@@ -0,0 +1,20 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/** @internal */
export type AuthorityMetadataEntity = {
aliases: Array<string>;
preferred_cache: string;
preferred_network: string;
canonical_authority: string;
authorization_endpoint: string;
token_endpoint: string;
end_session_endpoint?: string;
issuer: string;
aliasesFromNetwork: boolean;
endpointsFromNetwork: boolean;
expiresAt: number;
jwks_uri: string;
};
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { IdTokenEntity } from "./IdTokenEntity.js";
import { AccessTokenEntity } from "./AccessTokenEntity.js";
import { RefreshTokenEntity } from "./RefreshTokenEntity.js";
import { AccountEntity } from "./AccountEntity.js";
import { AppMetadataEntity } from "./AppMetadataEntity.js";
/** @internal */
export type CacheRecord = {
account?: AccountEntity | null;
idToken?: IdTokenEntity | null;
accessToken?: AccessTokenEntity | null;
refreshToken?: RefreshTokenEntity | null;
appMetadata?: AppMetadataEntity | null;
};
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CredentialType, AuthenticationScheme } from "../../utils/Constants.js";
/**
* Credential Cache Type
*/
export type CredentialEntity = {
/** Identifier for the user in their home tenant*/
homeAccountId: string;
/** Entity that issued the token, represented as a full host */
environment: string;
/** Type of credential */
credentialType: CredentialType;
/** Client ID of the application */
clientId: string;
/** Actual credential as a string */
secret: string;
/** Family ID identifier, usually only used for refresh tokens */
familyId?: string;
/** Full tenant or organizational identifier that the account belongs to */
realm?: string;
/** Permissions that are included in the token, or for refresh tokens, the resource identifier. */
target?: string;
/** Matches the SHA 256 hash of the obo_assertion for the OBO flow */
userAssertionHash?: string;
/** Matches the authentication scheme for which the token was issued (i.e. Bearer or pop) */
tokenType?: AuthenticationScheme;
/** KeyId for PoP and SSH tokens stored in the kid claim */
keyId?: string;
/** Matches the SHA 256 hash of the claims object included in the token request */
requestedClaimsHash?: string;
};
+14
View File
@@ -0,0 +1,14 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CredentialEntity } from "./CredentialEntity.js";
/**
* Id Token Cache Type
*/
export type IdTokenEntity = CredentialEntity & {
/** Full tenant or organizational identifier that the account belongs to */
realm: string;
};
@@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CredentialEntity } from "./CredentialEntity.js";
/**
* Refresh Token Cache Type
*/
export type RefreshTokenEntity = CredentialEntity & {
expiresOn?: string;
};
@@ -0,0 +1,11 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type ServerTelemetryEntity = {
failedRequests: Array<string | number>;
errors: string[];
cacheHits: number;
nativeBrokerErrorCode?: string;
};
+14
View File
@@ -0,0 +1,14 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type ThrottlingEntity = {
// Unix-time value representing the expiration of the throttle
throttleTime: number;
// Information provided by the server
error?: string;
errorCodes?: Array<string>;
errorMessage?: string;
subError?: string;
};
+228
View File
@@ -0,0 +1,228 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountFilter } from "../utils/CacheTypes.js";
import { CacheRecord } from "../entities/CacheRecord.js";
import { AccountEntity } from "../entities/AccountEntity.js";
import { AccountInfo } from "../../account/AccountInfo.js";
import { AppMetadataEntity } from "../entities/AppMetadataEntity.js";
import { ServerTelemetryEntity } from "../entities/ServerTelemetryEntity.js";
import { ThrottlingEntity } from "../entities/ThrottlingEntity.js";
import { IdTokenEntity } from "../entities/IdTokenEntity.js";
import { AccessTokenEntity } from "../entities/AccessTokenEntity.js";
import { RefreshTokenEntity } from "../entities/RefreshTokenEntity.js";
import { AuthorityMetadataEntity } from "../entities/AuthorityMetadataEntity.js";
import { StoreInCache } from "../../request/StoreInCache.js";
export interface ICacheManager {
/**
* fetch the account entity from the platform cache
* @param accountKey
*/
getAccount(accountKey: string): AccountEntity | null;
/**
* set account entity in the platform cache
* @param account
*/
setAccount(account: AccountEntity, correlationId: string): Promise<void>;
/**
* Returns true if the given key matches our account key schema. Also matches homeAccountId and/or tenantId if provided
* @param key
* @param homeAccountId
* @param tenantId
* @returns
*/
isAccountKey(
key: string,
homeAccountId?: string,
tenantId?: string
): boolean;
/**
* fetch the idToken entity from the platform cache
* @param idTokenKey
*/
getIdTokenCredential(idTokenKey: string): IdTokenEntity | null;
/**
* set idToken entity to the platform cache
* @param idToken
*/
setIdTokenCredential(
idToken: IdTokenEntity,
correlationId: string
): Promise<void>;
/**
* fetch the idToken entity from the platform cache
* @param accessTokenKey
*/
getAccessTokenCredential(accessTokenKey: string): AccessTokenEntity | null;
/**
* set idToken entity to the platform cache
* @param accessToken
*/
setAccessTokenCredential(
accessToken: AccessTokenEntity,
correlationId: string
): Promise<void>;
/**
* fetch the idToken entity from the platform cache
* @param refreshTokenKey
*/
getRefreshTokenCredential(
refreshTokenKey: string
): RefreshTokenEntity | null;
/**
* set idToken entity to the platform cache
* @param refreshToken
*/
setRefreshTokenCredential(
refreshToken: RefreshTokenEntity,
correlationId: string
): Promise<void>;
/**
* fetch appMetadata entity from the platform cache
* @param appMetadataKey
*/
getAppMetadata(appMetadataKey: string): AppMetadataEntity | null;
/**
* set appMetadata entity to the platform cache
* @param appMetadata
*/
setAppMetadata(appMetadata: AppMetadataEntity): void;
/**
* fetch server telemetry entity from the platform cache
* @param serverTelemetryKey
*/
getServerTelemetry(
serverTelemetryKey: string
): ServerTelemetryEntity | null;
/**
* set server telemetry entity to the platform cache
* @param serverTelemetryKey
* @param serverTelemetry
*/
setServerTelemetry(
serverTelemetryKey: string,
serverTelemetry: ServerTelemetryEntity
): void;
/**
* fetch cloud discovery metadata entity from the platform cache
* @param key
*/
getAuthorityMetadata(key: string): AuthorityMetadataEntity | null;
/**
* Get cache keys for authority metadata
*/
getAuthorityMetadataKeys(): Array<string>;
/**
* set cloud discovery metadata entity to the platform cache
* @param key
* @param value
*/
setAuthorityMetadata(key: string, value: AuthorityMetadataEntity): void;
/**
* Provide an alias to find a matching AuthorityMetadataEntity in cache
* @param host
*/
getAuthorityMetadataByAlias(host: string): AuthorityMetadataEntity | null;
/**
* given an authority generates the cache key for authorityMetadata
* @param authority
*/
generateAuthorityMetadataCacheKey(authority: string): string;
/**
* fetch throttling entity from the platform cache
* @param throttlingCacheKey
*/
getThrottlingCache(throttlingCacheKey: string): ThrottlingEntity | null;
/**
* set throttling entity to the platform cache
* @param throttlingCacheKey
* @param throttlingCache
*/
setThrottlingCache(
throttlingCacheKey: string,
throttlingCache: ThrottlingEntity
): void;
/**
* Returns all accounts in cache
*/
getAllAccounts(): AccountInfo[];
/**
* saves a cache record
* @param cacheRecord
*/
saveCacheRecord(
cacheRecord: CacheRecord,
correlationId: string,
storeInCache?: StoreInCache
): Promise<void>;
/**
* retrieve accounts matching all provided filters; if no filter is set, get all accounts
* @param homeAccountId
* @param environment
* @param realm
*/
getAccountsFilteredBy(filter: AccountFilter): AccountEntity[];
/**
* Get AccountInfo object based on provided filters
* @param filter
*/
getAccountInfoFilteredBy(filter: AccountFilter): AccountInfo | null;
/**
* Removes all accounts and related tokens from cache.
*/
removeAllAccounts(): Promise<void>;
/**
* returns a boolean if the given account is removed
* @param account
*/
removeAccount(accountKey: string): Promise<void>;
/**
* returns a boolean if the given account is removed
* @param account
*/
removeAccountContext(account: AccountEntity): Promise<void>;
/**
* @param key
*/
removeIdToken(key: string): void;
/**
* @param key
*/
removeAccessToken(key: string): Promise<void>;
/**
* @param key
*/
removeRefreshToken(key: string): void;
}
+11
View File
@@ -0,0 +1,11 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TokenCacheContext } from "../persistence/TokenCacheContext.js";
export interface ICachePlugin {
beforeCacheAccess: (tokenCacheContext: TokenCacheContext) => Promise<void>;
afterCacheAccess: (tokenCacheContext: TokenCacheContext) => Promise<void>;
}
@@ -0,0 +1,9 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface ISerializableTokenCache {
deserialize: (cache: string) => void;
serialize: () => string;
}
@@ -0,0 +1,39 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ISerializableTokenCache } from "../interface/ISerializableTokenCache.js";
/**
* This class instance helps track the memory changes facilitating
* decisions to read from and write to the persistent cache
*/ export class TokenCacheContext {
/**
* boolean indicating cache change
*/
hasChanged: boolean;
/**
* serializable token cache interface
*/
cache: ISerializableTokenCache;
constructor(tokenCache: ISerializableTokenCache, hasChanged: boolean) {
this.cache = tokenCache;
this.hasChanged = hasChanged;
}
/**
* boolean which indicates the changes in cache
*/
get cacheHasChanged(): boolean {
return this.hasChanged;
}
/**
* function to retrieve the token cache
*/
get tokenCache(): ISerializableTokenCache {
return this.cache;
}
}
+463
View File
@@ -0,0 +1,463 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { extractTokenClaims } from "../../account/AuthToken.js";
import { TokenClaims } from "../../account/TokenClaims.js";
import { CloudDiscoveryMetadata } from "../../authority/CloudDiscoveryMetadata.js";
import { OpenIdConfigResponse } from "../../authority/OpenIdConfigResponse.js";
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "../../error/ClientAuthError.js";
import {
APP_METADATA,
AUTHORITY_METADATA_CONSTANTS,
AuthenticationScheme,
CredentialType,
SERVER_TELEM_CONSTANTS,
Separators,
ThrottlingConstants,
} from "../../utils/Constants.js";
import * as TimeUtils from "../../utils/TimeUtils.js";
import { AccessTokenEntity } from "../entities/AccessTokenEntity.js";
import { AppMetadataEntity } from "../entities/AppMetadataEntity.js";
import { AuthorityMetadataEntity } from "../entities/AuthorityMetadataEntity.js";
import { CredentialEntity } from "../entities/CredentialEntity.js";
import { IdTokenEntity } from "../entities/IdTokenEntity.js";
import { RefreshTokenEntity } from "../entities/RefreshTokenEntity.js";
/**
* Cache Key: <home_account_id>-<environment>-<credential_type>-<client_id or familyId>-<realm>-<scopes>-<claims hash>-<scheme>
* IdToken Example: uid.utid-login.microsoftonline.com-idtoken-app_client_id-contoso.com
* AccessToken Example: uid.utid-login.microsoftonline.com-accesstoken-app_client_id-contoso.com-scope1 scope2--pop
* RefreshToken Example: uid.utid-login.microsoftonline.com-refreshtoken-1-contoso.com
* @param credentialEntity
* @returns
*/
export function generateCredentialKey(
credentialEntity: CredentialEntity
): string {
const credentialKey = [
generateAccountId(credentialEntity),
generateCredentialId(credentialEntity),
generateTarget(credentialEntity),
generateClaimsHash(credentialEntity),
generateScheme(credentialEntity),
];
return credentialKey.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Create IdTokenEntity
* @param homeAccountId
* @param authenticationResult
* @param clientId
* @param authority
*/
export function createIdTokenEntity(
homeAccountId: string,
environment: string,
idToken: string,
clientId: string,
tenantId: string
): IdTokenEntity {
const idTokenEntity: IdTokenEntity = {
credentialType: CredentialType.ID_TOKEN,
homeAccountId: homeAccountId,
environment: environment,
clientId: clientId,
secret: idToken,
realm: tenantId,
};
return idTokenEntity;
}
/**
* Create AccessTokenEntity
* @param homeAccountId
* @param environment
* @param accessToken
* @param clientId
* @param tenantId
* @param scopes
* @param expiresOn
* @param extExpiresOn
*/
export function createAccessTokenEntity(
homeAccountId: string,
environment: string,
accessToken: string,
clientId: string,
tenantId: string,
scopes: string,
expiresOn: number,
extExpiresOn: number,
base64Decode: (input: string) => string,
refreshOn?: number,
tokenType?: AuthenticationScheme,
userAssertionHash?: string,
keyId?: string,
requestedClaims?: string,
requestedClaimsHash?: string
): AccessTokenEntity {
const atEntity: AccessTokenEntity = {
homeAccountId: homeAccountId,
credentialType: CredentialType.ACCESS_TOKEN,
secret: accessToken,
cachedAt: TimeUtils.nowSeconds().toString(),
expiresOn: expiresOn.toString(),
extendedExpiresOn: extExpiresOn.toString(),
environment: environment,
clientId: clientId,
realm: tenantId,
target: scopes,
tokenType: tokenType || AuthenticationScheme.BEARER,
};
if (userAssertionHash) {
atEntity.userAssertionHash = userAssertionHash;
}
if (refreshOn) {
atEntity.refreshOn = refreshOn.toString();
}
if (requestedClaims) {
atEntity.requestedClaims = requestedClaims;
atEntity.requestedClaimsHash = requestedClaimsHash;
}
/*
* Create Access Token With Auth Scheme instead of regular access token
* Cast to lower to handle "bearer" from ADFS
*/
if (
atEntity.tokenType?.toLowerCase() !==
AuthenticationScheme.BEARER.toLowerCase()
) {
atEntity.credentialType = CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME;
switch (atEntity.tokenType) {
case AuthenticationScheme.POP:
// Make sure keyId is present and add it to credential
const tokenClaims: TokenClaims | null = extractTokenClaims(
accessToken,
base64Decode
);
if (!tokenClaims?.cnf?.kid) {
throw createClientAuthError(
ClientAuthErrorCodes.tokenClaimsCnfRequiredForSignedJwt
);
}
atEntity.keyId = tokenClaims.cnf.kid;
break;
case AuthenticationScheme.SSH:
atEntity.keyId = keyId;
}
}
return atEntity;
}
/**
* Create RefreshTokenEntity
* @param homeAccountId
* @param authenticationResult
* @param clientId
* @param authority
*/
export function createRefreshTokenEntity(
homeAccountId: string,
environment: string,
refreshToken: string,
clientId: string,
familyId?: string,
userAssertionHash?: string,
expiresOn?: number
): RefreshTokenEntity {
const rtEntity: RefreshTokenEntity = {
credentialType: CredentialType.REFRESH_TOKEN,
homeAccountId: homeAccountId,
environment: environment,
clientId: clientId,
secret: refreshToken,
};
if (userAssertionHash) {
rtEntity.userAssertionHash = userAssertionHash;
}
if (familyId) {
rtEntity.familyId = familyId;
}
if (expiresOn) {
rtEntity.expiresOn = expiresOn.toString();
}
return rtEntity;
}
export function isCredentialEntity(entity: object): boolean {
return (
entity.hasOwnProperty("homeAccountId") &&
entity.hasOwnProperty("environment") &&
entity.hasOwnProperty("credentialType") &&
entity.hasOwnProperty("clientId") &&
entity.hasOwnProperty("secret")
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isAccessTokenEntity(entity: object): boolean {
if (!entity) {
return false;
}
return (
isCredentialEntity(entity) &&
entity.hasOwnProperty("realm") &&
entity.hasOwnProperty("target") &&
(entity["credentialType"] === CredentialType.ACCESS_TOKEN ||
entity["credentialType"] ===
CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME)
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isIdTokenEntity(entity: object): boolean {
if (!entity) {
return false;
}
return (
isCredentialEntity(entity) &&
entity.hasOwnProperty("realm") &&
entity["credentialType"] === CredentialType.ID_TOKEN
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isRefreshTokenEntity(entity: object): boolean {
if (!entity) {
return false;
}
return (
isCredentialEntity(entity) &&
entity["credentialType"] === CredentialType.REFRESH_TOKEN
);
}
/**
* Generate Account Id key component as per the schema: <home_account_id>-<environment>
*/
function generateAccountId(credentialEntity: CredentialEntity): string {
const accountId: Array<string> = [
credentialEntity.homeAccountId,
credentialEntity.environment,
];
return accountId.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Generate Credential Id key component as per the schema: <credential_type>-<client_id>-<realm>
*/
function generateCredentialId(credentialEntity: CredentialEntity): string {
const clientOrFamilyId =
credentialEntity.credentialType === CredentialType.REFRESH_TOKEN
? credentialEntity.familyId || credentialEntity.clientId
: credentialEntity.clientId;
const credentialId: Array<string> = [
credentialEntity.credentialType,
clientOrFamilyId,
credentialEntity.realm || "",
];
return credentialId.join(Separators.CACHE_KEY_SEPARATOR).toLowerCase();
}
/**
* Generate target key component as per schema: <target>
*/
function generateTarget(credentialEntity: CredentialEntity): string {
return (credentialEntity.target || "").toLowerCase();
}
/**
* Generate requested claims key component as per schema: <requestedClaims>
*/
function generateClaimsHash(credentialEntity: CredentialEntity): string {
return (credentialEntity.requestedClaimsHash || "").toLowerCase();
}
/**
* Generate scheme key componenet as per schema: <scheme>
*/
function generateScheme(credentialEntity: CredentialEntity): string {
/*
* PoP Tokens and SSH certs include scheme in cache key
* Cast to lowercase to handle "bearer" from ADFS
*/
return credentialEntity.tokenType &&
credentialEntity.tokenType.toLowerCase() !==
AuthenticationScheme.BEARER.toLowerCase()
? credentialEntity.tokenType.toLowerCase()
: "";
}
/**
* validates if a given cache entry is "Telemetry", parses <key,value>
* @param key
* @param entity
*/
export function isServerTelemetryEntity(key: string, entity?: object): boolean {
const validateKey: boolean =
key.indexOf(SERVER_TELEM_CONSTANTS.CACHE_KEY) === 0;
let validateEntity: boolean = true;
if (entity) {
validateEntity =
entity.hasOwnProperty("failedRequests") &&
entity.hasOwnProperty("errors") &&
entity.hasOwnProperty("cacheHits");
}
return validateKey && validateEntity;
}
/**
* validates if a given cache entry is "Throttling", parses <key,value>
* @param key
* @param entity
*/
export function isThrottlingEntity(key: string, entity?: object): boolean {
let validateKey: boolean = false;
if (key) {
validateKey = key.indexOf(ThrottlingConstants.THROTTLING_PREFIX) === 0;
}
let validateEntity: boolean = true;
if (entity) {
validateEntity = entity.hasOwnProperty("throttleTime");
}
return validateKey && validateEntity;
}
/**
* Generate AppMetadata Cache Key as per the schema: appmetadata-<environment>-<client_id>
*/
export function generateAppMetadataKey({
environment,
clientId,
}: AppMetadataEntity): string {
const appMetaDataKeyArray: Array<string> = [
APP_METADATA,
environment,
clientId,
];
return appMetaDataKeyArray
.join(Separators.CACHE_KEY_SEPARATOR)
.toLowerCase();
}
/*
* Validates an entity: checks for all expected params
* @param entity
*/
export function isAppMetadataEntity(key: string, entity: object): boolean {
if (!entity) {
return false;
}
return (
key.indexOf(APP_METADATA) === 0 &&
entity.hasOwnProperty("clientId") &&
entity.hasOwnProperty("environment")
);
}
/**
* Validates an entity: checks for all expected params
* @param entity
*/
export function isAuthorityMetadataEntity(
key: string,
entity: object
): boolean {
if (!entity) {
return false;
}
return (
key.indexOf(AUTHORITY_METADATA_CONSTANTS.CACHE_KEY) === 0 &&
entity.hasOwnProperty("aliases") &&
entity.hasOwnProperty("preferred_cache") &&
entity.hasOwnProperty("preferred_network") &&
entity.hasOwnProperty("canonical_authority") &&
entity.hasOwnProperty("authorization_endpoint") &&
entity.hasOwnProperty("token_endpoint") &&
entity.hasOwnProperty("issuer") &&
entity.hasOwnProperty("aliasesFromNetwork") &&
entity.hasOwnProperty("endpointsFromNetwork") &&
entity.hasOwnProperty("expiresAt") &&
entity.hasOwnProperty("jwks_uri")
);
}
/**
* Reset the exiresAt value
*/
export function generateAuthorityMetadataExpiresAt(): number {
return (
TimeUtils.nowSeconds() +
AUTHORITY_METADATA_CONSTANTS.REFRESH_TIME_SECONDS
);
}
export function updateAuthorityEndpointMetadata(
authorityMetadata: AuthorityMetadataEntity,
updatedValues: OpenIdConfigResponse,
fromNetwork: boolean
): void {
authorityMetadata.authorization_endpoint =
updatedValues.authorization_endpoint;
authorityMetadata.token_endpoint = updatedValues.token_endpoint;
authorityMetadata.end_session_endpoint = updatedValues.end_session_endpoint;
authorityMetadata.issuer = updatedValues.issuer;
authorityMetadata.endpointsFromNetwork = fromNetwork;
authorityMetadata.jwks_uri = updatedValues.jwks_uri;
}
export function updateCloudDiscoveryMetadata(
authorityMetadata: AuthorityMetadataEntity,
updatedValues: CloudDiscoveryMetadata,
fromNetwork: boolean
): void {
authorityMetadata.aliases = updatedValues.aliases;
authorityMetadata.preferred_cache = updatedValues.preferred_cache;
authorityMetadata.preferred_network = updatedValues.preferred_network;
authorityMetadata.aliasesFromNetwork = fromNetwork;
}
/**
* Returns whether or not the data needs to be refreshed
*/
export function isAuthorityMetadataExpired(
metadata: AuthorityMetadataEntity
): boolean {
return metadata.expiresAt <= TimeUtils.nowSeconds();
}
+105
View File
@@ -0,0 +1,105 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountEntity } from "../entities/AccountEntity.js";
import { IdTokenEntity } from "../entities/IdTokenEntity.js";
import { AccessTokenEntity } from "../entities/AccessTokenEntity.js";
import { RefreshTokenEntity } from "../entities/RefreshTokenEntity.js";
import { AppMetadataEntity } from "../entities/AppMetadataEntity.js";
import { ServerTelemetryEntity } from "../entities/ServerTelemetryEntity.js";
import { ThrottlingEntity } from "../entities/ThrottlingEntity.js";
import { AuthorityMetadataEntity } from "../entities/AuthorityMetadataEntity.js";
import { AuthenticationScheme } from "../../utils/Constants.js";
import { ScopeSet } from "../../request/ScopeSet.js";
import { AccountInfo } from "../../account/AccountInfo.js";
/** @internal */
export type AccountCache = Record<string, AccountEntity>;
/** @internal */
export type IdTokenCache = Record<string, IdTokenEntity>;
/** @internal */
export type AccessTokenCache = Record<string, AccessTokenEntity>;
/** @internal */
export type RefreshTokenCache = Record<string, RefreshTokenEntity>;
/** @internal */
export type AppMetadataCache = Record<string, AppMetadataEntity>;
/**
* Object type of all accepted cache types
* @internal
*/
export type ValidCacheType =
| AccountEntity
| IdTokenEntity
| AccessTokenEntity
| RefreshTokenEntity
| AppMetadataEntity
| AuthorityMetadataEntity
| ServerTelemetryEntity
| ThrottlingEntity
| string;
/**
* Object type of all credential types
* @internal
*/
export type ValidCredentialType =
| IdTokenEntity
| AccessTokenEntity
| RefreshTokenEntity;
/**
* Account: <home_account_id>-<environment>-<realm*>
*/
export type AccountFilter = Omit<
Partial<AccountInfo>,
"idToken" | "idTokenClaims"
> & {
realm?: string;
loginHint?: string;
sid?: string;
isHomeTenant?: boolean;
};
export type TenantProfileFilter = Pick<
AccountFilter,
| "localAccountId"
| "loginHint"
| "name"
| "sid"
| "isHomeTenant"
| "username"
>;
/**
* Credential: <home_account_id*>-<environment>-<credential_type>-<client_id>-<realm*>-<target*>-<scheme*>
*/
export type CredentialFilter = {
homeAccountId?: string;
environment?: string;
credentialType?: string;
clientId?: string;
familyId?: string;
realm?: string;
target?: ScopeSet;
userAssertionHash?: string;
tokenType?: AuthenticationScheme;
keyId?: string;
requestedClaimsHash?: string;
};
/**
* AppMetadata: appmetadata-<environment>-<client_id>
*/
export type AppMetadataFilter = {
environment?: string;
clientId?: string;
};
export type TokenKeys = {
idToken: string[];
accessToken: string[];
refreshToken: string[];
};