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

Authorization Code Flow for Single Page Applications: Response Handling Code #1183

Merged
102 changes: 97 additions & 5 deletions lib/msal-common/src/app/module/AuthorizationCodeModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PublicClientSPAConfiguration, buildPublicClientSPAConfiguration } from
import { AuthenticationParameters } from "../../request/AuthenticationParameters";
import { TokenExchangeParameters } from "../../request/TokenExchangeParameters";
// response
import { TokenResponse } from "../../response/TokenResponse";
import { TokenResponse, setResponseIdToken } from "../../response/TokenResponse";
import { ClientConfigurationError } from "../../error/ClientConfigurationError";
import { AuthorityFactory } from "../../auth/authority/AuthorityFactory";
import { ServerCodeRequestParameters } from "../../server/ServerCodeRequestParameters";
Expand All @@ -23,6 +23,11 @@ import { ProtocolUtils } from "../../utils/ProtocolUtils";
import { TemporaryCacheKeys, PersistentCacheKeys } from "../../utils/Constants";
import { AuthError } from "../../error/AuthError";
import { ServerTokenRequestParameters } from "../../server/ServerTokenRequestParameters";
import { ServerAuthorizationTokenResponse, validateServerAuthorizationTokenResponse } from "../../server/ServerAuthorizationTokenResponse";
import { IdToken } from "../../auth/IdToken";
import { buildClientInfo } from "../../auth/ClientInfo";
import { Account } from "../../auth/Account";
import { ScopeSet } from "../../auth/ScopeSet";

