ch4mpy / spring-addons

Ease spring OAuth2 resource-servers configuration and testing
Apache License 2.0
521 stars 84 forks source link

Resource server token introspection - ClassCastException #153

Closed cezaryluksza closed 9 months ago

cezaryluksza commented 9 months ago

Hi

I decided to add token introspection to my resource server. I followed this tutorial: https://github.com/ch4mpy/spring-addons/blob/spring-addons-7.1.8/samples/tutorials/resource-server_with_introspection/README.md

And I'm getting this exception in introspectionAuthenticationConverter():

java.lang.ClassCastException: class java.time.Instant cannot be cast to class java.lang.Integer (java.time.Instant and java.lang.Integer are in module java.base of loader 'bootstrap')
at com.c4_soft.springaddons.security.oidc.starter.synchronised.SpringAddonsOidcBeans.lambda$introspectionAuthenticationConverter$2(SpringAddonsOidcBeans.java:115)
at org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider.authenticate(OpaqueTokenAuthenticationProvider.java:109)

Attributes from my token: iat -> {Instant@22266} "2023-11-06T10:48:02Z" exp -> {Instant@22264} "2023-11-06T20:48:00Z"

I use keycloak token introspection endpoint.

Expected behavior I think converter shouldn't assume that these attributes are castable to integers, but please correct me if I'm wrong.

Sample token JSON payload after anonymization (generated random UUID):

{
  "exp": 1699312319,
  "iat": 1699277575,
  "auth_time": 1699276319,
  "jti": "f260b55c-bb20-4976-be99-76d565944261",
  "iss": "XXX",
  "aud": [
    "account"
  ],
  "sub": "af298c5e-201d-40a9-a570-8bf06c4ec700",
  "typ": "Bearer",
  "azp": "XXX",
  "nonce": "69f9cff0-4178-41a3-a500-0721744e7002",
  "session_state": "60f12d89-721e-4598-bbf5-eaa7368a699c",
  "acr": "0",
  "allowed-origins": [
    "*"
  ],
  "realm_access": {
    "roles": [
      "offline_access",
      "default-roles-XXX",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "XXX": {
      "roles": [
        "XXX"
      ]
    }
  },
  "scope": "openid profile email",
  "sid": "7e202aad-3447-4bc4-bf86-7a4b870a4466",
  "email_verified": true,
  "name": "XXX",
  "preferred_username": "XXX",
  "given_name": "XXX",
  "family_name": "XXX",
  "email": "XXX"
}
ch4mpy commented 9 months ago

This is a tricky question: the JWT spec states that the JSON value in the token payload should be a NumericDate (the number of seconds since January the 1st 1970 UTC), but there is nothing about the Java representation of the parsed JSON...

I am almost sure that Spring didn't map the value at the time the default OpaqueTokenAuthenticationConverter was written (accessing the iat claim was returning the number of seconds since EPOCH).

It would certainly be safer if this authentication converter was adapting to the type of value returned by the Java representation of the token claims.

Edit

The spec I link about JWTs is completely irrelevant: the introspected token can be in any format. What we are interested in is the format of the payload for the token introspection endpoint. But, according to this spec, the iat should be an integer in the introspection response too.

Got it: the claims are altered in SpringOpaqueTokenIntrospector::convertClaimsSet, which is private (among other things, Long values with seconds since EPOCH are replaced with Instant instances).

As each introspector implementation could have its own claim alteration rules, I'll have the default authentication converter accept Long, Instant and Date values for exp, iat.

ch4mpy commented 9 months ago

@cezaryluksza I just released a fix in 7.1.13. Please confirm it solves your problem and close.

cezaryluksza commented 9 months ago

It works, thank you!