Skip to content

Commit 1393392

Browse files
authored
Merge pull request #1424 from AzureAD/msal-node-authority
[msal-common][msal-node] Authority changes
2 parents 45ae3a9 + c004275 commit 1393392

17 files changed

+352
-34
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export abstract class Authority {
111111
* @param urlString
112112
*/
113113
private replaceTenant(urlString: string): string {
114-
return urlString.replace("{tenant}", this.tenant);
114+
return urlString.replace(/{tenant}|{tenantid}/g, this.tenant);
115115
}
116116

117117
/**

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

+27-8
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,19 @@ import { ClientAuthError } from "./../error/ClientAuthError";
1010
import { INetworkModule } from "./../network/INetworkModule";
1111
import { StringUtils } from "./../utils/StringUtils";
1212
import { UrlString } from "./../url/UrlString";
13+
import { B2cAuthority, B2CTrustedHostList } from "./B2cAuthority";
1314

1415
export class AuthorityFactory {
16+
/**
17+
* Use when Authority is B2C to provide list of trusted/allowed domains.
18+
*/
19+
public static setKnownAuthorities(knownAuthorities: Array<string>): void {
20+
if (!B2CTrustedHostList.length) {
21+
knownAuthorities.forEach(function(authority){
22+
B2CTrustedHostList.push(authority);
23+
});
24+
}
25+
}
1526

1627
/**
1728
* Parse the url and determine the type of authority
@@ -20,14 +31,16 @@ export class AuthorityFactory {
2031
const authorityUrl = new UrlString(authorityString);
2132
const components = authorityUrl.getUrlComponents();
2233
const pathSegments = components.PathSegments;
23-
switch (pathSegments[0]) {
24-
case "tfp":
25-
// tfp denotes a b2c url
26-
return AuthorityType.B2C;
27-
default:
28-
// default authority is always AAD
29-
return AuthorityType.Aad;
34+
35+
if (pathSegments[0] === "adfs") {
36+
return AuthorityType.Adfs;
37+
}
38+
else if (B2CTrustedHostList.length) {
39+
return AuthorityType.B2C;
3040
}
41+
42+
// defaults to Aad
43+
return AuthorityType.Aad;
3144
}
3245

3346
/**
@@ -41,12 +54,18 @@ export class AuthorityFactory {
4154
}
4255

4356
const type = AuthorityFactory.detectAuthorityFromUrl(authorityUrl);
57+
4458
// Depending on above detection, create the right type.
4559
switch (type) {
4660
case AuthorityType.Aad:
4761
return new AadAuthority(authorityUrl, networkInterface);
62+
case AuthorityType.B2C:
63+
return new B2cAuthority(authorityUrl, networkInterface);
64+
// TODO: Support ADFS here in a later PR
4865
default:
49-
throw ClientAuthError.createInvalidAuthorityTypeError(`Given Url: ${authorityUrl}`);
66+
throw ClientAuthError.createInvalidAuthorityTypeError(
67+
`${authorityUrl}`
68+
);
5069
}
5170
}
5271
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
import { Authority } from "./Authority";
6+
import { AuthorityType } from "./AuthorityType";
7+
import { ClientConfigurationError } from "../error/ClientConfigurationError";
8+
import { INetworkModule } from "../network/INetworkModule";
9+
10+
export const B2CTrustedHostList: string[] = [];
11+
12+
/**
13+
* The AadAuthority class extends the Authority class and adds functionality specific to the Azure AD OAuth Authority.
14+
*/
15+
export class B2cAuthority extends Authority {
16+
// Set authority type to AAD
17+
public get authorityType(): AuthorityType {
18+
return AuthorityType.B2C;
19+
}
20+
21+
public constructor(authority: string, networkInterface: INetworkModule) {
22+
super(authority, networkInterface);
23+
}
24+
25+
/**
26+
* Returns a promise which resolves to the OIDC endpoint
27+
* Only responds with the endpoint
28+
*/
29+
public async getOpenIdConfigurationEndpointAsync(): Promise<string> {
30+
if (this.isInTrustedHostList(this.canonicalAuthorityUrlComponents.HostNameAndPort)) {
31+
return this.defaultOpenIdConfigurationEndpoint;
32+
}
33+
34+
throw ClientConfigurationError.createUntrustedAuthorityError();
35+
}
36+
37+
/**
38+
* Checks to see if the host is in a list of trusted hosts
39+
* @param {string} The host to look up
40+
*/
41+
private isInTrustedHostList(host: string): boolean {
42+
return B2CTrustedHostList.includes(host);
43+
}
44+
}

lib/msal-common/src/client/AuthorizationCodeClient.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { RequestParameterBuilder } from "../server/RequestParameterBuilder";
1111
import { RequestValidator } from "../request/RequestValidator";
1212
import { GrantType } from "../utils/Constants";
1313
import { Configuration } from "../config/Configuration";
14-
import {ServerAuthorizationTokenResponse} from "../server/ServerAuthorizationTokenResponse";
15-
import {NetworkResponse} from "../network/NetworkManager";
16-
import {ScopeSet} from "../request/ScopeSet";
14+
import { ServerAuthorizationTokenResponse } from "../server/ServerAuthorizationTokenResponse";
15+
import { NetworkResponse } from "../network/NetworkManager";
16+
import { ScopeSet } from "../request/ScopeSet";
1717

1818
/**
1919
* Oauth2.0 Authorization Code client
@@ -95,6 +95,7 @@ export class AuthorizationCodeClient extends BaseClient {
9595
}
9696

9797
parameterBuilder.addGrantType(GrantType.AUTHORIZATION_CODE_GRANT);
98+
parameterBuilder.addClientInfo();
9899

99100
return parameterBuilder.createQueryString();
100101
}

lib/msal-common/src/client/BaseClient.ts

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export abstract class BaseClient {
6565
this.networkClient = this.config.networkInterface;
6666

6767
// Default authority instance.
68+
AuthorityFactory.setKnownAuthorities(
69+
this.config.authOptions.knownAuthorities
70+
);
6871
this.defaultAuthorityInstance = AuthorityFactory.createInstance(
6972
this.config.authOptions.authority || Constants.DEFAULT_AUTHORITY,
7073
this.networkClient

lib/msal-common/src/config/Configuration.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type Configuration = {
3838
export type AuthOptions = {
3939
clientId: string;
4040
authority?: string;
41+
knownAuthorities?: Array<string>
4142
};
4243

4344
/**
@@ -74,7 +75,8 @@ export type LoggerOptions = {
7475

7576
const DEFAULT_AUTH_OPTIONS: AuthOptions = {
7677
clientId: "",
77-
authority: null
78+
authority: null,
79+
knownAuthorities: []
7880
};
7981

8082
// Default module system options

lib/msal-common/src/config/SPAConfiguration.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Configuration, buildConfiguration } from "./Configuration";
1515
export type SPAAuthOptions = {
1616
clientId: string;
1717
authority?: string;
18+
knownAuthorities?: Array<string>;
1819
redirectUri?: string | (() => string);
1920
postLogoutRedirectUri?: string | (() => string);
2021
};
@@ -32,6 +33,7 @@ export type SPAConfiguration = Configuration & {
3233
const DEFAULT_AUTH_OPTIONS: SPAAuthOptions = {
3334
clientId: "",
3435
authority: null,
36+
knownAuthorities: [],
3537
redirectUri: "",
3638
postLogoutRedirectUri: ""
3739
};

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

+24
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ export const ClientConfigurationErrorMessage = {
5959
invalidCodeChallengeParams: {
6060
code: "one_of_code_challenge_code_challenge_method_params_missing",
6161
desc: "Both params: code_challenge and code_challenge_method are to be passed if to be sent in the request"
62+
},
63+
b2cKnownAuthoritiesNotSet: {
64+
code: "b2c_known_authorities_not_set",
65+
desc: "Must set known authorities when validateAuthority is set to True and using B2C"
66+
},
67+
untrustedAuthority: {
68+
code: "untrusted_authority",
69+
desc: "The provided authority is not a trusted authority. If using B2C, please include this authority in the knownAuthorities config parameter."
6270
}
6371
};
6472

@@ -189,4 +197,20 @@ export class ClientConfigurationError extends ClientAuthError {
189197
ClientConfigurationErrorMessage.invalidCodeChallengeParams.desc
190198
);
191199
}
200+
201+
/**
202+
* Throws an error when the user passes B2C authority and does not set knownAuthorities
203+
*/
204+
static createKnownAuthoritiesNotSetError(): ClientConfigurationError {
205+
return new ClientConfigurationError(ClientConfigurationErrorMessage.b2cKnownAuthoritiesNotSet.code,
206+
ClientConfigurationErrorMessage.b2cKnownAuthoritiesNotSet.desc);
207+
}
208+
209+
/**
210+
* Throws error when provided authority is not a member of the trusted host list
211+
*/
212+
static createUntrustedAuthorityError(): ClientConfigurationError {
213+
return new ClientConfigurationError(ClientConfigurationErrorMessage.untrustedAuthority.code,
214+
ClientConfigurationErrorMessage.untrustedAuthority.desc);
215+
}
192216
}

