Skip to content

Commit

Permalink
Merge pull request #5233 from lvalentine/user/louisv/external_load_re…
Browse files Browse the repository at this point in the history
…fresh_tokens

Extend msal-browser TokenCache loadExternalTokens to load refresh tokens
  • Loading branch information
jasonnutter committed Oct 10, 2022
2 parents fe12dee + 4ac77d4 commit b929529
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Extend msal-browser TokenCache to load refresh tokens #5233",
"packageName": "@azure/msal-browser",
"email": "louisv@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Include refresh token in ExternalTokenResponse #5233",
"packageName": "@azure/msal-common",
"email": "louisv@microsoft.com",
"dependentChangeType": "patch"
}
10 changes: 7 additions & 3 deletions lib/msal-browser/src/cache/ITokenCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
* Licensed under the MIT License.
*/

import { ExternalTokenResponse } from "@azure/msal-common";
import { ExternalTokenResponse, AuthenticationResult } from "@azure/msal-common";
import { SilentRequest } from "../request/SilentRequest";
import { LoadTokenOptions } from "./TokenCache";

export interface ITokenCache {

/** API to side-load tokens to MSAL cache */
loadExternalTokens(request: SilentRequest, response: ExternalTokenResponse, options: LoadTokenOptions): void;
/**
* API to side-load tokens to MSAL cache
* @returns `AuthenticationResult` for the response that was loaded.
*/
loadExternalTokens(request: SilentRequest, response: ExternalTokenResponse, options: LoadTokenOptions): AuthenticationResult;

}
176 changes: 155 additions & 21 deletions lib/msal-browser/src/cache/TokenCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/

