Skip to content

[msal-common][msal-node] Authority changes #1424

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 20 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion lib/msal-common/src/authority/Authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export abstract class Authority {
* @param urlString
*/
private replaceTenant(urlString: string): string {
return urlString.replace("{tenant}", this.tenant);
return urlString.replace(/{tenant}|{tenantid}/g, this.tenant);
}

/**
Expand Down
35 changes: 27 additions & 8 deletions lib/msal-common/src/authority/AuthorityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@ import { ClientAuthError } from "./../error/ClientAuthError";
import { INetworkModule } from "./../network/INetworkModule";
import { StringUtils } from "./../utils/StringUtils";
import { UrlString } from "./../url/UrlString";
import { B2cAuthority, B2CTrustedHostList } from "./B2cAuthority";

export class AuthorityFactory {
/**
* Use when Authority is B2C to provide list of trusted/allowed domains.
*/
public static setKnownAuthorities(knownAuthorities: Array<string>): void {
if (!B2CTrustedHostList.length) {
knownAuthorities.forEach(function(authority){
B2CTrustedHostList.push(authority);
});
}
}

/**
* Parse the url and determine the type of authority
Expand All @@ -20,14 +31,16 @@ export class AuthorityFactory {
const authorityUrl = new UrlString(authorityString);
const components = authorityUrl.getUrlComponents();
const pathSegments = components.PathSegments;
switch (pathSegments[0]) {
case "tfp":
// tfp denotes a b2c url
return AuthorityType.B2C;
default:
// default authority is always AAD
return AuthorityType.Aad;

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

// defaults to Aad
return AuthorityType.Aad;
}

/**
Expand All @@ -41,12 +54,18 @@ export class AuthorityFactory {
}

const type = AuthorityFactory.detectAuthorityFromUrl(authorityUrl);

// Depending on above detection, create the right type.
switch (type) {
case AuthorityType.Aad:
return new AadAuthority(authorityUrl, networkInterface);
case AuthorityType.B2C:
return new B2cAuthority(authorityUrl, networkInterface);
// TODO: Support ADFS here in a later PR
default:
throw ClientAuthError.createInvalidAuthorityTypeError(`Given Url: ${authorityUrl}`);
throw ClientAuthError.createInvalidAuthorityTypeError(
`${authorityUrl}`
);
}
}
}
44 changes: 44 additions & 0 deletions lib/msal-common/src/authority/B2cAuthority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Authority } from "./Authority";
import { AuthorityType } from "./AuthorityType";
import { ClientConfigurationError } from "../error/ClientConfigurationError";
import { INetworkModule } from "../network/INetworkModule";

export const B2CTrustedHostList: string[] = [];

/**
* The AadAuthority class extends the Authority class and adds functionality specific to the Azure AD OAuth Authority.
*/
export class B2cAuthority extends Authority {
// Set authority type to AAD
public get authorityType(): AuthorityType {
return AuthorityType.B2C;
}

public constructor(authority: string, networkInterface: INetworkModule) {
super(authority, networkInterface);
}

/**
* Returns a promise which resolves to the OIDC endpoint
* Only responds with the endpoint
*/
public async getOpenIdConfigurationEndpointAsync(): Promise<string> {
if (this.isInTrustedHostList(this.canonicalAuthorityUrlComponents.HostNameAndPort)) {
return this.defaultOpenIdConfigurationEndpoint;
}

throw ClientConfigurationError.createUntrustedAuthorityError();
}

/**
* Checks to see if the host is in a list of trusted hosts
* @param {string} The host to look up
*/
private isInTrustedHostList(host: string): boolean {
return B2CTrustedHostList.includes(host);
}
}
7 changes: 4 additions & 3 deletions lib/msal-common/src/client/AuthorizationCodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { RequestParameterBuilder } from "../server/RequestParameterBuilder";
import { RequestValidator } from "../request/RequestValidator";
import { GrantType } from "../utils/Constants";
import { Configuration } from "../config/Configuration";
import {ServerAuthorizationTokenResponse} from "../server/ServerAuthorizationTokenResponse";
import {NetworkResponse} from "../network/NetworkManager";
import {ScopeSet} from "../request/ScopeSet";
import { ServerAuthorizationTokenResponse } from "../server/ServerAuthorizationTokenResponse";
import { NetworkResponse } from "../network/NetworkManager";
import { ScopeSet } from "../request/ScopeSet";

/**
* Oauth2.0 Authorization Code client
Expand Down Expand Up @@ -95,6 +95,7 @@ export class AuthorizationCodeClient extends BaseClient {
}

parameterBuilder.addGrantType(GrantType.AUTHORIZATION_CODE_GRANT);
parameterBuilder.addClientInfo();

return parameterBuilder.createQueryString();
}
Expand Down
3 changes: 3 additions & 0 deletions lib/msal-common/src/client/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export abstract class BaseClient {
this.networkClient = this.config.networkInterface;

// Default authority instance.
AuthorityFactory.setKnownAuthorities(
this.config.authOptions.knownAuthorities
);
this.defaultAuthorityInstance = AuthorityFactory.createInstance(
this.config.authOptions.authority || Constants.DEFAULT_AUTHORITY,
this.networkClient
Expand Down
4 changes: 3 additions & 1 deletion lib/msal-common/src/config/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type Configuration = {
export type AuthOptions = {
clientId: string;
authority?: string;
knownAuthorities?: Array<string>
};

/**
Expand Down Expand Up @@ -74,7 +75,8 @@ export type LoggerOptions = {

const DEFAULT_AUTH_OPTIONS: AuthOptions = {
clientId: "",
authority: null
authority: null,
knownAuthorities: []
};

// Default module system options
Expand Down
2 changes: 2 additions & 0 deletions lib/msal-common/src/config/SPAConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Configuration, buildConfiguration } from "./Configuration";
export type SPAAuthOptions = {
clientId: string;
authority?: string;
knownAuthorities?: Array<string>;
redirectUri?: string | (() => string);
postLogoutRedirectUri?: string | (() => string);
};
Expand All @@ -32,6 +33,7 @@ export type SPAConfiguration = Configuration & {
const DEFAULT_AUTH_OPTIONS: SPAAuthOptions = {
clientId: "",
authority: null,
knownAuthorities: [],
redirectUri: "",
postLogoutRedirectUri: ""
};
Expand Down
24 changes: 24 additions & 0 deletions lib/msal-common/src/error/ClientConfigurationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export const ClientConfigurationErrorMessage = {
invalidCodeChallengeParams: {
code: "one_of_code_challenge_code_challenge_method_params_missing",
desc: "Both params: code_challenge and code_challenge_method are to be passed if to be sent in the request"
},
b2cKnownAuthoritiesNotSet: {
code: "b2c_known_authorities_not_set",
desc: "Must set known authorities when validateAuthority is set to True and using B2C"
},
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."
}
};

Expand Down Expand Up @@ -189,4 +197,20 @@ export class ClientConfigurationError extends ClientAuthError {
ClientConfigurationErrorMessage.invalidCodeChallengeParams.desc
);
}

/**
* Throws an error when the user passes B2C authority and does not set knownAuthorities
*/
static createKnownAuthoritiesNotSetError(): ClientConfigurationError {
return new ClientConfigurationError(ClientConfigurationErrorMessage.b2cKnownAuthoritiesNotSet.code,
ClientConfigurationErrorMessage.b2cKnownAuthoritiesNotSet.desc);
}

/**
* Throws error when provided authority is not a member of the trusted host list
*/
static createUntrustedAuthorityError(): ClientConfigurationError {
return new ClientConfigurationError(ClientConfigurationErrorMessage.untrustedAuthority.code,
ClientConfigurationErrorMessage.untrustedAuthority.desc);
}
}
11 changes: 9 additions & 2 deletions lib/msal-common/src/server/RequestParameterBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
* Licensed under the MIT License.
*/

