Skip to content

Add OIDC issuer validation and new testing style #970

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 9 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -579,6 +579,10 @@ public T correlationId(String val) {
((OidcAuthority) authenticationAuthority).setAuthorityProperties(
OidcDiscoveryProvider.performOidcDiscovery(
(OidcAuthority) authenticationAuthority, this));

if (!((OidcAuthority) authenticationAuthority).isIssuerValid()) {
Copy link
Member

Choose a reason for hiding this comment

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

This upcasting is a code smell. If you use inheritance, have you considered adding isIssuerValid to the base class Authority, where it simply returns true and have custom logic in OidcAuthority ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In the latest commit I rearranged the methods: the call to the validation method now happens in the setAuthorityProperties() method, so there are now no changes to this class and all the logic is contained within OidcAuthority

throw new MsalClientException(String.format("Invalid issuer from OIDC discovery: %s", ((OidcAuthority) authenticationAuthority).issuerFromOidcDiscovery), "issuer_validation");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public class OidcAuthority extends Authority {
//Part of the OpenIdConnect standard, this is appended to the authority to create the endpoint that has OIDC metadata
static final String WELL_KNOWN_OPENID_CONFIGURATION = ".well-known/openid-configuration";
private static final String AUTHORITY_FORMAT = "https://%s/%s/";
private static final String CIAM_AUTHORITY_FORMAT = "https://%s.ciamlogin.com/%s";

String issuerFromOidcDiscovery;

OidcAuthority(URL authorityUrl) throws MalformedURLException {
super(createOidcDiscoveryUrl(authorityUrl), AuthorityType.OIDC);
Expand All @@ -29,5 +32,42 @@ void setAuthorityProperties(OidcDiscoveryResponse instanceDiscoveryResponse) {
this.tokenEndpoint = instanceDiscoveryResponse.tokenEndpoint();
this.deviceCodeEndpoint = instanceDiscoveryResponse.deviceCodeEndpoint();
this.selfSignedJwtAudience = this.tokenEndpoint;
this.issuerFromOidcDiscovery = instanceDiscoveryResponse.issuer();
}

/**
* Validates the issuer from OIDC discovery.
* Issuer is valid if it matches the authority URL (without the well-known segment)
* or if it follows the CIAM issuer format.
*
* @return true if the issuer is valid, false otherwise
*/
boolean isIssuerValid() {
if (issuerFromOidcDiscovery == null) {
return false;
}

// Case 1: Check against canonicalAuthorityUrl without the well-known segment
String authorityWithoutWellKnown = canonicalAuthorityUrl.toString();
if (authorityWithoutWellKnown.endsWith(WELL_KNOWN_OPENID_CONFIGURATION)) {
authorityWithoutWellKnown = authorityWithoutWellKnown.substring(0,
authorityWithoutWellKnown.length() - WELL_KNOWN_OPENID_CONFIGURATION.length());

// Normalize both URLs to ensure consistent comparison
String normalizedAuthority = Authority.enforceTrailingSlash(authorityWithoutWellKnown);
String normalizedIssuer = Authority.enforceTrailingSlash(issuerFromOidcDiscovery);

if (normalizedIssuer.equals(normalizedAuthority)) {
return true;
}
}

// Case 2: Check CIAM format: "https://{tenant}.ciamlogin.com/{tenant}"
if (!StringHelper.isNullOrBlank(tenant)) {
String ciamPattern = String.format(CIAM_AUTHORITY_FORMAT, tenant, tenant);
return issuerFromOidcDiscovery.startsWith(ciamPattern);
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class OidcDiscoveryResponse implements JsonSerializable<OidcDiscoveryResponse> {
private String authorizationEndpoint;
private String tokenEndpoint;
private String deviceCodeEndpoint;
private String issuer;

public static OidcDiscoveryResponse fromJson(JsonReader jsonReader) throws IOException {
OidcDiscoveryResponse response = new OidcDiscoveryResponse();
Expand All @@ -32,6 +33,9 @@ public static OidcDiscoveryResponse fromJson(JsonReader jsonReader) throws IOExc
case "device_authorization_endpoint":
response.deviceCodeEndpoint = reader.getString();
break;
case "issuer":
response.issuer = reader.getString();
break;
default:
reader.skipChildren();
break;
Expand Down Expand Up @@ -61,4 +65,8 @@ String tokenEndpoint() {
String deviceCodeEndpoint() {
return this.deviceCodeEndpoint;
}

String issuer() {
return this.issuer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,9 @@ static void setHttpClient(IHttpClient client) {
httpClient = client;
httpHelper = new HttpHelper(httpClient, new ManagedIdentityRetryPolicy());
}

static void resetHttpClient() {
httpClient = new DefaultHttpClientManagedIdentity(null, null, null, null);
httpHelper = new HttpHelper(httpClient, new ManagedIdentityRetryPolicy());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ static void resetRetryPolicies() {
IMDSRetryPolicy.resetToDefaults();
}

@AfterAll
static void resetServiceFabricHttpClient() {
ServiceFabricManagedIdentitySource.resetHttpClient();
}

private String getSuccessfulResponse(String resource) {
long expiresOn = (System.currentTimeMillis() / 1000) + (24 * 3600);//A long-lived, 24 hour token
return "{\"access_token\":\"accesstoken\",\"expires_on\":\"" + expiresOn + "\",\"resource\":\"" + resource + "\",\"token_type\":" +
Expand Down
Loading