Skip to content

Commit

Permalink
Add support for both current and legacy B2C authority formats (#594)
Browse files Browse the repository at this point in the history
* Add support for both current and legacy B2C authority formats

* Fix B2C format test
  • Loading branch information
Avery-Dunn authored Feb 16, 2023
1 parent d1cb3be commit 92eace8
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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://<something.b2clogin.com/<tenant>/<policy>
*
* 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

This comment has been minimized.

Copy link
@bpossolo

bpossolo Mar 2, 2023

this url doesn't work: https://aka.ms/msal4j-b2c

This comment has been minimized.

Copy link
@Avery-Dunn

Avery-Dunn Mar 3, 2023

Author Collaborator

Thanks for letting us know, seems like something when wrong when I set up that URL. The link should be working now, and it should point to this doc: https://learn.microsoft.com/en-us/azure/active-directory-b2c/b2clogin

*
* @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);

Expand Down
14 changes: 10 additions & 4 deletions msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Authority.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -79,9 +80,10 @@ static AuthorityType detectAuthorityType(URL authorityUrl) {
"authority Uri should have at least one segment in the path (i.e. https://<host>/<path>/...)");
}

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;
Expand Down Expand Up @@ -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];
}
Expand All @@ -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() {
Expand Down
39 changes: 27 additions & 12 deletions msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/B2CAuthority.java
Original file line number Diff line number Diff line change
Expand Up @@ -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://<host>/tfp/<tenant>/<policy>/...)");
"Valid B2C 'authority' URLs should follow either of these formats: https://<host>/<tenant>/<policy>/... or https://<host>/something/<tenant>/<policy>/...");
}
}

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://<host>/tfp/<tenant>/<policy>/...
//
// 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://<something>.b2clogin.com/<tenant>/<policy>/...
//
// 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 comment has been minimized.

Copy link
@bpossolo

bpossolo Mar 3, 2023

it would be better to check the segment length if (segments.length() > 2)rather than doing an illegal array access because triggering an exception involves a lot of additional work under the scene (like triggering security handlers, building stack trace, etc)


this.authorizationEndpoint = String.format(B2C_AUTHORIZATION_ENDPOINT_FORMAT, host, tenant, policy);
this.tokenEndpoint = String.format(B2C_TOKEN_ENDPOINT_FORMAT, host, tenant, policy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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://<host>/tfp/<tenant>/<policy>/...\\)")
"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")
Expand Down

0 comments on commit 92eace8

Please sign in to comment.