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

OIDC ID token introspection #36563

Closed
calvernaz opened this issue Oct 18, 2023 · 37 comments · Fixed by #36935
Closed

OIDC ID token introspection #36563

calvernaz opened this issue Oct 18, 2023 · 37 comments · Fixed by #36935
Assignees
Labels
area/oidc kind/bug Something isn't working
Milestone

Comments

@calvernaz
Copy link

Describe the bug

Before starting the issue I'm facing, I must say I've looked around and tried everything possible before creating this issue, I feel like i'm facing a very particular issue given the environment I'm in.

We are evaluating quarkus-oidc and we have a in-house OIDC provider.

OIDC Provider:

  • does not offer an introspection endpoint
  • the JSON Web Key Set (JWKS) endpoint (jwks_uri) requires authentication.
  • we want to use local introspection, we will make an authenticated call to jwks_uri and cache the public keys temporarily, to validate the token

Flow:

  1. ID and access tokens are fetched once the code flow is completed.
  2. Both of these tokens are kept in the session cookie
  3. ID token is the primary token which we verify on every user request, we get the roles from it, etc.

Problems:

  1. If we enable discovery, I can avoid an error trying to get metadata from jwks_uri (authentication required) but fails in step number 3 of the flow, given that ID tokens are always verified and tries to use the introspection endpoint which we don't have.
  • quarkus.oidc.discovery-enabled=true
  • quarkus.oidc.token.require-jwt-introspection-only=true
  1. If we disable discovery, fails to fetch jwks_uri (authentication required)
  • provide all the endpoints (jwks_uri, token, authorize)
  • quarkus.oidc.token.allow-jwt-introspection=false
  • quarkus.oidc.token.require-jwt-introspection-only=false

Again, this seems very specific, but would be possible to have no need for remote introspection and avoid trying to fetch the metadata from jwks_uri on boot or even during step 3 of the flow.

Thanks

Expected behavior

No response

Actual behavior

No response

How to Reproduce?

No response

Output of uname -a or ver

No response

Output of java -version

No response

Quarkus version or git rev

No response

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

No response

Additional information

No response

@calvernaz calvernaz added the kind/bug Something isn't working label Oct 18, 2023
@quarkus-bot
Copy link

quarkus-bot bot commented Oct 18, 2023

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

@sberyozkin
Copy link
Member

Thanks for opening this issue, I'm away from my laptop, but we'll sort it out, we may support jwks uri endpoint authentication like we do for the introspection endpoint , etc

Will add more comments tomorrow

Cheers

@sberyozkin
Copy link
Member

sberyozkin commented Oct 18, 2023

@calvernaz I've read the description again, IMHO adding quarkus.oidc.* configuration option to use an authenticated JWK set request should resolve the problem, as I said we already allow to have a custom authentication request to the introspection endpoint, so it seems easiest to me to allow the same for the JWKS URI endpoint

Do you agree ? Please provide some more details if yes, what type of authentication it is, does it require the use of client id and secret, Basic authentication or another dedicated pair ? Quarkus OIDC will manage the keys itself, refresh them if needed.

@sberyozkin
Copy link
Member

Here is another option which we use for pure OAuth2 providers which won't have introspection and jwks uri endpoints, we say quarkus.oidc.authentication.id-token-required=false - which means, that since the ID token is not available, and only access token is available, an indirect access token verification is required by posting to a UserInfo endpoint (whatever endpoint represents some user profile etc).

The endpoint then just works with the injected UserInfo - an internal ID token is generated simply to manage the session and one can cache this UserInfo in the generated ID token as a dedicated claim, to avoid requiring a remote UserInfo request on every request.

In your case, ID token will also be returned, but I think it should still work, quarkus.oidc.authentication.id-token-required=false, if your provider has a user info endpoint.

I'd still prefer to enhance a bit quarkus-oidc to allow an authenticated access to the JWKS URI, but I can help with the 2nd option if it can of interest

@calvernaz
Copy link
Author

calvernaz commented Oct 18, 2023

Hi @sberyozkin thanks for coming back so quickly!

Will comment from the end,

