diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index 11c4ea79e0a8e..0d5af51006b8e 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -36,6 +36,7 @@ import io.quarkus.oidc.runtime.OidcIdentityProvider; import io.quarkus.oidc.runtime.OidcJsonWebTokenProducer; import io.quarkus.oidc.runtime.OidcRecorder; +import io.quarkus.oidc.runtime.OidcSessionImpl; import io.quarkus.oidc.runtime.OidcTokenCredentialProducer; import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.runtime.TlsConfig; @@ -87,7 +88,8 @@ public void additionalBeans(BuildProducer additionalBea .addBeanClass(OidcConfigurationMetadataProducer.class) .addBeanClass(OidcIdentityProvider.class) .addBeanClass(DefaultTenantConfigResolver.class) - .addBeanClass(DefaultTokenStateManager.class); + .addBeanClass(DefaultTokenStateManager.class) + .addBeanClass(OidcSessionImpl.class); additionalBeans.produce(builder.build()); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcSession.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcSession.java new file mode 100644 index 0000000000000..1f590ffd0083b --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcSession.java @@ -0,0 +1,19 @@ +package io.quarkus.oidc; + +import java.time.Instant; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.smallrye.mutiny.Uni; + +public interface OidcSession { + + String getTenantId(); + + Instant expiresIn(); + + Uni logout(); + + JsonWebToken getIdToken(); + +} 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 be4229c25707f..348183cf14905 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 @@ -45,9 +45,9 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha static final String AMP = "&"; static final String EQ = "="; + static final String UNDERSCORE = "_"; static final String COOKIE_DELIM = "|"; static final Pattern COOKIE_PATTERN = Pattern.compile("\\" + COOKIE_DELIM); - static final String SESSION_COOKIE_NAME = "q_session"; static final String SESSION_MAX_AGE_PARAM = "session-max-age"; static final Uni VOID_UNI = Uni.createFrom().voidItem(); static final Integer MAX_COOKIE_VALUE_LENGTH = 4096; @@ -59,7 +59,6 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha private final BlockingTaskRunner createTokenStateRequestContext = new BlockingTaskRunner(); private final BlockingTaskRunner getTokenStateRequestContext = new BlockingTaskRunner(); - private final BlockingTaskRunner deleteTokensRequestContext = new BlockingTaskRunner(); public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { @@ -71,6 +70,8 @@ public Uni apply(OidcTenantConfig oidcTenantConfig) // if session already established, try to re-authenticate if (sessionCookie != null) { + context.put(OidcUtils.SESSION_COOKIE_NAME, sessionCookie.getName()); + setTenantIdAttribute(context, OidcUtils.SESSION_COOKIE_NAME, sessionCookie.getName()); Uni resolvedContext = resolver.resolveContext(context); return resolvedContext.onItem() .transformToUni(new Function>() { @@ -271,7 +272,8 @@ private Uni performCodeFlow(IdentityProviderManager identityPr userPath = pair[1]; } } - removeCookie(context, configContext, getStateCookieName(configContext)); + setTenantIdAttribute(context, STATE_COOKIE_NAME, stateCookie.getName()); + OidcUtils.removeCookie(context, configContext.oidcConfig, stateCookie.getName()); } } else { // State cookie must be available to minimize the risk of CSRF @@ -458,7 +460,7 @@ private String generateCodeFlowState(RoutingContext context, TenantConfigContext } private String generatePostLogoutState(RoutingContext context, TenantConfigContext configContext) { - removeCookie(context, configContext, getPostLogoutCookieName(configContext)); + OidcUtils.removeCookie(context, configContext.oidcConfig, getPostLogoutCookieName(configContext)); return createCookie(context, configContext.oidcConfig, getPostLogoutCookieName(configContext), UUID.randomUUID().toString(), 60 * 30).getValue(); @@ -472,7 +474,7 @@ static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcCo cookie.setMaxAge(maxAge); LOG.debugf(name + " cookie 'max-age' parameter is set to %d", maxAge); Authentication auth = oidcConfig.getAuthentication(); - setCookiePath(context, auth, cookie); + OidcUtils.setCookiePath(context, auth, cookie); if (auth.cookieDomain.isPresent()) { cookie.setDomain(auth.getCookieDomain().get()); } @@ -480,14 +482,6 @@ static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcCo return cookie; } - static void setCookiePath(RoutingContext context, Authentication auth, ServerCookie cookie) { - if (auth.cookiePathHeader.isPresent() && context.request().headers().contains(auth.cookiePathHeader.get())) { - cookie.setPath(context.request().getHeader(auth.cookiePathHeader.get())); - } else { - cookie.setPath(auth.getCookiePath()); - } - } - private String buildUri(RoutingContext context, boolean forceHttps, String path) { String authority = URI.create(context.request().absoluteURI()).getAuthority(); return buildUri(context, forceHttps, authority, path); @@ -512,38 +506,6 @@ private String buildUri(RoutingContext context, boolean forceHttps, String autho .toString(); } - private Uni removeSessionCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) { - String cookieValue = removeCookie(context, configContext, cookieName); - if (cookieValue != null) { - return resolver.getTokenStateManager().deleteTokens(context, configContext.oidcConfig, cookieValue, - deleteTokensRequestContext); - } else { - return VOID_UNI; - } - } - - private String removeCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) { - ServerCookie cookie = (ServerCookie) context.cookieMap().get(cookieName); - String cookieValue = null; - if (cookie != null) { - cookieValue = cookie.getValue(); - removeCookie(context, cookie, configContext.oidcConfig); - } - return cookieValue; - } - - static void removeCookie(RoutingContext context, ServerCookie cookie, OidcTenantConfig oidcConfig) { - if (cookie != null) { - cookie.setValue(""); - cookie.setMaxAge(0); - Authentication auth = oidcConfig.getAuthentication(); - setCookiePath(context, auth, cookie); - if (auth.cookieDomain.isPresent()) { - cookie.setDomain(auth.cookieDomain.get()); - } - } - } - private boolean isLogout(RoutingContext context, TenantConfigContext configContext) { Optional logoutPath = configContext.oidcConfig.logout.path; @@ -678,15 +640,32 @@ private static String getPostLogoutCookieName(TenantConfigContext configContext) } private static String getSessionCookieName(OidcTenantConfig oidcConfig) { - return SESSION_COOKIE_NAME + getCookieSuffix(oidcConfig); + return OidcUtils.SESSION_COOKIE_NAME + getCookieSuffix(oidcConfig); + } + + private Uni removeSessionCookie(RoutingContext context, TenantConfigContext configContext, String cookieName) { + return OidcUtils.removeSessionCookie(context, configContext.oidcConfig, cookieName, resolver.getTokenStateManager()); } static String getCookieSuffix(OidcTenantConfig oidcConfig) { String tenantId = oidcConfig.tenantId.get(); - String tenantIdSuffix = !"Default".equals(tenantId) ? "_" + tenantId : ""; + String tenantIdSuffix = !"Default".equals(tenantId) ? UNDERSCORE + tenantId : ""; return oidcConfig.authentication.cookieSuffix.isPresent() - ? (tenantIdSuffix + "_" + oidcConfig.authentication.cookieSuffix.get()) + ? (tenantIdSuffix + UNDERSCORE + oidcConfig.authentication.cookieSuffix.get()) : tenantIdSuffix; } + + private static void setTenantIdAttribute(RoutingContext context, String cookiePrefix, String cookieName) { + String tenantId; + if (cookieName.equals(cookiePrefix)) { + context.put(OidcUtils.TENANT_ID_ATTRIBUTE, OidcUtils.DEFAULT_TENANT_ID); + } else { + String suffix = cookieName.substring(cookiePrefix.length() + 1); + // It can be either be a tenant_id or tenand_id and cookie suffix property + int index = suffix.indexOf(UNDERSCORE); + tenantId = index == -1 ? suffix : suffix.substring(0, index); + context.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId); + } + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java index dd477dfc5d8c1..13657f406139a 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenStateManager.java @@ -14,8 +14,8 @@ @ApplicationScoped public class DefaultTokenStateManager implements TokenStateManager { - private static final String SESSION_AT_COOKIE_NAME = CodeAuthenticationMechanism.SESSION_COOKIE_NAME + "_at"; - private static final String SESSION_RT_COOKIE_NAME = CodeAuthenticationMechanism.SESSION_COOKIE_NAME + "_rt"; + private static final String SESSION_AT_COOKIE_NAME = OidcUtils.SESSION_COOKIE_NAME + "_at"; + private static final String SESSION_RT_COOKIE_NAME = OidcUtils.SESSION_COOKIE_NAME + "_rt"; @Override public Uni createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig, @@ -101,9 +101,9 @@ public Uni getTokens(RoutingContext routingContext, Oid public Uni deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState, OidcRequestContext requestContext) { if (oidcConfig.tokenStateManager.splitTokens) { - CodeAuthenticationMechanism.removeCookie(routingContext, getAccessTokenCookie(routingContext, oidcConfig), + OidcUtils.removeCookie(routingContext, getAccessTokenCookie(routingContext, oidcConfig), oidcConfig); - CodeAuthenticationMechanism.removeCookie(routingContext, getRefreshTokenCookie(routingContext, oidcConfig), + OidcUtils.removeCookie(routingContext, getRefreshTokenCookie(routingContext, oidcConfig), oidcConfig); } return CodeAuthenticationMechanism.VOID_UNI; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index e84df789458ab..d355640e96083 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -35,7 +35,6 @@ public class OidcRecorder { private static final Logger LOG = Logger.getLogger(OidcRecorder.class); - private static final String DEFAULT_TENANT_ID = "Default"; private static final Map dynamicTenantsConfig = new ConcurrentHashMap<>(); @@ -46,7 +45,7 @@ public Supplier setupTokenCache(OidcConf public Supplier setup(OidcConfig config, Supplier vertx, TlsConfig tlsConfig) { final Vertx vertxValue = vertx.get(); - String defaultTenantId = config.defaultTenant.getTenantId().orElse(DEFAULT_TENANT_ID); + String defaultTenantId = config.defaultTenant.getTenantId().orElse(OidcUtils.DEFAULT_TENANT_ID); TenantConfigContext defaultTenantContext = createStaticTenantContext(vertxValue, config.defaultTenant, tlsConfig, defaultTenantId); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcSessionImpl.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcSessionImpl.java new file mode 100644 index 0000000000000..80f897682aa82 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcSessionImpl.java @@ -0,0 +1,64 @@ +package io.quarkus.oidc.runtime; + +import java.time.Instant; +import java.util.function.Function; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.oidc.OidcSession; +import io.quarkus.oidc.OidcTenantConfig; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class OidcSessionImpl implements OidcSession { + + @Inject + RoutingContext routingContext; + + @Inject + DefaultTenantConfigResolver resolver; + + @Inject + @IdToken + JsonWebToken idToken; + + @Override + public String getTenantId() { + return routingContext.get(OidcUtils.TENANT_ID_ATTRIBUTE); + } + + @Override + public Uni logout() { + String sessionCookieName = routingContext.get(OidcUtils.SESSION_COOKIE_NAME); + if (sessionCookieName != null) { + Uni oidcConfigUni = resolver.resolveConfig(routingContext); + return oidcConfigUni.onItem().transformToUni(new Function>() { + + @Override + public Uni apply(OidcTenantConfig oidcConfig) { + return OidcUtils.removeSessionCookie(routingContext, oidcConfig, sessionCookieName, + resolver.getTokenStateManager()); + } + + }); + } + return Uni.createFrom().voidItem(); + } + + @Override + public Instant expiresIn() { + final long nowSecs = System.currentTimeMillis() / 1000; + return Instant.ofEpochSecond(idToken.getExpirationTime() - nowSecs); + } + + @Override + public JsonWebToken getIdToken() { + return idToken; + } + +} 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 c61086e0e1fe0..bf7e2752327b2 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 @@ -20,14 +20,18 @@ import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.OidcTenantConfig.Authentication; import io.quarkus.oidc.RefreshToken; import io.quarkus.oidc.TokenIntrospection; +import io.quarkus.oidc.TokenStateManager; import io.quarkus.oidc.UserInfo; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.security.runtime.QuarkusSecurityIdentity.Builder; +import io.smallrye.mutiny.Uni; +import io.vertx.core.http.impl.ServerCookie; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; @@ -39,6 +43,11 @@ public final class OidcUtils { public static final String USER_INFO_ATTRIBUTE = "userinfo"; public static final String INTROSPECTION_ATTRIBUTE = "introspection"; public static final String TENANT_ID_ATTRIBUTE = "tenant-id"; + public static final String DEFAULT_TENANT_ID = "Default"; + public static final String SESSION_COOKIE_NAME = "q_session"; + static final Uni VOID_UNI = Uni.createFrom().voidItem(); + static final BlockingTaskRunner deleteTokensRequestContext = new BlockingTaskRunner(); + /** * This pattern uses a positive lookahead to split an expression around the forward slashes * ignoring those which are located inside a pair of the double quotes. @@ -230,4 +239,45 @@ public static void validatePrimaryJwtTokenType(OidcTenantConfig.Token tokenConfi } } } + + static Uni removeSessionCookie(RoutingContext context, OidcTenantConfig oidcConfig, String cookieName, + TokenStateManager tokenStateManager) { + String cookieValue = removeCookie(context, oidcConfig, cookieName); + if (cookieValue != null) { + return tokenStateManager.deleteTokens(context, oidcConfig, cookieValue, + deleteTokensRequestContext); + } else { + return VOID_UNI; + } + } + + static String removeCookie(RoutingContext context, OidcTenantConfig oidcConfig, String cookieName) { + ServerCookie cookie = (ServerCookie) context.cookieMap().get(cookieName); + String cookieValue = null; + if (cookie != null) { + cookieValue = cookie.getValue(); + removeCookie(context, cookie, oidcConfig); + } + return cookieValue; + } + + static void removeCookie(RoutingContext context, ServerCookie cookie, OidcTenantConfig oidcConfig) { + if (cookie != null) { + cookie.setValue(""); + cookie.setMaxAge(0); + Authentication auth = oidcConfig.getAuthentication(); + setCookiePath(context, auth, cookie); + if (auth.cookieDomain.isPresent()) { + cookie.setDomain(auth.cookieDomain.get()); + } + } + } + + static void setCookiePath(RoutingContext context, Authentication auth, ServerCookie cookie) { + if (auth.cookiePathHeader.isPresent() && context.request().headers().contains(auth.cookiePathHeader.get())) { + cookie.setPath(context.request().getHeader(auth.cookiePathHeader.get())); + } else { + cookie.setPath(auth.getCookiePath()); + } + } } diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 0d6947562f813..b3e22478b9b1c 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -23,6 +23,7 @@ public String resolve(RoutingContext context) { if ("tenant-hybrid".equals(tenantId)) { return context.request().getHeader("Authorization") != null ? "tenant-hybrid-service" : "tenant-hybrid-webapp"; } + return tenantId; } diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantResource.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantResource.java index 7a3b0cf337f81..5cc47509fe30a 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantResource.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantResource.java @@ -5,6 +5,7 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; @@ -13,6 +14,7 @@ import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.IdToken; import io.quarkus.oidc.OIDCException; +import io.quarkus.oidc.OidcSession; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.oidc.UserInfo; import io.quarkus.security.identity.SecurityIdentity; @@ -36,6 +38,9 @@ public class TenantResource { @IdToken JsonWebToken idToken; + @Inject + OidcSession oidcSession; + @GET @RolesAllowed("user") public String userNameService(@PathParam("tenant") String tenant) { @@ -72,23 +77,37 @@ public String userNameServiceNoDiscovery(@PathParam("tenant") String tenant) { @GET @Path("webapp") @RolesAllowed("user") - public String userNameWebApp(@PathParam("tenant") String tenant) { + public String userNameWebApp(@PathParam("tenant") String tenant, @QueryParam("logout") boolean localLogout) { if (!tenant.equals("tenant-web-app") && !tenant.equals("tenant-web-app-dynamic") && !tenant.equals("tenant-web-app-no-discovery")) { throw new OIDCException("Wrong tenant"); } + if (!tenant.equals(oidcSession.getTenantId())) { + throw new OIDCException("'tenant' parameter does not match the OIDC session tenantid"); + } UserInfo userInfo = getUserInfo(); if (!idToken.getGroups().contains("user")) { throw new OIDCException("Groups expected"); } - return tenant + ":" + getNameWebAppType(userInfo.getString("upn"), "upn", "preferred_username"); + + if (!idToken.getRawToken().equals(oidcSession.getIdToken().getRawToken())) { + throw new OIDCException("Wrong ID token injection"); + } + + String response = tenant + ":" + getNameWebAppType(userInfo.getString("upn"), "upn", "preferred_username"); + + if (localLogout) { + oidcSession.logout().await().indefinitely(); + response += ":logout"; + } + return response; } @GET @Path("webapp-no-discovery") @RolesAllowed("user") public String userNameWebAppNoDiscovery(@PathParam("tenant") String tenant) { - return userNameWebApp(tenant); + return userNameWebApp(tenant, false); } private UserInfo getUserInfo() { diff --git a/integration-tests/oidc-tenancy/src/main/resources/application.properties b/integration-tests/oidc-tenancy/src/main/resources/application.properties index b5b68a95062fe..ed7675f879647 100644 --- a/integration-tests/oidc-tenancy/src/main/resources/application.properties +++ b/integration-tests/oidc-tenancy/src/main/resources/application.properties @@ -101,4 +101,9 @@ quarkus.oidc.tenant-public-key.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCg smallrye.jwt.sign.key.location=/privateKey.pem quarkus.log.category."com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet".level=FATAL -quarkus.http.auth.proactive=false \ No newline at end of file +quarkus.http.auth.proactive=false + +quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".min-level=TRACE +quarkus.log.category."io.quarkus.oidc.runtime.CodeAuthenticationMechanism".level=TRACE +quarkus.log.category."io.quarkus.oidc.runtime.OidcProviderClient".min-level=TRACE +quarkus.log.category."io.quarkus.oidc.runtime.OidcProviderClient".level=TRACE diff --git a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java index a18de4bab3635..bef447d591b3b 100644 --- a/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java +++ b/integration-tests/oidc-tenancy/src/test/java/io/quarkus/it/keycloak/BearerTokenAuthorizationTest.java @@ -7,12 +7,15 @@ import static org.junit.jupiter.api.Assertions.assertNull; import java.io.IOException; +import java.net.URI; import org.junit.jupiter.api.Test; import org.keycloak.representations.AccessTokenResponse; import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebRequest; +import com.gargoylesoftware.htmlunit.WebResponse; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.util.Cookie; @@ -38,13 +41,41 @@ public void testResolveTenantIdentifierWebApp() throws IOException { HtmlPage page = webClient.getPage("http://localhost:8081/tenant/tenant-web-app/api/user/webapp"); // State cookie is available but there must be no saved path parameter // as the tenant-web-app configuration does not set a redirect-path property + assertNull(getSessionCookie(webClient, "tenant-web-app")); + assertNotNull(getStateCookie(webClient, "tenant-web-app")); assertNull(getStateCookieSavedPath(webClient, "tenant-web-app")); assertEquals("Sign in to quarkus-webapp", page.getTitleText()); HtmlForm loginForm = page.getForms().get(0); loginForm.getInputByName("username").setValueAttribute("alice"); loginForm.getInputByName("password").setValueAttribute("alice"); page = loginForm.getInputByName("login").click(); + // First call after a redirect, tenant-id is calculated from the state `q_auth` cookie assertEquals("tenant-web-app:alice", page.getBody().asText()); + assertNotNull(getSessionCookie(webClient, "tenant-web-app")); + assertNull(getStateCookie(webClient, "tenant-web-app")); + + // Second call after a redirect, tenant-id is calculated from the state `q_session` cookie + page = webClient.getPage("http://localhost:8081/tenant/tenant-web-app/api/user/webapp"); + assertEquals("tenant-web-app:alice", page.getBody().asText()); + assertNotNull(getSessionCookie(webClient, "tenant-web-app")); + assertNull(getStateCookie(webClient, "tenant-web-app")); + + // Local logout + page = webClient.getPage("http://localhost:8081/tenant/tenant-web-app/api/user/webapp?logout=true"); + assertEquals("tenant-web-app:alice:logout", page.getBody().asText()); + assertNull(getSessionCookie(webClient, "tenant-web-app")); + assertNull(getStateCookie(webClient, "tenant-web-app")); + + // Check a new login is requested via redirect + webClient.getOptions().setRedirectEnabled(false); + WebResponse webResponse = webClient + .loadWebResponse( + new WebRequest(URI.create("http://localhost:8081/tenant/tenant-web-app/api/user/webapp").toURL())); + assertEquals(302, webResponse.getStatusCode()); + assertNull(getSessionCookie(webClient, "tenant-web-app")); + assertNotNull(getStateCookie(webClient, "tenant-web-app")); + assertNull(getStateCookieSavedPath(webClient, "tenant-web-app")); + webClient.getCookieManager().clearCookies(); } } @@ -508,6 +539,10 @@ private Cookie getStateCookie(WebClient webClient, String tenantId) { return webClient.getCookieManager().getCookie("q_auth" + (tenantId == null ? "" : "_" + tenantId)); } + private Cookie getSessionCookie(WebClient webClient, String tenantId) { + return webClient.getCookieManager().getCookie("q_session" + (tenantId == null ? "" : "_" + tenantId)); + } + private String getStateCookieSavedPath(WebClient webClient, String tenantId) { String[] parts = getStateCookie(webClient, tenantId).getValue().split("\\|"); return parts.length == 2 ? parts[1] : null;