Skip to content

Commit 39b9592

Browse files
authored
Merge pull request #1276 from AzureAD/known-authorities
B2C Authority fixes
2 parents 153d810 + 10779ca commit 39b9592

12 files changed

+359
-30
lines changed

lib/msal-core/src/Configuration.ts

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const NAVIGATE_FRAME_WAIT = 500;
2727
* - 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
2828
* - authority - You can configure a specific authority, defaults to " " or "https://login.microsoftonline.com/common"
2929
* - 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.
30+
* - knownAuthorities - If validateAuthority is set to True, this will be used to set the Trusted Host list. Defaults to empty array
3031
* - redirectUri - The redirect URI of the application, this should be same as the value in the application registration portal.Defaults to `window.location.href`.
3132
* - postLogoutRedirectUri - Used to redirect the user to this location after logout. Defaults to `window.location.href`.
3233
* - navigateToLoginRequestUrl - Used to turn off default navigation to start page after login. Default is true. This is used only for redirect flows.
@@ -36,6 +37,7 @@ export type AuthOptions = {
3637
clientId: string;
3738
authority?: string;
3839
validateAuthority?: boolean;
40+
knownAuthorities?: Array<string>;
3941
redirectUri?: string | (() => string);
4042
postLogoutRedirectUri?: string | (() => string);
4143
navigateToLoginRequestUrl?: boolean;
@@ -115,6 +117,7 @@ const DEFAULT_AUTH_OPTIONS: AuthOptions = {
115117
clientId: "",
116118
authority: null,
117119
validateAuthority: true,
120+
knownAuthorities: [],
118121
redirectUri: () => UrlUtils.getCurrentUrl(),
119122
postLogoutRedirectUri: () => UrlUtils.getCurrentUrl(),
120123
navigateToLoginRequestUrl: true
@@ -145,6 +148,7 @@ const DEFAULT_FRAMEWORK_OPTIONS: FrameworkOptions = {
145148
* @param TCacheOptions
146149
* @param TSystemOptions
147150
* @param TFrameworkOptions
151+
* @param TAuthorityDataOptions
148152
*
149153
* @returns TConfiguration object
150154
*/

lib/msal-core/src/UserAgentApplication.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { Constants,
3939
libraryVersion,
4040
TemporaryCacheKeys,
4141
PersistentCacheKeys,
42-
ErrorCacheKeys,
42+
ErrorCacheKeys
4343
} from "./utils/Constants";
4444
import { CryptoUtils } from "./utils/CryptoUtils";
4545

@@ -217,6 +217,8 @@ export class UserAgentApplication {
217217

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

220+
AuthorityFactory.setKnownAuthorities(this.config.auth.validateAuthority, this.config.auth.knownAuthorities);
221+
220222
// if no authority is passed, set the default: "https://login.microsoftonline.com/common"
221223
this.authority = this.config.auth.authority || DEFAULT_AUTHORITY;
222224

lib/msal-core/src/authority/Authority.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,17 @@ export abstract class Authority {
4141

4242
public get AuthorizationEndpoint(): string {
4343
this.validateResolved();
44-
return this.tenantDiscoveryResponse.AuthorizationEndpoint.replace("{tenant}", this.Tenant);
44+
return this.tenantDiscoveryResponse.AuthorizationEndpoint.replace(/{tenant}|{tenantid}/g, this.Tenant);
4545
}
4646

4747
public get EndSessionEndpoint(): string {
4848
this.validateResolved();
49-
return this.tenantDiscoveryResponse.EndSessionEndpoint.replace("{tenant}", this.Tenant);
49+
return this.tenantDiscoveryResponse.EndSessionEndpoint.replace(/{tenant}|{tenantid}/g, this.Tenant);
5050
}
5151

5252
public get SelfSignedJwtAudience(): string {
5353
this.validateResolved();
54-
return this.tenantDiscoveryResponse.Issuer.replace("{tenant}", this.Tenant);
54+
return this.tenantDiscoveryResponse.Issuer.replace(/{tenant}|{tenantid}/g, this.Tenant);
5555
}
5656

5757
private validateResolved() {

lib/msal-core/src/authority/AuthorityFactory.ts

+28-11
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,42 @@
99
import { AadAuthority } from "./AadAuthority";
1010
import { B2cAuthority } from "./B2cAuthority";
1111
import { Authority, AuthorityType } from "./Authority";
12-
import { ClientConfigurationErrorMessage } from "../error/ClientConfigurationError";
13-
import { UrlUtils } from "../utils/UrlUtils";
1412
import { StringUtils } from "../utils/StringUtils";
13+
import { UrlUtils } from "../utils/UrlUtils";
14+
import { ClientConfigurationError } from "../error/ClientConfigurationError";
15+
16+
export const B2CTrustedHostList: object = {};
1517

1618
export class AuthorityFactory {
19+
/**
20+
* Use when Authority is B2C and validateAuthority is set to True to provide list of allowed domains.
21+
*/
22+
public static setKnownAuthorities(validateAuthority: boolean, knownAuthorities: Array<string>): void {
23+
if (validateAuthority && !Object.keys(B2CTrustedHostList).length){
24+
knownAuthorities.forEach(function(authority){
25+
B2CTrustedHostList[authority] = authority;
26+
});
27+
}
28+
}
29+
1730
/**
1831
* Parse the url and determine the type of authority
1932
*/
20-
private static DetectAuthorityFromUrl(authorityUrl: string): AuthorityType {
33+
private static detectAuthorityFromUrl(authorityUrl: string): AuthorityType {
2134
authorityUrl = UrlUtils.CanonicalizeUri(authorityUrl);
2235
const components = UrlUtils.GetUrlComponents(authorityUrl);
2336
const pathSegments = components.PathSegments;
24-
switch (pathSegments[0]) {
25-
case "tfp":
26-
return AuthorityType.B2C;
27-
default:
28-
return AuthorityType.Aad;
37+
38+
if (pathSegments[0] === "adfs") {
39+
return AuthorityType.Adfs;
2940
}
30-
}
41+
else if (Object.keys(B2CTrustedHostList).length) {
42+
return AuthorityType.B2C;
43+
}
44+
45+
// Defaults to Aad
46+
return AuthorityType.Aad;
47+
}
3148

3249
/**
3350
* Create an authority object of the correct type based on the url
@@ -37,15 +54,15 @@ export class AuthorityFactory {
3754
if (StringUtils.isEmpty(authorityUrl)) {
3855
return null;
3956
}
40-
const type = AuthorityFactory.DetectAuthorityFromUrl(authorityUrl);
57+
const type = AuthorityFactory.detectAuthorityFromUrl(authorityUrl);
4158
// Depending on above detection, create the right type.
4259
switch (type) {
4360
case AuthorityType.B2C:
4461
return new B2cAuthority(authorityUrl, validateAuthority);
4562
case AuthorityType.Aad:
4663
return new AadAuthority(authorityUrl, validateAuthority);
4764
default:
48-
throw ClientConfigurationErrorMessage.invalidAuthorityType;
65+
throw ClientConfigurationError.createInvalidAuthorityTypeError();
4966
}
5067
}
5168

lib/msal-core/src/authority/B2cAuthority.ts

+17-13
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,18 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import { AadAuthority } from "./AadAuthority";
6+
import { Authority } from "./Authority";
77
import { AuthorityType } from "./Authority";
8-
import { ClientConfigurationErrorMessage } from "../error/ClientConfigurationError";
9-
import { UrlUtils } from "../utils/UrlUtils";
8+
import { ClientConfigurationError } from "../error/ClientConfigurationError";
9+
import { AuthorityFactory, B2CTrustedHostList } from "./AuthorityFactory";
1010

1111
/**
1212
* @hidden
1313
*/
14-
export class B2cAuthority extends AadAuthority {
14+
export class B2cAuthority extends Authority {
1515
public static B2C_PREFIX: String = "tfp";
1616
public constructor(authority: string, validateAuthority: boolean) {
1717
super(authority, validateAuthority);
18-
const urlComponents = UrlUtils.GetUrlComponents(authority);
19-
20-
const pathSegments = urlComponents.PathSegments;
21-
if (pathSegments.length < 3) {
22-
throw ClientConfigurationErrorMessage.b2cAuthorityUriInvalidPath;
23-
}
24-
25-
this.CanonicalAuthority = `https://${urlComponents.HostNameAndPort}/${pathSegments[0]}/${pathSegments[1]}/${pathSegments[2]}/`;
2618
}
2719

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

40-
throw ClientConfigurationErrorMessage.unsupportedAuthorityValidation;
32+
throw ClientConfigurationError.createUntrustedAuthorityError();
33+
}
34+
35+
/**
36+
* Checks to see if the host is in a list of trusted hosts
37+
* @param {string} The host to look up
38+
*/
39+
public IsInTrustedHostList(host: string): boolean {
40+
if (this.IsValidationEnabled && !Object.keys(B2CTrustedHostList).length) {
41+
throw ClientConfigurationError.createKnownAuthoritiesNotSetError();
42+
}
43+
44+
return B2CTrustedHostList[host.toLowerCase()];
4145
}
4246
}

lib/msal-core/src/error/ClientConfigurationError.ts

+23
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,18 @@ export const ClientConfigurationErrorMessage = {
6262
code: "unsupported_authority_validation",
6363
desc: "The authority validation is not supported for this authority type."
6464
},
65+
untrustedAuthority: {
66+
code: "untrusted_authority",
67+
desc: "The provided authority is not a trusted authority. If using B2C, please include this authority in the knownAuthorities config parameter."
68+
},
6569
b2cAuthorityUriInvalidPath: {
6670
code: "b2c_authority_uri_invalid_path",
6771
desc: "The given URI for the B2C authority is invalid."
6872
},
73+
b2cKnownAuthoritiesNotSet: {
74+
code: "b2c_known_authorities_not_set",
75+
desc: "Must set known authorities when validateAuthority is set to True and using B2C"
76+
},
6977
claimsRequestParsingError: {
7078
code: "claims_request_parsing_error",
7179
desc: "Could not parse the given claims request object."
@@ -154,6 +162,21 @@ export class ClientConfigurationError extends ClientAuthError {
154162
ClientConfigurationErrorMessage.invalidCorrelationIdError.desc);
155163
}
156164

165+
static createKnownAuthoritiesNotSetError(): ClientConfigurationError {
166+
return new ClientConfigurationError(ClientConfigurationErrorMessage.b2cKnownAuthoritiesNotSet.code,
167+
ClientConfigurationErrorMessage.b2cKnownAuthoritiesNotSet.desc);
168+
}
169+
170+
static createInvalidAuthorityTypeError(): ClientConfigurationError {
171+
return new ClientConfigurationError(ClientConfigurationErrorMessage.invalidAuthorityType.code,
172+
ClientConfigurationErrorMessage.invalidAuthorityType.desc);
173+
}
174+
175+
static createUntrustedAuthorityError(): ClientConfigurationError {
176+
return new ClientConfigurationError(ClientConfigurationErrorMessage.untrustedAuthority.code,
177+
ClientConfigurationErrorMessage.untrustedAuthority.desc);
178+
}
179+
157180
static createTelemetryConfigError(config: TelemetryOptions): ClientConfigurationError {
158181
const { code, desc } = ClientConfigurationErrorMessage.telemetryConfigError;
159182
const requiredKeys = {

lib/msal-core/test/TestConstants.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,15 @@ export const TEST_CONFIG = {
6666
CorrelationId: "ed2a3125-a597-416f-9d12-68b9add1c268"
6767
};
6868

69+
export const B2C_TEST_CONFIG = {
70+
validAuthority: "https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/b2c_1_susi",
71+
MSAL_CLIENT_ID: "e760cab2-b9a1-4c0d-86fb-ff7084abd902",
72+
knownAuthorities: ["fabrikamb2c.b2clogin.com"]
73+
};
74+
6975
export const TEST_RESPONSE_TYPE = {
7076
id_token: "id_token",
7177
token: "token",
7278
id_token_token: "id_token token"
73-
}
79+
};
7480

lib/msal-core/test/UserAgentApplication.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1721,7 +1721,7 @@ describe("UserAgentApplication.ts Class", function () {
17211721
};
17221722
});
17231723
});
1724-
1724+
17251725
describe('Logger', () => {
17261726
it('getLogger and setLogger', done => {
17271727
const config: Configuration = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect } from "chai";
2+
import { AuthorityType } from "../../src/authority/Authority";
3+
import { AadAuthority } from "../../src/authority/AadAuthority";
4+
import { AADTrustedHostList } from "../../src/utils/Constants";
5+
import { TEST_CONFIG } from "../TestConstants";
6+
7+
describe("AadAuthority.ts Class", function () {
8+
9+
it("tests initialization of aad authority", function() {
10+
const authority = new AadAuthority(TEST_CONFIG.validAuthority, false);
11+
12+
expect(authority).to.be.instanceOf(AadAuthority);
13+
expect(authority.AuthorityType).to.be.equal(AuthorityType.Aad);
14+
});
15+
16+
it("tests GetOpenIdConfigurationEndpointAsync with validateAuthority false", async function () {
17+
const authority = new AadAuthority(TEST_CONFIG.validAuthority, false);
18+
const endpoint = await authority.GetOpenIdConfigurationEndpointAsync();
19+
20+
expect(endpoint).to.include("/v2.0/.well-known/openid-configuration");
21+
});
22+
23+
it("tests GetOpenIdConfigurationEndpointAsync with validateAuthority true", async function () {
24+
const authority = new AadAuthority(TEST_CONFIG.validAuthority, true);
25+
const endpoint = await authority.GetOpenIdConfigurationEndpointAsync();
26+
27+
expect(endpoint).to.include("/v2.0/.well-known/openid-configuration");
28+
});
29+
30+
it("tests GetOpenIdConfigurationEndpointAsync with validateAuthority true and not in trusted host list", async function () {
31+
32+
delete AADTrustedHostList["login.microsoftonline.com"];
33+
const authority = new AadAuthority(TEST_CONFIG.validAuthority, true);
34+
const endpoint = await authority.GetOpenIdConfigurationEndpointAsync();
35+
AADTrustedHostList["login.microsoftonline.com"] = "login.microsoftonline.com";
36+
37+
expect(endpoint).to.include("/v2.0/.well-known/openid-configuration");
38+
});
39+
40+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { expect } from "chai";
2+
import { Authority, AuthorityType } from "../../src/authority/Authority";
3+
import { ClientConfigurationErrorMessage } from "../../src/error/ClientConfigurationError"
4+
import { TEST_CONFIG } from "../TestConstants";
5+
6+
class testAuthority extends Authority{
7+
public constructor(authority: string, validateAuthority: boolean) {
8+
super(authority, validateAuthority);
9+
}
10+
11+
public get AuthorityType(): AuthorityType {
12+
return AuthorityType.Aad;
13+
}
14+
15+
public async GetOpenIdConfigurationEndpointAsync(): Promise<string> {
16+
return this.DefaultOpenIdConfigurationEndpoint;
17+
}
18+
}
19+
20+
let authority: testAuthority;
21+
22+
describe("Authority.ts Class", function () {
23+
beforeEach(function() {
24+
authority = new testAuthority(TEST_CONFIG.validAuthority, false);
25+
});
26+
27+
afterEach(function () {
28+
authority = null;
29+
});
30+
31+
it("tests initialization of Authority", function() {
32+
33+
expect(authority).to.be.instanceOf(testAuthority);
34+
expect(authority.AuthorityType).to.be.equal(AuthorityType.Aad);
35+
});
36+
37+
it("throws error if ResolveEndpointsAsync hasn't been called yet", function () {
38+
try {
39+
const authEndpoint = authority.AuthorizationEndpoint
40+
}
41+
catch(e) {
42+
expect(e).to.be.equal("Please call ResolveEndpointsAsync first");
43+
}
44+
});
45+
46+
it("tests EndSessionEndpoint", async function () {
47+
const response = await authority.resolveEndpointsAsync();
48+
const endSessionEndpoint = response.EndSessionEndpoint
49+
50+
expect(response).to.be.instanceOf(testAuthority);
51+
expect(endSessionEndpoint).to.contain(response.Tenant)
52+
});
53+
54+
it("tests SelfSignedJwtAudience", async function () {
55+
const response = await authority.resolveEndpointsAsync();
56+
const selfSignedJwtAudience = response.SelfSignedJwtAudience
57+
58+
expect(response).to.be.instanceOf(testAuthority);
59+
expect(selfSignedJwtAudience).to.contain(response.Tenant)
60+
});
61+
62+
it("throws invalidAuthorityType on init if authority is not url", function () {
63+
try {
64+
authority = new testAuthority("", false);
65+
}
66+
catch(e) {
67+
expect(e).to.be.equal(ClientConfigurationErrorMessage.invalidAuthorityType)
68+
}
69+
});
70+
71+
it("throws authorityUriInsecure on init if not https", function () {
72+
try {
73+
authority = new testAuthority("http://login.microsoftonline.com/common", false);
74+
}
75+
catch(e) {
76+
expect(e).to.be.equal(ClientConfigurationErrorMessage.authorityUriInsecure)
77+
}
78+
});
79+
80+
it("throws authorityUriInvalidPath on init if there is no path", function () {
81+
try {
82+
authority = new testAuthority("https://login.microsoftonline.com", false);
83+
}
84+
catch(e) {
85+
expect(e).to.be.equal(ClientConfigurationErrorMessage.authorityUriInvalidPath)
86+
}
87+
});
88+
89+
90+
});

0 commit comments

Comments
 (0)