import { AADServerParamKeys, SSOTypes } from "../utils/Constants";
import { Constants } from "../utils/Constants";
import { AADServerParamKeys, SSOTypes, Constants, ClientInfo} from "../utils/Constants";
import { ScopeSet } from "../request/ScopeSet";
import { ClientConfigurationError } from "../error/ClientConfigurationError";

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

/**
* add client info
*
*/
addClientInfo(): void {
this.parameters.set(ClientInfo, "1");
}

/**
* Utility to create a URL from the params map
*/
Expand Down
1 change: 1 addition & 0 deletions lib/msal-common/src/utils/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,4 @@ export enum GrantType {
DEVICE_CODE_GRANT = "device_code"
};

export const ClientInfo = "client_info";
115 changes: 99 additions & 16 deletions lib/msal-common/test/authority/AuthorityFactory.spec.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,122 @@
import { expect } from "chai";
import { AuthorityFactory } from "../../src/authority/AuthorityFactory";
import { INetworkModule, NetworkRequestOptions } from "../../src/network/INetworkModule";
import { ClientConfigurationErrorMessage, ClientAuthErrorMessage, Constants, Authority } from "../../src";
import {
INetworkModule,
NetworkRequestOptions
} from "../../src/network/INetworkModule";
import {
ClientConfigurationErrorMessage,
Constants,
Authority,
ClientAuthError,
ClientAuthErrorMessage
} from "../../src";
import { AadAuthority } from "../../src/authority/AadAuthority";
import { B2cAuthority, B2CTrustedHostList} from "../../src/authority/B2cAuthority";
import { TEST_CONFIG } from "../utils/StringConstants";

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

