From 58cdb0a9d80b76b5f0dd4571acbc4ecba6327330 Mon Sep 17 00:00:00 2001 From: Jakub Jedlicka Date: Tue, 27 Aug 2024 22:59:24 +0200 Subject: [PATCH 1/3] Small fixes in oidc bearer token authentication docs --- .../security-oidc-bearer-token-authentication.adoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index f82f975dfddef..3052aa4ecffe2 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -48,6 +48,7 @@ If you need to authenticate and authorize users by using OIDC authorization code Also, if you use Keycloak and bearer tokens, see the Quarkus xref:security-keycloak-authorization.adoc[Using Keycloak to centralize authorization] guide. To learn about how you can protect service applications by using OIDC Bearer token authentication, see the following tutorial: + * xref:security-oidc-bearer-token-authentication-tutorial.adoc[Protect a web application by using OpenID Connect (OIDC) authorization code flow]. For information about how to support multiple tenants, see the Quarkus xref:security-openid-connect-multitenancy.adoc[Using OpenID Connect Multi-Tenancy] guide. @@ -183,7 +184,7 @@ public class ProtectedResource { @GET @Path("/order") public List listOrders() { - return List.of(new Order(1)); + return List.of(new Order("1")); } public static class Order { @@ -1018,7 +1019,6 @@ import io.quarkus.test.security.TestSecurity; import io.quarkus.test.security.oidc.Claim; import io.quarkus.test.security.oidc.ConfigMetadata; import io.quarkus.test.security.oidc.OidcSecurity; -import io.quarkus.test.security.oidc.OidcConfigurationMetadata; import io.quarkus.test.security.oidc.UserInfo; import io.restassured.RestAssured; @@ -1122,8 +1122,8 @@ public class TestSecurityAuthTest { } ) public void testOidcWithClaimsUserInfoAndMetadata() { - RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then() - .body(is("userOidc:viewer:userOidc:viewer")); + RestAssured.when().get("test-security-oidc-opaque-token").then() + .body(is("userOidc:viewer:userOidc:viewer:user@gmail.com")); } } @@ -1315,7 +1315,7 @@ public class OrderService { @Blocking @ConsumeEvent("product-order") - void processOrder(Product product) { + void processOrder(OrderResource.Product product) { AccessTokenCredential tokenCredential = new AccessTokenCredential(product.customerAccessToken); SecurityIdentity securityIdentity = identityProvider.authenticate(tokenCredential).await().indefinitely(); <2> ... From 4ae87455d1b04ab4ea17620d729c27674b9f4cf7 Mon Sep 17 00:00:00 2001 From: Jakub Jedlicka Date: Wed, 28 Aug 2024 13:14:32 +0200 Subject: [PATCH 2/3] Small fixes in openid connect multitenancy docs --- .../security-openid-connect-multitenancy.adoc | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc index a5f814041cd22..9aec236bbf032 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -170,6 +170,7 @@ import io.quarkus.oidc.OidcRequestContext; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.OidcTenantConfig.ApplicationType; import io.quarkus.oidc.TenantConfigResolver; +import io.quarkus.oidc.runtime.OidcUtils; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; @@ -211,6 +212,7 @@ Otherwise, it initiates an authorization code flow when authentication is requir # Default tenant configuration %prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.client-id=multi-tenant-client +quarkus.oidc.credentials.secret=secret quarkus.oidc.application-type=web-app # Tenant A configuration is created dynamically in CustomTenantConfigResolver @@ -235,11 +237,13 @@ Alternatively, you can configure the tenant `tenant-a` directly in `application. # Default tenant configuration %prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.client-id=multi-tenant-client +quarkus.oidc.credentials.secret=secret quarkus.oidc.application-type=web-app # Tenant A configuration quarkus.oidc.tenant-a.auth-server-url=http://localhost:8180/realms/tenant-a quarkus.oidc.tenant-a.client-id=multi-tenant-client +quarkus.oidc.tenant-a.credentials.secret=secret quarkus.oidc.tenant-a.application-type=web-app # HTTP security configuration @@ -703,7 +707,7 @@ public class CustomTenantConfigResolver implements TenantConfigResolver { if ("tenant-c".equals(parts[1])) { // Do 'return requestContext.runBlocking(createTenantConfig());' // if a blocking call is required to create a tenant config, - return Uni.createFromItem(createTenantConfig()); + return Uni.createFrom().item(createTenantConfig()); } //Resolve to default tenant configuration @@ -766,11 +770,12 @@ quarkus.oidc.b.client-id=client-b quarkus.oidc.b.credentials.secret=client-b-secret ---- -You can return the tenant id of either `a` or `b` from `quarkus.oidc.TenantResolver`: +You can return the tenant id of either `a` or `b` from `io.quarkus.oidc.TenantResolver`: [source,java] ---- -import quarkus.oidc.TenantResolver; +import io.quarkus.oidc.TenantResolver; +import io.vertx.ext.web.RoutingContext; public class CustomTenantResolver implements TenantResolver { @@ -825,7 +830,7 @@ quarkus.oidc.google.credentials.secret=${google-client-secret} quarkus.oidc.google.authentication.redirect-path=/signed-in # Tenant 'github' configuration -quarkus.oidc.github.provider=google +quarkus.oidc.github.provider=github quarkus.oidc.github.client-id=${github-client-id} quarkus.oidc.github.credentials.secret=${github-client-secret} quarkus.oidc.github.authentication.redirect-path=/signed-in @@ -926,9 +931,9 @@ public class CustomTenantConfigResolver implements TenantConfigResolver { String path = context.request().path(); <2> if (path.endsWith("tenant-a")) { - return Uni.createFromItem(createTenantConfig("tenant-a", "client-a", "secret-a")); + return Uni.createFrom().item(createTenantConfig("tenant-a", "client-a", "secret-a")); } else if (path.endsWith("tenant-b")) { - return Uni.createFromItem(createTenantConfig("tenant-b", "client-b", "secret-b")); + return Uni.createFrom().item(createTenantConfig("tenant-b", "client-b", "secret-b")); } // Default tenant id @@ -961,7 +966,7 @@ quarkus.oidc.application-type=web-app The preceeding example assumes that the `tenant-a`, `tenant-b` and default tenants are all used to protect the same endpoint paths. In other words, after the user has authenticated with the `tenant-a` configuration, this user will not be able to choose to authenticate with the `tenant-b` or default configuration before this user logs out and has a session cookie cleared or expired. -The situtaion where multiple OIDC `web-app` tenants protect the tenant-specific paths is less typical and also requires an extra care. +The situation where multiple OIDC `web-app` tenants protect the tenant-specific paths is less typical and also requires an extra care. When multiple OIDC `web-app` tenants such as `tenant-a`, `tenant-b` and default tenants are used to control access to the tenant specific paths, the users authenticated with one OIDC provider must not be able to access the paths requiring an authentication with another provider, otherwise the results can be unpredictable, most likely causing unexpected authentication failures. For example, if the `tenant-a` authentication requires a Keycloak authentication and the `tenant-b` authentication requires an Auth0 authentication, then, if the `tenant-a` authenticated user attempts to access a path secured by the `tenant-b` configuration, then the session cookie will not be verified, since the Auth0 public verification keys can not be used to verify the tokens signed by Keycloak. An easy, recommended way to avoid multiple `web-app` tenants conflicting with each other is to set the tenant specific session path as shown in the following example: @@ -991,9 +996,9 @@ public class CustomTenantConfigResolver implements TenantConfigResolver { String path = context.request().path(); <2> if (path.endsWith("tenant-a")) { - return Uni.createFromItem(createTenantConfig("tenant-a", "/tenant-a", "client-a", "secret-a")); + return Uni.createFrom().item(createTenantConfig("tenant-a", "/tenant-a", "client-a", "secret-a")); } else if (path.endsWith("tenant-b")) { - return Uni.createFromItem(createTenantConfig("tenant-b", "/tenant-b", "client-b", "secret-b")); + return Uni.createFrom().item(createTenantConfig("tenant-b", "/tenant-b", "client-b", "secret-b")); } // Default tenant id @@ -1059,7 +1064,7 @@ public class CustomTenantConfigResolver implements TenantConfigResolver { context.remove(OidcUtils.TENANT_ID_ATTRIBUTE); <3> } } - return Uni.createFromItem(createTenantConfig("tenant-a", "client-a", "secret-a")); + return Uni.createFrom().item(createTenantConfig("tenant-a", "client-a", "secret-a")); } else if (path.endsWith("tenant-b")) { String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE); if (resolvedTenantId != null) { @@ -1070,7 +1075,7 @@ public class CustomTenantConfigResolver implements TenantConfigResolver { context.remove(OidcUtils.TENANT_ID_ATTRIBUTE); <3> } } - return Uni.createFromItem(createTenantConfig("tenant-b", "client-b", "secret-b")); + return Uni.createFrom().item(createTenantConfig("tenant-b", "client-b", "secret-b")); } // Set default tenant id From b655d52db0738d2b18b281c46660efbd10faa3be Mon Sep 17 00:00:00 2001 From: Jakub Jedlicka Date: Wed, 28 Aug 2024 13:30:03 +0200 Subject: [PATCH 3/3] Small fixes in oidc code flow authentication docs --- ...idc-code-flow-authentication-tutorial.adoc | 2 +- ...ecurity-oidc-code-flow-authentication.adoc | 57 +++++++++++-------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc index 0f9ec594689f5..ab80868693399 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication-tutorial.adoc @@ -165,7 +165,7 @@ The OIDC extension allows you to define the configuration by using the `applicat [source,properties] ---- -quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus +%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.client-id=frontend quarkus.oidc.credentials.secret=secret quarkus.oidc.application-type=web-app 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 09616e3eb3c4d..3fd51fe7d7b08 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -319,6 +319,7 @@ For example: ---- package io.quarkus.it.keycloak; +import io.quarkus.oidc.OidcConfigurationMetadata; import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; @@ -350,6 +351,8 @@ Alternatively, you can use `OidcRequestFilter.Endpoint` enum to apply this filte [source,java] ---- +package io.quarkus.it.keycloak; + import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; @@ -480,6 +483,7 @@ import io.quarkus.arc.Unremovable; import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.OidcRedirectFilter; import io.quarkus.oidc.Redirect; +import io.quarkus.oidc.Redirect.Location; import io.quarkus.oidc.TenantFeature; import io.quarkus.oidc.runtime.OidcUtils; import io.smallrye.jwt.build.Jwt; @@ -526,12 +530,15 @@ import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.runtime.OidcUtils; import io.quarkus.oidc.runtime.TenantConfigBean; import io.smallrye.jwt.auth.principal.DefaultJWTParser; import io.vertx.ext.web.RoutingContext; @Path("/session-expired-page") public class SessionExpiredResource { + @Inject + RoutingContext context; @Inject TenantConfigBean tenantConfig; <1> @@ -572,6 +579,8 @@ You can access ID token claims by injecting `JsonWebToken` with an `IdToken` qua [source, java] ---- import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; import org.eclipse.microprofile.jwt.JsonWebToken; import io.quarkus.oidc.IdToken; import io.quarkus.security.Authenticated; @@ -597,6 +606,8 @@ You can access the raw access token as follows: [source, java] ---- import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; import org.eclipse.microprofile.jwt.JsonWebToken; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.security.Authenticated; @@ -765,8 +776,7 @@ For example: By default, `quarkus.oidc.authentication.cookie-path` is set to `/` but you can change this to a more specific path if required, for example, `/web-app`. To set the cookie path dynamically, configure the `quarkus.oidc.authentication.cookie-path-header` property. -Set the `quarkus.oidc.authentication.cookie-path-header` property. -For example, to set the cookie path dynamically by using the value of the`X-Forwarded-Prefix` HTTP header, configure the property to `quarkus.oidc.authentication.cookie-path-header=X-Forwarded-Prefix`. +For example, to set the cookie path dynamically by using the value of the `X-Forwarded-Prefix` HTTP header, configure the property to `quarkus.oidc.authentication.cookie-path-header=X-Forwarded-Prefix`. If `quarkus.oidc.authentication.cookie-path-header` is set but no configured HTTP header is available in the current request, then the `quarkus.oidc.authentication.cookie-path` will be checked. @@ -783,10 +793,10 @@ State cookies are used to support authorization code flow completion. When an authorization code flow is started, Quarkus creates a state cookie and a matching `state` query parameter, before redirecting the user to the OIDC provider. When the user is redirected back to Quarkus to complete the authorization code flow, Quarkus expects that the request URI must contain the `state` query parameter and it must match the current state cookie value. -The default state cookie age is 5 mins and you can change it with a `quarkus.oidc.authenticaion.state-cookie-age` Duration property. +The default state cookie age is 5 mins and you can change it with a `quarkus.oidc.authentication.state-cookie-age` Duration property. Quarkus creates a unique state cookie name every time a new authorization code flow is started to support multi-tab authentication. Many concurrent authentication requests on behalf of the same user may cause a lot of state cookies be created. -If you do not want to allow your users use multiple browser tabs to authenticate then it is recommended to disable it with `quarkus.oidc.authenticaion.allow-multiple-code-flows=false`. It also ensures that the same state cookie name is created for every new user authentication. +If you do not want to allow your users use multiple browser tabs to authenticate then it is recommended to disable it with `quarkus.oidc.authentication.allow-multiple-code-flows=false`. It also ensures that the same state cookie name is created for every new user authentication. [[token-state-manager]] ==== Session cookie and default TokenStateManager @@ -891,14 +901,14 @@ public class CustomTokenStateManager implements TokenStateManager { @Override public Uni createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, - AuthorizationCodeTokens sessionContent, TokenStateManager.CreateTokenStateRequestContext requestContext) { + AuthorizationCodeTokens sessionContent, OidcRequestContext requestContext) { return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext) .map(t -> (t + "|custom")); } @Override public Uni getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, - String tokenState, TokenStateManager.GetTokensRequestContext requestContext) { + String tokenState, OidcRequestContext requestContext) { if (!tokenState.endsWith("|custom")) { throw new IllegalStateException(); } @@ -908,7 +918,7 @@ public class CustomTokenStateManager implements TokenStateManager { @Override public Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, - TokenStateManager.DeleteTokensRequestContext requestContext) { + OidcRequestContext requestContext) { if (!tokenState.endsWith("|custom")) { throw new IllegalStateException(); } @@ -1184,8 +1194,9 @@ public class ServiceResource { @Path("logout") public String logout() { oidcSession.logout().await().indefinitely(); - return "You are logged out". + return "You are logged out"; } +} ---- [[oidc-session]] @@ -1287,10 +1298,8 @@ To support the integration with such OAuth2 servers, `quarkus-oidc` needs to be Even though you configure the extension to support the authorization code flows without `IdToken`, an internal `IdToken` is generated to standardize the way `quarkus-oidc` operates. You use an internal `IdToken` to support the authentication session and to avoid redirecting the user to the provider, such as GitHub, on every request. In this case, the `IdToken` age is set to the value of a standard `expires_in` property in the authorization code flow response. -You can use a `quarkus.oidc.authentication.internal-id-token-lifespan`property to customize the ID token age. -The default ID token age is 5 minutes. - -, which you can extend further as described in the <> section. +You can use a `quarkus.oidc.authentication.internal-id-token-lifespan` property to customize the ID token age. +The default ID token age is 5 minutes, which you can extend further as described in the <> section. This simplifies how you handle an application that supports multiple OIDC providers. ==== @@ -1345,6 +1354,8 @@ This is all that is needed for an endpoint like this one to return the currently [source,java] ---- +package io.quarkus.it.keycloak; + import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -1417,6 +1428,8 @@ Now, the following code will work when the user signs into your application by u [source,java] ---- +package io.quarkus.it.keycloak; + import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -1435,15 +1448,15 @@ public class TokenResource { @GET @Path("/google") @Produces("application/json") - public String getUserName() { + public String getGoogleUserName() { return identity.getPrincipal().getName(); } @GET @Path("/github") @Produces("application/json") - public String getUserName() { - return identity.getPrincipal().getUserName(); + public String getGitHubUserName() { + return identity.getPrincipal().getName(); } } ---- @@ -1451,7 +1464,7 @@ public class TokenResource { Possibly a simpler alternative is to inject both `@IdToken JsonWebToken` and `UserInfo` and use `JsonWebToken` when handling the providers that return `IdToken` and use `UserInfo` with the providers that do not return `IdToken`. You must ensure that the callback path you enter in the GitHub OAuth application configuration matches the endpoint path where you want the user to be redirected after a successful GitHub authentication and application authorization. -In this case, it has to be set to `http:localhost:8080/github/userinfo`. +In this case, it has to be set to `http://localhost:8080/github/userinfo`. [[listen-to-authentication-events]] === Listening to important authentication events @@ -1466,9 +1479,7 @@ For example: import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; -import io.quarkus.oidc.IdTokenCredential; import io.quarkus.oidc.SecurityEvent; -import io.quarkus.security.identity.AuthenticationRequestContext; import io.vertx.ext.web.RoutingContext; @ApplicationScoped @@ -1640,8 +1651,8 @@ For example, name it as `OktaSaml`: image::okta-saml-general-settings.png[alt=Okta SAML General Settings,role="center"] Next, configure it to point to a Keycloak SAML broker endpoint. -At this point, you need to know the name of the Keycloak realm, for example, `quarkus`, and, assuming that the Keycloak SAML broker alias is `saml`, enter the endpoint address as `http:localhost:8081/realms/quarkus/broker/saml/endpoint`. -Enter the service provider (SP) entity ID as `http:localhost:8081/realms/quarkus`, where `http://localhost:8081` is a Keycloak base address and `saml` is a broker alias: +At this point, you need to know the name of the Keycloak realm, for example, `quarkus`, and, assuming that the Keycloak SAML broker alias is `saml`, enter the endpoint address as `http://localhost:8081/realms/quarkus/broker/saml/endpoint`. +Enter the service provider (SP) entity ID as `http://localhost:8081/realms/quarkus`, where `http://localhost:8081` is a Keycloak base address and `saml` is a broker alias: image::okta-saml-configuration.png[alt=Okta SAML Configuration,role="center"] @@ -1658,7 +1669,7 @@ Now, in the `quarkus` realm properties, navigate to `Identity Providers` and add image::keycloak-add-saml-provider.png[alt=Keycloak Add SAML Provider,role="center"] -Note the alias is set to `saml`, `Redirect URI` is `http:localhost:8081/realms/quarkus/broker/saml/endpoint` and `Service provider entity ID` is `http:localhost:8081/realms/quarkus` - these are the same values you entered when creating the Okta SAML integration in the previous step. +Note the alias is set to `saml`, `Redirect URI` is `http://localhost:8081/realms/quarkus/broker/saml/endpoint` and `Service provider entity ID` is `http://localhost:8081/realms/quarkus` - these are the same values you entered when creating the Okta SAML integration in the previous step. Finally, set `Service entity descriptor` to point to the Okta SAML Integration Metadata URL you noted at the end of the previous step. @@ -1776,7 +1787,7 @@ public class CodeFlowAuthorizationTest { page = form.getInputByValue("login").click(); - assertEquals("alice", page.getBody().asText()); + assertEquals("alice", page.getBody().asNormalizedText()); } } @@ -1901,7 +1912,7 @@ endif::no-deprecated-test-resource[] [[code-flow-integration-testing-security-annotation]] === TestSecurity annotation -You can use @TestSecurity and @OidcSecurity annotations to test the `web-app` application endpoint code, which depends on either one of the following injections, or all four: +You can use `@TestSecurity` and `@OidcSecurity` annotations to test the `web-app` application endpoint code, which depends on either one of the following injections, or all four: * ID `JsonWebToken` * Access `JsonWebToken`