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: Adding Redirect Handling Code #1164

5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@
],
"eslint.quiet": true,
"files.eol": "\n",
"github-pr.targetBranch": "dev"
"github-pr.targetBranch": "dev",
"editor.codeActionsOnSave": {
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
"source.fixAll.eslint": true
}
}
8 changes: 6 additions & 2 deletions lib/msal-browser/src/app/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ const FRAME_TIMEOUT = 6000;
const OFFSET = 300;
const NAVIGATE_FRAME_WAIT = 500;

export type BrowserAuthOptions = AuthOptions & {
navigateToLoginRequestUrl?: boolean;
};

/**
* Use this to configure the below cache configuration options:
*
Expand Down Expand Up @@ -77,12 +81,12 @@ export type BrowserLoggerOptions = {
* - framework: this is where you can configure the running mode of angular. More to come here soon.
*/
export type Configuration = {
auth?: AuthOptions,
auth?: BrowserAuthOptions,
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
cache?: CacheOptions,
system?: SystemOptions
};

const DEFAULT_AUTH_OPTIONS: AuthOptions = {
const DEFAULT_AUTH_OPTIONS: BrowserAuthOptions = {
clientId: "",
clientSecret: "",
authority: null,
Expand Down
23 changes: 17 additions & 6 deletions lib/msal-browser/src/app/PublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
* Licensed under the MIT License.
*/

import { AuthError, AuthResponse, AuthorizationCodeModule, AuthenticationParameters, INetworkModule, TokenResponse } from "msal-common";
import { AuthError, AuthResponse, AuthorizationCodeModule, AuthenticationParameters, INetworkModule, TokenResponse, UrlString, TemporaryCacheKeys } from "msal-common";
import { BrowserStorage } from "../cache/BrowserStorage";
import { Configuration, buildConfiguration } from "./Configuration";
import { CryptoOps } from "../crypto/CryptoOps";
import { IInteractionHandler } from "../interaction_handler/IInteractionHandler";
import { RedirectHandler } from "../interaction_handler/RedirectHandler";
import { BrowserConfigurationAuthError } from "../error/BrowserConfigurationAuthError";

/**
* A type alias for an authResponseCallback function.
Expand Down Expand Up @@ -100,8 +103,17 @@ export class PublicClientApplication {
* or the library, depending on the origin of the error, or the AuthResponse object
* containing data from the server (returned with a null or non-blocking error).
*/
handleRedirectCallback(authCallback: AuthCallback): void {
throw new Error("Method not implemented.");
async handleRedirectCallback(authCallback: AuthCallback): Promise<void> {
if(!authCallback) {
throw BrowserConfigurationAuthError.createInvalidCallbackObjectError(authCallback);
}

this.authCallback = authCallback;
const { location: { hash } } = window;
if (UrlString.hashContainsKnownProperties(hash)) {
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.authCallback);
interactionHandler.handleCodeResponse(hash);
}
}

/**
Expand All @@ -110,9 +122,8 @@ export class PublicClientApplication {
* @param {@link (AuthenticationParameters:type)}
*/
loginRedirect(request: AuthenticationParameters): void {
this.authModule.createLoginUrl(request).then((urlNavigate) => {
this.authModule.logger.info(urlNavigate);
});
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.authCallback);
interactionHandler.showUI(request);
}

/**
Expand Down
22 changes: 19 additions & 3 deletions lib/msal-browser/src/error/BrowserAuthError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export const BrowserAuthErrorMessage = {
httpMethodNotImplementedError: {
code: "http_method_not_implemented",
desc: "The HTTP method given has not been implemented in this library."
},
emptyRedirectUriError: {
code: "empty_redirect_uri",
desc: "Redirect URI is empty. Please check stack trace for more info."
},
hashEmptyError: {
code: "hash_empty_error",
desc: "Hash value cannot be processed because it is empty."
}
};

Expand All @@ -42,18 +50,26 @@ export class BrowserAuthError extends AuthError {
return new BrowserAuthError(BrowserAuthErrorMessage.noWindowObjectError.code, BrowserAuthErrorMessage.noWindowObjectError.desc);
}

static createPkceNotGeneratedError(errDetail: string) {
static createPkceNotGeneratedError(errDetail: string): BrowserAuthError {
return new BrowserAuthError(BrowserAuthErrorMessage.pkceNotGenerated.code,
`${BrowserAuthErrorMessage.pkceNotGenerated.desc} Detail:${errDetail}`);
}

static createCryptoNotAvailableError(errDetail: string) {
static createCryptoNotAvailableError(errDetail: string): BrowserAuthError {
return new BrowserAuthError(BrowserAuthErrorMessage.cryptoDoesNotExist.code,
`${BrowserAuthErrorMessage.cryptoDoesNotExist.desc} Detail:${errDetail}`);
}

static createHttpMethodNotImplementedError(method: string) {
static createHttpMethodNotImplementedError(method: string): BrowserAuthError {
return new BrowserAuthError(BrowserAuthErrorMessage.httpMethodNotImplementedError.code,
`${BrowserAuthErrorMessage.httpMethodNotImplementedError.desc} Given Method: ${method}`);
}

static createEmptyRedirectUriError(): BrowserAuthError {
return new BrowserAuthError(BrowserAuthErrorMessage.emptyRedirectUriError.code, BrowserAuthErrorMessage.emptyRedirectUriError.desc);
}

static createEmptyHashError(hashValue: string): BrowserAuthError {
return new BrowserAuthError(BrowserAuthErrorMessage.hashEmptyError.code, `${BrowserAuthErrorMessage.hashEmptyError.desc} Given Url: ${hashValue}`);
}
}
21 changes: 20 additions & 1 deletion lib/msal-browser/src/error/BrowserConfigurationAuthError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@ export const BrowserConfigurationAuthErrorMessage = {
storageNotSupportedError: {
code: "storage_not_supported",
desc: "Given storage configuration option was not supported."
}
},
noRedirectCallbacksSet: {
code: "no_redirect_callbacks",
desc: "No redirect callbacks have been set. Please call setRedirectCallbacks() with the appropriate function arguments before continuing. " +
"More information is available here: https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/MSAL-basics."
},
invalidCallbackObject: {
code: "invalid_callback_object",
desc: "The object passed for the callback was invalid. " +
"More information is available here: https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/MSAL-basics."
},
};

/**
Expand All @@ -29,4 +39,13 @@ export class BrowserConfigurationAuthError extends AuthError {
static createStorageNotSupportedError(givenStorageLocation: string): BrowserConfigurationAuthError {
return new BrowserConfigurationAuthError(BrowserConfigurationAuthErrorMessage.storageNotSupportedError.code, `${BrowserConfigurationAuthErrorMessage.storageNotSupportedError.desc} Given Location: ${givenStorageLocation}`);
}

static createInvalidCallbackObjectError(callbackObject: object): BrowserConfigurationAuthError {
return new BrowserConfigurationAuthError(BrowserConfigurationAuthErrorMessage.invalidCallbackObject.code,
`${BrowserConfigurationAuthErrorMessage.invalidCallbackObject.desc} Given value for callback function: ${callbackObject}`);
}

static createRedirectCallbacksNotSetError(): BrowserConfigurationAuthError {
return new BrowserConfigurationAuthError(BrowserConfigurationAuthErrorMessage.noRedirectCallbacksSet.code, BrowserConfigurationAuthErrorMessage.noRedirectCallbacksSet.desc);
}
}
29 changes: 29 additions & 0 deletions lib/msal-browser/src/interaction_handler/IInteractionHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthorizationCodeModule, AuthenticationParameters } from "msal-common";
import { BrowserStorage } from "../cache/BrowserStorage";

export abstract class IInteractionHandler {
DarylThayil marked this conversation as resolved.
Show resolved Hide resolved

protected authModule: AuthorizationCodeModule;
protected browserStorage: BrowserStorage;

constructor(authCodeModule: AuthorizationCodeModule, storageImpl: BrowserStorage) {
this.authModule = authCodeModule;
this.browserStorage = storageImpl;
}

/**
* Function to enable user interaction.
* @param urlNavigate
*/
abstract showUI(authRequest: AuthenticationParameters): void;

/**
* Function to handle response parameters from hash.
* @param hash
*/
abstract async handleCodeResponse(hash: string): Promise<void>;
}
78 changes: 78 additions & 0 deletions lib/msal-browser/src/interaction_handler/RedirectHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { IInteractionHandler } from "./IInteractionHandler";
import { BrowserAuthError } from "../error/BrowserAuthError";
import { StringUtils, AuthorizationCodeModule, TemporaryCacheKeys, AuthenticationParameters, AuthError, TokenResponse } from "msal-common";
import { AuthCallback } from "../app/PublicClientApplication";
import { BrowserConfigurationAuthError } from "../error/BrowserConfigurationAuthError";
import { BrowserStorage } from "../cache/BrowserStorage";

