From 91b0008e7a52c607d31149e463d50cdd41900b69 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Sun, 28 Nov 2021 19:08:28 +0000 Subject: [PATCH] Update OIDC GitHub docs and update the test --- ...ity-openid-connect-web-authentication.adoc | 114 ++++++++++++++++-- .../it/keycloak/CodeFlowUserInfoResource.java | 6 +- .../CustomSecurityIdentityAugmentor.java | 37 ++++++ .../keycloak/CodeFlowAuthorizationTest.java | 2 +- 4 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java diff --git a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc index a48efc0b4c292..3323f02300bae 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -587,21 +587,32 @@ If you plan to consume this application from a Single Page Application running o Some well known providers such as `GitHub` or `LinkedIn` are not `OpenId Connect` but `OAuth2` providers which support the `authorization code flow`, for example, link:https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps[GitHub OAuth2] and link:https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow[LinkedIn OAuth2]. -The main difference between `OAuth2` and `OpenId Connect` providers is that `OpenId Connect` providers, by building on top of `OAuth2`, return an `ID Token` representing a user authentication, in addition to the standard authorization code flow access and refresh tokens returned by `OAuth2` providers. With `OAuth2` providers, the fact of the user authentication is implicit and is indirectly represented by the access token which represents an authenticated user authorizing the current Quarkus `web-app` application to access some data on behalf of this user. +The main difference between `OpenId Connect` and `OAuth2` providers is that `OpenId Connect` providers, by building on top of `OAuth2`, return an `ID Token` representing a user authentication, in addition to the standard authorization code flow `access` and `refresh` tokens returned by `OAuth2` providers. -For example, when working with `GitHub`, the Quarkus endpoint can acquire an access token which will allow it to request a `GitHub` user profile of the current user. -In fact this is exactly how a standard `OpenId Connect` `UserInfo` acqusition also works - by authenticating into your `OpenId Connect` provider you also give a permission to Quarkus application to acquire your `UserInfo` on your behalf - and it also shows what is meant by `OpenId Connect` being built on top of `OAuth2`. +`OAuth2` providers such as `GitHub` do not return `IdToken`, the fact of the user authentication is implicit and is indirectly represented by the `access` token which represents an authenticated user authorizing the current Quarkus `web-app` application to access some data on behalf of the authenticated user. + +For example, when working with `GitHub`, the Quarkus endpoint can acquire an `access` token which will allow it to request a `GitHub` profile of the current user. +In fact this is exactly how a standard `OpenId Connect` `UserInfo` acqusition also works - by authenticating into your `OpenId Connect` provider you also give a permission to Quarkus application to acquire your <> on your behalf - and it also shows what is meant by `OpenId Connect` being built on top of `OAuth2`. In order to support the integration with such `OAuth2` servers, `quarkus-oidc` needs to be configured to allow the authorization code flow responses without `IdToken`: `quarkus.oidc.authentication.id-token-required=false`. +It is required because `quarkus-oidc` expects that not only `access` and `refresh` tokens but also `IdToken` will be returned once the authorization code flow completes. + +Note, even though you will configure the extension to support the authorization code flows without `IdToken`, an internal `IdToken` will be generated to support the way `quarkus-oidc` operates where an `IdToken` is used to support the authentication session and to avoid redirecting the user to the provider such as `GitHub` on every request. In this case the session lifespan is set to 5 minutes which can be extended further as described in the link:#session_management[session management] section. + The next step is to ensure that the returned access token can be useful to the current Quarkus endpoint. -If the `OAuth2` provider supports the introspection endpoint then you may be able to use this access token as a source of roles with `quarkus.oidc.roles.source=accesstoken`. If no introspection endpoint is available then at the very least it should be possible to request a user information from this provider with `quarkus.oidc.authentication.user-info-required` - this is the case with `GitHib`. +If the `OAuth2` provider supports the introspection endpoint then you may be able to use this access token as a source of roles with `quarkus.oidc.roles.source=accesstoken`. If no introspection endpoint is available then at the very least it should be possible to request <> from this provider with `quarkus.oidc.authentication.user-info-required` - this is the case with `GitHib`. + +Configuring the endpoint to request <> is the only way `quarkus-oidc` can be integrated with the providers such as `GitHib`. + +Note that requiring <> involves making a remote call on every request - therefore you may want tp consider caching `UserInfo` data, see < for more details. Also, OAuth2 servers may not support a well-known configuration endpoint in which case the discovery has to be disabled and the authorization, token, and introspection and/or userinfo endpoint paths have to be configured manually. Here is how you can integrate `quarkus-oidc` with `GitHub` after you have link:https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app[created a GitHub OAuth application]. Configure your Quarkus endpoint like this: -```properties +[source,properties] +---- quarkus.oidc.auth-server-url=https://github.com/login/oauth quarkus.oidc.discovery-enabled=false quarkus.oidc.authorization-path=authorize @@ -614,6 +625,10 @@ quarkus.oidc.authentication.scopes=user:email # Make sure a user info is required quarkus.oidc.authentication.user-info-required=true +# Consider enabling UserInfo Cache +# quarkus.oidc.token-cache.max-size=1000 +# quarkus.oidc.token-cache.time-to-live=5M + # Allow the code flow responses without ID tokens quarkus.oidc.authentication.id-token-required=false @@ -621,11 +636,12 @@ quarkus.oidc.application-type=web-app quarkus.oidc.client-id=github_app_clientid quarkus.oidc.credentials.secret=github_app_clientsecret -``` +---- -This is all what is needed for an endpoint like this one to return the currently authenticated user's profile with `GET http:localhost:8080/github/userinfo`: +This is all what is needed for an endpoint like this one to return the currently authenticated user's profile with `GET http://localhost:8080/github/userinfo` and access it as the individual `UserInfo` properties: -```java +[source,java] +---- import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -648,9 +664,87 @@ public class TokenResource { return userInfo.getUserInfoString(); } } -``` +---- + +If you support more than one social provider with the help of link:security-openid-connect-multitenancy[OpenId Connect Multi-Tenancy], for example, `Google` which is an OpenId Connect Provider returning `IdToken` and `GitHub` which is an `OAuth2` provider returning no `IdToken` and only allowing to access `UserInfo` then you can have your endpoint working with only the injected `SecurityIdentity` for both `Google` and `GitHub` flows. A simple augmentation of `SecurityIdentity` will be required where a principal created with the internally generated `IdToken` will be replaced with the `UserInfo` based principal when the GiHub flow is active: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import java.security.Principal; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.oidc.UserInfo; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + @Override + public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context) { + RoutingContext routingContext = identity.getAttribute(RoutingContext.class.getName()); + if (routingContext != null && routingContext.normalizedPath().endsWith("/github")) { + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); + UserInfo userInfo = identity.getAttribute("userinfo"); + builder.setPrincipal(new Principal() { + + @Override + public String getName() { + return userInfo.getString("preferred_username"); + } + + }); + identity = builder.build(); + } + return Uni.createFrom().item(identity); + } + +} +---- + +Now, the following code will work when the user is signing in into your application with both `Google` or `GitHub`: + +[source,java] +---- +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/service") +@Authenticated +public class TokenResource { + + @Inject + SecurityIdentity identity; + + @GET + @Path("/google") + @Produces("application/json") + public String getUserName() { + return identity.getPrincipal().getName(); + } + + @GET + @Path("/github") + @Produces("application/json") + public String getUserName() { + return identity.getPrincipal().getUserName(); + } +} +---- -In your own endpoint you can access individual `UserInfo` properties as required. +Possibly a simpler alternative is to inject both `@IdToken JsonWebToken` and `UserInfo` and use `JsonWebToken` when dealing with the providers returning `IdToken` and `UserInfo` - with the providers which do not return `IdToken`. The last important point is to make sure the callback path you enter in the GitHub OAuth application configuration matches the endpoint path where you'd like the user be redirected to after a successful GitHub authentication and application authorization, in this case it has to be set to `http:localhost:8080/github/userinfo`. diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java index 90e2ced96f7ba..f8987b6574305 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java @@ -6,6 +6,7 @@ import io.quarkus.oidc.UserInfo; import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; @Path("/code-flow-user-info") @Authenticated @@ -14,8 +15,11 @@ public class CodeFlowUserInfoResource { @Inject UserInfo userInfo; + @Inject + SecurityIdentity identity; + @GET public String access() { - return userInfo.getString("preferred_username"); + return identity.getPrincipal().getName() + ":" + userInfo.getString("preferred_username"); } } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java new file mode 100644 index 0000000000000..5a2f0cb268a44 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java @@ -0,0 +1,37 @@ +package io.quarkus.it.keycloak; + +import java.security.Principal; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.oidc.UserInfo; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor { + + @Override + public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context) { + RoutingContext routingContext = identity.getAttribute(RoutingContext.class.getName()); + if (routingContext != null && routingContext.normalizedPath().endsWith("code-flow-user-info")) { + QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); + UserInfo userInfo = identity.getAttribute("userinfo"); + builder.setPrincipal(new Principal() { + + @Override + public String getName() { + return userInfo.getString("preferred_username"); + } + + }); + identity = builder.build(); + } + return Uni.createFrom().item(identity); + } + +} \ No newline at end of file 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 68d365b2ada44..d032a97daf30e 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 @@ -56,7 +56,7 @@ public void testCodeFlowUserInfo() throws IOException { page = form.getInputByValue("login").click(); - assertEquals("alice", page.getBody().asText()); + assertEquals("alice:alice", page.getBody().asText()); } }