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: Authority and Protocol Classes #1133

Merged
2 changes: 0 additions & 2 deletions lib/msal-browser/src/app/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export type SystemOptions = {
*
*/
export type FrameworkOptions = {
isAngular?: boolean;
unprotectedResources?: Array<string>;
protectedResourceMap?: Map<string, Array<string>>;
};
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -107,7 +106,6 @@ const DEFAULT_SYSTEM_OPTIONS: SystemOptions = {
};

const DEFAULT_FRAMEWORK_OPTIONS: FrameworkOptions = {
isAngular: false,
unprotectedResources: new Array<string>(),
protectedResourceMap: new Map<string, Array<string>>()
};
Expand Down
2 changes: 1 addition & 1 deletion lib/msal-browser/src/utils/BrowserUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class BrowserUtils {
* Used to redirect the browser to the STS authorization endpoint
* @param {string} urlNavigate - URL of the authorization endpoint
*/
private navigateWindow(urlNavigate: string): void {
static navigateWindow(urlNavigate: string): void {
throw new Error("Method not implemented.");
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
}

Expand Down
7 changes: 1 addition & 6 deletions lib/msal-browser/test/app/Configuration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const TEST_FRAME_TIMEOUT = 3000;
const TEST_OFFSET = 100;
const TEST_NAVIGATE_FRAME_WAIT = 200;

describe("MsalPublicClientSPAConfiguration.ts Class Unit Tests", () => {
describe("Configuration.ts Class Unit Tests", () => {

it("buildConfiguration assigns default values", () => {
let emptyConfig: Configuration = buildConfiguration({auth: null});
Expand Down Expand Up @@ -58,8 +58,6 @@ describe("MsalPublicClientSPAConfiguration.ts Class Unit Tests", () => {
expect(emptyConfig.system.telemetry).to.be.undefined;
// Framework config checks
expect(emptyConfig.framework).to.be.not.null;
expect(emptyConfig.framework.isAngular).to.be.not.null;
expect(emptyConfig.framework.isAngular).to.be.false;
expect(emptyConfig.framework.unprotectedResources).to.be.not.null;
expect(emptyConfig.framework.unprotectedResources).to.be.empty;
expect(emptyConfig.framework.protectedResourceMap).to.be.not.null;
Expand Down Expand Up @@ -96,7 +94,6 @@ describe("MsalPublicClientSPAConfiguration.ts Class Unit Tests", () => {
}
},
framework: {
isAngular: true,
unprotectedResources: testUnprotectedResources,
protectedResourceMap: testProtectedResourceMap
}
Expand Down Expand Up @@ -129,8 +126,6 @@ describe("MsalPublicClientSPAConfiguration.ts Class Unit Tests", () => {
expect(newConfig.system.telemetry.applicationVersion).to.be.eq(testAppVersion);
// Framework config checks
expect(newConfig.framework).to.be.not.null;
expect(newConfig.framework.isAngular).to.be.not.null;
expect(newConfig.framework.isAngular).to.be.true;
expect(newConfig.framework.unprotectedResources).to.be.not.null;
expect(newConfig.framework.unprotectedResources).to.be.eq(testUnprotectedResources);
expect(newConfig.framework.protectedResourceMap).to.be.not.null;
Expand Down
6 changes: 6 additions & 0 deletions lib/msal-browser/test/utils/BrowserUtils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import { BrowserUtils } from "../../src/utils/BrowserUtils"
import { TEST_URIS } from "./StringConstants";

describe("BrowserUtils.ts Function Unit Tests", () => {

it("navigateWindow returns not implemented error", () => {
expect(() => BrowserUtils.navigateWindow("urlNavigate")).to.throw("Method not implemented.");
expect(() => BrowserUtils.navigateWindow("urlNavigate")).to.throw(Error);
});

it("getDefaultRedirectUri returns current location uri of browser", () => {
expect(BrowserUtils.getDefaultRedirectUri()).to.be.eq(TEST_URIS.TEST_REDIR_URI);
});

});
52 changes: 32 additions & 20 deletions lib/msal-common/src/app/config/MsalModuleConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { ICacheStorage } from "../../cache/ICacheStorage";
import { INetworkModule } from "../../network/INetworkModule";
import { ICrypto, PKCECodes } from "../../utils/crypto/ICrypto";
import { AuthError } from "../../error/AuthError";

/**
* Use the configuration object to configure MSAL Modules and initialize the base interfaces for MSAL.
Expand All @@ -22,49 +23,60 @@ export type MsalModuleConfiguration = {

const DEFAULT_STORAGE_OPTIONS: ICacheStorage = {
clear: () => {
console.warn("clear() has not been implemented for the cacheStorage interface.");
const notImplErr = "Storage interface - clear() has not been implemented for the cacheStorage interface.";
console.warn(notImplErr);
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
throw AuthError.createUnexpectedError(notImplErr);
},
containsKey: (key: string): boolean => {
console.warn("containsKey() has not been implemented for the cacheStorage interface.");
return false;
const notImplErr = "Storage interface - containsKey() has not been implemented for the cacheStorage interface.";
console.warn(notImplErr);
throw AuthError.createUnexpectedError(notImplErr);
},
getItem: (key: string): string => {
console.warn("getItem() has not been implemented for the cacheStorage interface.");
return "";
const notImplErr = "Storage interface - getItem() has not been implemented for the cacheStorage interface.";
console.warn(notImplErr);
throw AuthError.createUnexpectedError(notImplErr);
},
getKeys: (): string[] => {
console.warn("getKeys() has not been implemented for the cacheStorage interface.");
return null;
const notImplErr = "Storage interface - getKeys() has not been implemented for the cacheStorage interface.";
console.warn(notImplErr);
throw AuthError.createUnexpectedError(notImplErr);
},
removeItem: (key: string) => {
console.warn("removeItem() has not been implemented for the cacheStorage interface.");
return;
const notImplErr = "Storage interface - removeItem() has not been implemented for the cacheStorage interface.";
console.warn(notImplErr);
throw AuthError.createUnexpectedError(notImplErr);
},
setItem: (key: string, value: string) => {
console.warn("setItem() has not been implemented for the cacheStorage interface.");
return;
const notImplErr = "Storage interface - setItem() has not been implemented for the cacheStorage interface.";
console.warn(notImplErr);
throw AuthError.createUnexpectedError(notImplErr);
}
};

const DEFAULT_NETWORK_OPTIONS: INetworkModule = {
sendRequestAsync: async (url: string, method: RequestInit, enableCaching?: boolean): Promise<any> => {
console.warn("Network interface - sendRequestAsync() has not been implemented");
return null;
async sendRequestAsync(url: string, method: RequestInit, enableCaching?: boolean): Promise<any> {
const notImplErr = "Network interface - sendRequestAsync() has not been implemented";
console.warn(notImplErr);
throw AuthError.createUnexpectedError(notImplErr);
}
};

const DEFAULT_CRYPTO_IMPLEMENTATION: ICrypto = {
base64Decode: (input: string): string => {
console.warn("Crypto interface - base64Decode() has not been implemented");
return "";
const notImplErr = "Crypto interface - base64Decode() has not been implemented";
console.warn(notImplErr);
throw AuthError.createUnexpectedError(notImplErr);
},
base64Encode: (input: string): string => {
console.warn("Crypto interface - base64Encode() has not been implemented");
return "";
const notImplErr = "Crypto interface - base64Encode() has not been implemented";
console.warn(notImplErr);
throw AuthError.createUnexpectedError(notImplErr);
},
async generatePKCECodes(): Promise<PKCECodes> {
console.warn("Crypto interface - generatePKCECodes() has not been implemented");
return null;
const notImplErr = "Crypto interface - generatePKCECodes() has not been implemented";
console.warn(notImplErr);
throw AuthError.createUnexpectedError(notImplErr);
}
};

Expand Down
4 changes: 4 additions & 0 deletions lib/msal-common/src/app/module/AuthModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ICacheStorage } from "../../cache/ICacheStorage";
import { INetworkModule } from "../../network/INetworkModule";
// utils
import { ICrypto } from "../../utils/crypto/ICrypto";
import { MsalAccount } from "../../auth/MsalAccount";

/**
* @hidden
Expand Down Expand Up @@ -45,6 +46,9 @@ export abstract class AuthModule {
// Network Interface
protected networkClient: INetworkModule;

// Account object
protected account: MsalAccount;

constructor(configuration: MsalModuleConfiguration) {
// Set the configuration
this.config = buildMsalModuleConfiguration(configuration);
Expand Down
52 changes: 52 additions & 0 deletions lib/msal-common/src/auth/ClientInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { ClientAuthError } from "../error/ClientAuthError";
import { StringUtils } from "../utils/StringUtils";
import { ICrypto } from "../utils/crypto/ICrypto";

/**
* @hidden
*/
export class ClientInfo {

private _uid: string;
get uid(): string {
return this._uid;
}

set uid(uid: string) {
this._uid = uid;
}

private _utid: string;
get utid(): string {
return this._utid;
}

set utid(utid: string) {
this._utid = utid;
}

constructor(rawClientInfo: string, crypto: ICrypto) {
if (!rawClientInfo || StringUtils.isEmpty(rawClientInfo)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there times there are empty client info and we want to construct the class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there are cases in B2C where we may not receive the client info object, but I will double check to verify. It would probably be a better pattern to throw a warning and not create the object at all, so I will make some changes to reflect that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, please re-review

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some cases of device flow had this issue but eSTS guarantees client_info and if it is empty, we need to raise a bug against them. However it may be necessary to confirm the expected behavior from msal js in case it returns empty.

throw ClientAuthError.createClientInfoEmptyError(rawClientInfo);
}

try {
const decodedClientInfo: string = crypto.base64Decode(rawClientInfo);
const clientInfo = JSON.parse(decodedClientInfo);
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
if (clientInfo.hasOwnProperty("uid")) {
this.uid = clientInfo.uid;
}

if (clientInfo.hasOwnProperty("utid")) {
this.utid = clientInfo.utid;
}
} catch (e) {
throw ClientAuthError.createClientInfoDecodingError(e);
}
}
}
47 changes: 47 additions & 0 deletions lib/msal-common/src/auth/IdToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { ClientAuthError } from "../error/ClientAuthError";
import { StringUtils } from "../utils/StringUtils";
import { ICrypto } from "../utils/crypto/ICrypto";
import { IdTokenClaims } from "./IdTokenClaims";

/**
* @hidden
*/
export class IdToken {

rawIdToken: string;
claims: IdTokenClaims;
constructor(rawIdToken: string, crypto: ICrypto) {
if (StringUtils.isEmpty(rawIdToken)) {
throw ClientAuthError.createIdTokenNullOrEmptyError(rawIdToken);
}

this.rawIdToken = rawIdToken;
this.claims = IdToken.extractIdToken(rawIdToken, crypto) as IdTokenClaims;
}

/**
* Extract IdToken by decoding the RAWIdToken
*
* @param encodedIdToken
*/
static extractIdToken(encodedIdToken: string, crypto: ICrypto): any {
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
// id token will be decoded to get the username
const decodedToken = StringUtils.decodeJwt(encodedIdToken);
if (!decodedToken) {
return null;
}
try {
const base64IdToken = decodedToken.JWSPayload;
// base64Decode() should throw an error if there is an issue
const base64Decoded = crypto.base64Decode(base64IdToken);
return JSON.parse(base64Decoded);
} catch (err) {
throw ClientAuthError.createIdTokenParsingError(err);
}
}
}
19 changes: 19 additions & 0 deletions lib/msal-common/src/auth/IdTokenClaims.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

export type IdTokenClaims = {
iss: string,
oid: string,
sub: string,
tid: string,
ver: string,
preferred_username: string,
name: string,
nonce: string,
exp: string,
home_oid: string,
sid: string,
cloud_instance_host_name: string
};
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
88 changes: 88 additions & 0 deletions lib/msal-common/src/auth/MsalAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { ClientInfo } from "./ClientInfo";
import { IdToken } from "./IdToken";
import { StringUtils } from "../utils/StringUtils";
import { StringDict } from "../utils/MsalTypes";
import { ICrypto } from "../utils/crypto/ICrypto";
import { IdTokenClaims } from "./IdTokenClaims";

/**
* accountIdentifier combination of idToken.uid and idToken.utid
* homeAccountIdentifier combination of clientInfo.uid and clientInfo.utid
* userName idToken.preferred_username
* name idToken.name
* idToken idToken
* sid idToken.sid - session identifier
* environment idtoken.issuer (the authority that issues the token)
*/
export class MsalAccount {
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved

accountIdentifier: string;
homeAccountIdentifier: string;
userName: string;
name: string;
idToken: string;
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
idTokenClaims: StringDict;
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
sid: string;
environment: string;

/**
* Creates an Account Object
* @praram accountIdentifier
* @param homeAccountIdentifier
* @param userName
* @param name
* @param idToken
* @param sid
* @param environment
*/
constructor(accountIdentifier: string, homeAccountIdentifier: string, idTokenClaims: IdTokenClaims, rawIdToken: string) {
this.accountIdentifier = accountIdentifier;
this.homeAccountIdentifier = homeAccountIdentifier;
this.userName = idTokenClaims.preferred_username;
this.name = idTokenClaims.name;
// will be deprecated soon
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
this.idToken = rawIdToken;
this.idTokenClaims = idTokenClaims;
this.sid = idTokenClaims.sid;
this.environment = idTokenClaims.iss;
}

/**
* @hidden
* @param idToken
* @param clientInfo
*/
static createAccount(idToken: IdToken, clientInfo: ClientInfo, crypto: ICrypto): MsalAccount {

// create accountIdentifier
const accountIdentifier: string = idToken.claims.oid || idToken.claims.sub;

// create homeAccountIdentifier
const uid: string = clientInfo ? clientInfo.uid : "";
const utid: string = clientInfo ? clientInfo.utid : "";

let homeAccountIdentifier: string;
if (!StringUtils.isEmpty(uid) && !StringUtils.isEmpty(utid)) {
homeAccountIdentifier = crypto.base64Encode(uid) + "." + crypto.base64Encode(utid);
}
return new MsalAccount(accountIdentifier, homeAccountIdentifier, idToken.claims, idToken.rawIdToken);
}

/**
* Utils function to compare two Account objects - used to check if the same user account is logged in
*
* @param a1: Account object
* @param a2: Account object
*/
static compareAccounts(a1: MsalAccount, a2: MsalAccount): boolean {
if (!(a1 && a1.homeAccountIdentifier) || !(a2 && a2.homeAccountIdentifier)) {
return false;
}
return a1.homeAccountIdentifier === a2.homeAccountIdentifier;
}
}