426 lines
13 KiB
TypeScript
Executable file
426 lines
13 KiB
TypeScript
Executable file
/*
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/
|
|
|
|
import {
|
|
AccessTokenEntity,
|
|
ICrypto,
|
|
IdTokenEntity,
|
|
Logger,
|
|
ScopeSet,
|
|
Authority,
|
|
AuthorityOptions,
|
|
ExternalTokenResponse,
|
|
AccountEntity,
|
|
AuthToken,
|
|
RefreshTokenEntity,
|
|
CacheRecord,
|
|
TokenClaims,
|
|
CacheHelpers,
|
|
buildAccountToCache,
|
|
TimeUtils,
|
|
} from "@azure/msal-common/browser";
|
|
import { BrowserConfiguration } from "../config/Configuration.js";
|
|
import type { SilentRequest } from "../request/SilentRequest.js";
|
|
import { BrowserCacheManager } from "./BrowserCacheManager.js";
|
|
import type { ITokenCache } from "./ITokenCache.js";
|
|
import {
|
|
createBrowserAuthError,
|
|
BrowserAuthErrorCodes,
|
|
} from "../error/BrowserAuthError.js";
|
|
import type { AuthenticationResult } from "../response/AuthenticationResult.js";
|
|
import { base64Decode } from "../encode/Base64Decode.js";
|
|
import * as BrowserCrypto from "../crypto/BrowserCrypto.js";
|
|
|
|
export type LoadTokenOptions = {
|
|
clientInfo?: string;
|
|
expiresOn?: number;
|
|
extendedExpiresOn?: number;
|
|
};
|
|
|
|
/**
|
|
* Token cache manager
|
|
*/
|
|
export class TokenCache implements ITokenCache {
|
|
// Flag to indicate if in browser environment
|
|
public isBrowserEnvironment: boolean;
|
|
// Input configuration by developer/user
|
|
protected config: BrowserConfiguration;
|
|
// Browser cache storage
|
|
private storage: BrowserCacheManager;
|
|
// Logger
|
|
private logger: Logger;
|
|
// Crypto class
|
|
private cryptoObj: ICrypto;
|
|
|
|
constructor(
|
|
configuration: BrowserConfiguration,
|
|
storage: BrowserCacheManager,
|
|
logger: Logger,
|
|
cryptoObj: ICrypto
|
|
) {
|
|
this.isBrowserEnvironment = typeof window !== "undefined";
|
|
this.config = configuration;
|
|
this.storage = storage;
|
|
this.logger = logger;
|
|
this.cryptoObj = cryptoObj;
|
|
}
|
|
|
|
// Move getAllAccounts here and cache utility APIs
|
|
|
|
/**
|
|
* API to load tokens to msal-browser cache.
|
|
* @param request
|
|
* @param response
|
|
* @param options
|
|
* @returns `AuthenticationResult` for the response that was loaded.
|
|
*/
|
|
async loadExternalTokens(
|
|
request: SilentRequest,
|
|
response: ExternalTokenResponse,
|
|
options: LoadTokenOptions
|
|
): Promise<AuthenticationResult> {
|
|
if (!this.isBrowserEnvironment) {
|
|
throw createBrowserAuthError(
|
|
BrowserAuthErrorCodes.nonBrowserEnvironment
|
|
);
|
|
}
|
|
|
|
const correlationId =
|
|
request.correlationId || BrowserCrypto.createNewGuid();
|
|
|
|
const idTokenClaims = response.id_token
|
|
? AuthToken.extractTokenClaims(response.id_token, base64Decode)
|
|
: undefined;
|
|
|
|
const authorityOptions: AuthorityOptions = {
|
|
protocolMode: this.config.auth.protocolMode,
|
|
knownAuthorities: this.config.auth.knownAuthorities,
|
|
cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata,
|
|
authorityMetadata: this.config.auth.authorityMetadata,
|
|
skipAuthorityMetadataCache:
|
|
this.config.auth.skipAuthorityMetadataCache,
|
|
};
|
|
const authority = request.authority
|
|
? new Authority(
|
|
Authority.generateAuthority(
|
|
request.authority,
|
|
request.azureCloudOptions
|
|
),
|
|
this.config.system.networkClient,
|
|
this.storage,
|
|
authorityOptions,
|
|
this.logger,
|
|
request.correlationId || BrowserCrypto.createNewGuid()
|
|
)
|
|
: undefined;
|
|
|
|
const cacheRecordAccount: AccountEntity = await this.loadAccount(
|
|
request,
|
|
options.clientInfo || response.client_info || "",
|
|
correlationId,
|
|
idTokenClaims,
|
|
authority
|
|
);
|
|
|
|
const idToken = await this.loadIdToken(
|
|
response,
|
|
cacheRecordAccount.homeAccountId,
|
|
cacheRecordAccount.environment,
|
|
cacheRecordAccount.realm,
|
|
correlationId
|
|
);
|
|
|
|
const accessToken = await this.loadAccessToken(
|
|
request,
|
|
response,
|
|
cacheRecordAccount.homeAccountId,
|
|
cacheRecordAccount.environment,
|
|
cacheRecordAccount.realm,
|
|
options,
|
|
correlationId
|
|
);
|
|
|
|
const refreshToken = await this.loadRefreshToken(
|
|
response,
|
|
cacheRecordAccount.homeAccountId,
|
|
cacheRecordAccount.environment,
|
|
correlationId
|
|
);
|
|
|
|
return this.generateAuthenticationResult(
|
|
request,
|
|
{
|
|
account: cacheRecordAccount,
|
|
idToken,
|
|
accessToken,
|
|
refreshToken,
|
|
},
|
|
idTokenClaims,
|
|
authority
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function to load account to msal-browser cache
|
|
* @param idToken
|
|
* @param environment
|
|
* @param clientInfo
|
|
* @param authorityType
|
|
* @param requestHomeAccountId
|
|
* @returns `AccountEntity`
|
|
*/
|
|
private async loadAccount(
|
|
request: SilentRequest,
|
|
clientInfo: string,
|
|
correlationId: string,
|
|
idTokenClaims?: TokenClaims,
|
|
authority?: Authority
|
|
): Promise<AccountEntity> {
|
|
this.logger.verbose("TokenCache - loading account");
|
|
|
|
if (request.account) {
|
|
const accountEntity = AccountEntity.createFromAccountInfo(
|
|
request.account
|
|
);
|
|
await this.storage.setAccount(accountEntity, correlationId);
|
|
return accountEntity;
|
|
} else if (!authority || (!clientInfo && !idTokenClaims)) {
|
|
this.logger.error(
|
|
"TokenCache - if an account is not provided on the request, authority and either clientInfo or idToken must be provided instead."
|
|
);
|
|
throw createBrowserAuthError(
|
|
BrowserAuthErrorCodes.unableToLoadToken
|
|
);
|
|
}
|
|
|
|
const homeAccountId = AccountEntity.generateHomeAccountId(
|
|
clientInfo,
|
|
authority.authorityType,
|
|
this.logger,
|
|
this.cryptoObj,
|
|
idTokenClaims
|
|
);
|
|
|
|
const claimsTenantId = idTokenClaims?.tid;
|
|
|
|
const cachedAccount = buildAccountToCache(
|
|
this.storage,
|
|
authority,
|
|
homeAccountId,
|
|
base64Decode,
|
|
correlationId,
|
|
idTokenClaims,
|
|
clientInfo,
|
|
authority.hostnameAndPort,
|
|
claimsTenantId,
|
|
undefined, // authCodePayload
|
|
undefined, // nativeAccountId
|
|
this.logger
|
|
);
|
|
|
|
await this.storage.setAccount(cachedAccount, correlationId);
|
|
return cachedAccount;
|
|
}
|
|
|
|
/**
|
|
* Helper function to load id tokens to msal-browser cache
|
|
* @param idToken
|
|
* @param homeAccountId
|
|
* @param environment
|
|
* @param tenantId
|
|
* @returns `IdTokenEntity`
|
|
*/
|
|
private async loadIdToken(
|
|
response: ExternalTokenResponse,
|
|
homeAccountId: string,
|
|
environment: string,
|
|
tenantId: string,
|
|
correlationId: string
|
|
): Promise<IdTokenEntity | null> {
|
|
if (!response.id_token) {
|
|
this.logger.verbose("TokenCache - no id token found in response");
|
|
return null;
|
|
}
|
|
|
|
this.logger.verbose("TokenCache - loading id token");
|
|
const idTokenEntity = CacheHelpers.createIdTokenEntity(
|
|
homeAccountId,
|
|
environment,
|
|
response.id_token,
|
|
this.config.auth.clientId,
|
|
tenantId
|
|
);
|
|
|
|
await this.storage.setIdTokenCredential(idTokenEntity, correlationId);
|
|
return idTokenEntity;
|
|
}
|
|
|
|
/**
|
|
* Helper function to load access tokens to msal-browser cache
|
|
* @param request
|
|
* @param response
|
|
* @param homeAccountId
|
|
* @param environment
|
|
* @param tenantId
|
|
* @returns `AccessTokenEntity`
|
|
*/
|
|
private async loadAccessToken(
|
|
request: SilentRequest,
|
|
response: ExternalTokenResponse,
|
|
homeAccountId: string,
|
|
environment: string,
|
|
tenantId: string,
|
|
options: LoadTokenOptions,
|
|
correlationId: string
|
|
): Promise<AccessTokenEntity | null> {
|
|
if (!response.access_token) {
|
|
this.logger.verbose(
|
|
"TokenCache - no access token found in response"
|
|
);
|
|
return null;
|
|
} else if (!response.expires_in) {
|
|
this.logger.error(
|
|
"TokenCache - no expiration set on the access token. Cannot add it to the cache."
|
|
);
|
|
return null;
|
|
} else if (
|
|
!response.scope &&
|
|
(!request.scopes || !request.scopes.length)
|
|
) {
|
|
this.logger.error(
|
|
"TokenCache - scopes not specified in the request or response. Cannot add token to the cache."
|
|
);
|
|
return null;
|
|
}
|
|
|
|
this.logger.verbose("TokenCache - loading access token");
|
|
|
|
const scopes = response.scope
|
|
? ScopeSet.fromString(response.scope)
|
|
: new ScopeSet(request.scopes);
|
|
const expiresOn =
|
|
options.expiresOn || response.expires_in + TimeUtils.nowSeconds();
|
|
|
|
const extendedExpiresOn =
|
|
options.extendedExpiresOn ||
|
|
(response.ext_expires_in || response.expires_in) +
|
|
TimeUtils.nowSeconds();
|
|
|
|
const accessTokenEntity = CacheHelpers.createAccessTokenEntity(
|
|
homeAccountId,
|
|
environment,
|
|
response.access_token,
|
|
this.config.auth.clientId,
|
|
tenantId,
|
|
scopes.printScopes(),
|
|
expiresOn,
|
|
extendedExpiresOn,
|
|
base64Decode
|
|
);
|
|
|
|
await this.storage.setAccessTokenCredential(
|
|
accessTokenEntity,
|
|
correlationId
|
|
);
|
|
return accessTokenEntity;
|
|
}
|
|
|
|
/**
|
|
* Helper function to load refresh tokens to msal-browser cache
|
|
* @param request
|
|
* @param response
|
|
* @param homeAccountId
|
|
* @param environment
|
|
* @returns `RefreshTokenEntity`
|
|
*/
|
|
private async loadRefreshToken(
|
|
response: ExternalTokenResponse,
|
|
homeAccountId: string,
|
|
environment: string,
|
|
correlationId: string
|
|
): Promise<RefreshTokenEntity | null> {
|
|
if (!response.refresh_token) {
|
|
this.logger.verbose(
|
|
"TokenCache - no refresh token found in response"
|
|
);
|
|
return null;
|
|
}
|
|
|
|
this.logger.verbose("TokenCache - loading refresh token");
|
|
const refreshTokenEntity = CacheHelpers.createRefreshTokenEntity(
|
|
homeAccountId,
|
|
environment,
|
|
response.refresh_token,
|
|
this.config.auth.clientId,
|
|
response.foci,
|
|
undefined, // userAssertionHash
|
|
response.refresh_token_expires_in
|
|
);
|
|
|
|
await this.storage.setRefreshTokenCredential(
|
|
refreshTokenEntity,
|
|
correlationId
|
|
);
|
|
return refreshTokenEntity;
|
|
}
|
|
|
|
/**
|
|
* Helper function to generate an `AuthenticationResult` for the result.
|
|
* @param request
|
|
* @param idTokenObj
|
|
* @param cacheRecord
|
|
* @param authority
|
|
* @returns `AuthenticationResult`
|
|
*/
|
|
private generateAuthenticationResult(
|
|
request: SilentRequest,
|
|
cacheRecord: CacheRecord & { account: AccountEntity },
|
|
idTokenClaims?: TokenClaims,
|
|
authority?: Authority
|
|
): AuthenticationResult {
|
|
let accessToken: string = "";
|
|
let responseScopes: Array<string> = [];
|
|
let expiresOn: Date | null = null;
|
|
let extExpiresOn: Date | undefined;
|
|
|
|
if (cacheRecord?.accessToken) {
|
|
accessToken = cacheRecord.accessToken.secret;
|
|
responseScopes = ScopeSet.fromString(
|
|
cacheRecord.accessToken.target
|
|
).asArray();
|
|
// Access token expiresOn stored in seconds, converting to Date for AuthenticationResult
|
|
expiresOn = TimeUtils.toDateFromSeconds(
|
|
cacheRecord.accessToken.expiresOn
|
|
);
|
|
extExpiresOn = TimeUtils.toDateFromSeconds(
|
|
cacheRecord.accessToken.extendedExpiresOn
|
|
);
|
|
}
|
|
|
|
const accountEntity = cacheRecord.account;
|
|
|
|
return {
|
|
authority: authority ? authority.canonicalAuthority : "",
|
|
uniqueId: cacheRecord.account.localAccountId,
|
|
tenantId: cacheRecord.account.realm,
|
|
scopes: responseScopes,
|
|
account: accountEntity.getAccountInfo(),
|
|
idToken: cacheRecord.idToken?.secret || "",
|
|
idTokenClaims: idTokenClaims || {},
|
|
accessToken: accessToken,
|
|
fromCache: true,
|
|
expiresOn: expiresOn,
|
|
correlationId: request.correlationId || "",
|
|
requestId: "",
|
|
extExpiresOn: extExpiresOn,
|
|
familyId: cacheRecord.refreshToken?.familyId || "",
|
|
tokenType: cacheRecord?.accessToken?.tokenType || "",
|
|
state: request.state || "",
|
|
cloudGraphHostName: accountEntity.cloudGraphHostName || "",
|
|
msGraphHost: accountEntity.msGraphHost || "",
|
|
fromNativeBroker: false,
|
|
};
|
|
}
|
|
}
|