Skip to content

Add support for both current and legacy B2C authority formats #594

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 2 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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/";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did legacy urls have ".b2clogin.com" in them? If not, we should modify this value so the execution goes to the place where tfp is checked.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like older B2C URLs could have it, since this constant has been unchanged since the original commit for it back in 2019:

public final static String B2C_AUTHORITY = "https://msidlabb2c.b2clogin.com/tfp/msidlabb2c.onmicrosoft.com/";

I think they just weren't guaranteed to have it, which is why the original B2C_MICROSOFTLOGIN_AUTHORITY used 'login.microsoftonline.com':

public final static String B2C_MICROSOFTLOGIN_AUTHORITY = "https://login.microsoftonline.com/tfp/msidlabb2c.onmicrosoft.com/";

Unfortunately it seems like ID labs updated some URLs so the old B2C_MICROSOFTLOGIN_AUTHORITY doesn't work anymore (you fixed that when you were updating the regional formats in #574), so I don't think there's a way for us to test a non-'b2clogin.com' URL.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice documentation!

*
* @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
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
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.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