Skip to content

Commit

Permalink
Merge f7a7492 into 64835e2
Browse files Browse the repository at this point in the history
  • Loading branch information
tnorling committed Aug 27, 2020
2 parents 64835e2 + f7a7492 commit ce89726
Show file tree
Hide file tree
Showing 6 changed files with 411 additions and 265 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "patch",
"comment": "Separate cache lookup from token refresh (#2189)",
"packageName": "@azure/msal-common",
"email": "thomas.norling@microsoft.com",
"dependentChangeType": "patch",
"date": "2020-08-26T20:56:48.140Z"
}
81 changes: 80 additions & 1 deletion lib/msal-common/src/cache/CacheManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,24 @@ export abstract class CacheManager implements ICacheManager {
}
}

getCacheRecord(account: AccountInfo, clientId: string, scopes: ScopeSet): CacheRecord {
// Get account object for this request.
const accountKey: string = AccountEntity.generateAccountCacheKey(account);
const cachedAccount = this.getAccount(accountKey);

// Get current cached tokens
const cachedAccessToken = this.getAccessTokenEntity(clientId, account, scopes);
const cachedRefreshToken = this.getRefreshTokenEntity(clientId, account);
const cachedIdToken = this.getIdTokenEntity(clientId, account);

return {
account: cachedAccount,
accessToken: cachedAccessToken,
idToken: cachedIdToken,
refreshToken: cachedRefreshToken
};
}

