Skip to content

Commit aac54c1

Browse files
committed
authority Path validation (#202)
authority Path validation
1 parent 269df71 commit aac54c1

File tree

7 files changed

+117
-67
lines changed

7 files changed

+117
-67
lines changed

src/main/java/com/microsoft/aad/msal4j/AADAuthority.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ class AADAuthority extends Authority {
2626
String deviceCodeEndpoint;
2727

2828
AADAuthority(final URL authorityUrl) {
29-
super(authorityUrl);
30-
validateAuthorityUrl();
29+
super(authorityUrl, AuthorityType.AAD);
3130
setAuthorityProperties();
3231
this.authority = String.format(AAD_AUTHORITY_FORMAT, host, tenant);
3332
}

src/main/java/com/microsoft/aad/msal4j/ADFSAuthority.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class ADFSAuthority extends Authority{
1313
private final static String ADFS_AUTHORITY_FORMAT = "https://%s/%s/";
1414

1515
ADFSAuthority(final URL authorityUrl) {
16-
super(authorityUrl);
16+
super(authorityUrl, AuthorityType.ADFS);
1717
this.authority = String.format(ADFS_AUTHORITY_FORMAT, host, tenant);
1818
this.authorizationEndpoint = authority + AUTHORIZATION_ENDPOINT;
1919
this.tokenEndpoint = authority + TOKEN_ENDPOINT;

src/main/java/com/microsoft/aad/msal4j/Authority.java

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* Represents Authentication Authority responsible for issuing access tokens.
1515
*/
1616

17-
@Accessors(fluent=true)
17+
@Accessors(fluent = true)
1818
@Getter(AccessLevel.PACKAGE)
1919
abstract class Authority {
2020

@@ -40,9 +40,9 @@ URL tokenEndpointUrl() throws MalformedURLException {
4040
return new URL(tokenEndpoint);
4141
}
4242

43-
Authority(URL canonicalAuthorityUrl){
43+
Authority(URL canonicalAuthorityUrl, AuthorityType authorityType) {
4444
this.canonicalAuthorityUrl = canonicalAuthorityUrl;
45-
this.authorityType = detectAuthorityType(canonicalAuthorityUrl);
45+
this.authorityType = authorityType;
4646
setCommonAuthorityProperties();
4747
}
4848

@@ -51,15 +51,19 @@ private void setCommonAuthorityProperties() {
5151
this.host = canonicalAuthorityUrl.getAuthority().toLowerCase();
5252
}
5353

54-
static Authority createAuthority(URL authorityUrl){
55-
AuthorityType authorityType = detectAuthorityType(authorityUrl);
56-
if(authorityType == AuthorityType.AAD){
57-
return new AADAuthority(authorityUrl);
58-
} else if(authorityType == AuthorityType.B2C) {
59-
return new B2CAuthority(authorityUrl);
60-
} else {
61-
throw new IllegalArgumentException("Unsupported Authority Type");
62-
}
54+
static Authority createAuthority(URL authorityUrl) {
55+
validateAuthority(authorityUrl);
56+
57+
AuthorityType authorityType = detectAuthorityType(authorityUrl);
58+
if (authorityType == AuthorityType.AAD) {
59+
return new AADAuthority(authorityUrl);
60+
} else if (authorityType == AuthorityType.B2C) {
61+
return new B2CAuthority(authorityUrl);
62+
} else if (authorityType == AuthorityType.ADFS) {
63+
return new ADFSAuthority(authorityUrl);
64+
} else {
65+
throw new IllegalArgumentException("Unsupported Authority Type");
66+
}
6367
}
6468

6569
static AuthorityType detectAuthorityType(URL authorityUrl) {
@@ -75,36 +79,56 @@ static AuthorityType detectAuthorityType(URL authorityUrl) {
7579

7680
final String firstPath = path.substring(0, path.indexOf("/"));
7781

78-
79-
if(isB2CAuthority(firstPath)){
82+
if (isB2CAuthority(firstPath)) {
8083
return AuthorityType.B2C;
81-
} else if(isAdfsAuthority(firstPath)) {
84+
} else if (isAdfsAuthority(firstPath)) {
8285
return AuthorityType.ADFS;
8386
} else {
8487
return AuthorityType.AAD;
8588
}
8689
}
8790

88-
void validateAuthorityUrl() {
89-
if (!this.canonicalAuthorityUrl.getProtocol().equalsIgnoreCase("https")) {
91+
static void validateAuthority(URL authorityUrl) {
92+
if (!authorityUrl.getProtocol().equalsIgnoreCase("https")) {
9093
throw new IllegalArgumentException(
9194
"authority should use the 'https' scheme");
9295
}
9396

94-
if (this.canonicalAuthorityUrl.toString().contains("#")) {
97+
if (authorityUrl.toString().contains("#")) {
9598
throw new IllegalArgumentException(
9699
"authority is invalid format (contains fragment)");
97100
}
98101

99-
if (!StringHelper.isBlank(this.canonicalAuthorityUrl.getQuery())) {
102+
if (!StringHelper.isBlank(authorityUrl.getQuery())) {
100103
throw new IllegalArgumentException(
101104
"authority cannot contain query parameters");
102105
}
106+
107+
final String path = authorityUrl.getPath();
108+
109+
if (path.length() == 0) {
110+
throw new IllegalArgumentException(
111+
IllegalArgumentExceptionMessages.AUTHORITY_URI_EMPTY_PATH);
112+
}
113+
114+
String[] segments = path.substring(1).split("/");
115+
116+
if (segments.length == 0) {
117+
throw new IllegalArgumentException(
118+
IllegalArgumentExceptionMessages.AUTHORITY_URI_EMPTY_PATH_SEGMENT);
119+
}
120+
121+
for (String segment : segments) {
122+
if (StringHelper.isBlank(segment)) {
123+
throw new IllegalArgumentException(
124+
IllegalArgumentExceptionMessages.AUTHORITY_URI_EMPTY_PATH_SEGMENT);
125+
}
126+
}
103127
}
104128

105129
static String getTenant(URL authorityUrl, AuthorityType authorityType) {
106130
String[] segments = authorityUrl.getPath().substring(1).split("/");
107-
if(authorityType == AuthorityType.B2C){
131+
if (authorityType == AuthorityType.B2C) {
108132
return segments[1];
109133
}
110134
return segments[0];
@@ -118,7 +142,7 @@ private static boolean isAdfsAuthority(final String firstPath) {
118142
return firstPath.compareToIgnoreCase(ADFS_PATH_SEGMENT) == 0;
119143
}
120144

121-
private static boolean isB2CAuthority(final String firstPath){
145+
private static boolean isB2CAuthority(final String firstPath) {
122146
return firstPath.compareToIgnoreCase(B2C_PATH_SEGMENT) == 0;
123147
}
124148
}

src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,23 @@ class B2CAuthority extends Authority{
2121
private String policy;
2222

2323
B2CAuthority(final URL authorityUrl){
24-
super(authorityUrl);
25-
validateAuthorityUrl();
24+
super(authorityUrl, AuthorityType.B2C);
2625
setAuthorityProperties();
2726
}
2827

29-
private void setAuthorityProperties() {
30-
String[] segments = canonicalAuthorityUrl.getPath().substring(1).split("/");
31-
28+
private void validatePathSegments(String[] segments){
3229
if(segments.length < 3){
3330
throw new IllegalArgumentException(
3431
"B2C 'authority' Uri should have at least 3 segments in the path " +
3532
"(i.e. https://<host>/tfp/<tenant>/<policy>/...)");
3633
}
34+
}
35+
36+
private void setAuthorityProperties() {
37+
String[] segments = canonicalAuthorityUrl.getPath().substring(1).split("/");
38+
39+
validatePathSegments(segments);
40+
3741
policy = segments[2];
3842

3943
final String b2cAuthorityFormat = "https://%s/%s/%s/%s/";

src/main/java/com/microsoft/aad/msal4j/ClientApplicationBase.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ ServiceBundle getServiceBundle() {
235235
return serviceBundle;
236236
}
237237

238-
protected static String canonicalizeUrl(String authority) {
238+
protected static String enforceTrailingSlash(String authority) {
239239
authority = authority.toLowerCase();
240240

241241
if (!authority.endsWith("/")) {
@@ -288,14 +288,17 @@ public Builder(String clientId) {
288288
* @throws MalformedURLException if val is malformed URL
289289
*/
290290
public T authority(String val) throws MalformedURLException {
291-
authority = canonicalizeUrl(val);
291+
authority = enforceTrailingSlash(val);
292292

293-
switch (Authority.detectAuthorityType(new URL(authority))) {
293+
URL authorityURL = new URL(authority);
294+
Authority.validateAuthority(authorityURL);
295+
296+
switch (Authority.detectAuthorityType(authorityURL)) {
294297
case AAD:
295-
authenticationAuthority = new AADAuthority(new URL(authority));
298+
authenticationAuthority = new AADAuthority(authorityURL);
296299
break;
297300
case ADFS:
298-
authenticationAuthority = new ADFSAuthority(new URL(authority));
301+
authenticationAuthority = new ADFSAuthority(authorityURL);
299302
break;
300303
default:
301304
throw new IllegalArgumentException("Unsupported authority type.");
@@ -305,12 +308,15 @@ public T authority(String val) throws MalformedURLException {
305308
}
306309

307310
public T b2cAuthority(String val) throws MalformedURLException{
308-
authority = canonicalizeUrl(val);
311+
authority = enforceTrailingSlash(val);
312+
313+
URL authorityURL = new URL(authority);
314+
Authority.validateAuthority(authorityURL);
309315

310-
if(Authority.detectAuthorityType(new URL(authority)) != AuthorityType.B2C){
316+
if(Authority.detectAuthorityType(authorityURL) != AuthorityType.B2C){
311317
throw new IllegalArgumentException("Unsupported authority type. Please use B2C authority");
312318
}
313-
authenticationAuthority = new B2CAuthority(new URL(authority));
319+
authenticationAuthority = new B2CAuthority(authorityURL);
314320

315321
validateAuthority = false;
316322
return self();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
class IllegalArgumentExceptionMessages {
7+
8+
final static String AUTHORITY_URI_EMPTY_PATH_SEGMENT = "Authority Uri should not have empty path segments";
9+
10+
final static String AUTHORITY_URI_EMPTY_PATH = "Authority Uri should have at least one segment in the path";
11+
12+
}

src/test/java/com/microsoft/aad/msal4j/AuthorityTest.java

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
import org.powermock.core.classloader.annotations.PrepareForTest;
1010
import org.testng.Assert;
11+
import org.testng.annotations.DataProvider;
1112
import org.testng.annotations.Test;
1213

13-
@Test(groups = { "checkin" })
14-
@PrepareForTest({ AADAuthority.class, HttpHelper.class,
15-
JsonHelper.class, AadInstanceDiscoveryResponse.class })
14+
@Test(groups = {"checkin"})
15+
@PrepareForTest({AADAuthority.class, HttpHelper.class,
16+
JsonHelper.class, AadInstanceDiscoveryResponse.class})
1617
public class AuthorityTest extends AbstractMsalTests {
1718

1819
@Test
@@ -33,45 +34,28 @@ public void testDetectAuthorityType_B2C() throws Exception {
3334
Assert.assertEquals(Authority.detectAuthorityType(url), AuthorityType.B2C);
3435
}
3536

36-
@Test(expectedExceptions = IllegalArgumentException.class,
37-
expectedExceptionsMessageRegExp =
38-
"authority Uri should have at least one segment in the path \\(i.e. https://<host>/<path>/...\\)")
39-
public void testAADAuthorityConstructor_NoPathAuthority() throws MalformedURLException {
40-
new AADAuthority(new URL("https://something.com/"));
41-
}
42-
4337
@Test(expectedExceptions = IllegalArgumentException.class,
4438
expectedExceptionsMessageRegExp =
4539
"B2C 'authority' Uri should have at least 3 segments in the path \\(i.e. https://<host>/tfp/<tenant>/<policy>/...\\)")
4640
public void testB2CAuthorityConstructor_NotEnoughSegments() throws MalformedURLException {
4741
new B2CAuthority(new URL("https://something.com/tfp/somethingelse/"));
4842
}
4943

50-
@Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "canonicalAuthorityUrl")
51-
public void testAADAuthorityConstructor_NullAuthority() {
52-
new AADAuthority(null);
53-
}
54-
55-
@Test(expectedExceptions = NullPointerException.class, expectedExceptionsMessageRegExp = "canonicalAuthorityUrl")
56-
public void testB2CAuthorityConstructor_NullAuthority() {
57-
new B2CAuthority(null);
58-
}
59-
6044
@Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "authority should use the 'https' scheme")
6145
public void testAADAuthorityConstructor_HttpAuthority() throws MalformedURLException {
62-
new AADAuthority(new URL("http://I.com/not/h/t/t/p/s/"));
46+
Authority.validateAuthority(new URL("http://I.com/not/h/t/t/p/s/"));
6347
}
6448

6549
@Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "authority is invalid format \\(contains fragment\\)")
6650
public void testAADAuthorityConstructor_UrlHasFragment() throws MalformedURLException {
67-
new AADAuthority(new URL("https://I.com/something/#haha"));
51+
Authority.validateAuthority(new URL("https://I.com/something/#haha"));
6852
}
6953

7054

7155
@Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "authority cannot contain query parameters")
7256
public void testAADAuthorityConstructor_AuthorityHasQuery()
7357
throws MalformedURLException {
74-
new AADAuthority(new URL("https://I.com/not/?query=not-allowed"));
58+
Authority.validateAuthority(new URL("https://I.com/not/?query=not-allowed"));
7559
}
7660

7761

@@ -96,7 +80,7 @@ public void testConstructor_AADAuthority() throws MalformedURLException {
9680

9781
@Test
9882
public void testConstructor_B2CAuthority() throws MalformedURLException {
99-
final B2CAuthority aa = new B2CAuthority (new URL(TestConfiguration.B2C_AUTHORITY));
83+
final B2CAuthority aa = new B2CAuthority(new URL(TestConfiguration.B2C_AUTHORITY));
10084
Assert.assertNotNull(aa);
10185
Assert.assertEquals(aa.authority(),
10286
TestConfiguration.B2C_AUTHORITY + "/");
@@ -128,22 +112,22 @@ public void testConstructor_ADFSAuthority() throws MalformedURLException {
128112
}
129113

130114
@Test
131-
public void testB2CAuthority_SameCanonicalAuthority() throws MalformedURLException{
115+
public void testB2CAuthority_SameCanonicalAuthority() throws MalformedURLException {
132116

133-
PublicClientApplication pca = PublicClientApplication.builder("client_id").
117+
PublicClientApplication pca = PublicClientApplication.builder("client_id").
134118
b2cAuthority(TestConfiguration.B2C_AUTHORITY_CUSTOM_PORT).build();
135119
Assert.assertEquals(pca.authenticationAuthority.authority,
136120
TestConfiguration.B2C_AUTHORITY_CUSTOM_PORT_TAIL_SLASH);
137121

138-
PublicClientApplication pca2 = PublicClientApplication.builder("client_id").
122+
PublicClientApplication pca2 = PublicClientApplication.builder("client_id").
139123
b2cAuthority(TestConfiguration.B2C_AUTHORITY_CUSTOM_PORT_TAIL_SLASH).build();
140124
Assert.assertEquals(pca2.authenticationAuthority.authority,
141125
TestConfiguration.B2C_AUTHORITY_CUSTOM_PORT_TAIL_SLASH);
142126
}
143127

144128
@Test
145-
public void testNoAuthorityPassedIn_DefaultsToCommonAuthority(){
146-
PublicClientApplication pca = PublicClientApplication.builder("client_id").build();
129+
public void testNoAuthorityPassedIn_DefaultsToCommonAuthority() {
130+
PublicClientApplication pca = PublicClientApplication.builder("client_id").build();
147131

148132
Assert.assertEquals(pca.authority(), TestConfiguration.AAD_COMMON_AUTHORITY);
149133
Assert.assertNotNull(pca.authenticationAuthority);
@@ -153,7 +137,7 @@ public void testNoAuthorityPassedIn_DefaultsToCommonAuthority(){
153137
public void testDoStaticInstanceDiscovery_ValidateTrue_TrustedAuthority()
154138
throws MalformedURLException, Exception {
155139
final AADAuthority aa = new AADAuthority(new URL(TestConfiguration.AAD_TENANT_ENDPOINT));
156-
//PS Assert.assertTrue(aa.doStaticInstanceDiscovery(true));
140+
//PS Assert.assertTrue(aa.doStaticInstanceDiscovery(true));
157141
}
158142

159143
@Test
@@ -169,4 +153,25 @@ public void testDoStaticInstanceDiscovery_ValidateFalse_TrustedAuthority()
169153
final AADAuthority aa = new AADAuthority(new URL(TestConfiguration.AAD_UNKNOWN_TENANT_ENDPOINT));
170154
//PS Assert.assertTrue(aa.doStaticInstanceDiscovery(false));
171155
}
156+
157+
158+
@DataProvider(name = "authoritiesWithEmptyPath")
159+
public static Object[][] createData() {
160+
return new Object[][]{{"https://login.microsoftonline.com/"},
161+
{"https://login.microsoftonline.com//"},
162+
{"https://login.microsoftonline.com//tenant"},
163+
{"https://login.microsoftonline.com////tenant//path1"}};
164+
}
165+
166+
@Test(dataProvider = "authoritiesWithEmptyPath", expectedExceptions = IllegalArgumentException.class,
167+
expectedExceptionsMessageRegExp = IllegalArgumentExceptionMessages.AUTHORITY_URI_EMPTY_PATH_SEGMENT)
168+
public void testValidateAuthorityEmptyPathSegments(String authority) throws MalformedURLException {
169+
Authority.validateAuthority(new URL(authority));
170+
}
171+
172+
@Test(expectedExceptions = IllegalArgumentException.class,
173+
expectedExceptionsMessageRegExp = IllegalArgumentExceptionMessages.AUTHORITY_URI_EMPTY_PATH)
174+
public void testValidateAuthorityEmptyPath() throws MalformedURLException {
175+
Authority.validateAuthority(new URL("https://login.microsoftonline.com"));
176+
}
172177
}

0 commit comments

Comments
 (0)