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

msal-browser uses AuthCodeClient and SilentFlowClient #1793

Merged
merged 20 commits into from
Jun 24, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
230 changes: 147 additions & 83 deletions lib/msal-browser/src/app/PublicClientApplication.ts

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions lib/msal-browser/src/cache/BrowserStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,17 @@ export class BrowserStorage extends CacheManager {
this.setItem(this.generateCacheKey(authorityKey), authority, CacheSchemaType.TEMPORARY);
}

/**
* Gets the cached authority based on the cached state. Returns empty if no cached state found.
*/
getCachedAuthority(): string {
const state = this.getItem(this.generateCacheKey(TemporaryCacheKeys.REQUEST_STATE), CacheSchemaType.TEMPORARY) as string;
if (!state) {
return "";
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
}
return this.getItem(this.generateCacheKey(this.generateAuthorityKey(state)), CacheSchemaType.TEMPORARY) as string;
}

/**
* Updates account, authority, and state in cache
* @param serverAuthenticationRequest
Expand Down
24 changes: 24 additions & 0 deletions lib/msal-browser/src/error/BrowserConfigurationAuthError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import { AuthError } from "@azure/msal-common";
* BrowserAuthErrorMessage class containing string constants used by error codes and messages.
*/
export const BrowserConfigurationAuthErrorMessage = {
redirectUriNotSet: {
code: "redirect_uri_empty",
desc: "A redirect URI is required for all calls, and none has been set."
},
postLogoutUriNotSet: {
code: "post_logout_uri_empty",
desc: "A post logout redirect has not been set."
},
storageNotSupportedError: {
code: "storage_not_supported",
desc: "Given storage configuration option was not supported."
Expand Down Expand Up @@ -36,6 +44,22 @@ export class BrowserConfigurationAuthError extends AuthError {
Object.setPrototypeOf(this, BrowserConfigurationAuthError.prototype);
}

/**
* Creates an error thrown when the redirect uri is empty (not set by caller)
*/
static createRedirectUriEmptyError(): BrowserConfigurationAuthError {
return new BrowserConfigurationAuthError(BrowserConfigurationAuthErrorMessage.redirectUriNotSet.code,
BrowserConfigurationAuthErrorMessage.redirectUriNotSet.desc);
}

/**
* Creates an error thrown when the post-logout redirect uri is empty (not set by caller)
*/
static createPostLogoutRedirectUriEmptyError(): BrowserConfigurationAuthError {
return new BrowserConfigurationAuthError(BrowserConfigurationAuthErrorMessage.postLogoutUriNotSet.code,
BrowserConfigurationAuthErrorMessage.postLogoutUriNotSet.desc);
}

/**
* Creates error thrown when given storage location is not supported.
* @param givenStorageLocation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { SPAClient, StringUtils, AuthorizationCodeRequest, CacheSchemaType, AuthenticationResult } from "@azure/msal-common";
import { StringUtils, AuthorizationCodeRequest, CacheSchemaType, AuthenticationResult, AuthorizationCodeClient } from "@azure/msal-common";
import { BrowserStorage } from "../cache/BrowserStorage";
import { BrowserAuthError } from "../error/BrowserAuthError";
import { TemporaryCacheKeys } from "../utils/BrowserConstants";
Expand All @@ -12,11 +12,11 @@ import { TemporaryCacheKeys } from "../utils/BrowserConstants";
*/
export abstract class InteractionHandler {

protected authModule: SPAClient;
protected authModule: AuthorizationCodeClient;
protected browserStorage: BrowserStorage;
protected authCodeRequest: AuthorizationCodeRequest;

constructor(authCodeModule: SPAClient, storageImpl: BrowserStorage) {
constructor(authCodeModule: AuthorizationCodeClient, storageImpl: BrowserStorage) {
this.authModule = authCodeModule;
this.browserStorage = storageImpl;
}
Expand Down Expand Up @@ -49,7 +49,7 @@ export abstract class InteractionHandler {
this.authCodeRequest.code = authCode;

// Acquire token with retrieved code.
const tokenResponse = await this.authModule.acquireToken(this.authCodeRequest, requestState, cachedNonce);
const tokenResponse = await this.authModule.acquireToken(this.authCodeRequest, cachedNonce, requestState);
this.browserStorage.cleanRequest();
return tokenResponse;
}
Expand Down
4 changes: 2 additions & 2 deletions lib/msal-browser/src/interaction_handler/PopupHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { UrlString, StringUtils, Constants, SPAClient, AuthorizationCodeRequest, CacheSchemaType } from "@azure/msal-common";
import { UrlString, StringUtils, Constants, AuthorizationCodeRequest, CacheSchemaType, AuthorizationCodeClient } from "@azure/msal-common";
import { InteractionHandler } from "./InteractionHandler";
import { BrowserAuthError } from "../error/BrowserAuthError";
import { BrowserConstants } from "../utils/BrowserConstants";
Expand All @@ -16,7 +16,7 @@ export class PopupHandler extends InteractionHandler {

private currentWindow: Window;

constructor(authCodeModule: SPAClient, storageImpl: BrowserStorage) {
constructor(authCodeModule: AuthorizationCodeClient, storageImpl: BrowserStorage) {
super(authCodeModule, storageImpl);

// Properly sets this reference for the unload event.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class RedirectHandler extends InteractionHandler {
const requestState = this.browserStorage.getItem(this.browserStorage.generateCacheKey(TemporaryCacheKeys.REQUEST_STATE), CacheSchemaType.TEMPORARY) as string;
const cachedNonceKey = this.browserStorage.generateNonceKey(requestState);
const cachedNonce = this.browserStorage.getItem(this.browserStorage.generateCacheKey(cachedNonceKey), CacheSchemaType.TEMPORARY) as string;
console.log(cachedNonce);
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
this.authCodeRequest = this.browserStorage.getCachedRequest(requestState, browserCrypto);

// Handle code response.
Expand All @@ -69,7 +70,7 @@ export class RedirectHandler extends InteractionHandler {
this.browserStorage.removeItem(this.browserStorage.generateCacheKey(TemporaryCacheKeys.URL_HASH));

// Acquire token with retrieved code.
const tokenResponse = await this.authModule.acquireToken(this.authCodeRequest, requestState, cachedNonce);
const tokenResponse = await this.authModule.acquireToken(this.authCodeRequest, cachedNonce, requestState);
this.browserStorage.cleanRequest();
return tokenResponse;
}
Expand Down
4 changes: 2 additions & 2 deletions lib/msal-browser/src/interaction_handler/SilentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { UrlString, SPAClient, StringUtils, AuthorizationCodeRequest } from "@azure/msal-common";
import { UrlString, StringUtils, AuthorizationCodeRequest, AuthorizationCodeClient } from "@azure/msal-common";
import { InteractionHandler } from "./InteractionHandler";
import { BrowserConstants } from "../utils/BrowserConstants";
import { BrowserAuthError } from "../error/BrowserAuthError";
Expand All @@ -11,7 +11,7 @@ import { BrowserStorage } from "../cache/BrowserStorage";
export class SilentHandler extends InteractionHandler {

private loadFrameTimeout: number;
constructor(authCodeModule: SPAClient, storageImpl: BrowserStorage, configuredLoadFrameTimeout: number) {
constructor(authCodeModule: AuthorizationCodeClient, storageImpl: BrowserStorage, configuredLoadFrameTimeout: number) {
super(authCodeModule, storageImpl);
this.loadFrameTimeout = configuredLoadFrameTimeout;
}
Expand Down
79 changes: 53 additions & 26 deletions lib/msal-common/src/authority/AuthorityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,71 @@ import { AdfsAuthority } from "./AdfsAuthority";
export class AuthorityFactory {

/**
* Parse the url and determine the type of authority
* Create an authority object of the correct type based on the url
* Performs basic authority validation - checks to see if the authority is of a valid type (i.e. aad, b2c, adfs)
*
* Also performs endpoint discovery.
*
* @param defaultAuthority
* @param networkClient
* @param authorityUri
* @param adfsDisabled
*/
private static detectAuthorityFromUrl(authorityString: string): AuthorityType {
const authorityUrl = new UrlString(authorityString);
const components = authorityUrl.getUrlComponents();
const pathSegments = components.PathSegments;
static async createDiscoveredInstance(authorityUri: string, networkClient: INetworkModule, adfsDisabled?: boolean): Promise<Authority> {
// Initialize authority and perform discovery endpoint check.
const acquireTokenAuthority: Authority = AuthorityFactory.createInstance(authorityUri, networkClient);

if (pathSegments.length && pathSegments[0].toLowerCase() === Constants.ADFS)
return AuthorityType.Adfs;
else if (B2cAuthority.B2CTrustedHostList.length)
return AuthorityType.B2C;
// This is temporary. Remove when ADFS is supported for browser
if (adfsDisabled && acquireTokenAuthority.authorityType == AuthorityType.Adfs) {
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
throw ClientAuthError.createInvalidAuthorityTypeError(acquireTokenAuthority.canonicalAuthority);
}

// defaults to Aad
return AuthorityType.Aad;
if (acquireTokenAuthority.discoveryComplete()) {
return acquireTokenAuthority;
}

try {
await acquireTokenAuthority.resolveEndpointsAsync();
return acquireTokenAuthority;
} catch (e) {
throw ClientAuthError.createEndpointDiscoveryIncompleteError(e);
}
}

/**
* Create an authority object of the correct type based on the url
* Performs basic authority validation - checks to see if the authority is of a valid type (eg aad, b2c)
* Performs basic authority validation - checks to see if the authority is of a valid type (i.e. aad, b2c, adfs)
*
* Does not perform endpoint discovery.
*
* @param authorityUrl
* @param networkInterface
*/
public static createInstance(authorityUrl: string, networkInterface: INetworkModule): Authority {
static createInstance(authorityUrl: string, networkInterface: INetworkModule): Authority {
// Throw error if authority url is empty
if (StringUtils.isEmpty(authorityUrl)) {
throw ClientConfigurationError.createUrlEmptyError();
}

const type = AuthorityFactory.detectAuthorityFromUrl(authorityUrl);

// Depending on above detection, create the right type.
switch (type) {
case AuthorityType.Aad:
return new AadAuthority(authorityUrl, networkInterface);
case AuthorityType.B2C:
return new B2cAuthority(authorityUrl, networkInterface);
case AuthorityType.Adfs:
return new AdfsAuthority(authorityUrl, networkInterface);
default:
throw ClientAuthError.createInvalidAuthorityTypeError(`${authorityUrl}`);
}
return AuthorityFactory.detectAuthorityFromUrl(authorityUrl, networkInterface);
}

/**
* Parse the url and determine the type of authority.
* @param authorityString
* @param networkInterface
*/
private static detectAuthorityFromUrl(authorityString: string, networkInterface: INetworkModule): Authority {
const authorityUrl: UrlString = new UrlString(authorityString);
const components = authorityUrl.getUrlComponents();
const pathSegments = components.PathSegments;

if (pathSegments.length && pathSegments[0].toLowerCase() === Constants.ADFS)
return new AdfsAuthority(authorityString, networkInterface);
else if (B2cAuthority.B2CTrustedHostList.length)
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
return new B2cAuthority(authorityString, networkInterface);

// defaults to Aad
return new AadAuthority(authorityString, networkInterface);
}
}
50 changes: 38 additions & 12 deletions lib/msal-common/src/client/AuthorizationCodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { NetworkResponse } from "../network/NetworkManager";
import { ScopeSet } from "../request/ScopeSet";
import { ResponseHandler } from "../response/ResponseHandler";
import { AuthenticationResult } from "../response/AuthenticationResult";
import { StringUtils } from "../utils/StringUtils";
import { ClientAuthError } from "../error/ClientAuthError";
import { UrlString } from "../url/UrlString";
import { ServerAuthorizationCodeResponse } from "../server/ServerAuthorizationCodeResponse";

/**
* Oauth2.0 Authorization Code client
Expand All @@ -37,19 +41,22 @@ export class AuthorizationCodeClient extends BaseClient {
*/
async getAuthCodeUrl(request: AuthorizationUrlRequest): Promise<string> {
const queryString = this.createAuthCodeUrlQueryString(request);
return `${this.defaultAuthority.authorizationEndpoint}?${queryString}`;
return `${this.authority.authorizationEndpoint}?${queryString}`;
}

/**
* API to acquire a token in exchange of 'authorization_code` acquired by the user in the first leg of the
* authorization_code_grant
* @param request
*/
async acquireToken(request: AuthorizationCodeRequest): Promise<AuthenticationResult> {

async acquireToken(request: AuthorizationCodeRequest, cachedNonce?: string, cachedState?: string): Promise<AuthenticationResult> {
this.logger.info("in acquireToken call");
// If no code response is given, we cannot acquire a token.
if (!request || StringUtils.isEmpty(request.code)) {
throw ClientAuthError.createTokenRequestCannotBeMadeError();
}

const response = await this.executeTokenRequest(this.defaultAuthority, request);
const response = await this.executeTokenRequest(this.authority, request);

const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
Expand All @@ -58,20 +65,36 @@ export class AuthorizationCodeClient extends BaseClient {
this.logger
);

// Validate response. This function throws a server error if an error is returned by the server.
responseHandler.validateTokenResponse(response.body);
const tokenResponse = responseHandler.generateAuthenticationResult(response.body, this.defaultAuthority);
const tokenResponse = responseHandler.generateAuthenticationResult(response.body, this.authority, cachedNonce, cachedState);
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved

return tokenResponse;
}

/**
* Handles the hash fragment response from public client code request. Returns a code response used by
* the client to exchange for a token in acquireToken.
* @param hashFragment
*/
public handleFragmentResponse(hashFragment: string, cachedState: string): string {
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
// Handle responses.
const responseHandler = new ResponseHandler(this.config.authOptions.clientId, this.cacheStorage, this.cryptoUtils, this.logger);
// Deserialize hash fragment response parameters.
const hashUrlString = new UrlString(hashFragment);
const serverParams = hashUrlString.getDeserializedHash<ServerAuthorizationCodeResponse>();

// Get code response
responseHandler.validateServerAuthorizationCodeResponse(serverParams, cachedState, this.cryptoUtils);
return serverParams.code;
}

/**
* Executes POST request to token endpoint
* @param authority
* @param request
*/
private async executeTokenRequest(authority: Authority, request: AuthorizationCodeRequest)
: Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {

private async executeTokenRequest(authority: Authority, request: AuthorizationCodeRequest): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
const requestBody = this.createTokenRequestBody(request);
const headers: Map<string, string> = this.createDefaultTokenRequestHeaders();

Expand Down Expand Up @@ -138,14 +161,13 @@ export class AuthorizationCodeClient extends BaseClient {
// add library info parameters
parameterBuilder.addLibraryInfo(this.config.libraryInfo);

// add client_info=1
parameterBuilder.addClientInfo();

if (request.codeChallenge) {
parameterBuilder.addCodeChallengeParams(request.codeChallenge, request.codeChallengeMethod);
}

if (request.state) {
parameterBuilder.addState(request.state);
}

if (request.prompt) {
parameterBuilder.addPrompt(request.prompt);
}
Expand All @@ -162,6 +184,10 @@ export class AuthorizationCodeClient extends BaseClient {
parameterBuilder.addNonce(request.nonce);
}

if (request.state) {
parameterBuilder.addState(request.state);
}

if (request.claims) {
parameterBuilder.addClaims(request.claims);
}
Expand Down
23 changes: 21 additions & 2 deletions lib/msal-common/src/client/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { NetworkResponse } from "../network/NetworkManager";
import { ServerAuthorizationTokenResponse } from "../server/ServerAuthorizationTokenResponse";
import { B2cAuthority } from "../authority/B2cAuthority";
import { CacheManager } from "../cache/CacheManager";
import { AccountEntity } from "../cache/entities/AccountEntity";
import { EndSessionRequest } from "../request/EndSessionRequest";

/**
* Base application class which will construct requests to send to and handle responses from the Microsoft STS using the authorization code flow.
Expand All @@ -34,7 +36,7 @@ export abstract class BaseClient {
protected networkClient: INetworkModule;

// Default authority object
protected defaultAuthority: Authority;
protected authority: Authority;

protected constructor(configuration: ClientConfiguration) {
// Set the configuration
Expand All @@ -54,7 +56,24 @@ export abstract class BaseClient {

B2cAuthority.setKnownAuthorities(this.config.authOptions.knownAuthorities);

this.defaultAuthority = this.config.authOptions.authority;
this.authority = this.config.authOptions.authority;
}

/**
* Use to log out the current user, and redirect the user to the postLogoutRedirectUri.
* Default behaviour is to redirect the user to `window.location.href`.
* @param authorityUri
*/
async getLogoutUri(logoutRequest: EndSessionRequest): Promise<string> {
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
// Clear current account.
this.cacheStorage.removeAccount(AccountEntity.generateAccountCacheKey(logoutRequest.account));

// Get postLogoutRedirectUri.
const postLogoutUriParam = logoutRequest.postLogoutRedirectUri ? `?${AADServerParamKeys.POST_LOGOUT_URI}=` + encodeURIComponent(logoutRequest.postLogoutRedirectUri) : "";

// Construct logout URI.
const logoutUri = `${this.authority.endSessionEndpoint}${postLogoutUriParam}`;
return logoutUri;
}

/**
Expand Down