From 1968c33f536ba7a89e02cb721a52d59a80408eb8 Mon Sep 17 00:00:00 2001 From: Avery-Dunn Date: Thu, 9 Feb 2023 08:59:07 -0800 Subject: [PATCH 1/2] Add support for both current and legacy B2C authority formats --- .../AcquireTokenInteractiveIT.java | 14 +++++-- .../TestConstants.java | 4 +- .../msal4j/AbstractClientApplicationBase.java | 12 ++++++ .../com/microsoft/aad/msal4j/Authority.java | 14 +++++-- .../microsoft/aad/msal4j/B2CAuthority.java | 39 +++++++++++++------ 5 files changed, 62 insertions(+), 21 deletions(-) diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java index b50e2fdb..79466c5f 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AcquireTokenInteractiveIT.java @@ -74,7 +74,15 @@ public void acquireTokenWithAuthorizationCode_B2C_Local(String environment) { cfg = new Config(environment); User user = labUserProvider.getB2cUser(cfg.azureEnvironment, B2CProvider.LOCAL); - assertAcquireTokenB2C(user); + assertAcquireTokenB2C(user, TestConstants.B2C_AUTHORITY); + } + + @Test(dataProvider = "environments", dataProviderClass = EnvironmentsProvider.class) + public void acquireTokenWithAuthorizationCode_B2C_LegacyFormat(String environment) { + cfg = new Config(environment); + + User user = labUserProvider.getB2cUser(cfg.azureEnvironment, B2CProvider.LOCAL); + assertAcquireTokenB2C(user, TestConstants.B2C_AUTHORITY_LEGACY_FORMAT); } @Test @@ -126,13 +134,13 @@ private void assertAcquireTokenADFS2019(User user) { Assert.assertEquals(user.getUpn(), result.account().username()); } - private void assertAcquireTokenB2C(User user) { + private void assertAcquireTokenB2C(User user, String authority) { PublicClientApplication pca; try { pca = PublicClientApplication.builder( user.getAppId()). - b2cAuthority(TestConstants.B2C_AUTHORITY_SIGN_IN). + b2cAuthority(authority + TestConstants.B2C_SIGN_IN_POLICY). build(); } catch (MalformedURLException ex) { throw new RuntimeException(ex.getMessage()); diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java index 97e65f16..bd81b076 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/TestConstants.java @@ -38,8 +38,8 @@ public class TestConstants { public final static String ARLINGTON_GRAPH_DEFAULT_SCOPE = "https://graph.microsoft.us/.default"; - public final static String B2C_AUTHORITY = "https://msidlabb2c.b2clogin.com/tfp/msidlabb2c.onmicrosoft.com/"; - public final static String B2C_AUTHORITY_URL = "https://msidlabb2c.b2clogin.com/msidlabb2c.onmicrosoft.com/"; + public final static String B2C_AUTHORITY = "https://msidlabb2c.b2clogin.com/msidlabb2c.onmicrosoft.com/"; + public final static String B2C_AUTHORITY_LEGACY_FORMAT = "https://msidlabb2c.b2clogin.com/tfp/msidlabb2c.onmicrosoft.com/"; public final static String B2C_ROPC_POLICY = "B2C_1_ROPC_Auth"; public final static String B2C_SIGN_IN_POLICY = "B2C_1_SignInPolicy"; public final static String B2C_AUTHORITY_SIGN_IN = B2C_AUTHORITY + B2C_SIGN_IN_POLICY; diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java index b1b7ba6b..b65949f3 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java @@ -373,6 +373,18 @@ public T authority(String val) throws MalformedURLException { return self(); } + /** + * Set URL of the authenticating B2C authority from which MSAL will acquire tokens + * + * Valid B2C authorities should look like: https://.b2clogin.com// + * + * MSAL Java also supports a legacy B2C authority format, which looks like: https:///tfp// + * + * However, MSAL Java will eventually stop supporting the legacy format. See here for information on how to migrate to the new format: https://aka.ms/msal4j-b2c + * + * @param val a boolean value for validateAuthority + * @return instance of the Builder on which method was called + */ public T b2cAuthority(String val) throws MalformedURLException { authority = Authority.enforceTrailingSlash(val); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Authority.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Authority.java index 0a15a355..dea2281b 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Authority.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Authority.java @@ -20,6 +20,7 @@ abstract class Authority { private static final String ADFS_PATH_SEGMENT = "adfs"; private static final String B2C_PATH_SEGMENT = "tfp"; + private static final String B2C_HOST_SEGMENT = "b2clogin.com"; private final static String USER_REALM_ENDPOINT = "common/userrealm"; private final static String userRealmEndpointFormat = "https://%s/" + USER_REALM_ENDPOINT + "/%s?api-version=1.0"; @@ -79,9 +80,10 @@ static AuthorityType detectAuthorityType(URL authorityUrl) { "authority Uri should have at least one segment in the path (i.e. https:////...)"); } + final String host = authorityUrl.getHost(); final String firstPath = path.substring(0, path.indexOf("/")); - if (isB2CAuthority(firstPath)) { + if (isB2CAuthority(host, firstPath)) { return AuthorityType.B2C; } else if (isAdfsAuthority(firstPath)) { return AuthorityType.ADFS; @@ -131,7 +133,11 @@ static void validateAuthority(URL authorityUrl) { static String getTenant(URL authorityUrl, AuthorityType authorityType) { String[] segments = authorityUrl.getPath().substring(1).split("/"); if (authorityType == AuthorityType.B2C) { - return segments[1]; + if (segments.length < 3){ + return segments[0]; + } else { + return segments[1]; + } } return segments[0]; } @@ -144,8 +150,8 @@ private static boolean isAdfsAuthority(final String firstPath) { return firstPath.compareToIgnoreCase(ADFS_PATH_SEGMENT) == 0; } - private static boolean isB2CAuthority(final String firstPath) { - return firstPath.compareToIgnoreCase(B2C_PATH_SEGMENT) == 0; + private static boolean isB2CAuthority(final String host, final String firstPath) { + return host.contains(B2C_HOST_SEGMENT) || firstPath.compareToIgnoreCase(B2C_PATH_SEGMENT) == 0; } String deviceCodeEndpoint() { diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java index bc94f7bf..3d15c846 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java @@ -26,27 +26,42 @@ class B2CAuthority extends Authority { } private void validatePathSegments(String[] segments) { - if (segments.length < 3) { + if (segments.length < 2) { throw new IllegalArgumentException( - "B2C 'authority' Uri should have at least 3 segments in the path " + - "(i.e. https:///tfp///...)"); + "Valid B2C 'authority' URLs should follow either of these formats: https://///... or https:///something///..."); } } private void setAuthorityProperties() { String[] segments = canonicalAuthorityUrl.getPath().substring(1).split("/"); + // In the early days of MSAL, the only way for the library to identify a B2C authority was whether or not the authority + // had three segments in the path, and the first segment was 'tfp'. Valid B2C authorities looked like: https:///tfp///... + // + // More recent changes to B2C should ensure that any new B2C authorities have 'b2clogin.com' in the host of the URL, + // so app developers shouldn't need to add 'tfp' and the first path segment should just be the tenant: https://.b2clogin.com///... + // + // However, legacy URLs using the old format must still be supported by these sorts of checks here and elsewhere, so for the near + // future at least we must consider both formats as valid until we're either sure all customers are swapped, + // or until we're comfortable with a potentially breaking change validatePathSegments(segments); - policy = segments[2]; - - final String b2cAuthorityFormat = "https://%s/%s/%s/%s/"; - this.authority = String.format( - b2cAuthorityFormat, - canonicalAuthorityUrl.getAuthority(), - segments[0], - segments[1], - segments[2]); + try { + policy = segments[2]; + this.authority = String.format( + "https://%s/%s/%s/%s/", + canonicalAuthorityUrl.getAuthority(), + segments[0], + segments[1], + segments[2]); + } catch (IndexOutOfBoundsException e){ + policy = segments[1]; + this.authority = String.format( + "https://%s/%s/%s/", + canonicalAuthorityUrl.getAuthority(), + segments[0], + segments[1]); + } this.authorizationEndpoint = String.format(B2C_AUTHORIZATION_ENDPOINT_FORMAT, host, tenant, policy); this.tokenEndpoint = String.format(B2C_TOKEN_ENDPOINT_FORMAT, host, tenant, policy); From 2e0616b11efd746845831dd4b705aff70cd13246 Mon Sep 17 00:00:00 2001 From: Avery-Dunn Date: Wed, 15 Feb 2023 08:57:38 -0800 Subject: [PATCH 2/2] Fix B2C format test --- .../microsoft/aad/msal4j/AbstractClientApplicationBase.java | 4 ++-- .../src/test/java/com/microsoft/aad/msal4j/AuthorityTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java index b65949f3..0bcd0077 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java @@ -376,9 +376,9 @@ public T authority(String val) throws MalformedURLException { /** * Set URL of the authenticating B2C authority from which MSAL will acquire tokens * - * Valid B2C authorities should look like: https://.b2clogin.com// + * Valid B2C authorities should look like: https://<something.b2clogin.com/<tenant>/<policy> * - * MSAL Java also supports a legacy B2C authority format, which looks like: https:///tfp// + * MSAL Java also supports a legacy B2C authority format, which looks like: https://<host>/tfp/<tenant>/<policy> * * However, MSAL Java will eventually stop supporting the legacy format. See here for information on how to migrate to the new format: https://aka.ms/msal4j-b2c * diff --git a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorityTest.java b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorityTest.java index 0db1b159..ea5a99a8 100644 --- a/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorityTest.java +++ b/msal4j-sdk/src/test/java/com/microsoft/aad/msal4j/AuthorityTest.java @@ -36,9 +36,9 @@ public void testDetectAuthorityType_B2C() throws Exception { @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = - "B2C 'authority' Uri should have at least 3 segments in the path \\(i.e. https:///tfp///...\\)") + "Valid B2C 'authority' URLs should follow either of these formats.*") public void testB2CAuthorityConstructor_NotEnoughSegments() throws MalformedURLException { - new B2CAuthority(new URL("https://something.com/tfp/somethingelse/")); + new B2CAuthority(new URL("https://something.com/somethingelse/")); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "authority should use the 'https' scheme")