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
34 changes: 34 additions & 0 deletions lib/msal-common/src/auth/ClientInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 type ClientInfo = {
uid: string,
utid: string
};

/**
* Function to build a client info object
* @param rawClientInfo
* @param crypto
*/
export function buildClientInfo(rawClientInfo: string, crypto: ICrypto): ClientInfo {
if (!rawClientInfo || StringUtils.isEmpty(rawClientInfo)) {
throw ClientAuthError.createClientInfoEmptyError(rawClientInfo);
}

try {
const decodedClientInfo: string = crypto.base64Decode(rawClientInfo);
return JSON.parse(decodedClientInfo) as ClientInfo;
} 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);
}

/**
* Extract IdToken by decoding the RAWIdToken
*
* @param encodedIdToken
*/
static extractIdToken(encodedIdToken: string, crypto: ICrypto): IdTokenClaims {
// 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) as IdTokenClaims;
} 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
87 changes: 87 additions & 0 deletions lib/msal-common/src/auth/MsalAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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 { 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: IdTokenClaims;
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;
}
}