const networkInterface: INetworkModule = {
sendGetRequestAsync<T>(url: string, options?: NetworkRequestOptions): T {
sendGetRequestAsync<T>(
url: string,
options?: NetworkRequestOptions
): T {
return null;
},
sendPostRequestAsync<T>(url: string, options?: NetworkRequestOptions): T {
sendPostRequestAsync<T>(
url: string,
options?: NetworkRequestOptions
): T {
return null;
}
};

it("AuthorityFactory returns null if given url is null or empty", () => {
expect(() => AuthorityFactory.createInstance("", networkInterface)).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
expect(() => AuthorityFactory.createInstance(null, networkInterface)).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
beforeEach(() => {
while (B2CTrustedHostList.length) {
B2CTrustedHostList.pop();
}
});

it("Throws error for B2C url strings that contain tfp", () => {
expect(() => AuthorityFactory.createInstance("https://contoso.b2clogin.com/tfp/contoso.onmicrosoft.com/B2C_1_signupsignin1", networkInterface)).to.throw(ClientAuthErrorMessage.invalidAuthorityType.desc);
it("AuthorityFactory returns null if given url is null or empty", () => {
expect(() =>
AuthorityFactory.createInstance("", networkInterface)
).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
expect(() =>
AuthorityFactory.createInstance(null, networkInterface)
).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
});

it("Throws error for malformed url strings", () => {
expect(() => AuthorityFactory.createInstance(`http://login.microsoftonline.com/common`, networkInterface)).to.throw(ClientConfigurationErrorMessage.authorityUriInsecure.desc);
expect(() => AuthorityFactory.createInstance(`https://login.microsoftonline.com/`, networkInterface)).to.throw(ClientConfigurationErrorMessage.urlParseError.desc);
expect(() => AuthorityFactory.createInstance("This is not a URI", networkInterface)).to.throw(ClientConfigurationErrorMessage.urlParseError.desc);
expect(() => AuthorityFactory.createInstance("", networkInterface)).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
expect(() =>
AuthorityFactory.createInstance(
`http://login.microsoftonline.com/common`,
networkInterface
)
).to.throw(ClientConfigurationErrorMessage.authorityUriInsecure.desc);
expect(() =>
AuthorityFactory.createInstance(
`https://login.microsoftonline.com/`,
networkInterface
)
).to.throw(ClientConfigurationErrorMessage.urlParseError.desc);
expect(() =>
AuthorityFactory.createInstance(
"This is not a URI",
networkInterface
)
).to.throw(ClientConfigurationErrorMessage.urlParseError.desc);
expect(() =>
AuthorityFactory.createInstance("", networkInterface)
).to.throw(ClientConfigurationErrorMessage.urlEmptyError.desc);
});

it("createInstance returns an AAD instance for any valid url string that does not contain a tfp", () => {
const authorityInstance = AuthorityFactory.createInstance(Constants.DEFAULT_AUTHORITY, networkInterface);
it("createInstance returns an AAD instance if knownAuthorities not provided", () => {
const authorityInstance = AuthorityFactory.createInstance(
Constants.DEFAULT_AUTHORITY,
networkInterface
);
expect(authorityInstance instanceof AadAuthority);
expect(authorityInstance instanceof Authority);
});

it("createInstance returns B2C instance if knownAuthorities is provided", () => {
AuthorityFactory.setKnownAuthorities(["fabrikamb2c.b2clogin.com"]);
const authorityInstance = AuthorityFactory.createInstance(
TEST_CONFIG.b2cValidAuthority,
networkInterface
);
expect(authorityInstance instanceof B2cAuthority);
expect(authorityInstance instanceof Authority);


});

it("Do not add additional authorities to trusted host list if it has already been populated", () => {
AuthorityFactory.setKnownAuthorities(["fabrikamb2c.b2clogin.com"]);
AuthorityFactory.setKnownAuthorities(["fake.b2clogin.com"]);

expect(B2CTrustedHostList).to.include("fabrikamb2c.b2clogin.com");
expect(B2CTrustedHostList).not.to.include("fake.b2clogin.com");
expect(B2CTrustedHostList.length).to.equal(1);
});

it("Throws error if AuthorityType is not AAD or B2C", done => {
//Right now only way to throw this is to send adfs authority. This will need to change when we implement ADFS
const errorAuthority = "https://login.microsoftonline.com/adfs";
try {
const authorityInstance = AuthorityFactory.createInstance(
errorAuthority,
networkInterface
);
} catch (e) {
expect(e).to.be.instanceOf(ClientAuthError);
expect(e.errorCode).to.be.eql(
ClientAuthErrorMessage.invalidAuthorityType.code
);
expect(e.errorMessage).to.be.eql(
`${ClientAuthErrorMessage.invalidAuthorityType.desc} Given Url: ${errorAuthority}`
);
done();
}
});
});
Loading