/**
* AuthorizationCodeModule class
Expand Down Expand Up @@ -81,7 +86,8 @@ export class AuthorizationCodeModule extends AuthModule {
codeVerifier: requestParameters.generatedPkce.verifier,
extraQueryParameters: request.extraQueryParameters,
authority: requestParameters.authorityInstance.canonicalAuthority,
correlationId: requestParameters.correlationId,
correlationId: requestParameters.correlationId,
userRequestState: ProtocolUtils.getUserRequestState(requestParameters.state)
};

this.cacheStorage.setItem(TemporaryCacheKeys.REQUEST_PARAMS, this.cryptoObj.base64Encode(JSON.stringify(tokenRequest)));
Expand Down Expand Up @@ -133,15 +139,22 @@ export class AuthorizationCodeModule extends AuthModule {
this.cryptoObj
);

const acquiredTokenResponse = this.networkClient.sendPostRequestAsync(
const acquiredTokenResponse = await this.networkClient.sendPostRequestAsync<ServerAuthorizationTokenResponse>(
tokenEndpoint,
{
body: await tokenReqParams.createRequestBody(),
headers: tokenReqParams.createRequestHeaders()
}
);

return null;
try {
validateServerAuthorizationTokenResponse(acquiredTokenResponse);
} catch (e) {
this.cacheManager.resetTempCacheItems(tokenReqParams.state);
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
throw e;
}

return this.createTokenResponse(acquiredTokenResponse, tokenReqParams.state);
}

// #region Response Handling
Expand All @@ -163,12 +176,91 @@ export class AuthorizationCodeModule extends AuthModule {
// Create response object
const response: CodeResponse = {
code: hashParams.code,
userRequestState: ProtocolUtils.getUserRequestState(hashParams.state)
userRequestState: hashParams.state
};

return response;
}

private createTokenResponse(serverTokenResponse: ServerAuthorizationTokenResponse, state: string): TokenResponse {
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
let tokenResponse: TokenResponse = {
uniqueId: "",
tenantId: "",
tokenType: "",
idToken: null,
idTokenClaims: null,
accessToken: "",
refreshToken: "",
scopes: [],
expiresOn: null,
account: null,
userRequestState: ""
};
// Set consented scopes in response
const requestScopes = ScopeSet.fromString(serverTokenResponse.scope, this.clientConfig.auth.clientId, false);
tokenResponse.scopes = requestScopes.asArray();

// Retrieve current id token object
let idTokenObj: IdToken;
const cachedIdToken: string = this.cacheStorage.getItem(PersistentCacheKeys.ID_TOKEN);
if (serverTokenResponse.id_token) {
idTokenObj = new IdToken(serverTokenResponse.id_token, this.cryptoObj);
tokenResponse = setResponseIdToken(tokenResponse, idTokenObj);
} else if (cachedIdToken) {
idTokenObj = new IdToken(cachedIdToken, this.cryptoObj);
tokenResponse = setResponseIdToken(tokenResponse, idTokenObj);
} else {
// TODO: No account scenario?
}

// check nonce integrity if idToken has nonce - throw an error if not matched
const nonce = this.cacheStorage.getItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|${state}`);

if (!idTokenObj || !idTokenObj.claims.nonce) {
throw ClientAuthError.createInvalidIdTokenError(idTokenObj);
}

if (idTokenObj.claims.nonce !== nonce) {
this.account = null;
throw ClientAuthError.createNonceMismatchError();
}

// TODO: This will be used when saving tokens
// const authorityKey: string = this.cacheManager.generateAuthorityKey(state);
// const cachedAuthority: string = this.cacheStorage.getItem(authorityKey);

// TODO: Save id token here

// Retrieve client info
const clientInfo = buildClientInfo(this.cacheStorage.getItem(PersistentCacheKeys.CLIENT_INFO), this.cryptoObj);

// Create account object for request
this.account = Account.createAccount(idTokenObj, clientInfo, this.cryptoObj);
tokenResponse.account = this.account;

// Set token type
tokenResponse.tokenType = serverTokenResponse.token_type;

// Save the access token if it exists
if (serverTokenResponse.access_token) {
const accountKey = this.cacheManager.generateAcquireTokenAccountKey(this.account.homeAccountIdentifier);

const cachedAccount = JSON.parse(this.cacheStorage.getItem(accountKey)) as Account;

if (!cachedAccount || Account.compareAccounts(cachedAccount, this.account)) {
tokenResponse.accessToken = serverTokenResponse.access_token;
tokenResponse.refreshToken = serverTokenResponse.refresh_token;
// TODO: Save the access token
} else {
throw ClientAuthError.createAccountMismatchError();
}
}

// Return user set state in the response
tokenResponse.userRequestState = ProtocolUtils.getUserRequestState(state);
return tokenResponse;
}

// #endregion

// #region Getters and setters
Expand Down
13 changes: 8 additions & 5 deletions lib/msal-common/src/cache/CacheHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export class CacheHelpers {
* @param accountId
* @param state
*/
generateAcquireTokenAccountKey(accountId: any, state: string): string {
return `${TemporaryCacheKeys.ACQUIRE_TOKEN_ACCOUNT}${Constants.RESOURCE_DELIM}${accountId}${Constants.RESOURCE_DELIM}${state}`;
generateAcquireTokenAccountKey(accountId: any): string {
return `${TemporaryCacheKeys.ACQUIRE_TOKEN_ACCOUNT}${Constants.RESOURCE_DELIM}${accountId}`;
}

/**
Expand All @@ -43,11 +43,11 @@ export class CacheHelpers {
* @param state
* @hidden
*/
setAccountCache(account: Account, state: string) {
setAccountCache(account: Account) {
// Cache acquireTokenAccountKey
const accountId = account && account.homeAccountIdentifier ? account.homeAccountIdentifier : Constants.NO_ACCOUNT;

const acquireTokenAccountKey = this.generateAcquireTokenAccountKey(accountId, state);
const acquireTokenAccountKey = this.generateAcquireTokenAccountKey(accountId);
this.cacheStorage.setItem(acquireTokenAccountKey, JSON.stringify(account));
}

Expand Down Expand Up @@ -76,12 +76,15 @@ export class CacheHelpers {
updateCacheEntries(serverAuthenticationRequest: ServerCodeRequestParameters, account: Account): void {
// Cache account and state
if (account) {
this.setAccountCache(account, serverAuthenticationRequest.state);
this.setAccountCache(account);
}

// Cache the request state
this.cacheStorage.setItem(TemporaryCacheKeys.REQUEST_STATE, serverAuthenticationRequest.state);

// Cache the nonce
this.cacheStorage.setItem(`${TemporaryCacheKeys.NONCE_IDTOKEN}|${serverAuthenticationRequest.state}`, serverAuthenticationRequest.nonce);
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved

// Cache authorityKey
this.setAuthorityCache(serverAuthenticationRequest.authorityInstance, serverAuthenticationRequest.state);
}
Expand Down
33 changes: 33 additions & 0 deletions lib/msal-common/src/error/ClientAuthError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { AuthError } from "./AuthError";
import { IdToken } from "../auth/IdToken";
/**
* ClientAuthErrorMessage class containing string constants used by error codes and messages.
*/
Expand Down Expand Up @@ -44,6 +45,18 @@ export const ClientAuthErrorMessage = {
code: "state_mismatch",
desc: "State mismatch error. Please check your network. Continued requests may cause cache overflow."
},
nonceMismatchError: {
code: "nonce_mismatch",
desc: "Nonce mismatch error. Please check whether concurrent requests are causing this issue."
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
},
accountMismatchError: {
code: "account_mismatch",
desc: "The cached account and account which made the token request do not match."
},
invalidIdToken: {
code: "invalid_id_token",
desc: "Invalid ID token format."
},
authCodeNullOrEmptyError: {
code: "auth_code_null_or_empty",
desc: "The authorization code or code response was null. Please check the stack trace and logs for more information."
Expand Down Expand Up @@ -128,6 +141,26 @@ export class ClientAuthError extends AuthError {
return new ClientAuthError(ClientAuthErrorMessage.stateMismatchError.code, ClientAuthErrorMessage.stateMismatchError.desc);
}

/**
* Creates an error thrown when the nonce does not match.
*/
static createNonceMismatchError(): ClientAuthError {
return new ClientAuthError(ClientAuthErrorMessage.nonceMismatchError.code, ClientAuthErrorMessage.nonceMismatchError.desc);
}

static createAccountMismatchError(): ClientAuthError {
return new ClientAuthError(ClientAuthErrorMessage.accountMismatchError.code, ClientAuthErrorMessage.accountMismatchError.desc);
}

/**
* Throws error if idToken is not correctly formed
* @param idToken
*/
static createInvalidIdTokenError(idToken: IdToken) : ClientAuthError {
return new ClientAuthError(ClientAuthErrorMessage.invalidIdToken.code,
`${ClientAuthErrorMessage.invalidIdToken.desc} Given token: ${idToken}`);
}

/**
* Creates an error thrown when the authorization code required for a token request is null or empty.
*/
Expand Down
22 changes: 22 additions & 0 deletions lib/msal-common/src/response/TokenResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Account } from "../auth/Account";
import { StringDict } from "../utils/MsalTypes";
import { AuthResponse } from "./AuthResponse";
import { IdToken } from "../auth/IdToken";

/**
* TokenResponse type returned by library containing id, access and/or refresh tokens.
Expand All @@ -31,3 +32,24 @@ export type TokenResponse = AuthResponse & {
expiresOn: Date;
account: Account;
};

export function setResponseIdToken(originalResponse: TokenResponse, idTokenObj: IdToken) : TokenResponse {
if (!originalResponse) {
return null;
} else if (!idTokenObj) {
return originalResponse;
}

const exp = Number(idTokenObj.claims.exp);
if (exp && !originalResponse.expiresOn) {
originalResponse.expiresOn = new Date(exp * 1000);
}

return {
...originalResponse,
idToken: idTokenObj.rawIdToken,
idTokenClaims: idTokenObj.claims,
uniqueId: idTokenObj.claims.oid || idTokenObj.claims.sub,
tenantId: idTokenObj.claims.tid,
};
}
2 changes: 2 additions & 0 deletions lib/msal-common/src/server/ServerAuthorizationCodeResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { ICrypto } from "../crypto/ICrypto";
* - code: authorization code from server
* - client_info: client info object
* - state: OAuth2 request state
* - error: error sent back in hash
* - error: description
*/
export type ServerAuthorizationCodeResponse = {
code?: string;
Expand Down
35 changes: 35 additions & 0 deletions lib/msal-common/src/server/ServerAuthorizationTokenResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ServerError } from "../error/ServerError";

/**
* Deserialized response object from server authorization code request.
* -
*/
export type ServerAuthorizationTokenResponse = {
// Success
token_type?: string;
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
scope?: string;
expires_in?: number;
ext_expires_in?: number;
access_token?: string;
refresh_token?: string;
id_token?: string;
// Error
error?: string;
error_description?: string;
error_codes?: Array<string>;
timestamp?: string;
trace_id?: string;
correlation_id?: string;
};

export function validateServerAuthorizationTokenResponse(serverResponse: ServerAuthorizationTokenResponse): void {
// Check for error
if (serverResponse.error || serverResponse.error_description) {
const errString = `${serverResponse.error_codes} - [${serverResponse.timestamp}]: ${serverResponse.error_description} - Correlation ID: ${serverResponse.correlation_id} - Trace ID: ${serverResponse.trace_id}`;
throw new ServerError(serverResponse.error, errString);
}
}
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 1 addition & 2 deletions lib/msal-common/src/server/ServerTokenRequestParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ export class ServerTokenRequestParameters extends ServerRequestParameters {

this.scopes = new ScopeSet(this.tokenRequest && this.tokenRequest.scopes, this.clientId, false);

const randomGuid = this.cryptoObj.createNewGuid();
this.state = ProtocolUtils.setRequestState(this.tokenRequest && this.tokenRequest.userRequestState, randomGuid);
this.state = tokenRequest.userRequestState;

this.correlationId = this.tokenRequest.correlationId || this.cryptoObj.createNewGuid();
}
Expand Down