import { AccessTokenEntity, ICrypto, IdTokenEntity, Logger, ScopeSet, Authority, AuthorityOptions, ExternalTokenResponse, AccountEntity, AuthToken } from "@azure/msal-common";
import { AccessTokenEntity, ICrypto, IdTokenEntity, Logger, ScopeSet, Authority, AuthorityOptions, ExternalTokenResponse, AccountEntity, AuthToken, RefreshTokenEntity , AuthorityType, CacheRecord, AuthenticationResult, Constants } from "@azure/msal-common";
import { BrowserConfiguration } from "../config/Configuration";
import { SilentRequest } from "../request/SilentRequest";
import { BrowserCacheManager } from "./BrowserCacheManager";
Expand Down Expand Up @@ -46,17 +46,28 @@ export class TokenCache implements ITokenCache {
* @param request
* @param response
* @param options
* @returns `AuthenticationResult` for the response that was loaded.
*/
loadExternalTokens(request: SilentRequest, response: ExternalTokenResponse, options: LoadTokenOptions): void {
loadExternalTokens(request: SilentRequest, response: ExternalTokenResponse, options: LoadTokenOptions): AuthenticationResult {
this.logger.info("TokenCache - loadExternalTokens called");

if (!response.id_token) {
throw BrowserAuthError.createUnableToLoadTokenError("Please ensure server response includes id token.");
}

const idToken = new AuthToken(response.id_token, this.cryptoObj);

let cacheRecord: CacheRecord | undefined;
let authority: Authority | undefined;

if (request.account) {
this.loadIdToken(response.id_token, request.account.homeAccountId, request.account.environment, request.account.tenantId, options);
this.loadAccessToken(request, response, request.account.homeAccountId, request.account.environment, request.account.tenantId, options);
const cacheRecordAccount = this.loadAccount(idToken, request.account.environment, undefined, undefined, request.account.homeAccountId);
cacheRecord = new CacheRecord(
cacheRecordAccount,
this.loadIdToken(idToken, 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);
Expand All @@ -67,23 +78,71 @@ export class TokenCache implements ITokenCache {
authorityMetadata: this.config.auth.authorityMetadata,
skipAuthorityMetadataCache: this.config.auth.skipAuthorityMetadataCache,
};
const authority = new Authority(authorityUrl, this.config.system.networkClient, this.storage, authorityOptions);
authority = new Authority(authorityUrl, this.config.system.networkClient, this.storage, authorityOptions);

// "clientInfo" from options takes precedence over "clientInfo" in response
if (options.clientInfo) {
this.logger.trace("TokenCache - homeAccountId from options");
this.loadIdToken(response.id_token, options.clientInfo, authority.hostnameAndPort, authority.tenant, options);
this.loadAccessToken(request, response, options.clientInfo, authority.hostnameAndPort, authority.tenant, options);
const cacheRecordAccount = this.loadAccount(idToken, authority.hostnameAndPort, options.clientInfo, authority.authorityType);
cacheRecord = new CacheRecord(
cacheRecordAccount,
this.loadIdToken(idToken, 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");
this.loadIdToken(response.id_token, response.client_info, authority.hostnameAndPort, authority.tenant, options);
this.loadAccessToken(request, response, response.client_info, authority.hostnameAndPort, authority.tenant, options);
const cacheRecordAccount = this.loadAccount(idToken, authority.hostnameAndPort, response.client_info, authority.authorityType);
cacheRecord = new CacheRecord(
cacheRecordAccount,
this.loadIdToken(idToken, 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 BrowserAuthError.createUnableToLoadTokenError("Please provide clientInfo in the response or options.");
}
} else {
throw BrowserAuthError.createUnableToLoadTokenError("Please provide a request with an account or a request with authority.");
}

return this.generateAuthenticationResult(request, idToken, cacheRecord, authority);
}

/**
* Helper function to load account to msal-browser cache
* @param idToken
* @param environment
* @param clientInfo
* @param authorityType
* @param requestHomeAccountId
* @returns `AccountEntity`
*/
private loadAccount(idToken: AuthToken, environment: string, clientInfo?: string, authorityType?: AuthorityType, requestHomeAccountId?: string): AccountEntity {

let homeAccountId;
if (requestHomeAccountId) {
homeAccountId = requestHomeAccountId;
} else if (authorityType !== undefined && clientInfo) {
homeAccountId = AccountEntity.generateHomeAccountId(clientInfo, authorityType, this.logger, this.cryptoObj, idToken);
}

if (!homeAccountId) {
throw BrowserAuthError.createUnableToLoadTokenError("Unexpected missing homeAccountId");
}

const accountEntity = clientInfo ?
AccountEntity.createAccount(clientInfo, homeAccountId, idToken, undefined, undefined, undefined, environment) :
AccountEntity.createGenericAccount(homeAccountId, idToken, undefined, undefined, undefined, environment);

if (this.isBrowserEnvironment) {
this.logger.verbose("TokenCache - loading account");

this.storage.setAccount(accountEntity);
return accountEntity;
} else {
throw BrowserAuthError.createUnableToLoadTokenError("loadExternalTokens is designed to work in browser environments only.");
}
}

/**
Expand All @@ -92,20 +151,16 @@ export class TokenCache implements ITokenCache {
* @param homeAccountId
* @param environment
* @param tenantId
* @param options
* @returns `IdTokenEntity`
*/
private loadIdToken(idToken: string, homeAccountId: string, environment: string, tenantId: string, options: LoadTokenOptions): void {
private loadIdToken(idToken: AuthToken, homeAccountId: string, environment: string, tenantId: string): IdTokenEntity {

const idTokenEntity = IdTokenEntity.createIdTokenEntity(homeAccountId, environment, idToken, this.config.auth.clientId, tenantId);
const idAuthToken = new AuthToken(idToken, this.cryptoObj);
const accountEntity = options.clientInfo ?
AccountEntity.createAccount(options.clientInfo, homeAccountId, idAuthToken, undefined, undefined, undefined, environment) :
AccountEntity.createGenericAccount(homeAccountId, idAuthToken, undefined, undefined, undefined, environment);
const idTokenEntity = IdTokenEntity.createIdTokenEntity(homeAccountId, environment, idToken.rawToken, this.config.auth.clientId, tenantId);

if (this.isBrowserEnvironment) {
this.logger.verbose("TokenCache - loading id token");
this.storage.setAccount(accountEntity);
this.storage.setIdTokenCredential(idTokenEntity);
return idTokenEntity;
} else {
throw BrowserAuthError.createUnableToLoadTokenError("loadExternalTokens is designed to work in browser environments only.");
}
Expand All @@ -115,17 +170,16 @@ export class TokenCache implements ITokenCache {
* Helper function to load access tokens to msal-browser cache
* @param request
* @param response
* @param options
* @param homeAccountId
* @param environment
* @param tenantId
* @returns
* @returns `AccessTokenEntity`
*/
private loadAccessToken(request: SilentRequest, response: ExternalTokenResponse, homeAccountId: string, environment: string, tenantId: string, options: LoadTokenOptions): void {
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;
return null;
}

if (!response.expires_in) {
Expand All @@ -145,9 +199,89 @@ export class TokenCache implements ITokenCache {
if (this.isBrowserEnvironment) {
this.logger.verbose("TokenCache - loading access token");
this.storage.setAccessTokenCredential(accessTokenEntity);
return accessTokenEntity;
} else {
throw BrowserAuthError.createUnableToLoadTokenError("loadExternalTokens is designed to work in browser environments only.");
}
}

/**
* 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 = RefreshTokenEntity.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 BrowserAuthError.createUnableToLoadTokenError("loadExternalTokens is designed to work in browser environments only.");
}
}

/**
* Helper function to generate an `AuthenticationResult` for the result.
* @param request
* @param idTokenObj
* @param cacheRecord
* @param authority
* @returns `AuthenticationResult`
*/
private generateAuthenticationResult(
request: SilentRequest,
idTokenObj: AuthToken,
cacheRecord?: CacheRecord,
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 = idTokenObj?.claims.oid || idTokenObj?.claims.sub || Constants.EMPTY_STRING;
const tid = idTokenObj?.claims.tid || Constants.EMPTY_STRING;

return {
authority: authority ? authority.canonicalAuthority : Constants.EMPTY_STRING,
uniqueId: uid,
tenantId: tid,
scopes: responseScopes,
account: cacheRecord?.account ? cacheRecord.account.getAccountInfo() : null,
idToken: idTokenObj ? idTokenObj.rawToken : Constants.EMPTY_STRING,
idTokenClaims: idTokenObj ? idTokenObj.claims : {},
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: cacheRecord?.account?.cloudGraphHostName || Constants.EMPTY_STRING,
msGraphHost: cacheRecord?.account?.msGraphHost || Constants.EMPTY_STRING,
code: undefined,
fromNativeBroker: false
};
}
}

0 comments on commit b929529

Please sign in to comment.