Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend msal-browser TokenCache loadExternalTokens to load refresh tokens #5233

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
};
}
}