lib/msal-common/src/server/RequestParameterBuilder.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import { AADServerParamKeys, SSOTypes } from "../utils/Constants";
7-
import { Constants } from "../utils/Constants";
6+
import { AADServerParamKeys, SSOTypes, Constants, ClientInfo} from "../utils/Constants";
87
import { ScopeSet } from "../request/ScopeSet";
98
import { ClientConfigurationError } from "../error/ClientConfigurationError";
109

@@ -179,6 +178,14 @@ export class RequestParameterBuilder {
179178
this.parameters.set(AADServerParamKeys.GRANT_TYPE, encodeURIComponent(grantType));
180179
}
181180

181+
/**
182+
* add client info
183+
*
184+
*/
185+
addClientInfo(): void {
186+
this.parameters.set(ClientInfo, "1");
187+
}
188+
182189
/**
183190
* Utility to create a URL from the params map
184191
*/

lib/msal-common/src/utils/Constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,4 @@ export enum GrantType {
216216
DEVICE_CODE_GRANT = "device_code"
217217
};
218218

219+
export const ClientInfo = "client_info";
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,122 @@
11
import { expect } from "chai";
22
import { AuthorityFactory } from "../../src/authority/AuthorityFactory";
3-
import { INetworkModule, NetworkRequestOptions } from "../../src/network/INetworkModule";
4-
import { ClientConfigurationErrorMessage, ClientAuthErrorMessage, Constants, Authority } from "../../src";
3+
import {
4+
INetworkModule,
5+
NetworkRequestOptions
6+
} from "../../src/network/INetworkModule";
7+
import {
8+
ClientConfigurationErrorMessage,
9+
Constants,
10+
Authority,
11+
ClientAuthError,
12+
ClientAuthErrorMessage
13+
} from "../../src";
514
import { AadAuthority } from "../../src/authority/AadAuthority";
15+
import { B2cAuthority, B2CTrustedHostList} from "../../src/authority/B2cAuthority";
16+
import { TEST_CONFIG } from "../utils/StringConstants";
617