/**
* saves account into cache
* @param account
Expand Down Expand Up @@ -168,10 +186,71 @@ export abstract class CacheManager implements ICacheManager {
* retrieve a credential - accessToken, idToken or refreshToken; given the cache key
* @param key
*/
getCredential(key: string): CredentialEntity {
getCredential(key: string): CredentialEntity | null {
return this.getItem(key, CacheSchemaType.CREDENTIAL) as CredentialEntity;
}

/**
* Helper function to retrieve IdTokenEntity from cache
* @param clientId
* @param account
* @param inputRealm
*/
getIdTokenEntity(clientId: string, account: AccountInfo): IdTokenEntity | null {
const idTokenKey: string = CredentialEntity.generateCredentialCacheKey(
account.homeAccountId,
account.environment,
CredentialType.ID_TOKEN,
clientId,
account.tenantId
);

return this.getCredential(idTokenKey) as IdTokenEntity;
}

/**
* Helper function to retrieve AccessTokenEntity from cache
* @param clientId
* @param account
* @param scopes
* @param inputRealm
*/
getAccessTokenEntity(clientId: string, account: AccountInfo, scopes: ScopeSet): AccessTokenEntity | null {
const accessTokenFilter: CredentialFilter = {
homeAccountId: account.homeAccountId,
environment: account.environment,
credentialType: CredentialType.ACCESS_TOKEN,
clientId,
realm: account.tenantId,
target: scopes.printScopesLowerCase()
};
const credentialCache: CredentialCache = this.getCredentialsFilteredBy(accessTokenFilter);
const accessTokens = Object.keys(credentialCache.accessTokens).map(key => credentialCache.accessTokens[key]);
if (accessTokens.length > 1) {
// TODO: Figure out what to throw or return here.
} else if (accessTokens.length < 1) {
return null;
}

return accessTokens[0] as AccessTokenEntity;
}

/**
* Helper function to retrieve RefreshTokenEntity from cache
* @param clientId
* @param account
*/
getRefreshTokenEntity(clientId: string, account: AccountInfo): RefreshTokenEntity | null {
const refreshTokenKey: string = CredentialEntity.generateCredentialCacheKey(
account.homeAccountId,
account.environment,
CredentialType.REFRESH_TOKEN,
clientId
);

return this.getCredential(refreshTokenKey) as RefreshTokenEntity;
}

/**
* retrieve accounts matching all provided filters; if no filter is set, get all accounts
* not checking for casing as keys are all generated in lower case, remember to convert to lower case if object properties are compared
Expand Down
162 changes: 66 additions & 96 deletions lib/msal-common/src/client/SilentFlowClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,16 @@ import { BaseClient } from "./BaseClient";
import { ClientConfiguration } from "../config/ClientConfiguration";
import { SilentFlowRequest } from "../request/SilentFlowRequest";
import { AuthenticationResult } from "../response/AuthenticationResult";
import { CredentialType } from "../utils/Constants";
import { IdTokenEntity } from "../cache/entities/IdTokenEntity";
import { AccessTokenEntity } from "../cache/entities/AccessTokenEntity";
import { RefreshTokenEntity } from "../cache/entities/RefreshTokenEntity";
import { ScopeSet } from "../request/ScopeSet";
import { IdToken } from "../account/IdToken";
import { TimeUtils } from "../utils/TimeUtils";
import { RefreshTokenRequest } from "../request/RefreshTokenRequest";
import { RefreshTokenClient } from "./RefreshTokenClient";
import { ClientAuthError } from "../error/ClientAuthError";
import { CredentialFilter, CredentialCache } from "../cache/utils/CacheTypes";
import { AccountEntity } from "../cache/entities/AccountEntity";
import { CredentialEntity } from "../cache/entities/CredentialEntity";
import { ClientAuthError, ClientAuthErrorMessage } from "../error/ClientAuthError";
import { ClientConfigurationError } from "../error/ClientConfigurationError";
import { ResponseHandler } from "../response/ResponseHandler";
import { CacheRecord } from "../cache/entities/CacheRecord";

export class SilentFlowClient extends BaseClient {

Expand All @@ -34,7 +29,23 @@ export class SilentFlowClient extends BaseClient {
* the given token and returns the renewed token
* @param request
*/
public async acquireToken(request: SilentFlowRequest): Promise<AuthenticationResult> {
async acquireToken(request: SilentFlowRequest): Promise<AuthenticationResult> {
try {
return this.acquireCachedToken(request);
} catch (e) {
if (e instanceof ClientAuthError && e.errorCode === ClientAuthErrorMessage.tokenRefreshRequired.code) {
return this.refreshToken(request);
} else {
throw e;
}
}
}

/**
* Retrieves token from cache or throws an error if it must be refreshed.
* @param request
*/
acquireCachedToken(request: SilentFlowRequest): AuthenticationResult {
// Cannot renew token if no request object is given.
if (!request) {
throw ClientConfigurationError.createEmptyTokenRequestError();
Expand All @@ -46,49 +57,62 @@ export class SilentFlowClient extends BaseClient {
}

const requestScopes = new ScopeSet(request.scopes || []);

// Get account object for this request.
const accountKey: string = AccountEntity.generateAccountCacheKey(request.account);
const cachedAccount = this.cacheManager.getAccount(accountKey);
const cacheRecord = this.cacheManager.getCacheRecord(request.account, this.config.authOptions.clientId, requestScopes);

const homeAccountId = cachedAccount.homeAccountId;
const environment = cachedAccount.environment;

// Get current cached tokens
const cachedAccessToken = this.readAccessTokenFromCache(homeAccountId, environment, requestScopes, cachedAccount.realm);
const cachedRefreshToken = this.readRefreshTokenFromCache(homeAccountId, environment);

// Check if refresh is forced, claims are being requested or if tokens are expired. If neither are true, return a token response with the found token entry.
if (this.isRefreshRequired(request, cachedAccessToken)) {
// no refresh Token
if (!cachedRefreshToken) {
throw ClientAuthError.createNoTokensFoundError();
if (this.isRefreshRequired(request, cacheRecord.accessToken)) {
throw ClientAuthError.createRefreshRequiredError();
} else {
if (this.config.serverTelemetryManager) {
this.config.serverTelemetryManager.incrementCacheHits();
}
return this.generateResultFromCacheRecord(cacheRecord);
}
}

const refreshTokenClient = new RefreshTokenClient(this.config);
const refreshTokenRequest: RefreshTokenRequest = {
...request,
refreshToken: cachedRefreshToken.secret
};

return refreshTokenClient.acquireToken(refreshTokenRequest);
/**
* Gets cached refresh token and uses RefreshTokenClient to refresh tokens
* @param request
*/
async refreshToken(request: SilentFlowRequest): Promise<AuthenticationResult> {
// Cannot renew token if no request object is given.
if (!request) {
throw ClientConfigurationError.createEmptyTokenRequestError();
}

// We currently do not support silent flow for account === null use cases; This will be revisited for confidential flow usecases
if (!request.account) {
throw ClientAuthError.createNoAccountInSilentRequestError();
}

// Return tokens from cache
if (this.config.serverTelemetryManager) {
this.config.serverTelemetryManager.incrementCacheHits();
const refreshToken = this.cacheManager.getRefreshTokenEntity(this.config.authOptions.clientId, request.account);
// no refresh Token
if (!refreshToken) {
throw ClientAuthError.createNoTokensFoundError();
}
const cachedIdToken = this.readIdTokenFromCache(homeAccountId, environment, cachedAccount.realm);
const idTokenObj = new IdToken(cachedIdToken.secret, this.config.cryptoInterface);

return ResponseHandler.generateAuthenticationResult({
account: cachedAccount,
accessToken: cachedAccessToken,
idToken: cachedIdToken,
refreshToken: cachedRefreshToken
}, idTokenObj, true);
const refreshTokenClient = new RefreshTokenClient(this.config);
const refreshTokenRequest: RefreshTokenRequest = {
...request,
refreshToken: refreshToken.secret
};

return refreshTokenClient.acquireToken(refreshTokenRequest);
}

/**
* Helper function to build response object from the CacheRecord
* @param cacheRecord
*/
private generateResultFromCacheRecord(cacheRecord: CacheRecord): AuthenticationResult {
const idTokenObj = new IdToken(cacheRecord.idToken.secret, this.config.cryptoInterface);
return ResponseHandler.generateAuthenticationResult(cacheRecord, idTokenObj, true);
}

/**
* Given a request object and an accessTokenEntity determine if the accessToken needs to be refreshed
* @param request
* @param cachedAccessToken
*/
private isRefreshRequired(request: SilentFlowRequest, cachedAccessToken: AccessTokenEntity|null): boolean {
if (request.forceRefresh || request.claims) {
// Must refresh due to request parameters
Expand All @@ -100,58 +124,4 @@ export class SilentFlowClient extends BaseClient {

return false;
}

/**
* fetches idToken from cache if present
* @param request
*/
private readIdTokenFromCache(homeAccountId: string, environment: string, inputRealm: string): IdTokenEntity {
const idTokenKey: string = CredentialEntity.generateCredentialCacheKey(
homeAccountId,
environment,
CredentialType.ID_TOKEN,
this.config.authOptions.clientId,
inputRealm
);
return this.cacheManager.getCredential(idTokenKey) as IdTokenEntity;
}

/**
* fetches accessToken from cache if present
* @param request
* @param scopes
*/
private readAccessTokenFromCache(homeAccountId: string, environment: string, scopes: ScopeSet, inputRealm: string): AccessTokenEntity {
const accessTokenFilter: CredentialFilter = {
homeAccountId,
environment,
credentialType: CredentialType.ACCESS_TOKEN,
clientId: this.config.authOptions.clientId,
realm: inputRealm,
target: scopes.printScopesLowerCase()
};
const credentialCache: CredentialCache = this.cacheManager.getCredentialsFilteredBy(accessTokenFilter);
const accessTokens = Object.keys(credentialCache.accessTokens).map(key => credentialCache.accessTokens[key]);
if (accessTokens.length > 1) {
// TODO: Figure out what to throw or return here.
} else if (accessTokens.length < 1) {
return null;
}
return accessTokens[0] as AccessTokenEntity;
}

/**
* fetches refreshToken from cache if present
* @param request
*/
private readRefreshTokenFromCache(homeAccountId: string, environment: string): RefreshTokenEntity {
const refreshTokenKey: string = CredentialEntity.generateCredentialCacheKey(
homeAccountId,
environment,
CredentialType.REFRESH_TOKEN,
this.config.authOptions.clientId
);
return this.cacheManager.getCredential(refreshTokenKey) as RefreshTokenEntity;
}

}
11 changes: 11 additions & 0 deletions lib/msal-common/src/error/ClientAuthError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ export const ClientAuthErrorMessage = {
invalidClientCredential: {
code: "invalid_client_credential",
desc: "Client credential (secret, certificate, or assertion) must not be empty when creating a confidential client. An application should at most have one credential"
},
tokenRefreshRequired: {
code: "token_refresh_required",
desc: "Cannot return token from cache because it must be refreshed. This may be due to one of the following reasons: forceRefresh parameter is set to true, claims have been requested, there is no cached access token or it is expired."
}
};

Expand Down Expand Up @@ -444,4 +448,11 @@ export class ClientAuthError extends AuthError {
static createInvalidCredentialError(): ClientAuthError {
return new ClientAuthError(ClientAuthErrorMessage.invalidClientCredential.code, `${ClientAuthErrorMessage.invalidClientCredential.desc}`);
}

/**
* Throws error if token cannot be retrieved from cache due to refresh being required.
*/
static createRefreshRequiredError(): ClientAuthError {
return new ClientAuthError(ClientAuthErrorMessage.tokenRefreshRequired.code, ClientAuthErrorMessage.tokenRefreshRequired.desc);
}
}

0 comments on commit ce89726

Please sign in to comment.