@azure/msal-browser
Version:
Microsoft Authentication Library for js
493 lines (461 loc) • 16.2 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,
Constants,
CacheRecord,
TokenClaims,
CacheHelpers,
buildAccountToCache,
} from "@azure/msal-common";
import { BrowserConfiguration } from "../config/Configuration";
import { SilentRequest } from "../request/SilentRequest";
import { BrowserCacheManager } from "./BrowserCacheManager";
import { ITokenCache } from "./ITokenCache";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError";
import { AuthenticationResult } from "../response/AuthenticationResult";
import { base64Decode } from "../encode/Base64Decode";
import * as BrowserCrypto from "../crypto/BrowserCrypto";
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.
*/
loadExternalTokens(
request: SilentRequest,
response: ExternalTokenResponse,
options: LoadTokenOptions
): AuthenticationResult {
this.logger.info("TokenCache - loadExternalTokens called");
if (!response.id_token) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
const idTokenClaims = AuthToken.extractTokenClaims(
response.id_token,
base64Decode
);
let cacheRecord: CacheRecord;
let authority: Authority | undefined;
let cacheRecordAccount: AccountEntity;
if (request.account) {
cacheRecordAccount = AccountEntity.createFromAccountInfo(
request.account
);
cacheRecord = new CacheRecord(
cacheRecordAccount,
this.loadIdToken(
response.id_token,
cacheRecordAccount.homeAccountId,
request.account.environment,
request.account.tenantId
),
this.loadAccessToken(
request,
response,
cacheRecordAccount.homeAccountId,
request.account.environment,
request.account.tenantId,
options
),
this.loadRefreshToken(
request,
response,
cacheRecordAccount.homeAccountId,
request.account.environment
)
);
} else if (request.authority) {
const authorityUrl = Authority.generateAuthority(
request.authority,
request.azureCloudOptions
);
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,
};
authority = new Authority(
authorityUrl,
this.config.system.networkClient,
this.storage,
authorityOptions,
this.logger,
request.correlationId || BrowserCrypto.createNewGuid()
);
// "clientInfo" from options takes precedence over "clientInfo" in response
if (options.clientInfo) {
this.logger.trace("TokenCache - homeAccountId from options");
cacheRecordAccount = this.loadAccount(
idTokenClaims,
authority,
options.clientInfo
);
cacheRecord = new CacheRecord(
cacheRecordAccount,
this.loadIdToken(
response.id_token,
cacheRecordAccount.homeAccountId,
authority.hostnameAndPort,
authority.tenant
),
this.loadAccessToken(
request,
response,
cacheRecordAccount.homeAccountId,
authority.hostnameAndPort,
authority.tenant,
options
),
this.loadRefreshToken(
request,
response,
cacheRecordAccount.homeAccountId,
authority.hostnameAndPort
)
);
} else if (response.client_info) {
this.logger.trace("TokenCache - homeAccountId from response");
cacheRecordAccount = this.loadAccount(
idTokenClaims,
authority,
response.client_info
);
cacheRecord = new CacheRecord(
cacheRecordAccount,
this.loadIdToken(
response.id_token,
cacheRecordAccount.homeAccountId,
authority.hostnameAndPort,
authority.tenant
),
this.loadAccessToken(
request,
response,
cacheRecordAccount.homeAccountId,
authority.hostnameAndPort,
authority.tenant,
options
),
this.loadRefreshToken(
request,
response,
cacheRecordAccount.homeAccountId,
authority.hostnameAndPort
)
);
} else {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
} else {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
return this.generateAuthenticationResult(
request,
idTokenClaims,
cacheRecord,
cacheRecordAccount,
authority
);
}
/**
* Helper function to load account to msal-browser cache
* @param idToken
* @param environment
* @param clientInfo
* @param authorityType
* @param requestHomeAccountId
* @returns `AccountEntity`
*/
private loadAccount(
idTokenClaims: TokenClaims,
authority: Authority,
clientInfo?: string,
requestHomeAccountId?: string
): AccountEntity {
if (this.isBrowserEnvironment) {
this.logger.verbose("TokenCache - loading account");
let homeAccountId;
if (requestHomeAccountId) {
homeAccountId = requestHomeAccountId;
} else if (authority.authorityType !== undefined && clientInfo) {
homeAccountId = AccountEntity.generateHomeAccountId(
clientInfo,
authority.authorityType,
this.logger,
this.cryptoObj,
idTokenClaims
);
}
if (!homeAccountId) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
const claimsTenantId = idTokenClaims.tid;
const cachedAccount = buildAccountToCache(
this.storage,
authority,
homeAccountId,
idTokenClaims,
base64Decode,
clientInfo,
authority.hostnameAndPort,
claimsTenantId,
undefined, // authCodePayload
undefined, // nativeAccountId
this.logger
);
this.storage.setAccount(cachedAccount);
return cachedAccount;
} else {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
}
/**
* Helper function to load id tokens to msal-browser cache
* @param idToken
* @param homeAccountId
* @param environment
* @param tenantId
* @returns `IdTokenEntity`
*/
private loadIdToken(
idToken: string,
homeAccountId: string,
environment: string,
tenantId: string
): IdTokenEntity {
const idTokenEntity = CacheHelpers.createIdTokenEntity(
homeAccountId,
environment,
idToken,
this.config.auth.clientId,
tenantId
);
if (this.isBrowserEnvironment) {
this.logger.verbose("TokenCache - loading id token");
this.storage.setIdTokenCredential(idTokenEntity);
return idTokenEntity;
} else {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
}
/**
* Helper function to load access tokens to msal-browser cache
* @param request
* @param response
* @param homeAccountId
* @param environment
* @param tenantId
* @returns `AccessTokenEntity`
*/
private loadAccessToken(
request: SilentRequest,
response: ExternalTokenResponse,
homeAccountId: string,
environment: string,
tenantId: string,
options: LoadTokenOptions
): AccessTokenEntity | null {
if (!response.access_token) {
this.logger.verbose(
"TokenCache - No access token provided for caching"
);
return null;
}
if (!response.expires_in) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
if (!options.extendedExpiresOn) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
const scopes = new ScopeSet(request.scopes).printScopes();
const expiresOn =
options.expiresOn ||
response.expires_in + new Date().getTime() / 1000;
const extendedExpiresOn = options.extendedExpiresOn;
const accessTokenEntity = CacheHelpers.createAccessTokenEntity(
homeAccountId,
environment,
response.access_token,
this.config.auth.clientId,
tenantId,
scopes,
expiresOn,
extendedExpiresOn,
base64Decode
);
if (this.isBrowserEnvironment) {
this.logger.verbose("TokenCache - loading access token");
this.storage.setAccessTokenCredential(accessTokenEntity);
return accessTokenEntity;
} else {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
}
/**
* Helper function to load refresh tokens to msal-browser cache
* @param request
* @param response
* @param homeAccountId
* @param environment
* @returns `RefreshTokenEntity`
*/
private loadRefreshToken(
request: SilentRequest,
response: ExternalTokenResponse,
homeAccountId: string,
environment: string
): RefreshTokenEntity | null {
if (!response.refresh_token) {
this.logger.verbose(
"TokenCache - No refresh token provided for caching"
);
return null;
}
const refreshTokenEntity = CacheHelpers.createRefreshTokenEntity(
homeAccountId,
environment,
response.refresh_token,
this.config.auth.clientId
);
if (this.isBrowserEnvironment) {
this.logger.verbose("TokenCache - loading refresh token");
this.storage.setRefreshTokenCredential(refreshTokenEntity);
return refreshTokenEntity;
} else {
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
}
/**
* Helper function to generate an `AuthenticationResult` for the result.
* @param request
* @param idTokenObj
* @param cacheRecord
* @param authority
* @returns `AuthenticationResult`
*/
private generateAuthenticationResult(
request: SilentRequest,
idTokenClaims: TokenClaims,
cacheRecord: CacheRecord,
accountEntity: AccountEntity,
authority?: Authority
): AuthenticationResult {
let accessToken: string = Constants.EMPTY_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();
expiresOn = new Date(
Number(cacheRecord.accessToken.expiresOn) * 1000
);
extExpiresOn = new Date(
Number(cacheRecord.accessToken.extendedExpiresOn) * 1000
);
}
const uid =
idTokenClaims.oid || idTokenClaims.sub || Constants.EMPTY_STRING;
const tid = idTokenClaims.tid || Constants.EMPTY_STRING;
return {
authority: authority
? authority.canonicalAuthority
: Constants.EMPTY_STRING,
uniqueId: uid,
tenantId: tid,
scopes: responseScopes,
account: accountEntity.getAccountInfo(),
idToken: cacheRecord.idToken?.secret || "",
idTokenClaims: idTokenClaims || {},
accessToken: accessToken,
fromCache: true,
expiresOn: expiresOn,
correlationId: request.correlationId || Constants.EMPTY_STRING,
requestId: Constants.EMPTY_STRING,
extExpiresOn: extExpiresOn,
familyId: Constants.EMPTY_STRING,
tokenType:
cacheRecord?.accessToken?.tokenType || Constants.EMPTY_STRING,
state: Constants.EMPTY_STRING,
cloudGraphHostName:
accountEntity.cloudGraphHostName || Constants.EMPTY_STRING,
msGraphHost: accountEntity.msGraphHost || Constants.EMPTY_STRING,
code: undefined,
fromNativeBroker: false,
};
}
}