diff --git a/docs/src/main/asciidoc/images/oidc-strava-1.png b/docs/src/main/asciidoc/images/oidc-strava-1.png new file mode 100644 index 00000000000000..3a73837972af44 Binary files /dev/null and b/docs/src/main/asciidoc/images/oidc-strava-1.png differ diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index 1cb6e84bf9f723..ef517571a245aa 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -266,6 +266,18 @@ quarkus.oidc.tls.trust-store-password=${trust-store-password} #quarkus.oidc.tls.trust-store-alias=certAlias ---- +===== POST query + +Some providers such as the xref:security-openid-connect-providers#strava[Strava OAuth2 provider] require client credentials be posted as HTTP POST query parameters: + +[source,properties] +---- +quarkus.oidc.provider=strava +quarkus.oidc.client-id=quarkus-app +quarkus.oidc.credentials.client-secret.value=mysecret +quarkus.oidc.credentials.client-secret.method=query +---- + ==== Introspection endpoint authentication Some OIDC providers require authenticating to its introspection endpoint by using Basic authentication and with credentials that are different from the `client_id` and `client_secret`. diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index f4a98eeaa2c4e9..7d7b50c7136859 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -8,9 +8,9 @@ https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc include::_attributes.adoc[] :diataxis-type: concept :categories: security,web -:keywords: oidc github twitter google facebook mastodon microsoft apple spotify twitch linkedin +:keywords: oidc github twitter google facebook mastodon microsoft apple spotify twitch linkedin strava :toclevels: 3 -:topics: security,oidc,github,twitter,google,facebook,mastodon,microsoft,apple,spotify,twitch +:topics: security,oidc,github,twitter,google,facebook,mastodon,microsoft,apple,spotify,twitch,linkedin,strava :extensions: io.quarkus:quarkus-oidc This document explains how to configure well-known social OIDC and OAuth2 providers. @@ -525,7 +525,26 @@ quarkus.oidc.client-id= quarkus.oidc.credentials.client-secret= ---- +[[strava]] +=== Strava +Create a https://www.strava.com/settings/api[Strava application]: + +image::oidc-strava-1.png[role="thumb"] + +For example, set `Category` to `SocialMotivation`, and set `ApplicationCallbackDomain` to either `localhost` or the domain name provided by Ngrok, see the <> for more information. + +You can now configure your `application.properties`: + +[source,properties] +---- +quarkus.oidc.provider=strava +quarkus.oidc.client-id= +quarkus.oidc.credentials.client-secret= +# default value is '/strava' +quarkus.oidc.authentication.redirect-path=/fitness/welcome <1> +---- +<1> Strava does not enforce that the redirect (callback) URI which is provided as an authorization code flow parameter is equal to the URI registered in the Strava application because it only requires configuring `ApplicationCallbackDomain`. For example, if `ApplicationCallbackDomain` is set to `www.my-strava-example.com`, Strava will accept redirect URIs such as `www.my-strava-example.com/a`, `www.my-strava-example.com/path/a`, which is not recommended by OAuth2 best security practices, see, for example, link:https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-insufficient-redirect-uri-v[Insufficent redirect_uri validation]. Therefore you must configure a redirect path when working with the Strava provider and Quarkus will itself enforce that the current request path matches the configured `quarkus.oidc.authentication.redirect-path` value before completing the authotization code flow. See the <> for more information. [[provider-scope]] == Provider scopes @@ -685,6 +704,30 @@ Follow the same approach if the endpoint must access other Google services. The pattern of authenticating with a given provider, where the endpoint uses either an ID token or UserInfo (especially if an OAuth2-only provider such as `GitHub` is used) to get some information about the currently authenticated user and using an access token to access some downstream services (provider or application specific ones) on behalf of this user can be universally applied, irrespectively of which provider is used to secure the application. +[[single_redirect_url_only]] +== Single redirect url only + +Most OIDC and OAuth2 providers with the exception of <> will enforce that the authorization code flow can be completed only if the redirect URL matches precisely the redirect URL configured in a given provider's dashboard. + +From the practical point of view, your Quarkus endpoint will most likely need to have the `quarkus.oidc.authentication.redirect-path` relative path property set to an initial entry path for all the authenticated users, for example, `quarkus.oidc.authentication.redirect-path=/authenticated`, which means that newly authenticated users will land on the `/authenticated` page, irrespectively of how many secured entry points your application has and which secured resource they initially accessed. + +It is a typical flow for many OIDC `web-app` applications. Once the user lands on the initial secured page, your application can return an HTML page which uses links to guide users to other parts of the application or users can be immediately redirected to other application resources with the help of JAX-RS API. + +If necessary, you can configure Quarkus to restore the original request URL after the authentication has been completed. For example: + +[source,properties] +---- +quarkus.oidc.provider=strava <1> +quarkus.oidc.client-id= +quarkus.oidc.credentials.secret= +quarkus.oidc.authentication.restore-path-after-redirect=true <2> +---- +<1> `strava` provider configuration is the only supported configuration which enforces the `quarkus.oidc.authentication.redirect-path` property with the `/strava` path which you can override with another path such as `/fitness`. +<2> If the users access the `/run` endpoint before the authentication, then, once they have authenticated and been redirected to the configured redirect path such as `/strava`, they will land on the original request `/run` path. + +You do not have to set `quarkus.oidc.authentication.redirect-path` immediately because Quarkus assumes the current request URL is an authorization code flow redirect URL if no `quarkus.oidc.authentication.redirect-path` is configured. For example, to test that a <> authentication is working, you can have a Quarkus endpoint listening on `/google` and update the Google dashboard that `http://localhost:8080/google` redirect URI is supported. Setting `quarkus.oidc.authentication.redirect-path` property will be required once your secured application URL space grows. + +[[redirect_url]] == HTTPS Redirect URL Some providers will only accept HTTPS-based redirect URLs. Tools such as https://ngrok.com/[ngrok] https://linuxhint.com/set-up-use-ngrok/[can be set up] to help testing such providers with Quarkus endpoints running on localhost in dev mode. diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index f3810d610a001b..34cdfd4c8857e8 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -176,7 +176,13 @@ public static enum Method { * form * parameters. */ - POST_JWT + POST_JWT, + + /** + * client id and secret are submitted as HTTP query parameters. This option is only supported for the OIDC + * extension. + */ + QUERY } /** diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index 8c178262454bce..805099be88105d 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -1812,6 +1812,7 @@ public static enum Provider { MASTODON, MICROSOFT, SPOTIFY, + STRAVA, TWITCH, TWITTER, // New name for Twitter diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index b4516752735512..95dd2346aeba7c 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -1247,8 +1247,13 @@ public AuthorizationCodeTokens apply(AuthorizationCodeTokens tokens) { private Uni getCodeFlowTokensUni(RoutingContext context, TenantConfigContext configContext, String code, String codeVerifier) { - // 'redirect_uri': typically it must match the 'redirect_uri' query parameter which was used during the code request. + // 'redirect_uri': it must match the 'redirect_uri' query parameter which was used during the code request. String redirectPath = getRedirectPath(configContext.oidcConfig, context); + if (configContext.oidcConfig.authentication.redirectPath.isPresent() + && !configContext.oidcConfig.authentication.redirectPath.get().equals(context.request().path())) { + LOG.warnf("Token redirect path %s does not match the current request path", redirectPath); + return Uni.createFrom().failure(new AuthenticationFailedException()); + } String redirectUriParam = buildUri(context, isForceHttps(configContext.oidcConfig), redirectPath); LOG.debugf("Token request redirect_uri parameter: %s", redirectUriParam); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index 0dce102574eb85..77f389b5c4baed 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -39,6 +39,7 @@ import io.quarkus.oidc.TokenCustomizer; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; +import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.credential.TokenCredential; @@ -551,7 +552,7 @@ private class SymmetricKeyResolver implements VerificationKeyResolver { @Override public Key resolveKey(JsonWebSignature jws, List nestingContext) throws UnresolvableKeyException { - return KeyUtils.createSecretKeyFromSecret(oidcConfig.credentials.secret.get()); + return KeyUtils.createSecretKeyFromSecret(OidcCommonUtils.clientSecret(oidcConfig.credentials)); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index 4aad5025906224..68aaec904843d9 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -19,6 +19,7 @@ import io.quarkus.oidc.common.OidcEndpoint; import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; +import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret.Method; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.oidc.common.runtime.OidcEndpointAccessException; @@ -51,6 +52,7 @@ public class OidcProviderClient implements Closeable { private final String introspectionBasicAuthScheme; private final Key clientJwtKey; private final Map> filters; + private final boolean clientSecretQueryAuthentication; public OidcProviderClient(WebClient client, Vertx vertx, @@ -65,6 +67,7 @@ public OidcProviderClient(WebClient client, this.clientJwtKey = OidcCommonUtils.initClientJwtKey(oidcConfig); this.introspectionBasicAuthScheme = initIntrospectionBasicAuthScheme(oidcConfig); this.filters = filters; + this.clientSecretQueryAuthentication = oidcConfig.credentials.clientSecret.method.orElse(null) == Method.QUERY; } private static String initIntrospectionBasicAuthScheme(OidcTenantConfig oidcConfig) { @@ -139,38 +142,54 @@ public Uni refreshAuthorizationCodeTokens(String refres private UniOnItem> getHttpResponse(String uri, MultiMap formBody, boolean introspect) { HttpRequest request = client.postAbs(uri); - request.putHeader(CONTENT_TYPE_HEADER, APPLICATION_X_WWW_FORM_URLENCODED); - request.putHeader(ACCEPT_HEADER, APPLICATION_JSON); - if (oidcConfig.codeGrant.headers != null) { - for (Map.Entry headerEntry : oidcConfig.codeGrant.headers.entrySet()) { - request.putHeader(headerEntry.getKey(), headerEntry.getValue()); - } - } - if (introspect && introspectionBasicAuthScheme != null) { - request.putHeader(AUTHORIZATION_HEADER, introspectionBasicAuthScheme); - if (oidcConfig.clientId.isPresent() && oidcConfig.introspectionCredentials.includeClientId) { - formBody.set(OidcConstants.CLIENT_ID, oidcConfig.clientId.get()); - } - } else if (clientSecretBasicAuthScheme != null) { - request.putHeader(AUTHORIZATION_HEADER, clientSecretBasicAuthScheme); - } else if (clientJwtKey != null) { - String jwt = OidcCommonUtils.signJwtWithKey(oidcConfig, metadata.getTokenUri(), clientJwtKey); - if (OidcCommonUtils.isClientSecretPostJwtAuthRequired(oidcConfig.credentials)) { + + Buffer buffer = null; + + if (!clientSecretQueryAuthentication) { + request.putHeader(CONTENT_TYPE_HEADER, APPLICATION_X_WWW_FORM_URLENCODED); + request.putHeader(ACCEPT_HEADER, APPLICATION_JSON); + + if (introspect && introspectionBasicAuthScheme != null) { + request.putHeader(AUTHORIZATION_HEADER, introspectionBasicAuthScheme); + if (oidcConfig.clientId.isPresent() && oidcConfig.introspectionCredentials.includeClientId) { + formBody.set(OidcConstants.CLIENT_ID, oidcConfig.clientId.get()); + } + } else if (clientSecretBasicAuthScheme != null) { + request.putHeader(AUTHORIZATION_HEADER, clientSecretBasicAuthScheme); + } else if (clientJwtKey != null) { + String jwt = OidcCommonUtils.signJwtWithKey(oidcConfig, metadata.getTokenUri(), clientJwtKey); + if (OidcCommonUtils.isClientSecretPostJwtAuthRequired(oidcConfig.credentials)) { + formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get()); + formBody.add(OidcConstants.CLIENT_SECRET, jwt); + } else { + formBody.add(OidcConstants.CLIENT_ASSERTION_TYPE, OidcConstants.JWT_BEARER_CLIENT_ASSERTION_TYPE); + formBody.add(OidcConstants.CLIENT_ASSERTION, jwt); + } + } else if (OidcCommonUtils.isClientSecretPostAuthRequired(oidcConfig.credentials)) { formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get()); - formBody.add(OidcConstants.CLIENT_SECRET, jwt); + formBody.add(OidcConstants.CLIENT_SECRET, OidcCommonUtils.clientSecret(oidcConfig.credentials)); } else { - formBody.add(OidcConstants.CLIENT_ASSERTION_TYPE, OidcConstants.JWT_BEARER_CLIENT_ASSERTION_TYPE); - formBody.add(OidcConstants.CLIENT_ASSERTION, jwt); + formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get()); } - } else if (OidcCommonUtils.isClientSecretPostAuthRequired(oidcConfig.credentials)) { - formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get()); - formBody.add(OidcConstants.CLIENT_SECRET, OidcCommonUtils.clientSecret(oidcConfig.credentials)); + buffer = OidcCommonUtils.encodeForm(formBody); } else { formBody.add(OidcConstants.CLIENT_ID, oidcConfig.clientId.get()); + formBody.add(OidcConstants.CLIENT_SECRET, OidcCommonUtils.clientSecret(oidcConfig.credentials)); + for (Map.Entry entry : formBody) { + request.addQueryParam(entry.getKey(), OidcCommonUtils.urlEncode(entry.getValue())); + } + request.putHeader(ACCEPT_HEADER, APPLICATION_JSON); + buffer = Buffer.buffer(); } + + if (oidcConfig.codeGrant.headers != null) { + for (Map.Entry headerEntry : oidcConfig.codeGrant.headers.entrySet()) { + request.putHeader(headerEntry.getKey(), headerEntry.getValue()); + } + } + LOG.debugf("Get token on: %s params: %s headers: %s", metadata.getTokenUri(), formBody, request.headers()); // Retry up to three times with a one-second delay between the retries if the connection is closed. - Buffer buffer = OidcCommonUtils.encodeForm(formBody); OidcEndpoint.Type endpoint = introspect ? OidcEndpoint.Type.INTROSPECTION : OidcEndpoint.Type.TOKEN; Uni> response = filter(endpoint, request, buffer, null).sendBuffer(buffer) @@ -178,6 +197,7 @@ private UniOnItem> getHttpResponse(String uri, MultiMap for .retry() .atMost(oidcConfig.connectionRetryCount).onFailure().transform(t -> t.getCause()); return response.onItem(); + } private AuthorizationCodeTokens getAuthorizationCodeTokens(HttpResponse resp) { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 8fcc88f5c15ce4..d962c6d17368a7 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -484,6 +484,9 @@ static OidcTenantConfig mergeTenantConfig(OidcTenantConfig tenant, OidcTenantCon if (tenant.authentication.responseMode.isEmpty()) { tenant.authentication.responseMode = provider.authentication.responseMode; } + if (tenant.authentication.redirectPath.isEmpty()) { + tenant.authentication.redirectPath = provider.authentication.redirectPath; + } // credentials if (tenant.credentials.clientSecret.method.isEmpty()) { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java index 129262cd29b2b0..d59f5fec66fb49 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java @@ -20,6 +20,7 @@ public static OidcTenantConfig provider(OidcTenantConfig.Provider provider) { case MASTODON -> mastodon(); case MICROSOFT -> microsoft(); case SPOTIFY -> spotify(); + case STRAVA -> strava(); case TWITCH -> twitch(); case TWITTER, X -> twitter(); }; @@ -153,6 +154,28 @@ private static OidcTenantConfig spotify() { return ret; } + private static OidcTenantConfig strava() { + OidcTenantConfig ret = new OidcTenantConfig(); + ret.setDiscoveryEnabled(false); + ret.setAuthServerUrl("https://www.strava.com/oauth"); + ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); + ret.setAuthorizationPath("authorize"); + + ret.setTokenPath("token"); + ret.setUserInfoPath("https://www.strava.com/api/v3/athlete"); + + OidcTenantConfig.Authentication authentication = ret.getAuthentication(); + authentication.setAddOpenidScope(false); + authentication.setScopes(List.of("activity:read")); + authentication.setIdTokenRequired(false); + authentication.setRedirectPath("/strava"); + + ret.getToken().setVerifyAccessTokenWithUserInfo(true); + ret.getCredentials().getClientSecret().setMethod(Method.QUERY); + + return ret; + } + private static OidcTenantConfig twitch() { // Ref https://dev.twitch.tv/docs/authentication/getting-tokens-oidc/#oidc-authorization-code-grant-flow diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/KnownOidcProvidersTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/KnownOidcProvidersTest.java new file mode 100644 index 00000000000000..1bafcd14e7b914 --- /dev/null +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/KnownOidcProvidersTest.java @@ -0,0 +1,586 @@ +package io.quarkus.oidc.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.OidcTenantConfig.ApplicationType; +import io.quarkus.oidc.OidcTenantConfig.Authentication.ResponseMode; +import io.quarkus.oidc.OidcTenantConfig.Provider; +import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret.Method; +import io.quarkus.oidc.runtime.providers.KnownOidcProviders; +import io.smallrye.jwt.algorithm.SignatureAlgorithm; + +public class KnownOidcProvidersTest { + + @Test + public void testAcceptGitHubProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GITHUB)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertFalse(config.isDiscoveryEnabled().get()); + assertEquals("https://github.com/login/oauth", config.getAuthServerUrl().get()); + assertEquals("authorize", config.getAuthorizationPath().get()); + assertEquals("access_token", config.getTokenPath().get()); + assertEquals("https://api.github.com/user", config.getUserInfoPath().get()); + + assertFalse(config.authentication.idTokenRequired.get()); + assertTrue(config.authentication.userInfoRequired.get()); + assertTrue(config.token.verifyAccessTokenWithUserInfo.get()); + assertEquals(List.of("user:email"), config.authentication.scopes.get()); + assertEquals("name", config.getToken().getPrincipalClaim().get()); + } + + @Test + public void testOverrideGitHubProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setDiscoveryEnabled(true); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.setAuthorizationPath("authorization"); + tenant.setTokenPath("tokens"); + tenant.setUserInfoPath("userinfo"); + + tenant.authentication.setIdTokenRequired(true); + tenant.authentication.setUserInfoRequired(false); + tenant.token.setVerifyAccessTokenWithUserInfo(false); + tenant.authentication.setScopes(List.of("write")); + tenant.token.setPrincipalClaim("firstname"); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GITHUB)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertTrue(config.isDiscoveryEnabled().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals("authorization", config.getAuthorizationPath().get()); + assertEquals("tokens", config.getTokenPath().get()); + assertEquals("userinfo", config.getUserInfoPath().get()); + + assertTrue(config.authentication.idTokenRequired.get()); + assertFalse(config.authentication.userInfoRequired.get()); + assertFalse(config.token.verifyAccessTokenWithUserInfo.get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertEquals("firstname", config.getToken().getPrincipalClaim().get()); + } + + @Test + public void testAcceptTwitterProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertFalse(config.isDiscoveryEnabled().get()); + assertEquals("https://api.twitter.com/2/oauth2", config.getAuthServerUrl().get()); + assertEquals("https://twitter.com/i/oauth2/authorize", config.getAuthorizationPath().get()); + assertEquals("token", config.getTokenPath().get()); + assertEquals("https://api.twitter.com/2/users/me", config.getUserInfoPath().get()); + + assertFalse(config.authentication.idTokenRequired.get()); + assertTrue(config.authentication.userInfoRequired.get()); + assertFalse(config.authentication.addOpenidScope.get()); + assertEquals(List.of("offline.access", "tweet.read", "users.read"), config.authentication.scopes.get()); + assertTrue(config.authentication.pkceRequired.get()); + } + + @Test + public void testOverrideTwitterProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setDiscoveryEnabled(true); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.setAuthorizationPath("authorization"); + tenant.setTokenPath("tokens"); + tenant.setUserInfoPath("userinfo"); + + tenant.authentication.setIdTokenRequired(true); + tenant.authentication.setUserInfoRequired(false); + tenant.authentication.setAddOpenidScope(true); + tenant.authentication.setPkceRequired(false); + tenant.authentication.setScopes(List.of("write")); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertTrue(config.isDiscoveryEnabled().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals("authorization", config.getAuthorizationPath().get()); + assertEquals("tokens", config.getTokenPath().get()); + assertEquals("userinfo", config.getUserInfoPath().get()); + + assertTrue(config.authentication.idTokenRequired.get()); + assertFalse(config.authentication.userInfoRequired.get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertTrue(config.authentication.addOpenidScope.get()); + assertFalse(config.authentication.pkceRequired.get()); + } + + @Test + public void testAcceptMastodonProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MASTODON)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertFalse(config.isDiscoveryEnabled().get()); + assertEquals("https://mastodon.social", config.getAuthServerUrl().get()); + assertEquals("/oauth/authorize", config.getAuthorizationPath().get()); + assertEquals("/oauth/token", config.getTokenPath().get()); + assertEquals("/api/v1/accounts/verify_credentials", config.getUserInfoPath().get()); + + assertFalse(config.authentication.idTokenRequired.get()); + assertTrue(config.authentication.userInfoRequired.get()); + assertFalse(config.authentication.addOpenidScope.get()); + assertEquals(List.of("read"), config.authentication.scopes.get()); + } + + @Test + public void testOverrideMastodonProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setDiscoveryEnabled(true); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.setAuthorizationPath("authorization"); + tenant.setTokenPath("tokens"); + tenant.setUserInfoPath("userinfo"); + + tenant.authentication.setIdTokenRequired(true); + tenant.authentication.setUserInfoRequired(false); + tenant.authentication.setAddOpenidScope(true); + tenant.authentication.setScopes(List.of("write")); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MASTODON)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertTrue(config.isDiscoveryEnabled().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals("authorization", config.getAuthorizationPath().get()); + assertEquals("tokens", config.getTokenPath().get()); + assertEquals("userinfo", config.getUserInfoPath().get()); + + assertTrue(config.authentication.idTokenRequired.get()); + assertFalse(config.authentication.userInfoRequired.get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertTrue(config.authentication.addOpenidScope.get()); + } + + @Test + public void testAcceptXProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.X)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertFalse(config.isDiscoveryEnabled().get()); + assertEquals("https://api.twitter.com/2/oauth2", config.getAuthServerUrl().get()); + assertEquals("https://twitter.com/i/oauth2/authorize", config.getAuthorizationPath().get()); + assertEquals("token", config.getTokenPath().get()); + assertEquals("https://api.twitter.com/2/users/me", config.getUserInfoPath().get()); + + assertFalse(config.authentication.idTokenRequired.get()); + assertTrue(config.authentication.userInfoRequired.get()); + assertFalse(config.authentication.addOpenidScope.get()); + assertEquals(List.of("offline.access", "tweet.read", "users.read"), config.authentication.scopes.get()); + assertTrue(config.authentication.pkceRequired.get()); + } + + @Test + public void testOverrideXProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setDiscoveryEnabled(true); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.setAuthorizationPath("authorization"); + tenant.setTokenPath("tokens"); + tenant.setUserInfoPath("userinfo"); + + tenant.authentication.setIdTokenRequired(true); + tenant.authentication.setUserInfoRequired(false); + tenant.authentication.setAddOpenidScope(true); + tenant.authentication.setPkceRequired(false); + tenant.authentication.setScopes(List.of("write")); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.X)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertTrue(config.isDiscoveryEnabled().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals("authorization", config.getAuthorizationPath().get()); + assertEquals("tokens", config.getTokenPath().get()); + assertEquals("userinfo", config.getUserInfoPath().get()); + + assertTrue(config.authentication.idTokenRequired.get()); + assertFalse(config.authentication.userInfoRequired.get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertTrue(config.authentication.addOpenidScope.get()); + assertFalse(config.authentication.pkceRequired.get()); + } + + @Test + public void testAcceptFacebookProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.FACEBOOK)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertFalse(config.isDiscoveryEnabled().get()); + assertEquals("https://www.facebook.com", config.getAuthServerUrl().get()); + assertEquals("https://facebook.com/dialog/oauth/", config.getAuthorizationPath().get()); + assertEquals("https://www.facebook.com/.well-known/oauth/openid/jwks/", config.getJwksPath().get()); + assertEquals("https://graph.facebook.com/v12.0/oauth/access_token", config.getTokenPath().get()); + + assertEquals(List.of("email", "public_profile"), config.authentication.scopes.get()); + assertTrue(config.authentication.forceRedirectHttpsScheme.get()); + } + + @Test + public void testOverrideFacebookProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setDiscoveryEnabled(true); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.setAuthorizationPath("authorization"); + tenant.setJwksPath("jwks"); + tenant.setTokenPath("tokens"); + + tenant.authentication.setScopes(List.of("write")); + tenant.authentication.setForceRedirectHttpsScheme(false); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.FACEBOOK)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertTrue(config.isDiscoveryEnabled().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals("authorization", config.getAuthorizationPath().get()); + assertFalse(config.getAuthentication().isForceRedirectHttpsScheme().get()); + assertEquals("jwks", config.getJwksPath().get()); + assertEquals("tokens", config.getTokenPath().get()); + + assertEquals(List.of("write"), config.authentication.scopes.get()); + } + + @Test + public void testAcceptGoogleProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GOOGLE)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertEquals("https://accounts.google.com", config.getAuthServerUrl().get()); + assertEquals("name", config.getToken().getPrincipalClaim().get()); + assertEquals(List.of("openid", "email", "profile"), config.authentication.scopes.get()); + assertTrue(config.token.verifyAccessTokenWithUserInfo.get()); + } + + @Test + public void testOverrideGoogleProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.authentication.setScopes(List.of("write")); + tenant.token.setPrincipalClaim("firstname"); + tenant.token.setVerifyAccessTokenWithUserInfo(false); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GOOGLE)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals("firstname", config.getToken().getPrincipalClaim().get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertFalse(config.token.verifyAccessTokenWithUserInfo.get()); + } + + @Test + public void testAcceptMicrosoftProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MICROSOFT)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertEquals("https://login.microsoftonline.com/common/v2.0", config.getAuthServerUrl().get()); + assertEquals(List.of("openid", "email", "profile"), config.authentication.scopes.get()); + assertEquals("any", config.getToken().getIssuer().get()); + } + + @Test + public void testOverrideMicrosoftProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.getToken().setIssuer("http://localhost/wiremock"); + tenant.authentication.setScopes(List.of("write")); + tenant.authentication.setForceRedirectHttpsScheme(false); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MICROSOFT)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertEquals("http://localhost/wiremock", config.getToken().getIssuer().get()); + assertFalse(config.authentication.forceRedirectHttpsScheme.get()); + } + + @Test + public void testAcceptAppleProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.APPLE)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertEquals("https://appleid.apple.com/", config.getAuthServerUrl().get()); + assertEquals(List.of("openid", "email", "name"), config.authentication.scopes.get()); + assertEquals(ResponseMode.FORM_POST, config.authentication.responseMode.get()); + assertEquals(Method.POST_JWT, config.credentials.clientSecret.method.get()); + assertEquals("https://appleid.apple.com/", config.credentials.jwt.audience.get()); + assertEquals(SignatureAlgorithm.ES256.getAlgorithm(), config.credentials.jwt.signatureAlgorithm.get()); + assertTrue(config.authentication.forceRedirectHttpsScheme.get()); + } + + @Test + public void testOverrideAppleProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.authentication.setScopes(List.of("write")); + tenant.authentication.setResponseMode(ResponseMode.QUERY); + tenant.credentials.clientSecret.setMethod(Method.POST); + tenant.credentials.jwt.setAudience("http://localhost/audience"); + tenant.credentials.jwt.setSignatureAlgorithm(SignatureAlgorithm.ES256.getAlgorithm()); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.APPLE)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertEquals(ResponseMode.QUERY, config.authentication.responseMode.get()); + assertEquals(Method.POST, config.credentials.clientSecret.method.get()); + assertEquals("http://localhost/audience", config.credentials.jwt.audience.get()); + assertEquals(SignatureAlgorithm.ES256.getAlgorithm(), config.credentials.jwt.signatureAlgorithm.get()); + } + + @Test + public void testAcceptSpotifyProperties() { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.SPOTIFY)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertEquals("https://accounts.spotify.com", config.getAuthServerUrl().get()); + assertEquals(List.of("user-read-private", "user-read-email"), config.authentication.scopes.get()); + assertTrue(config.token.verifyAccessTokenWithUserInfo.get()); + assertEquals("display_name", config.getToken().getPrincipalClaim().get()); + } + + @Test + public void testOverrideSpotifyProperties() { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.getToken().setIssuer("http://localhost/wiremock"); + tenant.authentication.setScopes(List.of("write")); + tenant.authentication.setForceRedirectHttpsScheme(false); + tenant.token.setPrincipalClaim("firstname"); + tenant.token.setVerifyAccessTokenWithUserInfo(false); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.SPOTIFY)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertEquals("http://localhost/wiremock", config.getToken().getIssuer().get()); + assertFalse(config.authentication.forceRedirectHttpsScheme.get()); + assertEquals("firstname", config.getToken().getPrincipalClaim().get()); + assertFalse(config.token.verifyAccessTokenWithUserInfo.get()); + } + + @Test + public void testAcceptStravaProperties() { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.STRAVA)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + + assertFalse(config.discoveryEnabled.get()); + assertEquals("https://www.strava.com/oauth", config.getAuthServerUrl().get()); + assertEquals("authorize", config.getAuthorizationPath().get()); + assertEquals("token", config.getTokenPath().get()); + assertEquals("https://www.strava.com/api/v3/athlete", config.getUserInfoPath().get()); + assertEquals(List.of("activity:read"), config.authentication.scopes.get()); + assertTrue(config.token.verifyAccessTokenWithUserInfo.get()); + assertFalse(config.getAuthentication().idTokenRequired.get()); + assertEquals(Method.QUERY, config.credentials.clientSecret.method.get()); + assertEquals("/strava", config.authentication.redirectPath.get()); + } + + @Test + public void testOverrideStravaProperties() { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.setAuthorizationPath("authorizations"); + tenant.setTokenPath("tokens"); + tenant.setUserInfoPath("users"); + + tenant.authentication.setScopes(List.of("write")); + tenant.token.setVerifyAccessTokenWithUserInfo(false); + tenant.credentials.clientSecret.setMethod(Method.BASIC); + tenant.authentication.setRedirectPath("/fitness-app"); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.STRAVA)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertEquals("authorizations", config.getAuthorizationPath().get()); + assertEquals("tokens", config.getTokenPath().get()); + assertEquals("users", config.getUserInfoPath().get()); + assertEquals(List.of("write"), config.authentication.scopes.get()); + assertFalse(config.token.verifyAccessTokenWithUserInfo.get()); + assertEquals(Method.BASIC, config.credentials.clientSecret.method.get()); + assertEquals("/fitness-app", config.authentication.redirectPath.get()); + } + + @Test + public void testAcceptTwitchProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITCH)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); + assertEquals("https://id.twitch.tv/oauth2", config.getAuthServerUrl().get()); + assertEquals(Method.POST, config.credentials.clientSecret.method.get()); + assertTrue(config.authentication.forceRedirectHttpsScheme.get()); + } + + @Test + public void testOverrideTwitchProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.credentials.clientSecret.setMethod(Method.BASIC); + tenant.authentication.setForceRedirectHttpsScheme(false); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.FACEBOOK)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertFalse(config.getAuthentication().isForceRedirectHttpsScheme().get()); + assertEquals(Method.BASIC, config.credentials.clientSecret.method.get()); + } + + @Test + public void testAcceptDiscordProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.DISCORD)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertFalse(config.discoveryEnabled.get()); + assertEquals("https://discord.com/api/oauth2", config.getAuthServerUrl().get()); + assertEquals("authorize", config.getAuthorizationPath().get()); + assertEquals("token", config.getTokenPath().get()); + assertEquals("https://discord.com/api/users/@me", config.getUserInfoPath().get()); + assertEquals(List.of("identify", "email"), config.authentication.scopes.get()); + assertFalse(config.getAuthentication().idTokenRequired.get()); + } + + @Test + public void testOverrideDiscordProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.credentials.clientSecret.setMethod(Method.BASIC); + tenant.authentication.setForceRedirectHttpsScheme(false); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.DISCORD)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertFalse(config.getAuthentication().isForceRedirectHttpsScheme().get()); + assertEquals(Method.BASIC, config.credentials.clientSecret.method.get()); + } + + @Test + public void testAcceptLinkedInProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.LINKEDIN)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals("https://www.linkedin.com/oauth", config.getAuthServerUrl().get()); + assertEquals(List.of("email", "profile"), config.authentication.scopes.get()); + } + + @Test + public void testOverrideLinkedInProperties() throws Exception { + OidcTenantConfig tenant = new OidcTenantConfig(); + tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); + + tenant.setApplicationType(ApplicationType.HYBRID); + tenant.setAuthServerUrl("http://localhost/wiremock"); + tenant.credentials.clientSecret.setMethod(Method.BASIC); + tenant.authentication.setForceRedirectHttpsScheme(false); + + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.LINKEDIN)); + + assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); + assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); + assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); + assertFalse(config.getAuthentication().isForceRedirectHttpsScheme().get()); + assertEquals(Method.BASIC, config.credentials.clientSecret.method.get()); + } +} diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java index b2ac3a6788151f..af6c6a74e25e65 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java @@ -23,532 +23,11 @@ import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcTenantConfig; -import io.quarkus.oidc.OidcTenantConfig.ApplicationType; -import io.quarkus.oidc.OidcTenantConfig.Authentication.ResponseMode; -import io.quarkus.oidc.OidcTenantConfig.Provider; -import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret.Method; -import io.quarkus.oidc.runtime.providers.KnownOidcProviders; -import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.jwt.build.Jwt; import io.vertx.core.json.JsonObject; public class OidcUtilsTest { - @Test - public void testAcceptGitHubProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GITHUB)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertFalse(config.isDiscoveryEnabled().get()); - assertEquals("https://github.com/login/oauth", config.getAuthServerUrl().get()); - assertEquals("authorize", config.getAuthorizationPath().get()); - assertEquals("access_token", config.getTokenPath().get()); - assertEquals("https://api.github.com/user", config.getUserInfoPath().get()); - - assertFalse(config.authentication.idTokenRequired.get()); - assertTrue(config.authentication.userInfoRequired.get()); - assertTrue(config.token.verifyAccessTokenWithUserInfo.get()); - assertEquals(List.of("user:email"), config.authentication.scopes.get()); - assertEquals("name", config.getToken().getPrincipalClaim().get()); - } - - @Test - public void testOverrideGitHubProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setDiscoveryEnabled(true); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.setAuthorizationPath("authorization"); - tenant.setTokenPath("tokens"); - tenant.setUserInfoPath("userinfo"); - - tenant.authentication.setIdTokenRequired(true); - tenant.authentication.setUserInfoRequired(false); - tenant.token.setVerifyAccessTokenWithUserInfo(false); - tenant.authentication.setScopes(List.of("write")); - tenant.token.setPrincipalClaim("firstname"); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GITHUB)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertTrue(config.isDiscoveryEnabled().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertEquals("authorization", config.getAuthorizationPath().get()); - assertEquals("tokens", config.getTokenPath().get()); - assertEquals("userinfo", config.getUserInfoPath().get()); - - assertTrue(config.authentication.idTokenRequired.get()); - assertFalse(config.authentication.userInfoRequired.get()); - assertFalse(config.token.verifyAccessTokenWithUserInfo.get()); - assertEquals(List.of("write"), config.authentication.scopes.get()); - assertEquals("firstname", config.getToken().getPrincipalClaim().get()); - } - - @Test - public void testAcceptTwitterProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertFalse(config.isDiscoveryEnabled().get()); - assertEquals("https://api.twitter.com/2/oauth2", config.getAuthServerUrl().get()); - assertEquals("https://twitter.com/i/oauth2/authorize", config.getAuthorizationPath().get()); - assertEquals("token", config.getTokenPath().get()); - assertEquals("https://api.twitter.com/2/users/me", config.getUserInfoPath().get()); - - assertFalse(config.authentication.idTokenRequired.get()); - assertTrue(config.authentication.userInfoRequired.get()); - assertFalse(config.authentication.addOpenidScope.get()); - assertEquals(List.of("offline.access", "tweet.read", "users.read"), config.authentication.scopes.get()); - assertTrue(config.authentication.pkceRequired.get()); - } - - @Test - public void testOverrideTwitterProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setDiscoveryEnabled(true); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.setAuthorizationPath("authorization"); - tenant.setTokenPath("tokens"); - tenant.setUserInfoPath("userinfo"); - - tenant.authentication.setIdTokenRequired(true); - tenant.authentication.setUserInfoRequired(false); - tenant.authentication.setAddOpenidScope(true); - tenant.authentication.setPkceRequired(false); - tenant.authentication.setScopes(List.of("write")); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertTrue(config.isDiscoveryEnabled().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertEquals("authorization", config.getAuthorizationPath().get()); - assertEquals("tokens", config.getTokenPath().get()); - assertEquals("userinfo", config.getUserInfoPath().get()); - - assertTrue(config.authentication.idTokenRequired.get()); - assertFalse(config.authentication.userInfoRequired.get()); - assertEquals(List.of("write"), config.authentication.scopes.get()); - assertTrue(config.authentication.addOpenidScope.get()); - assertFalse(config.authentication.pkceRequired.get()); - } - - @Test - public void testAcceptMastodonProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MASTODON)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertFalse(config.isDiscoveryEnabled().get()); - assertEquals("https://mastodon.social", config.getAuthServerUrl().get()); - assertEquals("/oauth/authorize", config.getAuthorizationPath().get()); - assertEquals("/oauth/token", config.getTokenPath().get()); - assertEquals("/api/v1/accounts/verify_credentials", config.getUserInfoPath().get()); - - assertFalse(config.authentication.idTokenRequired.get()); - assertTrue(config.authentication.userInfoRequired.get()); - assertFalse(config.authentication.addOpenidScope.get()); - assertEquals(List.of("read"), config.authentication.scopes.get()); - } - - @Test - public void testOverrideMastodonProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setDiscoveryEnabled(true); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.setAuthorizationPath("authorization"); - tenant.setTokenPath("tokens"); - tenant.setUserInfoPath("userinfo"); - - tenant.authentication.setIdTokenRequired(true); - tenant.authentication.setUserInfoRequired(false); - tenant.authentication.setAddOpenidScope(true); - tenant.authentication.setScopes(List.of("write")); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MASTODON)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertTrue(config.isDiscoveryEnabled().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertEquals("authorization", config.getAuthorizationPath().get()); - assertEquals("tokens", config.getTokenPath().get()); - assertEquals("userinfo", config.getUserInfoPath().get()); - - assertTrue(config.authentication.idTokenRequired.get()); - assertFalse(config.authentication.userInfoRequired.get()); - assertEquals(List.of("write"), config.authentication.scopes.get()); - assertTrue(config.authentication.addOpenidScope.get()); - } - - @Test - public void testAcceptXProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.X)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertFalse(config.isDiscoveryEnabled().get()); - assertEquals("https://api.twitter.com/2/oauth2", config.getAuthServerUrl().get()); - assertEquals("https://twitter.com/i/oauth2/authorize", config.getAuthorizationPath().get()); - assertEquals("token", config.getTokenPath().get()); - assertEquals("https://api.twitter.com/2/users/me", config.getUserInfoPath().get()); - - assertFalse(config.authentication.idTokenRequired.get()); - assertTrue(config.authentication.userInfoRequired.get()); - assertFalse(config.authentication.addOpenidScope.get()); - assertEquals(List.of("offline.access", "tweet.read", "users.read"), config.authentication.scopes.get()); - assertTrue(config.authentication.pkceRequired.get()); - } - - @Test - public void testOverrideXProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setDiscoveryEnabled(true); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.setAuthorizationPath("authorization"); - tenant.setTokenPath("tokens"); - tenant.setUserInfoPath("userinfo"); - - tenant.authentication.setIdTokenRequired(true); - tenant.authentication.setUserInfoRequired(false); - tenant.authentication.setAddOpenidScope(true); - tenant.authentication.setPkceRequired(false); - tenant.authentication.setScopes(List.of("write")); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.X)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertTrue(config.isDiscoveryEnabled().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertEquals("authorization", config.getAuthorizationPath().get()); - assertEquals("tokens", config.getTokenPath().get()); - assertEquals("userinfo", config.getUserInfoPath().get()); - - assertTrue(config.authentication.idTokenRequired.get()); - assertFalse(config.authentication.userInfoRequired.get()); - assertEquals(List.of("write"), config.authentication.scopes.get()); - assertTrue(config.authentication.addOpenidScope.get()); - assertFalse(config.authentication.pkceRequired.get()); - } - - @Test - public void testAcceptFacebookProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.FACEBOOK)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertFalse(config.isDiscoveryEnabled().get()); - assertEquals("https://www.facebook.com", config.getAuthServerUrl().get()); - assertEquals("https://facebook.com/dialog/oauth/", config.getAuthorizationPath().get()); - assertEquals("https://www.facebook.com/.well-known/oauth/openid/jwks/", config.getJwksPath().get()); - assertEquals("https://graph.facebook.com/v12.0/oauth/access_token", config.getTokenPath().get()); - - assertEquals(List.of("email", "public_profile"), config.authentication.scopes.get()); - assertTrue(config.authentication.forceRedirectHttpsScheme.get()); - } - - @Test - public void testOverrideFacebookProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setDiscoveryEnabled(true); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.setAuthorizationPath("authorization"); - tenant.setJwksPath("jwks"); - tenant.setTokenPath("tokens"); - - tenant.authentication.setScopes(List.of("write")); - tenant.authentication.setForceRedirectHttpsScheme(false); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.FACEBOOK)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertTrue(config.isDiscoveryEnabled().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertEquals("authorization", config.getAuthorizationPath().get()); - assertFalse(config.getAuthentication().isForceRedirectHttpsScheme().get()); - assertEquals("jwks", config.getJwksPath().get()); - assertEquals("tokens", config.getTokenPath().get()); - - assertEquals(List.of("write"), config.authentication.scopes.get()); - } - - @Test - public void testAcceptGoogleProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GOOGLE)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertEquals("https://accounts.google.com", config.getAuthServerUrl().get()); - assertEquals("name", config.getToken().getPrincipalClaim().get()); - assertEquals(List.of("openid", "email", "profile"), config.authentication.scopes.get()); - assertTrue(config.token.verifyAccessTokenWithUserInfo.get()); - } - - @Test - public void testOverrideGoogleProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.authentication.setScopes(List.of("write")); - tenant.token.setPrincipalClaim("firstname"); - tenant.token.setVerifyAccessTokenWithUserInfo(false); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GOOGLE)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertEquals("firstname", config.getToken().getPrincipalClaim().get()); - assertEquals(List.of("write"), config.authentication.scopes.get()); - assertFalse(config.token.verifyAccessTokenWithUserInfo.get()); - } - - @Test - public void testAcceptMicrosoftProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MICROSOFT)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertEquals("https://login.microsoftonline.com/common/v2.0", config.getAuthServerUrl().get()); - assertEquals(List.of("openid", "email", "profile"), config.authentication.scopes.get()); - assertEquals("any", config.getToken().getIssuer().get()); - } - - @Test - public void testOverrideMicrosoftProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.getToken().setIssuer("http://localhost/wiremock"); - tenant.authentication.setScopes(List.of("write")); - tenant.authentication.setForceRedirectHttpsScheme(false); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MICROSOFT)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertEquals(List.of("write"), config.authentication.scopes.get()); - assertEquals("http://localhost/wiremock", config.getToken().getIssuer().get()); - assertFalse(config.authentication.forceRedirectHttpsScheme.get()); - } - - @Test - public void testAcceptAppleProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.APPLE)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertEquals("https://appleid.apple.com/", config.getAuthServerUrl().get()); - assertEquals(List.of("openid", "email", "name"), config.authentication.scopes.get()); - assertEquals(ResponseMode.FORM_POST, config.authentication.responseMode.get()); - assertEquals(Method.POST_JWT, config.credentials.clientSecret.method.get()); - assertEquals("https://appleid.apple.com/", config.credentials.jwt.audience.get()); - assertEquals(SignatureAlgorithm.ES256.getAlgorithm(), config.credentials.jwt.signatureAlgorithm.get()); - assertTrue(config.authentication.forceRedirectHttpsScheme.get()); - } - - @Test - public void testOverrideAppleProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.authentication.setScopes(List.of("write")); - tenant.authentication.setResponseMode(ResponseMode.QUERY); - tenant.credentials.clientSecret.setMethod(Method.POST); - tenant.credentials.jwt.setAudience("http://localhost/audience"); - tenant.credentials.jwt.setSignatureAlgorithm(SignatureAlgorithm.ES256.getAlgorithm()); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.APPLE)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertEquals(List.of("write"), config.authentication.scopes.get()); - assertEquals(ResponseMode.QUERY, config.authentication.responseMode.get()); - assertEquals(Method.POST, config.credentials.clientSecret.method.get()); - assertEquals("http://localhost/audience", config.credentials.jwt.audience.get()); - assertEquals(SignatureAlgorithm.ES256.getAlgorithm(), config.credentials.jwt.signatureAlgorithm.get()); - } - - @Test - public void testAcceptSpotifyProperties() { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.SPOTIFY)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertEquals("https://accounts.spotify.com", config.getAuthServerUrl().get()); - assertEquals(List.of("user-read-private", "user-read-email"), config.authentication.scopes.get()); - assertTrue(config.token.verifyAccessTokenWithUserInfo.get()); - assertEquals("display_name", config.getToken().getPrincipalClaim().get()); - } - - @Test - public void testOverrideSpotifyProperties() { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.getToken().setIssuer("http://localhost/wiremock"); - tenant.authentication.setScopes(List.of("write")); - tenant.authentication.setForceRedirectHttpsScheme(false); - tenant.token.setPrincipalClaim("firstname"); - tenant.token.setVerifyAccessTokenWithUserInfo(false); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.SPOTIFY)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertEquals(List.of("write"), config.authentication.scopes.get()); - assertEquals("http://localhost/wiremock", config.getToken().getIssuer().get()); - assertFalse(config.authentication.forceRedirectHttpsScheme.get()); - assertEquals("firstname", config.getToken().getPrincipalClaim().get()); - assertFalse(config.token.verifyAccessTokenWithUserInfo.get()); - } - - @Test - public void testAcceptTwitchProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITCH)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); - assertEquals("https://id.twitch.tv/oauth2", config.getAuthServerUrl().get()); - assertEquals(Method.POST, config.credentials.clientSecret.method.get()); - assertTrue(config.authentication.forceRedirectHttpsScheme.get()); - } - - @Test - public void testOverrideTwitchProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.credentials.clientSecret.setMethod(Method.BASIC); - tenant.authentication.setForceRedirectHttpsScheme(false); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.FACEBOOK)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertFalse(config.getAuthentication().isForceRedirectHttpsScheme().get()); - assertEquals(Method.BASIC, config.credentials.clientSecret.method.get()); - } - - @Test - public void testAcceptDiscordProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.DISCORD)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertFalse(config.discoveryEnabled.get()); - assertEquals("https://discord.com/api/oauth2", config.getAuthServerUrl().get()); - assertEquals("authorize", config.getAuthorizationPath().get()); - assertEquals("token", config.getTokenPath().get()); - assertEquals("https://discord.com/api/users/@me", config.getUserInfoPath().get()); - assertEquals(List.of("identify", "email"), config.authentication.scopes.get()); - assertFalse(config.getAuthentication().idTokenRequired.get()); - } - - @Test - public void testOverrideDiscordProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.credentials.clientSecret.setMethod(Method.BASIC); - tenant.authentication.setForceRedirectHttpsScheme(false); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.DISCORD)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertFalse(config.getAuthentication().isForceRedirectHttpsScheme().get()); - assertEquals(Method.BASIC, config.credentials.clientSecret.method.get()); - } - - @Test - public void testAcceptLinkedInProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.LINKEDIN)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals("https://www.linkedin.com/oauth", config.getAuthServerUrl().get()); - assertEquals(List.of("email", "profile"), config.authentication.scopes.get()); - } - - @Test - public void testOverrideLinkedInProperties() throws Exception { - OidcTenantConfig tenant = new OidcTenantConfig(); - tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - - tenant.setApplicationType(ApplicationType.HYBRID); - tenant.setAuthServerUrl("http://localhost/wiremock"); - tenant.credentials.clientSecret.setMethod(Method.BASIC); - tenant.authentication.setForceRedirectHttpsScheme(false); - - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.LINKEDIN)); - - assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); - assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); - assertEquals("http://localhost/wiremock", config.getAuthServerUrl().get()); - assertFalse(config.getAuthentication().isForceRedirectHttpsScheme().get()); - assertEquals(Method.BASIC, config.credentials.clientSecret.method.get()); - } - @Test public void testCorrectTokenType() throws Exception { OidcTenantConfig.Token tokenClaims = new OidcTenantConfig.Token(); diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index f3f8a78afc7ea5..8603c5093f442f 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -90,6 +90,7 @@ quarkus.oidc.bearer-user-info-github-service.credentials.secret=AyM1SysPpbyDfgZl quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.provider=github quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.authorization-path=/ +quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.token-path=access_token_refreshed quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.user-info-path=protocol/openid-connect/userinfo quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.jwks-path=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.code-grant.extra-params.extra-param=extra-param-value @@ -98,7 +99,8 @@ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.cache-user-info-in-idt quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.token.refresh-token-time-skew=298 quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.authentication.verify-access-token=true quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.client-id=quarkus-web-app -quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.credentials.client-secret.value=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.credentials.client-secret.method=query quarkus.oidc.code-flow-token-introspection.provider=github diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 63a292066b08bd..a21b2f14fc1170 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -2,9 +2,11 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.notContaining; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -85,6 +87,7 @@ public void testCodeFlow() throws IOException { // Clear the post logout cookie webClient.getCookieManager().clearCookies(); } + clearCache(); } @Test @@ -120,6 +123,7 @@ private void doTestCodeFlowEncryptedIdToken(String tenant) throws IOException { webClient.getCookieManager().clearCookies(); } + clearCache(); } @Test @@ -178,6 +182,7 @@ public void testCodeFlowFormPostAndBackChannelLogout() throws IOException { webClient.getCookieManager().clearCookies(); } + clearCache(); } @Test @@ -226,6 +231,7 @@ public void testCodeFlowFormPostAndFrontChannelLogout() throws Exception { webClient.getCookieManager().clearCookies(); } + clearCache(); } @Test @@ -238,7 +244,34 @@ public void testCodeFlowUserInfo() throws Exception { clearCache(); doTestCodeFlowUserInfo("code-flow-user-info-dynamic-github", 301); clearCache(); - doTestCodeFlowUserInfoCashedInIdToken(); + } + + @Test + public void testCodeFlowUserInfoCachedInIdToken() throws Exception { + defineCodeFlowUserInfoCachedInIdTokenStub(); + try (final WebClient webClient = createWebClient()) { + webClient.getOptions().setRedirectEnabled(true); + HtmlPage page = webClient.getPage("http://localhost:8081/code-flow-user-info-github-cached-in-idtoken"); + + HtmlForm form = page.getFormByName("form"); + form.getInputByName("username").type("alice"); + form.getInputByName("password").type("alice"); + + TextPage textPage = form.getInputByValue("login").click(); + + assertEquals("alice:alice:alice, cache size: 0", textPage.getContent()); + + JsonObject idTokenClaims = decryptIdToken(webClient, "code-flow-user-info-github-cached-in-idtoken"); + assertNotNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE)); + + // refresh + Thread.sleep(3000); + textPage = webClient.getPage("http://localhost:8081/code-flow-user-info-github-cached-in-idtoken"); + assertEquals("alice:alice:bob, cache size: 0", textPage.getContent()); + + webClient.getCookieManager().clearCookies(); + } + clearCache(); } @Test @@ -263,6 +296,7 @@ public void testCodeFlowTokenIntrospection() throws Exception { webClient.getCookieManager().clearCookies(); } + clearCache(); } private void doTestCodeFlowUserInfo(String tenantId, long internalIdTokenLifetime) throws Exception { @@ -316,31 +350,6 @@ private JsonObject decryptIdToken(WebClient webClient, String tenantId) throws E return OidcUtils.decodeJwtContent(encodedIdToken); } - private void doTestCodeFlowUserInfoCashedInIdToken() throws Exception { - try (final WebClient webClient = createWebClient()) { - webClient.getOptions().setRedirectEnabled(true); - HtmlPage page = webClient.getPage("http://localhost:8081/code-flow-user-info-github-cached-in-idtoken"); - - HtmlForm form = page.getFormByName("form"); - form.getInputByName("username").type("alice"); - form.getInputByName("password").type("alice"); - - TextPage textPage = form.getInputByValue("login").click(); - - assertEquals("alice:alice:alice, cache size: 0", textPage.getContent()); - - JsonObject idTokenClaims = decryptIdToken(webClient, "code-flow-user-info-github-cached-in-idtoken"); - assertNotNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE)); - - // refresh - Thread.sleep(3000); - textPage = webClient.getPage("http://localhost:8081/code-flow-user-info-github-cached-in-idtoken"); - assertEquals("alice:alice:bob, cache size: 0", textPage.getContent()); - - webClient.getCookieManager().clearCookies(); - } - } - private WebClient createWebClient() { WebClient webClient = new WebClient(); webClient.setCssErrorHandler(new SilentCssErrorHandler()); @@ -350,7 +359,9 @@ private WebClient createWebClient() { private void defineCodeFlowAuthorizationOauth2TokenStub() { wireMockServer .stubFor(WireMock.post("/auth/realms/quarkus/access_token") - .withHeader("X-Custom", matching("XCustomHeaderValue")) + .withHeader("X-Custom", equalTo("XCustomHeaderValue")) + .withBasicAuth("quarkus-web-app", + "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow") .withRequestBody(containing("extra-param=extra-param-value")) .withRequestBody(containing("authorization_code")) .willReturn(WireMock.aResponse() @@ -362,6 +373,8 @@ private void defineCodeFlowAuthorizationOauth2TokenStub() { + "}"))); wireMockServer .stubFor(WireMock.post("/auth/realms/quarkus/access_token") + .withBasicAuth("quarkus-web-app", + "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow") .withRequestBody(containing("refresh_token=refresh1234")) .willReturn(WireMock.aResponse() .withHeader("Content-Type", "application/json") @@ -372,6 +385,46 @@ private void defineCodeFlowAuthorizationOauth2TokenStub() { } + private void defineCodeFlowUserInfoCachedInIdTokenStub() { + wireMockServer + .stubFor(WireMock.post(urlPathMatching("/auth/realms/quarkus/access_token_refreshed")) + .withHeader("X-Custom", matching("XCustomHeaderValue")) + .withQueryParam("extra-param", equalTo("extra-param-value")) + .withQueryParam("grant_type", equalTo("authorization_code")) + .withQueryParam("client_id", equalTo("quarkus-web-app")) + .withQueryParam("client_secret", equalTo( + "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow")) + .withRequestBody(notContaining("extra-param=extra-param-value")) + .withRequestBody(notContaining("authorization_code")) + .withRequestBody(notContaining("client_id=quarkus-web-app")) + .withRequestBody(notContaining( + "client_secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"access_token\": \"" + + OidcWiremockTestResource.getAccessToken("alice", Set.of()) + "\"," + + " \"refresh_token\": \"refresh1234\"" + + "}"))); + wireMockServer + .stubFor(WireMock.post(urlPathMatching("/auth/realms/quarkus/access_token_refreshed")) + .withQueryParam("refresh_token", equalTo("refresh1234")) + .withQueryParam("client_id", equalTo("quarkus-web-app")) + .withQueryParam("client_secret", equalTo( + "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow")) + .withRequestBody(notContaining("refresh_token=refresh1234")) + .withRequestBody(notContaining("client_id=quarkus-web-app")) + .withRequestBody(notContaining( + "client_secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"access_token\": \"" + + OidcWiremockTestResource.getAccessToken("bob", Set.of()) + "\"" + + "}"))); + + } + private void defineCodeFlowTokenIntrospectionStub() { wireMockServer .stubFor(WireMock.post("/auth/realms/quarkus/access_token")