opa: JWKS validation cannot be configured to enforce that a token is considered valid, if any only if the token's signature can be validated using the key with the kid provided in the token

Assumption: The token validation is configured to use a JSON Web Key Set (JWKS) for signature validation.

Current behavior (tested with Opa 0.32.1): If the token’s signature can be validated with any public key provided in the JWKS, then the token’s signature considered to be valid. This is acceptable with RFC 7515 see Appendix D. Notes on Key Selection (https://datatracker.ietf.org/doc/html/rfc7515#appendix-D).

Appendix D.  Notes on Key Selection
...
   2.  Filter the set of collected keys.  For instance, some
       applications will use only keys referenced by "kid" (key ID) or
       "x5t" (X.509 certificate SHA-1 thumbprint) parameters.  If the
       application uses the JWK "alg" (algorithm), "use" (public key
       use), or "key_ops" (key operations) parameters, keys with
       inappropriate values of those parameters would be excluded.
       Additionally, keys might be filtered to include or exclude keys
       with certain other member values in an application-specific
       manner.  For some applications, no filtering will be applied.

   3.  Order the set of collected keys.  For instance, keys referenced
       by "kid" (key ID) or "x5t" (X.509 certificate SHA-1 thumbprint)
       parameters might be tried before keys with neither of these
       values.  Likewise, keys with certain member values might be
       ordered before keys with other member values.  For some
       applications, no ordering will be applied.
...

Requested behavior: Support to configure a “strict” token validation as follows: When using a JWKS for token validation, then a token’s signature must only considered to be valid, if and only if the token’s signature can be validated using the public key from the JWKS which as the same “kid” as the “kid” in the access token header. Note: In the section quoted above, the RFC states also that the “kid” (or other parameters) maybe used to “include or exlude” certain keys.

Background: The topic came up when comparing the token validation of Open Policy Agent with Spring Boot / Spring Security. The later shows the requested by behavior by default (no configuration option).

Test case (works also in playground)

package jwks

jwks := `{"keys":[{"kid":"right", "kty":"RSA", "use":"sig", "alg":"RS256", "e":"AQAB", "n":"sO7oRm0lkeuE4xEinffrR16XW7f0yAD-zNJV2WmmfqURxflhd9iy21CShCzAJZneRwVOGG3UKcmRn6AWrdR1NJE26Y6A8bgrrg6Ssl4o8QT5cLXNouKHQBTRwec0l_Y3RZIcCC2yJ3mJjZXiwWjFVFqZWM3h2JLK2Fr4z0GR2TKgoTLj2-9EeNyptVKNPxVvFLEE3-2YAA6CETBNmsl-J3hHFNea_LX5xB4Sq65NRYEPiAi0e4cEcMwAgePwa0gZRScSjp851vfkSJPPQe-dkkcOT52TtIPwQPOmnNtcAoUhVH0WKHBhG7Pjxe8ppd4zkg-qLbrWMBVhDsPxSYvWlGPyeExyUcInEwllOtlyKzVw9exX5-UDGoELYWC5kT0V7M17bPJKQk0QKq-n6ag03jI0WtI6I1jZcYICGALV6qOSMkPZTEX9Hh1cFXXSnUjzahtWHV6pjmAG7Qi7u8FD9sv532Y1N_rtVjXGhdkfFZ_sQK_3K7ZesK1kVZ_XMe3Ea_1k1Os2JBt5hoG4xF-CsYqQerxFZ5COezNqXIuMIJ0lXn-WpVqbGZ5ZdyGhhjZUNsxmtVKmkPWmO1zHnwKFbEcV5P2MawhlknV8kmlHBbPKbeT1vAs-bSy-5jGSWFfsDLY4tMHyXkGQsccgeyDKrPb9rG09YTAmlWv_VbuKAJk"}]}`

token = {"valid": valid, "payload": payload} {
    [valid, _ , payload] := io.jwt.decode_verify(input.encoded_token, {"cert": jwks})
}


# Test with RS256 signed-JWT with a token header indicating it was signed with a key with "kid" = "right".
# token.valid is expected to be true, 
# as the JWT contains "kid" = "right" and JWT's signature can be validated with the configured JWKS using the key with "kid" = "right".
# Note: The JWT was signed with the private key belonging to the public key in the JWKS.
test_right_kid_in_token {
  ip := {
     #JWT Header: {"alg": "RS256","typ": "JWT","kid": "right"}
     "encoded_token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJpZ2h0In0.eyJzdWIiOiJZV3hwWTJVPSIsImlzcyI6InRlc3Rpc3N1ZXIiLCJuYmYiOjE1MTQ4NTExMzksImV4cCI6MTc0MTA4MTUzOX0.F4Gx1fnBneQLQEqvCtE_eZT-1vMJAT7Wn1vXmfDdqL6hNYNZXAA-D7qIpasYE0Iaek7xNdICYMljWkm-fOSUNXi2WAw6JwFpwzFBb4Y336pOBiuLDKOW7xaw_52u6HWa0ItY8fpX_aJno1-1Hq95Dt8h-Oq1jsp6oliHuIQxaQZE6C8N1UU8ajID1vb2xPAm3X6NiOpSJH7iWOzX2N7t_7MRfmtZjCUkpK7_DgFZe7u7-yjUEbekA5_bIAMHb9BJYUk3N3IvjOvmmhtrBcMhv_zCeca8Y4BO4K8uSg42WqopDU33fePxbhj_1kD01M7u50Nyu-XqxduL7DWTVmUW98Wef9qC42XjMqI41zHzg1eP3xwxZ3LrLAirwrqjDyDWtU0GmHpbiUoaYDRqJxXIJwnL2RbzP77Kg9-lXd1e7jEKeJ3RY8MPS9LMOaTj8MNqL_kEsoJ3CSNpuPPER0YMiMIXIWIlAeFfYQstqAHtMjj79518xdliwV9AmQQBdKj09J1cXuoKSpBRVntIzD-67yVbTYUOK6hLHKzc8QgbNk54--6XGnYJJ8B0-liEH2wm_BCyRaZBaPmZEI7CfOo01jhdCflxikjmxekPnv4ypPkEnlL1CClCH1S4ojEmJ6rgI7mxXS04gvPzLBDTcBrMSdtQQlIa-vLoqAQq3tK9b0I"
  }
  token.valid with input as ip
}

# Test with RS256 signed-JWT with a token header indicating it was signed with a key with "kid" = "wrong"
# token.valid is expected to be FALSE (at least in the "STRICT" mode when key-ids are compared),
# as the JWT contains "kid" = "wrong" and JWT's signature can NOT be validated with the configured JWKS using the key with "kid" = "wrong" ,
# as there is no key with "kid"="wrong" in the JWKS.
# Yet, token.valid is TRUE as the token can be validated with a key that is part of the JWKS.
# Note: The JWT was signed with the private key belonging to the public key with kid "right" in the JWKS
# (only the kid claim in the token is not equal to the kid claim of that public key in the JWKS).
test_wrong_kid_in_token {
  ip := {
    #JWT Header: {"alg": "RS256","typ": "JWT","kid": "wrong"}
    "encoded_token" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Indyb25nIn0.eyJzdWIiOiJZV3hwWTJVPSIsImlzcyI6InRlc3Rpc3N1ZXIiLCJuYmYiOjE1MTQ4NTExMzksImV4cCI6MTc0MTA4MTUzOX0.jttzvndLzIUXrrKNAs5n4v0AeTWEX77LQBJL5O2-zRkA4ILB2kKOWi43x66zmfqu2Zi0KatazZftlbvIwDr2VaT6UhtrCEZxdVAnX_fnNt2D2dg9hZ6_B27W4ZZE59rmzar0gY2sNFK_VkvYetVkD1-5gB3IM1zQgn3HCIeR3D0O5I-AHm1fedZPuZI8zFCFHb5yo7I-HwOmp2wZEI2Bsi7lRtAGmtck0lhwyd7sIcks42Ma6rUTo0CYfX1GhHiswBWhQoS6LChuUIuBwlczJxiJlZ6eqcbPcmrKoG__UVG9UpAIbmZhqaNixd0YKQdVLmEHprF6dBiuwlTtuImQUPmz8f9DMAl13w18W6dPyebkLbDhM-rxmC8RRPJy8wJhgrXdZLEI6xE3kbHAyw8k5f5ExSALCHhlV5h-W-YSrHeeoIapaIZLGwWnnnbEBdJYd6E23-KQS9XgEkZGBNw5T-Kt5it_GFRX-x4jiq_pbpOgjg_1E5c5U_lKE6CyVYeZes08c0cVcs1IPBaP8I2S80tg8Wb1UC0znFyIaSZ7RcFdniVkVDlZ1Z2L0bHu1bLNOk7mY8_eAqDRo9VvMuhZKluW-ndDReM-9Pdv1kOXhs2PD7s-8O_bpW35aVkLYgTzzoLM6YLFy87-5Jl8IdyzUzrqt1avd3hPQu88iBCml4M"
  }
  not token.valid with input as ip
}

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 16 (8 by maintainers)

Most upvoted comments

Sounds good! Let’s keep it open for a while and see if others would like this addition. Thanks for an interesting conversation 👍

`As have been shown, comparing the kid header to the ones in a JWKS is still very much doable, should one want that.

Yes, for me the solution we came up with above is ok. The additional marshalling might become or might not become a performance topic, but for now, I’m fine with this.