Please provide some more details if yes, what type of authentication it is, does it require the use of client id and secret, Basic authentication or another dedicated pair ? Quarkus OIDC will manage the keys itself, refresh them if needed.

The authentication uses a signed JWT token passed as an authorization header. We would need a way to handle that signing since it's a custom code too.

IMHO adding quarkus.oidc.* configuration option to use an authenticated JWK set request should resolve the problem, as I said we already allow to have a custom authentication request to the introspection endpoint, so it seems easiest to me to allow the same for the JWKS URI endpoint

Totally agree, letting quarkus manage the lifecycle of the set and call that custom code used to get the key set or provide the signed token.
I also must say that the request to download the jwks, requires some custom headers so I can't this being only quarkus.oidc.* configuration, unless we really make it work on that extend.

@calvernaz
Copy link
Author

Here is another option which we use for pure OAuth2 providers which won't have introspection and jwks uri endpoints, we say quarkus.oidc.authentication.id-token-required=false - which means, that since the ID token is not available, and only access token is available, an indirect access token verification is required by posting to a UserInfo endpoint (whatever endpoint represents some user profile etc).

The endpoint then just works with the injected UserInfo - an internal ID token is generated simply to manage the session and one can cache this UserInfo in the generated ID token as a dedicated claim, to avoid requiring a remote UserInfo request on every request.

In your case, ID token will also be returned, but I think it should still work, quarkus.oidc.authentication.id-token-required=false, if your provider has a user info endpoint.

I'd still prefer to enhance a bit quarkus-oidc to allow an authenticated access to the JWKS URI, but I can help with the 2nd option if it can of interest

I can try this, in fact, I might have done it, but failed somewhere (will try again tomorrow) but I'd use this as a fallback option. Thanks again.

@sberyozkin
Copy link
Member

The authentication uses a signed JWT token passed as an authorization header. We would need a way to handle that signing since it's a custom code too.

Is it the same one as either the client_secret_jwt or private_key_jwt method ? These options are supported natively in Quarkus for OIDC provider endpoints which require the authentication:

https://quarkus.io/guides/security-oidc-code-flow-authentication#oidc-provider-client-authentication

If it were the case then we could enable it for the JWKs uri endpoint with a boolean flag.

The extra headers can be already configured for the token endpoint I believe - as long as those headers are static, so the same headers can be sent to all provider endpoints

Having a pluggable JWKS set support is an option too - it needs to be investigated

@calvernaz
Copy link
Author

The authentication uses a signed JWT token passed as an authorization header. We would need a way to handle that signing since it's a custom code too.

Is it the same one as either the client_secret_jwt or private_key_jwt method ? These options are supported natively in Quarkus for OIDC provider endpoints which require the authentication:

https://quarkus.io/guides/security-oidc-code-flow-authentication#oidc-provider-client-authentication

If it were the case then we could enable it for the JWKs uri endpoint with a boolean flag.

The extra headers can be already configured for the token endpoint I believe - as long as those headers are static, so the same headers can be sent to all provider endpoints

Thanks, so regarding the headers (static), there is a way already to pass those to all requests, or just the token for now?

@sberyozkin
Copy link
Member

There is a dedicated CodeGrant configuration group with the headers property which will be sent to the token endpoint but we can have headers property which would be documented to say that these headers will be sent to all of the OIDC provider's endpoints, for example, quarkus.oidc.headers

In general I prefer to let users enable things with the configuration only, though the configuration is getting quite saturated, that said, it looks like a reasonable customization option that can be done the at config level.

So, if either client_secret_jwt or private_key_jwt is what you use for the JWKS endpoint authentication, and and extra map of headers property will suffice then I can implement it.

Otherwise, I'll need to make sure the JWKS acquisition is customizable - which in itself maybe a good idea anyway

Let me know what you prefer please

@calvernaz
Copy link
Author

Indeed, the authentication for our JWKS endpoint is using private_key_jwt with the extra headers :)

Thanks for tackling this 👍

@sberyozkin
Copy link
Member

@calvernaz Np, thanks for the confirmation, should be an easy enough update

@sberyozkin sberyozkin self-assigned this Oct 19, 2023
@sberyozkin
Copy link
Member

sberyozkin commented Oct 19, 2023

@calvernaz Can you clarify one thing please - do you use the same authentication mechanism for the token endpoint ? I.e, private_key_jwt is only specific to the JWKS URI one or used to complete the authorization code flow too ?

@calvernaz
Copy link
Author

calvernaz commented Oct 19, 2023

@sberyozkin for the token endpoint we are using client_id + client_secret. That bit is actually working fine - but if I get in our head you may be thinking if these could have the same type of authentication, and it potentially could all be private_key_jwt but that's not what it's happening at the moment.

@sberyozkin
Copy link
Member

sberyozkin commented Oct 19, 2023

@calvernaz OK, thanks. That does complicate things, we'd need to introduces a dedicated configuration group for the JWKS endpoint authentication alone which would cover everything that the current group covering the token endpoint authentication does. (Just a note to myself: the same should've happened for the introspection endpoint instead of offering a basic authentication alternative only - but it can be deprecated)

OK, I'll see how it goes. I may have to re-prioritize on the idea of a pluggable JWKS provider - which I'd like to avoid - if users will have to write their own private_key_jwt code, etc, then it would not help supporting the message that we'd like Quarkus security be as accessible as possible.

I'll keep you up to date, thanks for the interesting enhancement request. By the way, I'll update this issue to be just that, an enhancement request, so that we can mark it as a release noteworthy feature once it is implemented, and tweak the issue name a bit for users to see clearer what we are trying to do here, hope you won't mind

Cheers

@sberyozkin sberyozkin added kind/enhancement New feature or request and removed kind/bug Something isn't working labels Oct 19, 2023
@sberyozkin sberyozkin changed the title OIDC ID token introspection Support for OIDC JWK set endpoint authentication Oct 19, 2023
@sberyozkin
Copy link
Member

Done, this is exactly what I believe the task is about: support retrieving keys from the JWK set endpoint, which is typically public, hence it can qualify as an enhancement request

Any concerns - please tweak the issue as you prefer, cheers

@sberyozkin
Copy link
Member

@calvernaz I'm also assuming that it is the quarkus.oidc.client-id which will be fed into the private_key_jwt token creation for the JWKS endpoint authentication

Thanks

@calvernaz
Copy link
Author

calvernaz commented Oct 20, 2023

@sberyozkin unfortunately it's not in our case we use a client-name that it's a human readable name representation of that client - basically the tuple (client_name, client_id, client_secret) is what we work with, client_id and client_secret for the token auth, and the JWT generation for certs we use client_name.

So client_name is what you are looking for in our case. The reason being we have generated the certs to this client name (edit)

@sberyozkin
Copy link
Member

@calvernaz OK, so let me make sure this can be covered with the existing configuration.

As far as private_key_jwt option is concerned, https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication, the audience, issuer, subject, all of these 3 properties can be customized, see
https://quarkus.io/guides/security-oidc-code-flow-authentication#additional-jwt-authentication-options,

so you'd configure client-name and refer to it to have the iss and sub claims customized, so it looks like we have it under control

@sberyozkin
Copy link
Member

@calvernaz I've been prototyping some code today and then I've realized that I should've asked for more details.

Is it an HTTP POST request in your case ? How else would you pass the JWT assertion to the JWKS endpoint, as a query parameter ?

I'm starting to think that letting users customize JWKS acquisition and also make the utility code for creating a signed assertion may be the right solution after all, but please provide more details first.

Thanks

@sberyozkin
Copy link
Member

This is an interesting case all right, for Apple we did a custom JWT assertion scheme support, so as long it does not become a highly custom code path for this case, we can do something like that, but I'm now at the decision junction point, so I'd appreciate more details:

  • Is it POST or GET, if POST then how are client_assertion and client_assertion_type parameters are passed, as form properties or as headers ?
  • Likewise, if it is GET, are these parameters passed as query parameters, as custom headers or as the custom Authorization authentication scheme - if the scheme - what is the format
  • If these parameters are passed as headers, with either GET or POST, is it what you meant when we were talking about the static headers, or the static headers is an extra requirement ?

We might be able to generalize it, but if JWT set acquisition interface will be introduced, at least I'll try to make some token signing code reusable, I'm assuming you have your own code in place, but may be Quarkus can be of some help there, lets see how it goes

Lets continue next week,
Cheers

@calvernaz
Copy link
Author

calvernaz commented Oct 20, 2023

This is an interesting case all right, for Apple we did a custom JWT assertion scheme support, so as long it does not become a highly custom code path for this case, we can do something like that, but I'm now at the decision junction point, so I'd appreciate more details:

  • Is it POST or GET, if POST then how are client_assertion and client_assertion_type parameters are passed, as form properties or as headers ?

It's a GET request and we values are passed as headers.

  • Likewise, if it is GET, are these parameters passed as query parameters, as custom headers or as the custom Authorization authentication scheme - if the scheme - what is the format

Headers:

  • Authorization: ID ey....

The token is signed with private key and adds custom claims to it besides sub and a full certificate chain (embedded). The certificate chain is something that I never mention before but it's also present.

  • If these parameters are passed as headers, with either GET or POST, is it what you meant when we were talking about the static headers, or the static headers is an extra requirement ?

The extra headers are custom identifier required for the request to be valid.

GET /certs

  • Authorization: ID ey...
  • X-HHH: foobar

We might be able to generalize it, but if JWT set acquisition interface will be introduced, at least I'll try to make some token signing code reusable, I'm assuming you have your own code in place, but may be Quarkus can be of some help there, lets see how it goes

Lets continue next week, Cheers

I wonder if a alternative until we get this all scoped out, is to provide a configuration to turn off introspection local and remote and leave to the service to handle the JWKS and the validation process. This would let custom implementation that are a bit more involved to work until we get that JWT acquisition interface.

@sberyozkin
Copy link
Member

Thanks for the explanation.

I wonder if a alternative until we get this all scoped out, is to provide a configuration to turn off introspection local and remote and leave to the service to handle the JWKS and the validation process. This would let custom implementation that are a bit more involved to work until we get that JWT acquisition interface.

But it would require introducing its own interface for Quarkus delegate the token verification to a 3rd party including managing the signature verification, various properties like issuer, time to live, this is the core of the quarkus-oidc functionality.

Let me investigate more next week what can be done

@sberyozkin sberyozkin added kind/bug Something isn't working and removed kind/enhancement New feature or request labels Oct 21, 2023
@sberyozkin sberyozkin changed the title Support for OIDC JWK set endpoint authentication OIDC ID token introspection Oct 21, 2023
@sberyozkin
Copy link
Member

@calvernaz I've thought more about it and decided to restore the original issue description and qualification, sorry for messing it up.

I've opened a PR, #36609, showing how your suggestion to leave to the service to handle the JWKS and the validation process can be implemented right now, I've totally forgotten that the security architecture allows plugging in custom IdentityProviders. For example, you can fetch the keys at the postconstruct step in a custom provider and then do the custom validation.

I've also opened smallrye/smallrye-jwt#733 to make it straightforward to set the cert chain claim, even though you may have the working code, you might want to give https://quarkus.io/guides/security-jwt-build a try in the future once the smallrye-jwt issue is fixed

Finally I've opened a dedicated enhancement request, instead of hijacking this issue, #36610, to support customizing the JWK set acquisition process only, example, one would get Vert.x WebClient already initialized with proxy, tls properties, etc.

I'd like to resolve this issue as not a bug once #36609 is merged then we can discuss some additional identity provider customization details in Discussions/Zulip, and in meantime I'll be looking at the #36610 enhancement.

Does it sound good to you ? If you'd like we can keep this issue open until you confirm yourself the custom identity provider works for you

@sberyozkin
Copy link
Member

#36609 has been closed - please give registering a custom IdentityProvider a try as shown in #36609 and let me know if that works in the short term for you

@sberyozkin
Copy link
Member

sberyozkin commented Oct 23, 2023

@calvernaz FYI, #36634, that should simplify things significantly for your case, you'd only need to set an Authorization header with the generated token and add other headers as required, and before 3.6.0 is released, using a custom IdentityProvider as shown in #36609 will help with the more involved customization

@calvernaz
Copy link
Author

Hi @sberyozkin sorry for the radio silence, there was a lot to catch up since :)

So, to uptake your changes what do I have to update on my side (version-wise) or if I understand correctly it's already supported and you have a test to prove it.

Yes, I'll give it a try and if it works (or not) I'd like to understand a bit better the enhancements you are proposing. Thanks a lot 👍

@sberyozkin
Copy link
Member

sberyozkin commented Oct 23, 2023

Hi @calvernaz, np at all, so 2 options are proposed.

One is the workaround around the current limitation, you just register a custom IdentityProvider, exactly as shown in #36609 (see also application.properties how to trick OIDC recorder not to fetch JWK keys itself), it will work right now without having to wait for any releases. In this provider, in a @PostConstruct method, you can get the JWK keys pulled in, and in authenticate - get the token from the TokenCredential, verify and then have SecurityIdentity returned - you can copy and paste the code from the test identity provider in #36609.

#36634 has now been merged too, so once 3.6.0.CR1/3.6.0 is out, the only thing you'd need to do is to register something like:

@ApplicationScoped
@Unremovable
public class JwksEndpointAuthenticator implements OidcClientRequestFilter {
     @Inject 
     OidcConfigurationMetadata oidcMetadata;
     public void filter(HttpRequest<Buffer> request, Buffer body) {
           if (request.uri().equals(oidcMetadata.getJsonWebKeySetUri()))) {
                request.putHeader("Authorization", "ID " + createJwtToken());
               // add more headers as needed
           }
     }
}

Quarkus itself will deal with JWK keys, parsing them, refreshing them, dealing with the OIDC client. May be we'll need to tweak a bit which algorithms can be supported but it is already quite comprehensive. You'd only have the token generated. May be the next version of Quarkus JWT build API can be of help with the token creation - it would do right now except that no typed support for setting the cert chain is available yet, so you'd need to create a base64 rep of the chain manually, which won't be needed later.

Hope it clarifies things.

Can you please give a custom IdentityProvider a quick try in the next few days ? I'll resolve this issue once you confirm it can keep you going for now. I recommend using Vert.x Mutiny Web Client - ping is please if you'll need some help with setting it up. Vert.x JsonObject is handy for parsing JSON.

You can also try to build the source, https://github.com/quarkusio/quarkus/blob/main/CONTRIBUTING.md#build, and check how the filter approach works - we can tune that if needed in separate issues.

@calvernaz
Copy link
Author

Hi @calvernaz, np at all, so 2 options are proposed.

One is the workaround around the current limitation, you just register a custom IdentityProvider, exactly as shown in #36609 (see also application.properties how to trick OIDC recorder not to fetch JWK keys itself), it will work right now without having to wait for any releases. In this provider, in a @PostConstruct method, you can get the JWK keys pulled in, and in authenticate - get the token from the TokenCredential, verify and then have SecurityIdentity returned - you can copy and paste the code from the test identity provider in #36609.

Hi @sberyozkin, I tested your proposal and correct me if I'm wrong. The authenticated method is being called after the token exchange. Your suggestion is then to inside authenticate given that i have the token, call the jwt keys endpoint and validate the code there.

I was expecting a similar handling but specifically to the /certs endpoint.
Either way I need to make sure your proposal works, because the SecurityIdentity that I'm returning is casing const redirects.

#36634 has now been merged too, so once 3.6.0.CR1/3.6.0 is out, the only thing you'd need to do is to register something like:

@ApplicationScoped
@Unremovable
public class JwksEndpointAuthenticator implements OidcClientRequestFilter {
     @Inject 
     OidcConfigurationMetadata oidcMetadata;
     public void filter(HttpRequest<Buffer> request, Buffer body) {
           if (request.uri().equals(oidcMetadata.getJsonWebKeySetUri()))) {
                request.putHeader("Authorization", "ID " + createJwtToken());
               // add more headers as needed
           }
     }
}

Quarkus itself will deal with JWK keys, parsing them, refreshing them, dealing with the OIDC client. May be we'll need to tweak a bit which algorithms can be supported but it is already quite comprehensive. You'd only have the token generated. May be the next version of Quarkus JWT build API can be of help with the token creation - it would do right now except that no typed support for setting the cert chain is available yet, so you'd need to create a base64 rep of the chain manually, which won't be needed later.

Hope it clarifies things.

Can you please give a custom IdentityProvider a quick try in the next few days ? I'll resolve this issue once you confirm it can keep you going for now. I recommend using Vert.x Mutiny Web Client - ping is please if you'll need some help with setting it up. Vert.x JsonObject is handy for parsing JSON.

You can also try to build the source, https://github.com/quarkusio/quarkus/blob/main/CONTRIBUTING.md#build, and check how the filter approach works - we can tune that if needed in separate issues.

Yes this second approach seems to be close to what we want but I'm still unclear if the filter is enough because once I get past the request (custom auth), I need to have the token access validate it and provide a signal that the session can be created.

@sberyozkin
Copy link
Member

sberyozkin commented Oct 24, 2023

Hi @calvernaz

Thanks for checking it,

Re the workaround:

The authenticated method is being called after the token exchange. Your suggestion is then to inside authenticate given that i have the token, call the jwt keys endpoint and validate the code there.

I propose to fetch JWT keys in the @PostConstruct method of the custom identity provider, before authenticate is called.
Then indeed, in the authenticate, verify the token - this will be an ID token, wrapped in IdTokenCredential which you can get as Credential from the token authentication request parameter

I was expecting a similar handling but specifically to the /certs endpoint.

May be I'm missing something but the token targeting the certs endpoint will be generated when the JWK keys will be requested and it will be your OIDC server which will verify it, or did you mean something else ?

Either way I need to make sure your proposal works, because the SecurityIdentity that I'm returning is casing const redirects.

I can have a look if you will share some code related to specifically how you work with this Identity Provider (please replace the internal details with some dummy code).

The redirect loop is usually happening if the state cookie is available but the OIDC provider is not returning a state query parameter, can it be the case ? You can confirm it in the browser tools.

Re the proposed solution in 3.6.0

Yes this second approach seems to be close to what we want but I'm still unclear if the filter is enough because once I get past the request (custom auth), I need to have the token access validate it and provide a signal that the session can be created.

Quarkus will handle it all, will fetch the keys, then when the ID token has to be verified, it will find the matching JWK key, using either kid or the cert thumbprint, and verify the signature. If the key can not be found, it will refresh the keys but restricting it to a specific interval.

Is there something specific to the ID token verification process that you may be concerned can't handle ? You can always complement the default signature, expiry, issuer, audience verification done by Quarkus with more checks in custom SecurityIdentityAugmentor

@calvernaz
Copy link
Author

Hi @calvernaz

Thanks for checking it,

Re the workaround:

The authenticated method is being called after the token exchange. Your suggestion is then to inside authenticate given that i have the token, call the jwt keys endpoint and validate the code there.

I propose to fetch JWT keys in the @PostConstruct method of the custom identity provider, before authenticate is called. Then indeed, in the authenticate, verify the token - this will be an ID token, wrapped in IdTokenCredential which you can get as Credential from the token authentication request parameter

Yep this makes sense!

I was expecting a similar handling but specifically to the /certs endpoint.

May be I'm missing something but the token targeting the certs endpoint will be generated when the JWK keys will be requested and it will be your OIDC server which will verify it, or did you mean something else ?

First part is correct, we fetch the certificates (jwks_uri a.k.a /certs) with custom auth, but we don't validate the token on the OIDC provider we use the public key in the cert (the one we downloaded) to validate the token locally.

Either way I need to make sure your proposal works, because the SecurityIdentity that I'm returning is casing const redirects.

I can have a look if you will share some code related to specifically how you work with this Identity Provider (please replace the internal details with some dummy code).

Is it possible that we arrange a call or something? Not only showing the code will be hard because it's internal libraries the flow and ability to customize the authentication seems to be at odds here.

The redirect loop is usually happening if the state cookie is available but the OIDC provider is not returning a state query parameter, can it be the case ? You can confirm it in the browser tools.

Re the proposed solution in 3.6.0

Yes this second approach seems to be close to what we want but I'm still unclear if the filter is enough because once I get past the request (custom auth), I need to have the token access validate it and provide a signal that the session can be created.

Quarkus will handle it all, will fetch the keys, then when the ID token has to be verified, it will find the matching JWK key, using either kid or the cert thumbprint, and verify the signature. If the key can not be found, it will refresh the keys but restricting it to a specific interval.

Is there something specific to the ID token verification process that you may be concerned can't handle ? You can always complement the default signature, expiry, issuer, audience verification done by Quarkus with more checks in custom SecurityIdentityAugmentor

@sberyozkin
Copy link
Member

sberyozkin commented Oct 24, 2023

First part is correct, we fetch the certificates (jwks_uri a.k.a /certs) with custom auth, but we don't validate the token on the OIDC provider we use the public key in the cert (the one we downloaded) to validate the token locally.

Well, I was referring to the token you had to generate for the JWKS endpoint authentication to work, indeed, the downloaded JWK would be used to verify the actual code flow tokens, so no ambiguities there

Is it possible that we arrange a call...

Sure, 12.00 or 16.00 tomorrow Dublin time ? Please reach out to me at sbiarozk dot redhat com or ping me privately on Zulip tomorrow and I will send you a call invite ?

@calvernaz
Copy link
Author

First part is correct, we fetch the certificates (jwks_uri a.k.a /certs) with custom auth, but we don't validate the token on the OIDC provider we use the public key in the cert (the one we downloaded) to validate the token locally.

Well, I was referring to the token you had to generate for the JWKS endpoint authentication to work, indeed, the downloaded JWK would be used to verify the actual code flow tokens, so no ambiguities there

right, that's it then.

Is it possible that we arrange a call...

Sure, 12.00 or 16.00 tomorrow Dublin time ? Please reach out to me at sbiarozk dot redhat com or ping me privately on Zulip tomorrow and I will send you a call invite ?

12.00 sounds good! Will ping you then!

@sberyozkin
Copy link
Member

sberyozkin commented Oct 25, 2023

@calvernaz Thanks, let me have a look at making the fetched JWKs visible to the custom code,
https://github.com/quarkusio/quarkus/blob/main/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/JsonWebKeySet.java

As far as #36634 is concerned, please give that a try too and see how far Quarkus can go with the verification.
FYI, by default, the signature is verified first (it is expected to handle certificates, I don't expect any issues there but we can always tweak things), issuer is verified by default, audience is checked by default for ID tokens, we don't do the subject though as it is typically a unique identifier, but if you were to let Quarkus do the verification for you then you can customize it further with a custom SecurityIdentityAugmentor.

FYI, as far as SecurityIdentity is concerned, having its Principal set is enough to get past @Authenticated checks, role based or permission based control would require a bit more work, though Quarkus can do some of it (converting some claims to roles or permissions automatically). As far as the Principal is concerned, you don't have to use Jose4j or the suggested implementation, unless you'd like to have @Inject @IdToken JsonWebToken idToken. You won't have to create it if Quarkus will do the verification itself though.

Access tokens can be propagated with attaching @AccessToken to the REST clients or by injecting @Inject AccessTokenCredential at; and forwarding its value as required.

@calvernaz
Copy link
Author

Hi @sberyozkin good news here :)

I've tested with this approach #36609 but using our internal library and could validate the token and the session was created.

Regarding all the suggestions:

  1. Will try Introduce OidcRequestFilter #36634 once I get all the details regarding token verification using the quarkus.
  2. We are thinking on using OPA for role base permissions, so will leave that for later.
  3. The goal is to forward the access token, so first need to get through the proxy configuration with quarkus.

So, having something working give me enough confidence to start tweaking on the edges.

@sberyozkin
Copy link
Member

sberyozkin commented Oct 27, 2023

Hi @calvernaz,

I missed your ping here as I got a bit all over the place in the last couple of days, glad to hear you had some good progress, appreciate it may have been a bit tricky to get started given the requirements like retaining the current verification strategy etc.

Will try #36634 once I get all the details regarding token verification using the quarkus.

Sure. I'm also keeping contemplating how to provide access to the public key, I've had an immediate idea to let uses check JsonWebTokenSet somehow, but then started thinking about providing some kind of Verifier interface which will replace the core verification logic - but having a doubt now as that will kind of duplicate what IdentityProvider does (which also has the control of how to create SecurityIdentity), so leaning more to the former option.

We are thinking on using OPA for role base permissions, so will leave that for later.

SecurityIdentityAugmentor will be your friend here, make sure the call to get the authorization decision is made in the blocking context to avoid blocking the IO thread, see the context.runBlocking comment at
https://quarkus.io/guides/security-customization#security-identity-customization

  1. The goal is to forward the access token, so first need to get through the proxy configuration with quarkus.

Hoping it should not be a problem, https://quarkus.io/guides/rest-client-reactive#proxy-support
(CC @geoand )

So, having something working give me enough confidence to start tweaking on the edges.

+1, thanks

@sberyozkin
Copy link
Member

sberyozkin commented Oct 27, 2023

@calvernaz Here is the code which shows how the core verification works:

https://github.com/quarkusio/quarkus/blob/main/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java#L165

Here is what happens:

  • Signature is checked first - Quarkus will find the JWK key matching the current token kid or x5t thumbprint. One thing I'm not 100% sure is how your system's JWK is structured - I'm presuming you have the chain encoded with the x5c claim but also the public key itself is encoded directly. Jose4j will process either option but I'm not sure what happens if the JWK key matched with kid has only x5c without the directly encoded public key - can you please, when you get a chance, type some general JWK structure (without any concrete values) and I can test it and we can tune the key resolution process if needed.
  • Expiry, audience, issuer are verified by default for ID tokens
  • Token age can be enforced, example, even if ID token can last for 1 day, you can restrict it to last for 2 hours for a given endpoint and then Quarkus will refresh it
  • A map of additional custom required claims can be registered, and the validator will enforce these claims are present and have the matching values
  • Subject sub claim: as it is typically a unique value we don't verify it by default, but you can request the token contains the claim in any case for the endpoint do something with it, or if in your system the values are predictable, example, it is set to the client-id, then you can use the previous option to enforce the correct subject value
  • Before the verification starts, one can update the token headers for the signature verification to succeed, this is a rare requirement though, supports legacy Azure tokens but listing it just in case

Next, if the above does not cover all the verification requirements, you can have a dedicated SecurityIdentityAugmentor which checks some complex claims etc: JsonWebToken token = (JsonWebToken) identity.getPrincipal(); JsonArray claim = token.getClaim("custom-array-claim"); etc

@sberyozkin
Copy link
Member

@calvernaz Hi, I've been thinking more about these options, and #36634 in particular.

So right now you have a custom identity provider, which itself fetches JWK keys, and itself verifies the token.

The idea behind #36634 is that, in your case, you only register a filter which ensures JWKS endpoint authentication works, Quarkus fetches JWKS, parses, caches and renews them if needed, and verifies the token using the steps described above. In this case, if needed, you can complement the provided SecurityIdentity with SecurityIdentityAugmentor.

I'm thinking that if you will get convinced that it is the way to go, then intermediate steps like letting your custom identity provider access the verification key may prove to be of transient use - and the challenges will still remain - for example, you'd need to continue enhancing this custom identity provider to handle JWK refresh, correctly report expiration errors for Quarkus to refresh the tokens.

So, can you please give #36634 a try - I'm positive it will help simplifying your current integration quite a bit and it can be verified with integration tests (we can discuss the integration test options). If #36634 works then the custom identity provider can be dropped. If however you will find that the custom identity provider option will need to be around for a while, then it will indeed confirm further support for such providers like an access to the matching verification key is required.

3.6 release is due in 2 weeks, https://github.com/quarkusio/quarkus/wiki/Release-Planning. But you can use snapshots to verify: https://github.com/quarkusio/quarkus/blob/main/CONTRIBUTING.md#using-snapshots

How does this plan sound ? Thanks

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

Successfully merging a pull request may close this issue.

2 participants