Expected Behavior

Access token validation passes on the Spring Security based resource server for an ADFS based OIDC provider.

Current Behavior

Access token validation fails with an issuer mismatch.

Context

I'm trying to create a custom JwtDecoder to work with an ADFS based Open ID Connect provider. Why does it have to be custom? ADFS uses the non-standard field access_token_issuer which may be different from the actual issuer. This creates the problem, that all access token validations on the resource server fail with an iss claim mismatch. I understand this is not a spec-compliant field and thus one could argue it's not supposed to be implemented in the core.

Now the solution is a custom JwtDecoder configuration that matches another issuer. Sadly a simple call to JwtDecoders.fromOidcIssuerLocation does not work, since the access_token_issuer won't have configuration at .well-known. Essentially I need the regular JwtDecoder that Spring creates anyways and change it's JwtIssuerValidator to use the access_token_issuer from the .well-known configuration.

This is is generally simple, the only hurdle I'm facing is that I would need to re-implement the logic to fetch the OIDC configuration since all the methods on JwtDecoderProviderConfigurationUtils are package-private or private. Instead of copying that whole thing I'm just hard-coding the issuer right now.

@Bean
public JwtDecoder jwtDecoder() {
    NimbusJwtDecoder decoder = (NimbusJwtDecoder) JwtDecoders.fromOidcIssuerLocation(oauthConfig.getIssuerUri());
     // Notice how this is hard coded even though .../.well-known/openid-configuration provides it
    decoder.setJwtValidator(JwtValidators.createDefaultWithIssuer("http://provider.com/adfs/services/trust"));
    return decoder;
}

Here is what I would think is the correct approach that actually uses the field from the configuration. This is just what JwtDecoders.fromOidcIssuerLocation does with a change for the JwtIssuerValidator parameter. Sadly the issuer passed to fromOidcIssuerLocation is reused as the match issuer for JwtIssuerValidator.

@Bean
public JwtDecoder jwtDecoder() {
    // Doesn't work because the method is protected
    Map<String, Object> configuration = JwtDecoderProviderConfigurationUtils.getConfigurationForOidcIssuerLocation(oauthConfig.getIssuerUri());
    // Doesn't work because the method is protected
    // This would pass since the "issuer" field on the configuration is fine. It's not part of the issue.
    JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, oauthConfig.getIssuerUri());
    OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(configuration.get("access_token_issuer").toString());
    NimbusJwtDecoder jwtDecoder = withJwkSetUri(configuration.get("jwks_uri").toString()).build();
    jwtDecoder.setJwtValidator(jwtValidator);

    return jwtDecoder;
}

This is technically a bit longer, but it's also not using a hardcoded String literal for the issuer. The only thing stopping this approach are package private utility methods that could be (in my opinion) public. Yes I could copy the whole utility class and use my copy, but that seems like unnecessary copy-paste. JwtDecoderProviderConfigurationUtils is a fully static class (that also isn't final for some reason) and could expose public methods without issue.

Comment From: jzheaux

@brfrth, thanks for the detailed explanation.

The nice thing about keeping JwtDecoderProviderConfigurationUtils package-private is it gives a location for placing common code without needing to publish it as part of the Spring Security API. For now, I think it's best to keep it package-private.

Additionally, you might find that the utils class does quite a bit more than you need for your application.

For example, I believe you can use Nimbus directly to get the behavior you need:

@Bean 
JwtDecoder jwtDecoder() {
    OIDCProviderMetadata metadata = OIDCProviderMetadata.resolve(new Issuer(oidcConfig.getIssuerUri()));
    String issuer = metadata.getCustomParameter("access_token_issuer").toString();
    String jwkSetUri = metadata.getJWKSetURI().toString();
    OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefaultWithIssuer(issuer);
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
    jwtDecoder.setJwtValidator(jwtValidator);

    return jwtDecoder;
}

Since I believe the above will address your issue, I'm going to close this. However, please feel free to continue to post if you feel there's more to discuss.