export class RedirectHandler extends IInteractionHandler {

private authCallback: AuthCallback;

constructor(authCodeModule: AuthorizationCodeModule, storageImpl: BrowserStorage, redirectCallback: AuthCallback) {
super(authCodeModule, storageImpl);
if (!redirectCallback) {
throw BrowserConfigurationAuthError.createRedirectCallbacksNotSetError();
}
this.authCallback = redirectCallback;
}

/**
* Redirects window to given URL.
* @param urlNavigate
*/
showUI(authRequest: AuthenticationParameters): void {
this.browserStorage.setItem(TemporaryCacheKeys.ORIGIN_URI, window.location.href);
this.authModule.createLoginUrl(authRequest).then((urlNavigate) => {
// Navigate if valid URL
if (urlNavigate && !StringUtils.isEmpty(urlNavigate)) {
this.authModule.logger.infoPii("Navigate to:" + urlNavigate);
window.location.assign(urlNavigate);
}
else {
this.authModule.logger.info("Navigate url is empty");
throw BrowserAuthError.createEmptyRedirectUriError();
}
});
}

/**
* Handle authorization code response in the window.
* @param hash
*/
async handleCodeResponse(locationHash: string, navigateToLoginRequestUrl?: boolean): Promise<void> {
if (StringUtils.isEmpty(locationHash)) {
throw BrowserAuthError.createEmptyHashError(locationHash);
}

if (navigateToLoginRequestUrl) {
this.browserStorage.setItem(TemporaryCacheKeys.URL_HASH, locationHash);
if (window.parent === window) {
const loginRequestUrl = this.browserStorage.getItem(TemporaryCacheKeys.ORIGIN_URI);

// Redirect to home page if login request url is null (real null or the string null)
if (!loginRequestUrl || loginRequestUrl === "null") {
this.authModule.logger.error("Unable to get valid login request url from cache, redirecting to home page");
window.location.href = "/";
} else {
window.location.href = loginRequestUrl;
}
}
return;
} else {
window.location.hash = "";
}

try {
const codeResponse = this.authModule.handleFragmentResponse(locationHash);
const tokenResponse: TokenResponse = await this.authModule.acquireToken(null, codeResponse);
this.authCallback(null, tokenResponse);
} catch (err) {
this.authCallback(err as AuthError, null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export type AuthOptions = {
validateAuthority?: boolean;
redirectUri?: string | (() => string);
postLogoutRedirectUri?: string | (() => string);
navigateToLoginRequestUrl?: boolean;
};

/**
Expand All @@ -41,8 +40,7 @@ const DEFAULT_AUTH_OPTIONS: AuthOptions = {
authority: null,
validateAuthority: true,
redirectUri: "",
postLogoutRedirectUri: "",
navigateToLoginRequestUrl: true
postLogoutRedirectUri: ""
};

/**
Expand Down
10 changes: 0 additions & 10 deletions lib/msal-common/src/app/module/AuthModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import { ModuleConfiguration, buildModuleConfiguration } from "../config/ModuleConfiguration";
// request
import { AuthenticationParameters } from "../../request/AuthenticationParameters";
// response
import { AuthResponse } from "../../response/AuthResponse";
// cache
import { ICacheStorage } from "../../cache/ICacheStorage";
// network
Expand Down Expand Up @@ -90,14 +88,6 @@ export abstract class AuthModule {
abstract async createLoginUrl(request: AuthenticationParameters): Promise<string>;
abstract async createAcquireTokenUrl(request: AuthenticationParameters): Promise<string>;

// #endregion

// #region Response Handling

public handleFragmentResponse(hashFragment: string): AuthResponse {
return null;
}

// #endregion

// #region Getters and Setters
Expand Down