Skip to content

B2C Authority fixes #1276

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

Merged
merged 25 commits into from
Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
775d59a
B2C Authority fixes
tnorling Feb 11, 2020
e6e44ca
Implement known authorities for B2C
tnorling Feb 20, 2020
8bef92f
Update setKnownAuthorities to use params
tnorling Feb 20, 2020
4fd2c47
Include optional authorityData configuration type
tnorling Feb 20, 2020
cf918c3
Update Errors
tnorling Feb 25, 2020
02f9528
Authority Unit Tests
tnorling Feb 25, 2020
1798a83
Update TestConstants
tnorling Feb 26, 2020
b9c7680
Update AAD and B2C tests
tnorling Feb 26, 2020
3b59547
Merge branch 'known-authorities' of https://github.com/AzureAD/micros…
tnorling Feb 26, 2020
0827b81
Removed authority type parameter
tnorling Mar 2, 2020
a78b15b
Update return to ternary
tnorling Mar 2, 2020
07e6dd2
Add authorityType parameter
tnorling Mar 20, 2020
92f2067
Update string replace
tnorling Mar 21, 2020
0a96df5
More unit tests
tnorling Mar 21, 2020
da3cf68
Fix unit test
tnorling Mar 21, 2020
bc90b09
Remove authorityType
tnorling Mar 25, 2020
352e3f1
Merge branch 'dev' into known-authorities
tnorling Mar 25, 2020
95c5fcb
Move B2CTrustedHostList and setKnownAuthorities
tnorling Mar 26, 2020
f8de5a2
Merge branch 'dev' into known-authorities
tnorling Mar 26, 2020
15d65d9
Update function description
tnorling Mar 26, 2020
8f5d27b
Merge branch 'known-authorities' of https://github.com/AzureAD/micros…
tnorling Mar 26, 2020
ee97b68
Update to untrustedAuthority error
tnorling Apr 7, 2020
21324e5
Merge branch 'dev' into known-authorities
tnorling Apr 7, 2020
3c3e5c7
Add unit test
tnorling Apr 7, 2020
10779ca
Merge branch 'known-authorities' of https://github.com/AzureAD/micros…
tnorling Apr 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/msal-core/src/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const NAVIGATE_FRAME_WAIT = 500;
* - clientId - Client ID of your app registered with our Application registration portal : https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview in Microsoft Identity Platform
* - authority - You can configure a specific authority, defaults to " " or "https://login.microsoftonline.com/common"
* - validateAuthority - Used to turn authority validation on/off. When set to true (default), MSAL will compare the application's authority against well-known URLs templates representing well-formed authorities. It is useful when the authority is obtained at run time to prevent MSAL from displaying authentication prompts from malicious pages.
* - knownAuthorities - If validateAuthority is set to True, this will be used to set the Trusted Host list. Defaults to empty array
* - redirectUri - The redirect URI of the application, this should be same as the value in the application registration portal.Defaults to `window.location.href`.
* - postLogoutRedirectUri - Used to redirect the user to this location after logout. Defaults to `window.location.href`.
* - navigateToLoginRequestUrl - Used to turn off default navigation to start page after login. Default is true. This is used only for redirect flows.
Expand All @@ -36,6 +37,7 @@ export type AuthOptions = {
clientId: string;
authority?: string;
validateAuthority?: boolean;
knownAuthorities?: Array<string>;
redirectUri?: string | (() => string);
postLogoutRedirectUri?: string | (() => string);
navigateToLoginRequestUrl?: boolean;
Expand Down Expand Up @@ -115,6 +117,7 @@ const DEFAULT_AUTH_OPTIONS: AuthOptions = {
clientId: "",
authority: null,
validateAuthority: true,
knownAuthorities: [],
redirectUri: () => UrlUtils.getCurrentUrl(),
postLogoutRedirectUri: () => UrlUtils.getCurrentUrl(),
navigateToLoginRequestUrl: true
Expand Down Expand Up @@ -145,6 +148,7 @@ const DEFAULT_FRAMEWORK_OPTIONS: FrameworkOptions = {
* @param TCacheOptions
* @param TSystemOptions
* @param TFrameworkOptions
* @param TAuthorityDataOptions
*
* @returns TConfiguration object
*/
Expand Down
4 changes: 3 additions & 1 deletion lib/msal-core/src/UserAgentApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { Constants,
libraryVersion,
TemporaryCacheKeys,
PersistentCacheKeys,
ErrorCacheKeys,
ErrorCacheKeys
} from "./utils/Constants";
import { CryptoUtils } from "./utils/CryptoUtils";

Expand Down Expand Up @@ -217,6 +217,8 @@ export class UserAgentApplication {

this.telemetryManager = this.getTelemetryManagerFromConfig(this.config.system.telemetry, this.clientId);

AuthorityFactory.setKnownAuthorities(this.config.auth.validateAuthority, this.config.auth.knownAuthorities);

// if no authority is passed, set the default: "https://login.microsoftonline.com/common"
this.authority = this.config.auth.authority || DEFAULT_AUTHORITY;

Expand Down
6 changes: 3 additions & 3 deletions lib/msal-core/src/authority/Authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@ export abstract class Authority {

public get AuthorizationEndpoint(): string {
this.validateResolved();
return this.tenantDiscoveryResponse.AuthorizationEndpoint.replace("{tenant}", this.Tenant);
return this.tenantDiscoveryResponse.AuthorizationEndpoint.replace(/{tenant}|{tenantid}/g, this.Tenant);
}

public get EndSessionEndpoint(): string {
this.validateResolved();
return this.tenantDiscoveryResponse.EndSessionEndpoint.replace("{tenant}", this.Tenant);
return this.tenantDiscoveryResponse.EndSessionEndpoint.replace(/{tenant}|{tenantid}/g, this.Tenant);
}

public get SelfSignedJwtAudience(): string {
this.validateResolved();
return this.tenantDiscoveryResponse.Issuer.replace("{tenant}", this.Tenant);
return this.tenantDiscoveryResponse.Issuer.replace(/{tenant}|{tenantid}/g, this.Tenant);
}

private validateResolved() {
Expand Down
39 changes: 28 additions & 11 deletions lib/msal-core/src/authority/AuthorityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,42 @@
import { AadAuthority } from "./AadAuthority";
import { B2cAuthority } from "./B2cAuthority";
Copy link
Contributor

Choose a reason for hiding this comment

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

Other end of the circle

Copy link

Choose a reason for hiding this comment

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

Per @tnorling this circular dep was resolved in #1479

import { Authority, AuthorityType } from "./Authority";
import { ClientConfigurationErrorMessage } from "../error/ClientConfigurationError";
import { UrlUtils } from "../utils/UrlUtils";
import { StringUtils } from "../utils/StringUtils";
import { UrlUtils } from "../utils/UrlUtils";
import { ClientConfigurationError } from "../error/ClientConfigurationError";

export const B2CTrustedHostList: object = {};

export class AuthorityFactory {
/**
* Use when Authority is B2C and validateAuthority is set to True to provide list of allowed domains.
*/
public static setKnownAuthorities(validateAuthority: boolean, knownAuthorities: Array<string>): void {
if (validateAuthority && !Object.keys(B2CTrustedHostList).length){
knownAuthorities.forEach(function(authority){
B2CTrustedHostList[authority] = authority;
});
}
}

/**
* Parse the url and determine the type of authority
*/
private static DetectAuthorityFromUrl(authorityUrl: string): AuthorityType {
private static detectAuthorityFromUrl(authorityUrl: string): AuthorityType {
authorityUrl = UrlUtils.CanonicalizeUri(authorityUrl);
const components = UrlUtils.GetUrlComponents(authorityUrl);
const pathSegments = components.PathSegments;
switch (pathSegments[0]) {
case "tfp":
return AuthorityType.B2C;
default:
return AuthorityType.Aad;

if (pathSegments[0] === "adfs") {
return AuthorityType.Adfs;
}
}
else if (Object.keys(B2CTrustedHostList).length) {
return AuthorityType.B2C;
}

// Defaults to Aad
return AuthorityType.Aad;
}

/**
* Create an authority object of the correct type based on the url
Expand All @@ -37,15 +54,15 @@ export class AuthorityFactory {
if (StringUtils.isEmpty(authorityUrl)) {
return null;
}
const type = AuthorityFactory.DetectAuthorityFromUrl(authorityUrl);
const type = AuthorityFactory.detectAuthorityFromUrl(authorityUrl);
// Depending on above detection, create the right type.
switch (type) {
case AuthorityType.B2C:
return new B2cAuthority(authorityUrl, validateAuthority);
case AuthorityType.Aad:
return new AadAuthority(authorityUrl, validateAuthority);
default:
throw ClientConfigurationErrorMessage.invalidAuthorityType;
throw ClientConfigurationError.createInvalidAuthorityTypeError();
}
}

Expand Down
30 changes: 17 additions & 13 deletions lib/msal-core/src/authority/B2cAuthority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,18 @@
* Licensed under the MIT License.
*/

import { AadAuthority } from "./AadAuthority";
import { Authority } from "./Authority";
import { AuthorityType } from "./Authority";
import { ClientConfigurationErrorMessage } from "../error/ClientConfigurationError";
import { UrlUtils } from "../utils/UrlUtils";
import { ClientConfigurationError } from "../error/ClientConfigurationError";
import { AuthorityFactory, B2CTrustedHostList } from "./AuthorityFactory";
Copy link
Contributor

Choose a reason for hiding this comment

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

This creates a circular dependency :( sorry didn't catch this before.


/**
* @hidden
*/
export class B2cAuthority extends AadAuthority {
export class B2cAuthority extends Authority {
public static B2C_PREFIX: String = "tfp";
public constructor(authority: string, validateAuthority: boolean) {
super(authority, validateAuthority);
const urlComponents = UrlUtils.GetUrlComponents(authority);

const pathSegments = urlComponents.PathSegments;
if (pathSegments.length < 3) {
throw ClientConfigurationErrorMessage.b2cAuthorityUriInvalidPath;
}

this.CanonicalAuthority = `https://${urlComponents.HostNameAndPort}/${pathSegments[0]}/${pathSegments[1]}/${pathSegments[2]}/`;
}

public get AuthorityType(): AuthorityType {
Expand All @@ -37,6 +29,18 @@ export class B2cAuthority extends AadAuthority {
return this.DefaultOpenIdConfigurationEndpoint;
}

throw ClientConfigurationErrorMessage.unsupportedAuthorityValidation;
throw ClientConfigurationError.createUntrustedAuthorityError();
}

/**
* Checks to see if the host is in a list of trusted hosts
* @param {string} The host to look up
*/
public IsInTrustedHostList(host: string): boolean {
if (this.IsValidationEnabled && !Object.keys(B2CTrustedHostList).length) {
throw ClientConfigurationError.createKnownAuthoritiesNotSetError();
}

return B2CTrustedHostList[host.toLowerCase()];
}
}
23 changes: 23 additions & 0 deletions lib/msal-core/src/error/ClientConfigurationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,18 @@ export const ClientConfigurationErrorMessage = {
code: "unsupported_authority_validation",
desc: "The authority validation is not supported for this authority type."
},
untrustedAuthority: {
code: "untrusted_authority",
desc: "The provided authority is not a trusted authority. If using B2C, please include this authority in the knownAuthorities config parameter."
},
b2cAuthorityUriInvalidPath: {
code: "b2c_authority_uri_invalid_path",
desc: "The given URI for the B2C authority is invalid."
},
b2cKnownAuthoritiesNotSet: {
code: "b2c_known_authorities_not_set",
desc: "Must set known authorities when validateAuthority is set to True and using B2C"
},
claimsRequestParsingError: {
code: "claims_request_parsing_error",
desc: "Could not parse the given claims request object."
Expand Down Expand Up @@ -154,6 +162,21 @@ export class ClientConfigurationError extends ClientAuthError {
ClientConfigurationErrorMessage.invalidCorrelationIdError.desc);
}

static createKnownAuthoritiesNotSetError(): ClientConfigurationError {
return new ClientConfigurationError(ClientConfigurationErrorMessage.b2cKnownAuthoritiesNotSet.code,
ClientConfigurationErrorMessage.b2cKnownAuthoritiesNotSet.desc);
}

static createInvalidAuthorityTypeError(): ClientConfigurationError {
return new ClientConfigurationError(ClientConfigurationErrorMessage.invalidAuthorityType.code,
ClientConfigurationErrorMessage.invalidAuthorityType.desc);
}

static createUntrustedAuthorityError(): ClientConfigurationError {
return new ClientConfigurationError(ClientConfigurationErrorMessage.untrustedAuthority.code,
ClientConfigurationErrorMessage.untrustedAuthority.desc);
}

static createTelemetryConfigError(config: TelemetryOptions): ClientConfigurationError {
const { code, desc } = ClientConfigurationErrorMessage.telemetryConfigError;
const requiredKeys = {
Expand Down
8 changes: 7 additions & 1 deletion lib/msal-core/test/TestConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,15 @@ export const TEST_CONFIG = {
CorrelationId: "ed2a3125-a597-416f-9d12-68b9add1c268"
};

export const B2C_TEST_CONFIG = {
validAuthority: "https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/b2c_1_susi",
MSAL_CLIENT_ID: "e760cab2-b9a1-4c0d-86fb-ff7084abd902",
knownAuthorities: ["fabrikamb2c.b2clogin.com"]
};

export const TEST_RESPONSE_TYPE = {
id_token: "id_token",
token: "token",
id_token_token: "id_token token"
}
};

2 changes: 1 addition & 1 deletion lib/msal-core/test/UserAgentApplication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1721,7 +1721,7 @@ describe("UserAgentApplication.ts Class", function () {
};
});
});

describe('Logger', () => {
it('getLogger and setLogger', done => {
const config: Configuration = {
Expand Down
40 changes: 40 additions & 0 deletions lib/msal-core/test/authority/AadAuthority.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect } from "chai";
import { AuthorityType } from "../../src/authority/Authority";
import { AadAuthority } from "../../src/authority/AadAuthority";
import { AADTrustedHostList } from "../../src/utils/Constants";
import { TEST_CONFIG } from "../TestConstants";

describe("AadAuthority.ts Class", function () {

it("tests initialization of aad authority", function() {
const authority = new AadAuthority(TEST_CONFIG.validAuthority, false);

expect(authority).to.be.instanceOf(AadAuthority);
expect(authority.AuthorityType).to.be.equal(AuthorityType.Aad);
});

it("tests GetOpenIdConfigurationEndpointAsync with validateAuthority false", async function () {
const authority = new AadAuthority(TEST_CONFIG.validAuthority, false);
const endpoint = await authority.GetOpenIdConfigurationEndpointAsync();

expect(endpoint).to.include("/v2.0/.well-known/openid-configuration");
});

it("tests GetOpenIdConfigurationEndpointAsync with validateAuthority true", async function () {
const authority = new AadAuthority(TEST_CONFIG.validAuthority, true);
const endpoint = await authority.GetOpenIdConfigurationEndpointAsync();

expect(endpoint).to.include("/v2.0/.well-known/openid-configuration");
});

it("tests GetOpenIdConfigurationEndpointAsync with validateAuthority true and not in trusted host list", async function () {

delete AADTrustedHostList["login.microsoftonline.com"];
const authority = new AadAuthority(TEST_CONFIG.validAuthority, true);
const endpoint = await authority.GetOpenIdConfigurationEndpointAsync();
AADTrustedHostList["login.microsoftonline.com"] = "login.microsoftonline.com";

expect(endpoint).to.include("/v2.0/.well-known/openid-configuration");
});

});
90 changes: 90 additions & 0 deletions lib/msal-core/test/authority/Authority.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { expect } from "chai";
import { Authority, AuthorityType } from "../../src/authority/Authority";
import { ClientConfigurationErrorMessage } from "../../src/error/ClientConfigurationError"
import { TEST_CONFIG } from "../TestConstants";

class testAuthority extends Authority{
public constructor(authority: string, validateAuthority: boolean) {
super(authority, validateAuthority);
}

public get AuthorityType(): AuthorityType {
return AuthorityType.Aad;
}

public async GetOpenIdConfigurationEndpointAsync(): Promise<string> {
return this.DefaultOpenIdConfigurationEndpoint;
}
}

let authority: testAuthority;

describe("Authority.ts Class", function () {
beforeEach(function() {
authority = new testAuthority(TEST_CONFIG.validAuthority, false);
});

afterEach(function () {
authority = null;
});

it("tests initialization of Authority", function() {

expect(authority).to.be.instanceOf(testAuthority);
expect(authority.AuthorityType).to.be.equal(AuthorityType.Aad);
});

it("throws error if ResolveEndpointsAsync hasn't been called yet", function () {
try {
const authEndpoint = authority.AuthorizationEndpoint
}
catch(e) {
expect(e).to.be.equal("Please call ResolveEndpointsAsync first");
}
});

it("tests EndSessionEndpoint", async function () {
const response = await authority.resolveEndpointsAsync();
const endSessionEndpoint = response.EndSessionEndpoint

expect(response).to.be.instanceOf(testAuthority);
expect(endSessionEndpoint).to.contain(response.Tenant)
});

it("tests SelfSignedJwtAudience", async function () {
const response = await authority.resolveEndpointsAsync();
const selfSignedJwtAudience = response.SelfSignedJwtAudience

expect(response).to.be.instanceOf(testAuthority);
expect(selfSignedJwtAudience).to.contain(response.Tenant)
});

it("throws invalidAuthorityType on init if authority is not url", function () {
try {
authority = new testAuthority("", false);
}
catch(e) {
expect(e).to.be.equal(ClientConfigurationErrorMessage.invalidAuthorityType)
}
});

it("throws authorityUriInsecure on init if not https", function () {
try {
authority = new testAuthority("http://login.microsoftonline.com/common", false);
}
catch(e) {
expect(e).to.be.equal(ClientConfigurationErrorMessage.authorityUriInsecure)
}
});

it("throws authorityUriInvalidPath on init if there is no path", function () {
try {
authority = new testAuthority("https://login.microsoftonline.com", false);
}
catch(e) {
expect(e).to.be.equal(ClientConfigurationErrorMessage.authorityUriInvalidPath)
}
});


});
Loading