@azure/msal-browser
Version:
Microsoft Authentication Library for js
426 lines (389 loc) • 13.5 kB
text/typescript
/*
* 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,
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,
};
}
}