Skip to content
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

401 for OIDC WEB_APP when kid is not found #45582

Closed
antoniomacri opened this issue Jan 14, 2025 · 13 comments
Closed

401 for OIDC WEB_APP when kid is not found #45582

antoniomacri opened this issue Jan 14, 2025 · 13 comments
Assignees
Labels
area/oidc kind/bug Something isn't working

Comments

@antoniomacri
Copy link

Describe the bug

Hi have the following configuration:

quarkus.oidc.application-type=WEB_APP
quarkus.oidc.discovery-enabled=true
quarkus.oidc.auth-server-url=https://my-idp.com
quarkus.oidc.client-id=Oauth_Bla
quarkus.oidc.credentials.secret=...
quarkus.oidc.authentication.redirect-path=/app
quarkus.oidc.authentication.scopes=openid
quarkus.oidc.authentication.restore-path-after-redirect=true
quarkus.oidc.authentication.cookie-path=${quarkus.http.root-path}
quarkus.oidc.token.refresh-expired=true

My IdP is configured to rotate keys in the JWKS every 12 hours. After some time, the key is removed from the JWKS endpoint.

When a user tries to refresh an old page (for instance it has a tab in the browser from the previous day), as a consequence of the key not present in the JWKS, the client obtains a 401:

GET http://localhost:8080/app?p HTTP/1.1
Cookie: q_session=eyJhbGciOiJBMjU2R0NNS1ci...

HTTP/1.1 401 Unauthorized
content-length: 0
set-cookie: q_session=; Max-Age=0; Expires=Tue, 14 Jan 2025 15:04:19 GMT; Path=/

Expected behavior

Since the OIDC extension is configured as a WEB_APP, I do not expect a plain 401. I would expect either (obviously after the cookie reset which is already done):

  • using the refresh token to obtain new id/access token
  • returning a 401 with an HTML redirect
  • returning a 302 to the IdP for a new authentication

Actual behavior

