micronaut-security icon indicating copy to clipboard operation
micronaut-security copied to clipboard

Token Propagation and OpenID "azp" claim validation issues

Open ArnauAregall opened this issue 1 year ago • 3 comments

Issue description

Hello,

I have a use case where two Micronaut services are secured using OpenID (idtoken) with an OAuth2 issuer (Keycloak) within the same realm.

Each service is configured to use it's own realm client.

At application level, one service calls the other using HTTP client interfaces, using JWT Token Propagation feature.

identity-service:

micronaut:
  application:
    name: identity-service
  security:
    intercept-url-map:
      - pattern: /api/**
        access:
          - isAuthenticated()
    authentication: idtoken
    oauth2:
      clients:
        keycloak:
          client-id: '${micronaut.application.name}'
          client-secret: '${OAUTH2_IDENTITY_CLIENT_SECRET}'
          issuer: 'http://localhost:8082/realms/petclinic'

pet-service (calls identity-service):

micronaut:
  application:
    name: pet-service
  http:
    services:
      identity-service:
        url: "http://identity-service"
  security:
    intercept-url-map:
      - pattern: /api/**
        access:
          - isAuthenticated()
    authentication: idtoken
    oauth2:
      clients:
        keycloak:
          client-id: '${micronaut.application.name}'
          client-secret: '${OAUTH2_PET_CLIENT_SECRET}'
          issuer: 'http://localhost:8082/realms/petclinic'
    token:
      propagation:
        enabled: true
        service-id-regex: "identity-service"
@Client(id = "identity-service")
internal fun interface IdentityServiceHttpClient {

    @Get("/api/identities/{identityId}")
    @SingleResult
    fun getIdentity(@PathVariable identityId: UUID): Mono<HttpResponse<GetIdentityResponse>>

}

The OAuth clients have configured scopes so the aud claims of the JWT token contains the two client-ids.

Example decoded JWT payload:

{
  "exp": 1703581274,
  "iat": 1703580974,
  "auth_time": 1703580974,
  "jti": "98e88f17-683f-4ecc-8e70-b29ed6a604ab",
  "iss": "http://localhost:8082/realms/petclinic",
  "aud": [
    "pet-service",
    "identity-service"
  ],
  "sub": "b40af7e9-392d-40e9-9eb7-55993c9d2a8e",
  "typ": "ID",
  "azp": "pet-service",
  "nonce": "5a647d92-97e0-4bec-ba17-d8a116e93494",
  "session_state": "7c02f73a-e92b-4187-a70a-b49464e1c4fb",
  "at_hash": "5LMA6RsrsdBKtNk21bMfMA",
  "acr": "1",
  "sid": "7c02f73a-e92b-4187-a70a-b49464e1c4fb",
  "email_verified": false,
  "preferred_username": "system_test_user"
}

The issue I'm experiencing is the following:

The Authorized Party claim (azp) of the token is the pet-service, and when pet-service performs the authenticated HTTP call to identity-service endpoints using the Token Propagation feature, identity-service runs the io.micronaut.security.oauth2.client.IdTokenClaimsValidator and fails at the validateAzp step, even though the audiences validation is successful.

I've verified that disabling openid claims validation on identity-service via configuration is a bypass to the issue.

micronaut:
  security:
    token:
      jwt:
        claims-validators:
          openid-idtoken: false

I have also noticed recent clarifications in regards to azp were added to OpenID specs, and if I got it right the azp should only be validated when using extensions beyond the scope of the spec.

  • https://bitbucket.org/openid/connect/issues/973/
  • https://bitbucket.org/openid/connect/pull-requests/340/errata-clarified-that-azp-does-not-occur

As framework core committers, which approach would you recommend to solve this issue?

Do you believe IdTokenClaimsValidator#validateAzp should be revisited after OpenID spec clarifications perhaps?

Thanks a lot in advance. Arnau.

ArnauAregall avatar Dec 26 '23 09:12 ArnauAregall

Thanks for the detailed issue report. I need to check it further. However, I recommend you not turn off the whole idtoken validator.

Instead, you can do a Bean Replacement and override the method causing issues.

@Singleton
@Replaces(IdTokenClaimsValidator.class)
public class IdTokenClaimsValidatorReplacement extends  IdTokenClaimsValidator {

@Override
 protected boolean validateAzp(@NonNull Claims claims,
                                  @NonNull String clientId,
                                  @NonNull List<String> audiences) {
// do custom logic
}

}

sdelamo avatar Jan 04 '24 21:01 sdelamo

Thanks for your answer @sdelamo, I'll try the custom bean replacement approach and get back with feedback.

ArnauAregall avatar Jan 05 '24 15:01 ArnauAregall

Hello @sdelamo, I confirm the suggested bean replacement approach suits as temporal workaround for my issue.

Here is the aforementioned example but with Kotlin that worked fine for me with the custom azp claim validation.

@Singleton
@Replaces(IdTokenClaimsValidator::class)
class CustomIdTokenClaimsValidator<T>(oauthClientConfigurations: Collection<OauthClientConfiguration>): IdTokenClaimsValidator<T>(oauthClientConfigurations) {

    override fun validateAzp(claims: Claims, clientId: String, audiences: MutableList<String>): Boolean {
        if (audiences.size < 2) {
            return true
        }
        return parseAzpClaim(claims)
            .filter { clientId.equals(it, ignoreCase = true) || audiences.containsIgnoreCase(it) }
            .isPresent
    }

}

private fun List<String>.containsIgnoreCase(element: String): Boolean {
    return this.any { it.equals(element, ignoreCase = true) }
}

Thanks again for your answer!

ArnauAregall avatar Jan 07 '24 10:01 ArnauAregall