718
describe("AuthorityFactory.ts Class Unit Tests", () => {
8-
919
const networkInterface: INetworkModule = {
10-
sendGetRequestAsync<T>(url: string, options?: NetworkRequestOptions): T {
20+
sendGetRequestAsync<T>(
21+
url: string,
22+
options?: NetworkRequestOptions
23+
): T {
1124
return null;
1225
},
13-
sendPostRequestAsync<T>(url: string, options?: NetworkRequestOptions): T {
26+
sendPostRequestAsync<T>(
27+
url: string,
28+
options?: NetworkRequestOptions
29+
): T {
1430
return null;
1531
}
1632
};
1733

18-
it("AuthorityFactory returns null if given url is null or empty", () => {
19-
expect(() => AuthorityFactory.createInstance("", networkInterface)).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
20-
expect(() => AuthorityFactory.createInstance(null, networkInterface)).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
34+
beforeEach(() => {
35+
while (B2CTrustedHostList.length) {
36+
B2CTrustedHostList.pop();
37+
}
2138
});
2239

23-
it("Throws error for B2C url strings that contain tfp", () => {
24-
expect(() => AuthorityFactory.createInstance("https://contoso.b2clogin.com/tfp/contoso.onmicrosoft.com/B2C_1_signupsignin1", networkInterface)).to.throw(ClientAuthErrorMessage.invalidAuthorityType.desc);
40+
it("AuthorityFactory returns null if given url is null or empty", () => {
41+
expect(() =>
42+
AuthorityFactory.createInstance("", networkInterface)
43+
).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
44+
expect(() =>
45+
AuthorityFactory.createInstance(null, networkInterface)
46+
).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
2547
});
2648

2749
it("Throws error for malformed url strings", () => {
28-
expect(() => AuthorityFactory.createInstance(`http://login.microsoftonline.com/common`, networkInterface)).to.throw(ClientConfigurationErrorMessage.authorityUriInsecure.desc);
29-
expect(() => AuthorityFactory.createInstance(`https://login.microsoftonline.com/`, networkInterface)).to.throw(ClientConfigurationErrorMessage.urlParseError.desc);
30-
expect(() => AuthorityFactory.createInstance("This is not a URI", networkInterface)).to.throw(ClientConfigurationErrorMessage.urlParseError.desc);
31-
expect(() => AuthorityFactory.createInstance("", networkInterface)).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
50+
expect(() =>
51+
AuthorityFactory.createInstance(
52+
`http://login.microsoftonline.com/common`,
53+
networkInterface
54+
)
55+
).to.throw(ClientConfigurationErrorMessage.authorityUriInsecure.desc);
56+
expect(() =>
57+
AuthorityFactory.createInstance(
58+
`https://login.microsoftonline.com/`,
59+
networkInterface
60+
)
61+
).to.throw(ClientConfigurationErrorMessage.urlParseError.desc);
62+
expect(() =>
63+
AuthorityFactory.createInstance(
64+
"This is not a URI",
65+
networkInterface
66+
)
67+
).to.throw(ClientConfigurationErrorMessage.urlParseError.desc);
68+
expect(() =>
69+
AuthorityFactory.createInstance("", networkInterface)
70+
).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
3271
});
3372

34-
it("createInstance returns an AAD instance for any valid url string that does not contain a tfp", () => {
35-
const authorityInstance = AuthorityFactory.createInstance(Constants.DEFAULT_AUTHORITY, networkInterface);
73+
it("createInstance returns an AAD instance if knownAuthorities not provided", () => {
74+
const authorityInstance = AuthorityFactory.createInstance(
75+
Constants.DEFAULT_AUTHORITY,
76+
networkInterface
77+
);
3678
expect(authorityInstance instanceof AadAuthority);
3779
expect(authorityInstance instanceof Authority);
3880
});
81+
82+
it("createInstance returns B2C instance if knownAuthorities is provided", () => {
83+
AuthorityFactory.setKnownAuthorities(["fabrikamb2c.b2clogin.com"]);
84+
const authorityInstance = AuthorityFactory.createInstance(
85+
TEST_CONFIG.b2cValidAuthority,
86+
networkInterface
87+
);
88+
expect(authorityInstance instanceof B2cAuthority);
89+
expect(authorityInstance instanceof Authority);
90+
91+
92+
});
93+
94+
it("Do not add additional authorities to trusted host list if it has already been populated", () => {
95+
AuthorityFactory.setKnownAuthorities(["fabrikamb2c.b2clogin.com"]);
96+
AuthorityFactory.setKnownAuthorities(["fake.b2clogin.com"]);
97+
98+
expect(B2CTrustedHostList).to.include("fabrikamb2c.b2clogin.com");
99+
expect(B2CTrustedHostList).not.to.include("fake.b2clogin.com");
100+
expect(B2CTrustedHostList.length).to.equal(1);
101+
});
102+
103+
it("Throws error if AuthorityType is not AAD or B2C", done => {
104+
//Right now only way to throw this is to send adfs authority. This will need to change when we implement ADFS
105+
const errorAuthority = "https://login.microsoftonline.com/adfs";
106+
try {
107+
const authorityInstance = AuthorityFactory.createInstance(
108+
errorAuthority,
109+
networkInterface
110+
);
111+
} catch (e) {
112+
expect(e).to.be.instanceOf(ClientAuthError);
113+
expect(e.errorCode).to.be.eql(
114+
ClientAuthErrorMessage.invalidAuthorityType.code
115+
);
116+
expect(e.errorMessage).to.be.eql(
117+
`${ClientAuthErrorMessage.invalidAuthorityType.desc} Given Url: ${errorAuthority}`
118+
);
119+
done();
120+
}
121+
});
39122
});

0 commit comments

Comments
 (0)