The OIDC extension returns a 401 and the user (yes, business user which doesn't bother clicking F5 on the browser) perceives the application is not working.

How to Reproduce?

I don't have a reproducer, since I cannot share the IdP. However, these are the logs:

2025-01-14 15:58:28,035 DEBUG [io.qua.oid.run.OidcAuthenticationMechanism] (vert.x-eventloop-thread-3) q_session cookie set a 'Default' tenant id on the /app request path
2025-01-14 15:58:28,039 DEBUG [io.qua.oid.run.DefaultTenantConfigResolver] (vert.x-eventloop-thread-3) Registered TenantResolver has not provided the configuration for tenant 'Default', using the default tenant
2025-01-14 15:58:28,039 DEBUG [io.qua.oid.run.OidcAuthenticationMechanism] (vert.x-eventloop-thread-3) Resolved OIDC tenant id: Default
2025-01-14 15:58:28,039 DEBUG [io.qua.oid.run.CodeAuthenticationMechanism] (vert.x-eventloop-thread-3) Session cookie is present, starting the reauthentication
2025-01-14 15:58:28,040 DEBUG [io.qua.oid.run.DefaultTenantConfigResolver] (vert.x-eventloop-thread-3) Registered TenantResolver has not provided the configuration for tenant 'Default', using the default tenant
2025-01-14 15:58:28,059 DEBUG [io.qua.oid.run.OidcIdentityProvider] (vert.x-eventloop-thread-3) Starting creating SecurityIdentity
2025-01-14 15:58:28,059 DEBUG [io.qua.oid.run.DefaultTenantConfigResolver] (vert.x-eventloop-thread-3) Registered TenantResolver has not provided the configuration for tenant 'Default', using the default tenant
2025-01-14 15:58:28,060 DEBUG [io.qua.oid.run.OidcIdentityProvider] (vert.x-eventloop-thread-3) Verifying the JWT token with the local JWK keys
2025-01-14 15:58:28,062 DEBUG [io.qua.oid.run.OidcProvider] (vert.x-eventloop-thread-3) Verification of the token issued to client Oauth_Bla has failed: Unable to process JOSE object (cause: org.jose4j.lang.UnresolvableKeyException: JWK with kid '_aCzhB-kcDnI03iXg0tHW2WwT0c_RS256' is not available): JsonWebSignature{"alg":"RS256","kid":"_aCzhB-kcDnI03iXg0tHW2WwT0c_RS256"}->eyJhbGciOiJSUz...
2025-01-14 15:58:28,062 DEBUG [io.qua.oid.run.OidcIdentityProvider] (vert.x-eventloop-thread-3) No matching JWK key is found, refreshing and repeating the token verification
2025-01-14 15:58:28,100 DEBUG [io.qua.oid.run.OidcProvider] (vert.x-eventloop-thread-3) Verification of the token issued to client Oauth_Bla has failed: Unable to process JOSE object (cause: org.jose4j.lang.UnresolvableKeyException: JWK with kid '_aCzhB-kcDnI03iXg0tHW2WwT0c_RS256' is not available): JsonWebSignature{"alg":"RS256","kid":"_aCzhB-kcDnI03iXg0tHW2WwT0c_RS256"}->eyJhbGciOiJSUz...
2025-01-14 15:58:28,101 DEBUG [io.qua.oid.run.OidcIdentityProvider] (vert.x-eventloop-thread-3) Local JWT token verification has failed, attempting the token introspection
2025-01-14 15:58:28,102 DEBUG [io.qua.oid.run.OidcProviderClient] (vert.x-eventloop-thread-3) Get token on: https://my-idp.com/as/token.oauth2 params: token=eyJhbGciOiJSUz...
token_type_hint=access_token
 headers: user-agent=Vert.x-WebClient/4.5.11
content-type=application/x-www-form-urlencoded
accept=application/json
authorization=Basic ...

2025-01-14 15:58:28,151 ERROR [io.qua.oid.run.OidcProviderClient] (vert.x-eventloop-thread-3) Request https://my-idp.com/as/introspect.oauth2 has failed: status: 401, error message: {"error":"unauthorized_client"}
2025-01-14 15:58:28,151 ERROR [io.qua.oid.run.CodeAuthenticationMechanism] (vert.x-eventloop-thread-3) ID token verification has failed: Multiple exceptions caught:
        [Exception 0] io.quarkus.oidc.OIDCException: {"error":"unauthorized_client"}
        [Exception 1] io.quarkus.security.AuthenticationFailedException
2025-01-14 15:58:28,151 DEBUG [io.qua.oid.run.OidcUtils] (vert.x-eventloop-thread-3) Remove session cookie names: [q_session]
2025-01-14 15:58:28,155 INFO  [io.qua.htt.access-log] (vert.x-eventloop-thread-3) ip=127.0.0.1 realIp=- user=- method=GET requestUri="/app?p" query="?p" responseCode=401 responseBytes=- referer="-" userAgent="IntelliJ HTTP Client/IntelliJ IDEA 2024.2.3" requestId="-" timeMs="126"

Output of uname -a or ver

No response

Output of java -version

Java version: 21.0.5, vendor: Oracle Corporation

Quarkus version or git rev

3.17.6

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.8.8 (4c87b05d9aedce574290d1acc98575ed5eb6cd39)

Additional information

No response

@antoniomacri antoniomacri added the kind/bug Something isn't working label Jan 14, 2025
Copy link

quarkus-bot bot commented Jan 14, 2025

/cc @pedroigor (oidc), @sberyozkin (oidc)

@sberyozkin
Copy link
Member

sberyozkin commented Jan 14, 2025

@antoniomacri Thanks for providing the logs, indeed, the verification sequence, as confirmed by the logs (and is consistent with how Vert.x OIDC does it as well) is as follows:

  1. Check local matching JWK
  2. If no matching key is found, refresh JWK set, try to find it again.
  3. If no matching key is found in the refreshed JWK set, fallback to introspecting it remotely

The failure happens at step 3, your IDP returns unauthorized client when the introspection is attempted, Request https://my-idp.com/as/introspect.oauth2 has failed: status: 401, error message: {"error":"unauthorized_client"}

Please check a couple of things:

  • Why does the introspection request fail - perhaps your provider requires different credentials for the introspection endpoint ?
  • Try to disable the introspection, quarkus.oidc.token.allow-jwt-introspection=false - and check what happens.

Also, FYI, the discovery is enabled by default so enabling it explicitly is not required. The oidc scope is also always added by default...

@antoniomacri
Copy link
Author

Hi @sberyozkin, thank you for the quick response!

  • The introspection fails since it is not enabled on the IdP -- and that is as expected (performance reasons).
  • I've already tried to disable the introspection using that config, but the final error is the same, with slightly different logs:
2025-01-14 22:25:34,009 DEBUG [io.qua.oid.run.OidcIdentityProvider] (vert.x-eventloop-thread-2) JWT token does not have a matching verification key but JWT token introspection is disabled
2025-01-14 22:25:34,009 ERROR [io.qua.oid.run.CodeAuthenticationMechanism] (vert.x-eventloop-thread-2) ID token verification has failed: JWT processing failed. Additional details: [[17] Unable to process JOSE object (cause: org.jose4j.lang.UnresolvableKeyException: JWK with kid '_aCzhB-kcDnI03iXg0tHW2WwT0c_RS256' is not available): JsonWebSignature{"alg":"RS256","kid":"_aCzhB-kcDnI03iXg0tHW2WwT0c_RS256"}->eyJhbGciOiJSUz...]
2025-01-14 22:25:34,009 DEBUG [io.qua.oid.run.OidcUtils] (vert.x-eventloop-thread-2) Remove session cookie names: [q_session]
2025-01-14 22:25:34,010 INFO  [io.qua.htt.access-log] (vert.x-eventloop-thread-2) ip=127.0.0.1 realIp=- user=- method=GET requestUri="/app?p" query="?p" responseCode=401 responseBytes=956 referer="-" userAgent="IntelliJ HTTP Client/IntelliJ IDEA 2024.2.3" requestId="-" timeMs="3"

Thanks for the hints on defaut configs, but I'm not using the oidc scope, did you mean openid?

What do you think about the three possibile ways I mentioned above to handle the failure?

@sberyozkin
Copy link
Member

Hi @antoniomacri, yes, I meant openid...

in quarkus-oidc, AuthenticationFailedException is 302, in some cases I thought of returning AuthenticationCompletionException leading to 401 to avoid loops, etc. But I guess, when we re-authenticate the existing session cookie, then the re-authentication failures should always be 302 requiring authentication...

Refreshing the tokens without verifying signatures seems risky...

I'm not sure right now, it requires some thinking...

@sberyozkin
Copy link
Member

@antoniomacri Is it the case that when the ID token has non-matching kid, this ID token has already expired ? For example if JWKS are rotated every 12 hours then the ID token which was issued 13 hours back and therefore will have no matching kid, was only valid for 12 hours ?

@sberyozkin
Copy link
Member

I can also think of one workaround: add ExceptionMapper<AuthenticationCompletionException> and return JAX-RS redirect to same current Quarkus request path, it would force a redirect to the OIDC provider since the session cookie has been removed

@antoniomacri
Copy link
Author

antoniomacri commented Jan 15, 2025

@antoniomacri Is it the case that when the ID token has non-matching kid, this ID token has already expired ? For example if JWKS are rotated every 12 hours then the ID token which was issued 13 hours back and therefore will have no matching kid, was only valid for 12 hours ?

Yes, typically the ID token is expired when the kid is not found, since it lasts in minutes to hour (I don't remember exactly but I could check if you think it's helpful).

I can also think of one workaround: add ExceptionMapper<AuthenticationCompletionException> and return JAX-RS redirect to same current Quarkus request path, it would force a redirect to the OIDC provider since the session cookie has been removed

It seems that the Set-Cookie is ignored on 302 by some clients (including curl and IDEA REST client).

So, if the possibility to refresh the tokens is excluded, I think the only workaround is returning a 401 with HTML refresh...

@sberyozkin
Copy link
Member

@antoniomacri

It seems that the Set-Cookie is ignored on 302 by some clients (including curl and IDEA REST client).

I'd not be concerned about it, curl (which is CLI tool) or IDEA REST client is not what Quarkus OIDC web-app users would use in production.

if the possibility to refresh the tokens is excluded

May be it can done optionally, with the configuration (like refresh when no matching key is available...)

I think the only workaround is returning a 401 with HTML refresh

This is what the custom ExceptionMapper workaround mentioned above can do, to prepare a specific type of response.... I believe you can make it work right now, can you please experiment ?

@antoniomacri
Copy link
Author

Hi @sberyozkin, as you suggested I tried with 302 and it seems to work on most browsers. Also 401 with HTML redirect works.

I'll experiment a bit and see if there is any side effect on our apps.

if the possibility to refresh the tokens is excluded

May be it can done optionally, with the configuration (like refresh when no matching key is available...)

I didn't dig deeper, but that seems a good fallback to me.

Thanks!

@sberyozkin
Copy link
Member

Hi @antoniomacri Thanks, I think for the specific verification failure related to a non-matching key, we can indeed redirect by default.
Such a refresh seems possible, but users have to accept a risk that someone just modifies kid and sends the token and it will DOS the system.
I guess, in the end of the day, it is very reasonable if someone returns after a long inactivity spell and is asked to re-authenticate, refreshing tokens in such cases, without verifying the session token's validity, does appear to be problematic.

So, yes, I'll just do 302 if no matching key is available for users to re-authenticate.
For other verification failures like invalid signature, when the key is found, 401 will remain

Thanks

@sberyozkin sberyozkin self-assigned this Jan 16, 2025
@sberyozkin
Copy link
Member

@antoniomacri FYI, #45659

@sberyozkin
Copy link
Member

sberyozkin commented Jan 23, 2025

Hi @antoniomacri, this PR will be merged shortly, FYI, at the moment this 302 redirect is optional, not enabled by default, given an overall sensitivity related to the situation where a token whose signature can not be verified due to an unresolved key causes the re-authentication. It makes quite a perfect sense in your case, but it making it a default behavior for any session token which for some reasons does not have a matching key, appears to be somewhat risky.
We can revisit it after this 302 variation gets tried a bit in practice...

Thanks

@sberyozkin
Copy link
Member

Fixed by #45659

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/oidc kind/bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants