quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.63k stars 2.64k forks source link

Support absolute OIDC endpoint URLs if discovery is disabled (Was: quarkus-oidc doesn't work if jwks endpoint has a different domain) #20046

Closed oleksandr-manko closed 2 years ago

oleksandr-manko commented 3 years ago

Description

tried to use quarkus-oidc with AWS Cognito and It doesn't work 'cause quarkus-oidc assumes that JWKS endpoint ALWAYS should have the same domain as the authorization server has. It's not always true. For instance, AWS Cognito has different domains for authorization servers and JWKS. In our case We have the following OIDC Configuration in Cognito:

{
  "authorization_endpoint": "https://my-test.auth.eu-central-1.amazoncognito.com/oauth2/authorize",
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "issuer": "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX",
  "jwks_uri": "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX/.well-known/jwks.json",
  "response_types_supported": [
    "code",
    "token"
  ],
  "scopes_supported": [
    "openid",
    "email",
    "phone",
    "profile"
  ],
  "subject_types_supported": [
    "public"
  ],
  "token_endpoint": "https://my-test.auth.eu-central-1.amazoncognito.com/oauth2/token",
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post"
  ],
  "userinfo_endpoint": "https://my-test.auth.eu-central-1.amazoncognito.com/oauth2/userInfo" 
}

We have https://cognito-idp.eu-central-1.amazonaws.com domain for JWKS and https://my-test.auth.eu-central-1.amazoncognito.com as auth-server.

We can only specify quarkus.oidc.auth-server-url and quarkus.oidc.jwks-path. JWKS uri is a result of concatenation of auth-server-url and jwks-path.

It happens in io.quarkus.oidc.runtime.OidcRecorder#createOidcClientUni(OidcTenantConfig, TlsConfig, Vertx) line 271.

  if (!oidcConfig.discoveryEnabled) {
            String tokenUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath);
            String introspectionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString,
                    oidcConfig.introspectionPath);
            String authorizationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString,
                    oidcConfig.authorizationPath);
            String jwksUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.jwksPath);
            String userInfoUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.userInfoPath);
            String endSessionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.endSessionPath);
            metadataUni = Uni.createFrom().item(new OidcConfigurationMetadata(tokenUri,
                    introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri,
                    oidcConfig.token.issuer.orElse(null)));
  }

  public static String getOidcEndpointUrl(String authServerUrl, Optional<String> endpointPath) {
        return endpointPath.isPresent() ? authServerUrl + prependSlash((String)endpointPath.get()) : null;
  } 

Implementation ideas

Should be an ability to specify a full URI to JWKS including domain name in properties.

quarkus-bot[bot] commented 3 years ago

/cc @pedroigor, @sberyozkin

sberyozkin commented 3 years ago

@oleksandr-manko thanks for not marking it as a bug :-).

Can you let me know please, what is returned with https://my-test.auth.eu-central-1.amazoncognito.com/.well-known/openid-configuration ? If the JWKS URI is different there to what you use, then can you clarify why you need to use different keys ?

thanks

oleksandr-manko commented 3 years ago

Hi @sberyozkin! https://my-test.auth.eu-central-1.amazoncognito.com/.well-known/openid-configuration is a wring url: image The right one is https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX/.well-known/openid-configuration image The response was provided in the description above

sberyozkin commented 3 years ago

@oleksandr-manko thanks, in that case it should just work if you set

quarkus.oidc.auth-server-url=https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX

Have you tried it ?

oleksandr-manko commented 3 years ago

@sberyozkin It work only if we work in service mode (quarkus.oidc.application-type=service) when we receive JWT token from outside and just validate the token and perform authorization based on the token. But If we want work in web-app mode when we perform authentication (obtain the token inside the app) it doesn't work 'cause we need all the endpoints e.g. :

quarkus.oidc.application-type=web-app
quarkus.oidc.auth-server-url=https://my-test.auth.eu-central-1.amazoncognito.com
quarkus.oidc.authorization-path=/oauth2/authorize
quarkus.oidc.token-path=/oauth2/token
# ??? doesn't work ??? 
quarkus.oidc.jwks-path=https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX/.well-known/jwks.json
quarkus.oidc.roles.source=accesstoken
quarkus.oidc.authentication.user-info-required=false
quarkus.oidc.roles.role-claim-path=scope
quarkus.oidc.authentication.scopes=test-api/read
quarkus.oidc.client-id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
quarkus.oidc.credentials.secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

So for all the endpoints I need a single domain except JWKS endpoint. For JWKS I need a different domain

sberyozkin commented 3 years ago

@oleksandr-manko I'm sorry, I don't understand why it does not work. As documented here one should only disable the discovery and set individual (relative) endpoint paths if the discovery does not work. But in your case it does work - https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX returns the authorization, jwks and token endpoint URLs, which quarkus-oidc will use. quarkus.oidc.auth-server-url should point to the base URL which will return a discovery doc and it does not look like the one you set supports it.

I think your configuration should look like this:

quarkus.oidc.application-type=web-app
quarkus.oidc.auth-server-url=https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX

quarkus.oidc.roles.source=accesstoken
quarkus.oidc.authentication.user-info-required=false
quarkus.oidc.roles.role-claim-path=scope
quarkus.oidc.authentication.scopes=test-api/read
quarkus.oidc.client-id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
quarkus.oidc.credentials.secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

That should work, I've seen users confirming quarkus-oidc works with Cognito, it does not really matter what these individual URLs returned in the discovery doc point to.

Can you try please with the proposed config ?

oleksandr-manko commented 3 years ago

@sberyozkin, no because authorization-path and token-path paths are on https://my-test.auth.eu-central-1.amazoncognito.com domain and jwks-path path is on https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX/ domain

2 endpoints are on one domain and 1 is on another one. But implementation considers all of them should be on the same domain. In case of AWS Cognito it doesn't work:

sberyozkin commented 3 years ago

@oleksandr-manko I must be slow today on Friday, sorry :-). Why do you say the implementation enforces it must be the same domain ? What is the error you are getting ? Do you see the code which enforces it ? AFAIK quarkus-oidc would use discovered URLs directly.

If it does then it is a bug but which I'd consider not related to the calculation of the actual URLs (linked to in the issue description) - as it only runs when the auto-discovery is disabled.

Thanks

sberyozkin commented 3 years ago

@oleksandr-manko

quarkus.oidc.auth-server-url=https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX will return all those endpoint URLs you have shown in your discovery doc above - and Quarkus will just use them - I don't see why it would not - so please help with more clarifications

sberyozkin commented 3 years ago

Note I'll review the possibility of supporting all the endpoint URLs being the absolute ones when the auto-discovery is disabled - I recall looking into it - but for now I'd expect it to work without users having to be concerned about finding the JWKs URLs and other individual endpoint URLs

oleksandr-manko commented 3 years ago

@sberyozkin Error the following

2021-09-10 17:02:50,753 ERROR [io.qua.run.Application] (Quarkus Main Thread) Failed to start application (with profile dev): io.quarkus.oidc.common.runtime.OidcEndpointAccessException
    at io.quarkus.oidc.runtime.OidcProviderClient.getJsonWebKeySet(OidcProviderClient.java:76)
    at io.quarkus.oidc.runtime.OidcProviderClient.lambda$getJsonWebKeySet$0(OidcProviderClient.java:55)
    at io.smallrye.context.impl.wrappers.SlowContextualFunction.apply(SlowContextualFunction.java:21)
    at io.smallrye.mutiny.operators.uni.UniOnItemTransform$UniOnItemTransformProcessor.onItem(UniOnItemTransform.java:36)
    at io.smallrye.mutiny.vertx.AsyncResultUni.lambda$subscribe$1(AsyncResultUni.java:35)
    at io.vertx.mutiny.ext.web.client.HttpRequest$10.handle(HttpRequest.java:717)
    at io.vertx.mutiny.ext.web.client.HttpRequest$10.handle(HttpRequest.java:714)
    at io.vertx.ext.web.client.impl.HttpContext.handleDispatchResponse(HttpContext.java:371)
    at io.vertx.ext.web.client.impl.HttpContext.execute(HttpContext.java:358)
    at io.vertx.ext.web.client.impl.HttpContext.next(HttpContext.java:336)
    at io.vertx.ext.web.client.impl.HttpContext.fire(HttpContext.java:303)
    at io.vertx.ext.web.client.impl.HttpContext.dispatchResponse(HttpContext.java:265)
    at io.vertx.ext.web.client.impl.HttpContext.lambda$null$8(HttpContext.java:520)
    at io.vertx.core.impl.AbstractContext.dispatch(AbstractContext.java:96)
    at io.vertx.core.impl.AbstractContext.dispatch(AbstractContext.java:59)
    at io.vertx.core.impl.EventLoopContext.lambda$runOnContext$0(EventLoopContext.java:37)
    at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:497)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:829)

place in the extension: https://github.com/quarkusio/quarkus/blob/c317c45d3ee47b9ec66ae01188f07ddc877fb151/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java#L271

sberyozkin commented 3 years ago

@oleksandr-manko

OK, so in your comment above you have confirmed that https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX returns a metadata doc which contains an absolute jwksURL. And therefore I'm saying to you that if you set quarkus.oidc,auth-server-url=https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX then the error above would not happen because https://github.com/quarkusio/quarkus/blob/c317c45d3ee47b9ec66ae01188f07ddc877fb151/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java#L271 will not be even executed.

oleksandr-manko commented 3 years ago

@sberyozkin thanks it works. But how did it figure out endpoints? Ok got it It found endpoints here

 } else {
            final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
            metadataUni = OidcCommonUtils.discoverMetadata(client, authServerUriString, connectionDelayInMillisecs)
                    .onItem().transform(json -> new OidcConfigurationMetadata(json));
        }
public static Uni<JsonObject> discoverMetadata(WebClient client, String authServerUrl, long connectionDelayInMillisecs) {
        String discoveryUrl = authServerUrl + "/.well-known/openid-configuration";
        return client.getAbs(discoveryUrl).send().onItem().transform((resp) -> {
            if (resp.statusCode() == 200) {
                return resp.bodyAsJsonObject();
            } else {
                LOG.tracef("Discovery has failed, status code: %d", resp.statusCode());
                throw new OidcEndpointAccessException(resp.statusCode());
            }
        }).onFailure(oidcEndpointNotAvailable()).retry().withBackOff(CONNECTION_BACKOFF_DURATION, CONNECTION_BACKOFF_DURATION).expireIn(connectionDelayInMillisecs).onFailure().transform((t) -> {
            return t.getCause();
        });
    }

In other words if quarkus.oidc.discovery-enabled=false quarkus-oidc retrieves all the endpoints from quarkus.oidc.auth-server-url + "/.well-known/openid-configuration".

So in case of Cognito we should specify https://cognito-idp.<region>.amazonaws.com/<YOUR_USER_POOL_ID>(e.g. https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_t12NbxXXX) as quarkus.oidc.auth-server-url

Thanks you @sberyozkin!

sberyozkin commented 3 years ago

@oleksandr-manko Np at all, glad it worked, nonetheless, as I said, I'll just take care of supporting the absolute URLs when the auto-discovery is disabled - it should also just work :-).

Let me re-open this issue and rename it so that the enhancement request is tracked