diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/AuthenticatedActionsHandler.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/AuthenticatedActionsHandler.java
index 2b218733fb..d754642db8 100644
--- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/AuthenticatedActionsHandler.java
+++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/AuthenticatedActionsHandler.java
@@ -36,6 +36,7 @@
* @author Farah Juma
*/
public class AuthenticatedActionsHandler {
+
private OidcClientConfiguration deployment;
private OidcHttpFacade facade;
@@ -52,6 +53,7 @@ public boolean handledRequest() {
queryBearerToken();
return true;
}
+
return false;
}
diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java
index d40be6bfce..2c733cdacd 100644
--- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java
+++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/IDToken.java
@@ -53,6 +53,7 @@ public class IDToken extends JsonWebToken {
public static final String CLAIMS_LOCALES = "claims_locales";
public static final String ACR = "acr";
public static final String S_HASH = "s_hash";
+ public static final String SID = "sid";
/**
* Construct a new instance.
@@ -228,4 +229,12 @@ public String getAcr() {
return getClaimValueAsString(ACR);
}
+ /**
+ * Get the sid claim.
+ *
+ * @return the sid claim
+ */
+ public String getSid() {
+ return getClaimValueAsString(SID);
+ }
}
diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/LogoutHandler.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/LogoutHandler.java
new file mode 100644
index 0000000000..68ae38f7ac
--- /dev/null
+++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/LogoutHandler.java
@@ -0,0 +1,264 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2024 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.wildfly.security.http.oidc;
+
+import static java.util.Collections.synchronizedMap;
+import static org.wildfly.security.http.oidc.ElytronMessages.log;
+
+import java.net.URISyntaxException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.http.HttpStatus;
+import org.apache.http.client.utils.URIBuilder;
+import org.jose4j.jwt.JwtClaims;
+import org.wildfly.security.http.HttpConstants;
+import org.wildfly.security.http.oidc.OidcHttpFacade.Request;
+
+/**
+ * @author Pedro Igor
+ */
+final class LogoutHandler {
+
+ private static final String POST_LOGOUT_REDIRECT_URI_PARAM = "post_logout_redirect_uri";
+ private static final String ID_TOKEN_HINT_PARAM = "id_token_hint";
+ private static final String LOGOUT_TOKEN_PARAM = "logout_token";
+ private static final String LOGOUT_TOKEN_TYPE = "Logout";
+ private static final String SID = "sid";
+ private static final String ISS = "iss";
+
+ /**
+ * A bounded map to store sessions marked for invalidation after receiving logout requests through the back-channel
+ */
+ private Map sessionsMarkedForInvalidation = synchronizedMap(new LinkedHashMap(16, 0.75f, true) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ boolean remove = sessionsMarkedForInvalidation.size() > eldest.getValue().getLogoutSessionWaitingLimit();
+
+ if (remove) {
+ log.debugf("Limit [%s] reached for sessions waiting [%s] for logout", eldest.getValue().getLogoutSessionWaitingLimit(), sessionsMarkedForInvalidation.size());
+ }
+
+ return remove;
+ }
+ });
+
+ boolean tryLogout(OidcHttpFacade facade) {
+ RefreshableOidcSecurityContext securityContext = getSecurityContext(facade);
+
+ if (securityContext == null) {
+ // no active session
+ return false;
+ }
+
+ if (isSessionMarkedForInvalidation(facade)) {
+ // session marked for invalidation, invalidate it
+ log.debug("Invalidating pending logout session");
+ facade.getTokenStore().logout(false);
+ return true;
+ }
+
+ if (isRpInitiatedLogoutUri(facade)) {
+ redirectEndSessionEndpoint(facade);
+ return true;
+ }
+
+ if (isLogoutCallbackUri(facade)) {
+ if (isFrontChannel(facade)) {
+ handleFrontChannelLogoutRequest(facade);
+ return true;
+ } else {
+ // we have an active session, should have received a GET logout request
+ facade.getResponse().setStatus(HttpStatus.SC_METHOD_NOT_ALLOWED);
+ facade.authenticationFailed();
+ }
+ }
+
+ return false;
+ }
+
+ boolean tryBackChannelLogout(OidcHttpFacade facade) {
+ if (isLogoutCallbackUri(facade)) {
+ if (isBackChannel(facade)) {
+ handleBackChannelLogoutRequest(facade);
+ return true;
+ } else {
+ // no active session, should have received a POST logout request
+ facade.getResponse().setStatus(HttpStatus.SC_METHOD_NOT_ALLOWED);
+ facade.authenticationFailed();
+ }
+ }
+ return false;
+ }
+
+ private boolean isSessionMarkedForInvalidation(OidcHttpFacade facade) {
+ RefreshableOidcSecurityContext securityContext = getSecurityContext(facade);
+ IDToken idToken = securityContext.getIDToken();
+
+ if (idToken == null) {
+ return false;
+ }
+
+ return sessionsMarkedForInvalidation.remove(idToken.getSid()) != null;
+ }
+
+ private void redirectEndSessionEndpoint(OidcHttpFacade facade) {
+ RefreshableOidcSecurityContext securityContext = getSecurityContext(facade);
+ OidcClientConfiguration clientConfiguration = securityContext.getOidcClientConfiguration();
+ String logoutUri;
+
+ try {
+ URIBuilder redirectUriBuilder = new URIBuilder(clientConfiguration.getEndSessionEndpointUrl())
+ .addParameter(ID_TOKEN_HINT_PARAM, securityContext.getIDTokenString());
+ String postLogoutUri = clientConfiguration.getPostLogoutUri();
+
+ if (postLogoutUri != null) {
+ redirectUriBuilder.addParameter(POST_LOGOUT_REDIRECT_URI_PARAM, getRedirectUri(facade) + postLogoutUri);
+ }
+
+ logoutUri = redirectUriBuilder.build().toString();
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+
+ log.debugf("Sending redirect to the end_session_endpoint: %s", logoutUri);
+ facade.getResponse().setStatus(HttpStatus.SC_MOVED_TEMPORARILY);
+ facade.getResponse().setHeader(HttpConstants.LOCATION, logoutUri);
+ }
+
+ private void handleBackChannelLogoutRequest(OidcHttpFacade facade) {
+ String logoutToken = facade.getRequest().getFirstParam(LOGOUT_TOKEN_PARAM);
+ TokenValidator tokenValidator = TokenValidator.builder(facade.getOidcClientConfiguration())
+ .setSkipExpirationValidator()
+ .setTokenType(LOGOUT_TOKEN_TYPE)
+ .build();
+ JwtClaims claims;
+
+ try {
+ claims = tokenValidator.verify(logoutToken);
+ } catch (Exception cause) {
+ log.debug("Unexpected error when verifying logout token", cause);
+ facade.getResponse().setStatus(HttpStatus.SC_BAD_REQUEST);
+ facade.authenticationFailed();
+ return;
+ }
+
+ if (!isSessionRequiredOnLogout(facade)) {
+ log.warn("Back-channel logout request received but can not infer sid from logout token to mark it for invalidation");
+ facade.getResponse().setStatus(HttpStatus.SC_BAD_REQUEST);
+ facade.authenticationFailed();
+ return;
+ }
+
+ String sessionId = claims.getClaimValueAsString(SID);
+
+ if (sessionId == null) {
+ facade.getResponse().setStatus(HttpStatus.SC_BAD_REQUEST);
+ facade.authenticationFailed();
+ return;
+ }
+
+ log.debug("Marking session for invalidation during back-channel logout");
+ sessionsMarkedForInvalidation.put(sessionId, facade.getOidcClientConfiguration());
+ }
+
+ private void handleFrontChannelLogoutRequest(OidcHttpFacade facade) {
+ if (isSessionRequiredOnLogout(facade)) {
+ Request request = facade.getRequest();
+ String sessionId = request.getQueryParamValue(SID);
+
+ if (sessionId == null) {
+ facade.getResponse().setStatus(HttpStatus.SC_BAD_REQUEST);
+ facade.authenticationFailed();
+ return;
+ }
+
+ RefreshableOidcSecurityContext context = getSecurityContext(facade);
+ IDToken idToken = context.getIDToken();
+ String issuer = request.getQueryParamValue(ISS);
+
+ if (idToken == null || !sessionId.equals(idToken.getSid()) || !idToken.getIssuer().equals(issuer)) {
+ facade.getResponse().setStatus(HttpStatus.SC_BAD_REQUEST);
+ facade.authenticationFailed();
+ return;
+ }
+ }
+
+ log.debug("Invalidating session during front-channel logout");
+ facade.getTokenStore().logout(false);
+ }
+
+ private String getRedirectUri(OidcHttpFacade facade) {
+ String uri = facade.getRequest().getURI();
+
+ if (uri.indexOf('?') != -1) {
+ uri = uri.substring(0, uri.indexOf('?'));
+ }
+
+ int logoutPathIndex = uri.indexOf(getLogoutUri(facade));
+
+ if (logoutPathIndex != -1) {
+ uri = uri.substring(0, logoutPathIndex);
+ }
+
+ return uri;
+ }
+
+ private boolean isLogoutCallbackUri(OidcHttpFacade facade) {
+ String path = facade.getRequest().getRelativePath();
+ return path.endsWith(getLogoutCallbackUri(facade));
+ }
+
+ private boolean isRpInitiatedLogoutUri(OidcHttpFacade facade) {
+ String path = facade.getRequest().getRelativePath();
+ return path.endsWith(getLogoutUri(facade));
+ }
+
+ private boolean isSessionRequiredOnLogout(OidcHttpFacade facade) {
+ return facade.getOidcClientConfiguration().isSessionRequiredOnLogout();
+ }
+
+ private RefreshableOidcSecurityContext getSecurityContext(OidcHttpFacade facade) {
+ RefreshableOidcSecurityContext securityContext = (RefreshableOidcSecurityContext) facade.getSecurityContext();
+
+ if (securityContext == null) {
+ facade.getResponse().setStatus(HttpStatus.SC_UNAUTHORIZED);
+ facade.authenticationFailed();
+ return null;
+ }
+
+ return securityContext;
+ }
+
+ private String getLogoutUri(OidcHttpFacade facade) {
+ return facade.getOidcClientConfiguration().getLogoutUrl();
+ }
+
+ private String getLogoutCallbackUri(OidcHttpFacade facade) {
+ return facade.getOidcClientConfiguration().getLogoutCallbackUrl();
+ }
+
+ private boolean isBackChannel(OidcHttpFacade facade) {
+ return "post".equalsIgnoreCase(facade.getRequest().getMethod());
+ }
+
+ private boolean isFrontChannel(OidcHttpFacade facade) {
+ return "get".equalsIgnoreCase(facade.getRequest().getMethod());
+ }
+}
diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcAuthenticationMechanism.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcAuthenticationMechanism.java
index b83fc58472..602cf23d3b 100644
--- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcAuthenticationMechanism.java
+++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcAuthenticationMechanism.java
@@ -41,6 +41,7 @@
*/
final class OidcAuthenticationMechanism implements HttpServerAuthenticationMechanism {
+ private static LogoutHandler logoutHandler = new LogoutHandler();
private final Map properties;
private final CallbackHandler callbackHandler;
private final OidcClientContext oidcClientContext;
@@ -83,7 +84,7 @@ public void evaluateRequest(HttpServerRequest request) throws HttpAuthentication
AuthOutcome outcome = authenticator.authenticate();
if (AuthOutcome.AUTHENTICATED.equals(outcome)) {
- if (new AuthenticatedActionsHandler(oidcClientConfiguration, httpFacade).handledRequest()) {
+ if (new AuthenticatedActionsHandler(oidcClientConfiguration, httpFacade).handledRequest() || logoutHandler.tryLogout(httpFacade)) {
httpFacade.authenticationInProgress();
} else {
httpFacade.authenticationComplete();
@@ -91,6 +92,13 @@ public void evaluateRequest(HttpServerRequest request) throws HttpAuthentication
return;
}
+ if (AuthOutcome.NOT_ATTEMPTED.equals(outcome)) {
+ if (logoutHandler.tryBackChannelLogout(httpFacade)) {
+ httpFacade.authenticationInProgress();
+ return;
+ }
+ }
+
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
httpFacade.noAuthenticationInProgress(challenge);
diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java
index ca56da2863..e7bc0943ec 100644
--- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java
+++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientConfiguration.java
@@ -75,7 +75,7 @@ public enum RelativeUrlsUsed {
protected String providerUrl;
protected String authUrl;
protected String tokenUrl;
- protected String logoutUrl;
+ protected String endSessionEndpointUrl;
protected String accountUrl;
protected String registerNodeUrl;
protected String unregisterNodeUrl;
@@ -144,6 +144,16 @@ public enum RelativeUrlsUsed {
protected String requestObjectSigningKeyStoreType;
protected JWKEncPublicKeyLocator encryptionPublicKeyLocator;
+ private String postLogoutUri;
+
+ private boolean sessionRequiredOnLogout = true;
+
+ private String logoutUrl = "/logout";
+
+ private String logoutCallbackUrl = "/logout/callback";
+
+ private int logoutSessionWaitingLimit = 100;
+
public OidcClientConfiguration() {
}
@@ -202,7 +212,7 @@ public void setAuthServerBaseUrl(OidcJsonConfiguration config) {
protected void resetUrls() {
authUrl = null;
tokenUrl = null;
- logoutUrl = null;
+ endSessionEndpointUrl = null;
accountUrl = null;
registerNodeUrl = null;
unregisterNodeUrl = null;
@@ -238,7 +248,7 @@ protected void resolveUrls() {
authUrl = config.getAuthorizationEndpoint();
issuerUrl = config.getIssuer();
tokenUrl = config.getTokenEndpoint();
- logoutUrl = config.getLogoutEndpoint();
+ endSessionEndpointUrl = config.getLogoutEndpoint();
jwksUrl = config.getJwksUri();
requestParameterSupported = config.getRequestParameterSupported();
requestObjectSigningAlgValuesSupported = config.getRequestObjectSigningAlgValuesSupported();
@@ -323,9 +333,9 @@ public String getTokenUrl() {
return tokenUrl;
}
- public String getLogoutUrl() {
+ public String getEndSessionEndpointUrl() {
resolveUrls();
- return logoutUrl;
+ return endSessionEndpointUrl;
}
public String getAccountUrl() {
@@ -692,6 +702,45 @@ public String getTokenSignatureAlgorithm() {
return tokenSignatureAlgorithm;
}
+ public void setPostLogoutUri(String postLogoutUri) {
+ this.postLogoutUri = postLogoutUri;
+ }
+
+ public String getPostLogoutUri() {
+ return postLogoutUri;
+ }
+
+ public boolean isSessionRequiredOnLogout() {
+ return sessionRequiredOnLogout;
+ }
+
+ public void setSessionRequiredOnLogout(boolean sessionRequiredOnLogout) {
+ this.sessionRequiredOnLogout = sessionRequiredOnLogout;
+ }
+
+ public String getLogoutUrl() {
+ return logoutUrl;
+ }
+
+ public void setLogoutUrl(String logoutUrl) {
+ this.logoutUrl = logoutUrl;
+ }
+
+ public String getLogoutCallbackUrl() {
+ return logoutCallbackUrl;
+ }
+
+ public void setLogoutCallbackUrl(String logoutCallbackUrl) {
+ this.logoutCallbackUrl = logoutCallbackUrl;
+ }
+ public int getLogoutSessionWaitingLimit() {
+ return logoutSessionWaitingLimit;
+ }
+
+ public void setLogoutSessionWaitingLimit(int logoutSessionWaitingLimit) {
+ this.logoutSessionWaitingLimit = logoutSessionWaitingLimit;
+ }
+
public String getAuthenticationRequestFormat() {
return authenticationRequestFormat;
}
diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientContext.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientContext.java
index f5d930bd52..eed194681a 100644
--- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientContext.java
+++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/OidcClientContext.java
@@ -136,8 +136,8 @@ public String getTokenUrl() {
}
@Override
- public String getLogoutUrl() {
- return (this.logoutUrl != null) ? this.logoutUrl : delegate.getLogoutUrl();
+ public String getEndSessionEndpointUrl() {
+ return (this.endSessionEndpointUrl != null) ? this.endSessionEndpointUrl : delegate.getEndSessionEndpointUrl();
}
@Override
diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java
index 3a203541ee..8af9d75f32 100644
--- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java
+++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/ServerRequest.java
@@ -102,7 +102,7 @@ public static AccessAndIDTokenResponse invokeRefresh(OidcClientConfiguration dep
public static void invokeLogout(OidcClientConfiguration deployment, String refreshToken) throws IOException, HttpFailure {
HttpClient client = deployment.getClient();
- String uri = deployment.getLogoutUrl();
+ String uri = deployment.getEndSessionEndpointUrl();
List formparams = new ArrayList<>();
formparams.add(new BasicNameValuePair(Oidc.REFRESH_TOKEN, refreshToken));
diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java
index 746318043f..1c2a0e9108 100644
--- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java
+++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java
@@ -69,10 +69,12 @@ public Boolean run() {
private static final int HEADER_INDEX = 0;
private JwtConsumerBuilder jwtConsumerBuilder;
private OidcClientConfiguration clientConfiguration;
+ private String tokenType;
private TokenValidator(Builder builder) {
this.jwtConsumerBuilder = builder.jwtConsumerBuilder;
this.clientConfiguration = builder.clientConfiguration;
+ this.tokenType = builder.tokenType;
}
/**
@@ -110,11 +112,17 @@ public VerifiedTokens parseAndVerifyToken(final String idToken, final String acc
* @throws OidcException if the bearer token is invalid
*/
public AccessToken parseAndVerifyToken(final String bearerToken) throws OidcException {
+ return new AccessToken(verify(bearerToken));
+ }
+
+ public JwtClaims verify(String bearerToken) throws OidcException {
+ JwtClaims jwtClaims;
+
try {
JwtContext jwtContext = setVerificationKey(bearerToken, jwtConsumerBuilder);
jwtConsumerBuilder.setRequireSubject();
if (! DISABLE_TYP_CLAIM_VALIDATION_PROPERTY) {
- jwtConsumerBuilder.registerValidator(new TypeValidator("Bearer"));
+ jwtConsumerBuilder.registerValidator(new TypeValidator(tokenType));
}
if (clientConfiguration.isVerifyTokenAudience()) {
jwtConsumerBuilder.setExpectedAudience(clientConfiguration.getResourceName());
@@ -123,15 +131,15 @@ public AccessToken parseAndVerifyToken(final String bearerToken) throws OidcExce
}
// second pass to validate
jwtConsumerBuilder.build().processContext(jwtContext);
- JwtClaims jwtClaims = jwtContext.getJwtClaims();
+ jwtClaims = jwtContext.getJwtClaims();
if (jwtClaims == null) {
throw log.invalidBearerTokenClaims();
}
- return new AccessToken(jwtClaims);
} catch (InvalidJwtException e) {
log.tracef("Problem parsing bearer token: " + bearerToken, e);
throw log.invalidBearerToken(e);
}
+ return jwtClaims;
}
private JwtContext setVerificationKey(final String token, final JwtConsumerBuilder jwtConsumerBuilder) throws InvalidJwtException {
@@ -164,6 +172,8 @@ public static Builder builder(OidcClientConfiguration clientConfiguration) {
}
public static class Builder {
+
+ public String tokenType = "Bearer";
private OidcClientConfiguration clientConfiguration;
private String expectedIssuer;
private String clientId;
@@ -171,6 +181,7 @@ public static class Builder {
private PublicKeyLocator publicKeyLocator;
private SecretKey clientSecretKey;
private JwtConsumerBuilder jwtConsumerBuilder;
+ private boolean skipExpirationValidator;
/**
* Construct a new uninitialized instance.
@@ -213,11 +224,24 @@ public TokenValidator build() throws IllegalArgumentException {
jwtConsumerBuilder = new JwtConsumerBuilder()
.setExpectedIssuer(expectedIssuer)
.setJwsAlgorithmConstraints(
- new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, expectedJwsAlgorithm))
- .setRequireExpirationTime();
+ new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, expectedJwsAlgorithm));
+
+ if (!skipExpirationValidator) {
+ jwtConsumerBuilder.setRequireExpirationTime();
+ }
return new TokenValidator(this);
}
+
+ public Builder setSkipExpirationValidator() {
+ this.skipExpirationValidator = true;
+ return this;
+ }
+
+ public Builder setTokenType(String tokenType) {
+ this.tokenType = tokenType;
+ return this;
+ }
}
private static class AzpValidator implements ErrorCodeValidator {
diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/AbstractLogoutTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/AbstractLogoutTest.java
new file mode 100644
index 0000000000..fc139a4a2b
--- /dev/null
+++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/AbstractLogoutTest.java
@@ -0,0 +1,241 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2024 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.wildfly.security.http.oidc;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assume.assumeTrue;
+import static org.wildfly.security.http.oidc.Oidc.OIDC_NAME;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+import io.restassured.RestAssured;
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.wildfly.security.http.HttpAuthenticationException;
+import org.wildfly.security.http.HttpConstants;
+import org.wildfly.security.http.HttpScope;
+import org.wildfly.security.http.HttpServerAuthenticationMechanism;
+import org.wildfly.security.http.Scope;
+
+/**
+ * @author Pedro Igor
+ */
+public abstract class AbstractLogoutTest extends OidcBaseTest {
+
+ private ElytronDispatcher dispatcher;
+ private OidcClientConfiguration clientConfig;
+ static boolean IS_BACK_CHANNEL_TEST = false;
+ static final String BACK_CHANNEL_LOGOUT_URL = "/logout/callback";
+
+ @BeforeClass
+ public static void onBeforeClass() {
+ assumeTrue("Docker isn't available, OIDC tests will be skipped", isDockerAvailable());
+ KEYCLOAK_CONTAINER = new KeycloakContainer();
+ KEYCLOAK_CONTAINER.start();
+ System.setProperty("oidc.provider.url", KEYCLOAK_CONTAINER.getAuthServerUrl() + "/realms/" + TEST_REALM);
+ }
+
+ @AfterClass
+ public static void onAfterClass() {
+ System.clearProperty("oidc.provider.url");
+ }
+
+ @AfterClass
+ public static void generalCleanup() {
+ // no-op
+ }
+
+ @Before
+ public void onBefore() throws IOException {
+ OidcBaseTest.client = new MockWebServer();
+ OidcBaseTest.client.start(new InetSocketAddress(0).getAddress(), CLIENT_PORT);
+ configureDispatcher();
+ RealmRepresentation realm = KeycloakConfiguration.getRealmRepresentation(TEST_REALM, CLIENT_ID, CLIENT_SECRET, CLIENT_HOST_NAME, CLIENT_PORT, CLIENT_APP, CONFIGURE_CLIENT_SCOPES);
+
+ realm.setAccessTokenLifespan(100);
+ realm.setSsoSessionMaxLifespan(100);
+
+ ClientRepresentation client = realm.getClients().get(0);
+
+ client.setAttributes(new HashMap<>());
+
+ doConfigureClient(client);
+
+ List redirectUris = new ArrayList<>(client.getRedirectUris());
+
+ redirectUris.add("*");
+
+ client.setRedirectUris(redirectUris);
+
+ sendRealmCreationRequest(realm);
+ }
+
+ @After
+ public void onAfter() throws IOException {
+ client.shutdown();
+ RestAssured
+ .given()
+ .auth().oauth2(KeycloakConfiguration.getAdminAccessToken(KEYCLOAK_CONTAINER.getAuthServerUrl()))
+ .when()
+ .delete(KEYCLOAK_CONTAINER.getAuthServerUrl() + "/admin/realms/" + TEST_REALM).then().statusCode(204);
+ }
+
+ protected void doConfigureClient(ClientRepresentation client) {
+ }
+
+ protected OidcJsonConfiguration getClientConfiguration() {
+ OidcJsonConfiguration config = new OidcJsonConfiguration();
+
+ config.setRealm(TEST_REALM);
+ config.setResource(CLIENT_ID);
+ config.setPublicClient(false);
+ config.setAuthServerUrl(KEYCLOAK_CONTAINER.getAuthServerUrl());
+ config.setSslRequired("EXTERNAL");
+ config.setCredentials(new HashMap<>());
+ config.getCredentials().put("secret", CLIENT_SECRET);
+
+ return config;
+ }
+
+ protected TestingHttpServerRequest getCurrentRequest() {
+ return dispatcher.getCurrentRequest();
+ }
+
+ protected HttpScope getCurrentSession() {
+ return getCurrentRequest().getScope(Scope.SESSION);
+ }
+
+ protected OidcClientConfiguration getClientConfig() {
+ return clientConfig;
+ }
+
+ protected TestingHttpServerResponse getCurrentResponse() {
+ try {
+ return dispatcher.getCurrentRequest().getResponse();
+ } catch (HttpAuthenticationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ class ElytronDispatcher extends Dispatcher {
+
+ volatile TestingHttpServerRequest currentRequest;
+
+ private final HttpServerAuthenticationMechanism mechanism;
+ private Dispatcher beforeDispatcher;
+ private HttpScope sessionScope;
+
+ public ElytronDispatcher(HttpServerAuthenticationMechanism mechanism, Dispatcher beforeDispatcher) {
+ this.mechanism = mechanism;
+ this.beforeDispatcher = beforeDispatcher;
+ }
+
+ @Override
+ public MockResponse dispatch(RecordedRequest serverRequest) throws InterruptedException {
+ if (beforeDispatcher != null) {
+ MockResponse response = beforeDispatcher.dispatch(serverRequest);
+
+ if (response != null) {
+ return response;
+ }
+ }
+
+ MockResponse mockResponse = new MockResponse();
+
+ try {
+ if (IS_BACK_CHANNEL_TEST && serverRequest.getRequestUrl().toString().endsWith(BACK_CHANNEL_LOGOUT_URL)) {
+ currentRequest = new TestingHttpServerRequest(serverRequest, null);
+ } else {
+ currentRequest = new TestingHttpServerRequest(serverRequest, sessionScope);
+ }
+
+ mechanism.evaluateRequest(currentRequest);
+
+ TestingHttpServerResponse response = currentRequest.getResponse();
+
+ if (Status.COMPLETE.equals(currentRequest.getResult())) {
+ mockResponse.setBody("Welcome, authenticated user");
+ sessionScope = currentRequest.getScope(Scope.SESSION);
+ } else {
+ boolean statusSet = response.getStatusCode() > 0;
+
+ if (statusSet) {
+ mockResponse.setResponseCode(response.getStatusCode());
+
+ if (response.getLocation() != null) {
+ mockResponse.setHeader(HttpConstants.LOCATION, response.getLocation());
+ }
+ } else {
+ mockResponse.setResponseCode(201);
+ mockResponse.setBody("from " + serverRequest.getPath());
+ }
+ }
+ } catch (Exception cause) {
+ cause.printStackTrace();
+ mockResponse.setResponseCode(500);
+ }
+
+ return mockResponse;
+ }
+
+ public TestingHttpServerRequest getCurrentRequest() {
+ return currentRequest;
+ }
+ }
+
+ protected void configureDispatcher() {
+ configureDispatcher(OidcClientConfigurationBuilder.build(getClientConfiguration()), null);
+ }
+
+ protected void configureDispatcher(OidcClientConfiguration clientConfig, Dispatcher beforeDispatch) {
+ this.clientConfig = clientConfig;
+ OidcClientContext oidcClientContext = new OidcClientContext(clientConfig);
+ oidcFactory = new OidcMechanismFactory(oidcClientContext);
+ HttpServerAuthenticationMechanism mechanism;
+ try {
+ mechanism = oidcFactory.createAuthenticationMechanism(OIDC_NAME, Collections.emptyMap(), getCallbackHandler());
+ } catch (HttpAuthenticationException e) {
+ throw new RuntimeException(e);
+ }
+ dispatcher = new ElytronDispatcher(mechanism, beforeDispatch);
+ client.setDispatcher(dispatcher);
+ }
+
+ protected void assertUserNotAuthenticated() {
+ assertNull(getCurrentSession().getAttachment(OidcAccount.class.getName()));
+ }
+
+ protected void assertUserAuthenticated() {
+ assertNotNull(getCurrentSession().getAttachment(OidcAccount.class.getName()));
+ }
+}
diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/BackChannelLogoutTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/BackChannelLogoutTest.java
new file mode 100644
index 0000000000..8540209498
--- /dev/null
+++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/BackChannelLogoutTest.java
@@ -0,0 +1,84 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2024 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.wildfly.security.http.oidc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.List;
+
+import com.gargoylesoftware.htmlunit.Page;
+import com.gargoylesoftware.htmlunit.WebClient;
+import org.apache.http.HttpStatus;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.keycloak.representations.idm.ClientRepresentation;
+
+public class BackChannelLogoutTest extends AbstractLogoutTest {
+
+ @BeforeClass
+ public static void setUp() {
+ IS_BACK_CHANNEL_TEST = true;
+ }
+
+ @Override
+ protected void doConfigureClient(ClientRepresentation client) {
+ List redirectUris = client.getRedirectUris();
+ String redirectUri = redirectUris.get(0);
+
+ client.setFrontchannelLogout(false);
+ client.getAttributes().put("backchannel.logout.session.required", "true");
+ client.getAttributes().put("backchannel.logout.url", rewriteHost(redirectUri) + BACK_CHANNEL_LOGOUT_URL);
+ }
+
+ private static String rewriteHost(String redirectUri) {
+ try {
+ return redirectUri.replace("localhost", InetAddress.getLocalHost().getHostAddress());
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void testRPInitiatedLogout() throws Exception {
+ URI requestUri = new URI(getClientUrl());
+ WebClient webClient = getWebClient();
+ webClient.getPage(getClientUrl());
+ TestingHttpServerResponse response = getCurrentResponse();
+ assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusCode());
+ assertEquals(Status.NO_AUTH, getCurrentRequest().getResult());
+
+ webClient = getWebClient();
+ Page page = loginToKeycloak(webClient, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
+ requestUri, response.getLocation(),
+ response.getCookies())
+ .click();
+ assertTrue(page.getWebResponse().getContentAsString().contains("Welcome, authenticated user"));
+
+ // logged out after finishing the redirections during frontchannel logout
+ assertUserAuthenticated();
+ webClient.getPage(getClientUrl() + "/logout");
+ //assertUserAuthenticated();
+ webClient.getPage(getClientUrl());
+ assertUserNotAuthenticated();
+ }
+}
\ No newline at end of file
diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/FrontChannelLogoutTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/FrontChannelLogoutTest.java
new file mode 100644
index 0000000000..dd4ca58c74
--- /dev/null
+++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/FrontChannelLogoutTest.java
@@ -0,0 +1,127 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2024 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.wildfly.security.http.oidc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.net.URI;
+import java.util.List;
+
+import com.gargoylesoftware.htmlunit.Page;
+import com.gargoylesoftware.htmlunit.TextPage;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.QueueDispatcher;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.apache.http.HttpStatus;
+import org.junit.Test;
+import org.keycloak.representations.idm.ClientRepresentation;
+
+/**
+ * Tests for the OpenID Connect authentication mechanism.
+ *
+ * @author Farah Juma
+ */
+public class FrontChannelLogoutTest extends AbstractLogoutTest {
+
+ @Override
+ protected void doConfigureClient(ClientRepresentation client) {
+ client.setFrontchannelLogout(true);
+ List redirectUris = client.getRedirectUris();
+ String redirectUri = redirectUris.get(0);
+
+ client.getAttributes().put("frontchannel.logout.url", redirectUri + "/logout/callback");
+ }
+
+ @Test
+ public void testRPInitiatedLogout() throws Exception {
+ URI requestUri = new URI(getClientUrl());
+ WebClient webClient = getWebClient();
+ webClient.getPage(getClientUrl());
+ TestingHttpServerResponse response = getCurrentResponse();
+ assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusCode());
+ assertEquals(Status.NO_AUTH, getCurrentRequest().getResult());
+
+ webClient = getWebClient();
+ Page page = loginToKeycloak(webClient, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD,
+ requestUri, response.getLocation(),
+ response.getCookies())
+ .click();
+ assertTrue(page.getWebResponse().getContentAsString().contains("Welcome, authenticated user"));
+
+ // logged out after finishing the redirections during frontchannel logout
+ assertUserAuthenticated();
+ webClient.getPage(getClientUrl() + "/logout");
+ assertUserNotAuthenticated();
+ }
+
+ @Test
+ public void testRPInitiatedLogoutWithPostLogoutUri() throws Exception {
+ OidcClientConfiguration oidcClientConfiguration = getClientConfig();
+ oidcClientConfiguration.setPostLogoutUri("/post-logout");
+ configureDispatcher(oidcClientConfiguration, new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+ if (request.getPath().contains("/post-logout")) {
+ return new MockResponse()
+ .setBody("you are logged out from app");
+ }
+ return null;
+ }
+ });
+
+ URI requestUri = new URI(getClientUrl());
+ WebClient webClient = getWebClient();
+ webClient.getPage(getClientUrl());
+ TestingHttpServerResponse response = getCurrentResponse();
+ Page page = loginToKeycloak(webClient, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, requestUri, response.getLocation(),
+ response.getCookies()).click();
+ assertTrue(page.getWebResponse().getContentAsString().contains("Welcome, authenticated user"));
+
+ assertUserAuthenticated();
+ HtmlPage continueLogout = webClient.getPage(getClientUrl() + "/logout");
+ page = continueLogout.getElementById("continue").click();
+ assertUserNotAuthenticated();
+ assertTrue(page.getWebResponse().getContentAsString().contains("you are logged out from app"));
+ }
+
+ @Test
+ public void testFrontChannelLogout() throws Exception {
+ try {
+ URI requestUri = new URI(getClientUrl());
+ WebClient webClient = getWebClient();
+ webClient.getPage(getClientUrl());
+ TextPage page = loginToKeycloak(webClient, KeycloakConfiguration.ALICE, KeycloakConfiguration.ALICE_PASSWORD, requestUri, getCurrentResponse().getLocation(),
+ getCurrentResponse().getCookies()).click();
+ assertTrue(page.getContent().contains("Welcome, authenticated user"));
+
+ HtmlPage logoutPage = webClient.getPage(getClientConfig().getEndSessionEndpointUrl() + "?client_id=" + CLIENT_ID);
+ HtmlForm form = logoutPage.getForms().get(0);
+ assertUserAuthenticated();
+ form.getInputByName("confirmLogout").click();
+ assertUserNotAuthenticated();
+ } finally {
+ client.setDispatcher(new QueueDispatcher());
+ }
+ }
+}
\ No newline at end of file
diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java
index 6eb698160a..b8a14b0d98 100644
--- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java
+++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcBaseTest.java
@@ -212,7 +212,7 @@ public MockResponse dispatch(RecordedRequest recordedRequest) throws Interrupted
}
protected static Dispatcher createAppResponse(HttpServerAuthenticationMechanism mechanism, int expectedStatusCode, String expectedLocation, String clientPageText,
- Map sessionScopeAttachments) {
+ Map attachments) {
return new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException {
@@ -225,8 +225,8 @@ public MockResponse dispatch(RecordedRequest recordedRequest) throws Interrupted
TestingHttpServerResponse response = request.getResponse();
assertEquals(expectedStatusCode, response.getStatusCode());
assertEquals(expectedLocation, response.getLocation());
- for (String key : request.getSessionScopeAttachments().keySet()) {
- sessionScopeAttachments.put(key, request.getSessionScopeAttachments().get(key));
+ for (String key : request.getAttachments().keySet()) {
+ attachments.put(key, request.getAttachments().get(key));
}
return new MockResponse().setBody(clientPageText);
} catch (Exception e) {
@@ -240,7 +240,7 @@ public MockResponse dispatch(RecordedRequest recordedRequest) throws Interrupted
}
protected static Dispatcher createAppResponse(HttpServerAuthenticationMechanism mechanism, String clientPageText,
- Map sessionScopeAttachments, String tenant, boolean sameTenant) {
+ Map attachments, String tenant, boolean sameTenant) {
return new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException {
@@ -248,7 +248,7 @@ public MockResponse dispatch(RecordedRequest recordedRequest) throws Interrupted
if (path.contains("/" + CLIENT_APP + "/" + tenant)) {
try {
TestingHttpServerRequest request = new TestingHttpServerRequest(new String[0],
- new URI(recordedRequest.getRequestUrl().toString()), sessionScopeAttachments);
+ new URI(recordedRequest.getRequestUrl().toString()), attachments);
mechanism.evaluateRequest(request);
TestingHttpServerResponse response = request.getResponse();
if (sameTenant) {
@@ -291,7 +291,10 @@ protected static String getClientUrlForTenant(String tenant) {
}
protected HtmlInput loginToKeycloak(String username, String password, URI requestUri, String location, List cookies) throws IOException {
- WebClient webClient = getWebClient();
+ return loginToKeycloak(getWebClient(), username, password, requestUri, location, cookies);
+ }
+
+ protected HtmlInput loginToKeycloak(WebClient webClient, String username, String password, URI requestUri, String location, List cookies) throws IOException {
if (cookies != null) {
for (HttpServerCookie cookie : cookies) {
webClient.addCookie(getCookieString(cookie), requestUri.toURL(), null);
diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java
index 4dede8b5ed..1d5a451005 100644
--- a/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java
+++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/OidcTest.java
@@ -584,7 +584,7 @@ private void performTenantRequestWithProviderUrl(String username, String passwor
private void performTenantRequest(String username, String password, String tenant, String otherTenant, boolean useAuthServerUrl) throws Exception {
try {
Map props = new HashMap<>();
- Map sessionScopeAttachments = new HashMap<>();
+ Map attachments = new HashMap<>();
String clientPageText = getClientPageTestForTenant(tenant);
String expectedLocation = getClientUrlForTenant(tenant);
@@ -604,14 +604,14 @@ private void performTenantRequest(String username, String password, String tenan
assertEquals(Status.NO_AUTH, request.getResult());
// log into Keycloak, we should then be redirected back to the tenant upon successful authentication
- client.setDispatcher(createAppResponse(mechanism, HttpStatus.SC_MOVED_TEMPORARILY, expectedLocation, clientPageText, sessionScopeAttachments));
+ client.setDispatcher(createAppResponse(mechanism, HttpStatus.SC_MOVED_TEMPORARILY, expectedLocation, clientPageText, attachments));
TextPage page = loginToKeycloak(username, password, requestUri, response.getLocation(),
response.getCookies()).click();
assertTrue(page.getContent().contains(clientPageText));
if (otherTenant != null) {
// attempt to access the other tenant
- client.setDispatcher(createAppResponse(mechanism, clientPageText, sessionScopeAttachments, otherTenant, tenant.equals(otherTenant)));
+ client.setDispatcher(createAppResponse(mechanism, clientPageText, attachments, otherTenant, tenant.equals(otherTenant)));
WebClient webClient = getWebClient();
page = webClient.getPage(getClientUrlForTenant(otherTenant));
if (otherTenant.equals(tenant)) {
diff --git a/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java b/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java
index 7b8308fd8c..9fe171c921 100644
--- a/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java
+++ b/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java
@@ -27,7 +27,9 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
import java.net.URI;
+import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.cert.Certificate;
@@ -51,6 +53,8 @@
import javax.security.auth.x500.X500Principal;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.RealmCallback;
+
+import okhttp3.mockwebserver.RecordedRequest;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.Assert;
@@ -144,6 +148,8 @@ protected enum Status {
protected static class TestingHttpServerRequest implements HttpServerRequest {
+ private String contentType;
+ private String body;
private Status result;
private HttpServerMechanismsResponder responder;
private String remoteUser;
@@ -151,8 +157,10 @@ protected static class TestingHttpServerRequest implements HttpServerRequest {
private List cookies;
private String requestMethod = "GET";
private Map> requestHeaders = new HashMap<>();
+ private Map attachments = new HashMap<>();
+ private Map scopes = new HashMap<>();
+ private HttpScope sessionScope;
private X500Principal testPrincipal = null;
- private Map sessionScopeAttachments = new HashMap<>();
public TestingHttpServerRequest(String[] authorization) {
if (authorization != null) {
@@ -180,14 +188,14 @@ public TestingHttpServerRequest(String[] authorization, URI requestURI) {
this.cookies = new ArrayList<>();
}
- public TestingHttpServerRequest(String[] authorization, URI requestURI, Map sessionScopeAttachments) {
+ public TestingHttpServerRequest(String[] authorization, URI requestURI, Map attachments) {
if (authorization != null) {
requestHeaders.put(AUTHORIZATION, Arrays.asList(authorization));
}
this.remoteUser = null;
this.requestURI = requestURI;
this.cookies = new ArrayList<>();
- this.sessionScopeAttachments = sessionScopeAttachments;
+ this.attachments = attachments;
}
public TestingHttpServerRequest(String[] authorization, URI requestURI, List cookies) {
@@ -208,6 +216,10 @@ public TestingHttpServerRequest(Map> requestHeaders, URI re
}
public TestingHttpServerRequest(String[] authorization, URI requestURI, String cookie) {
+ this(authorization, requestURI, cookie, null);
+ }
+
+ public TestingHttpServerRequest(String[] authorization, URI requestURI, String cookie, HttpScope sessionScope) {
if (authorization != null) {
requestHeaders.put(AUTHORIZATION, Arrays.asList(authorization));
}
@@ -219,6 +231,14 @@ public TestingHttpServerRequest(String[] authorization, URI requestURI, String c
final String cookieValue = cookie.substring(cookie.indexOf('=') + 1);
cookies.add(HttpServerCookie.getInstance(cookieName, cookieValue, null, -1, "/", false, 0, true));
}
+ this.sessionScope = sessionScope;
+ }
+
+ public TestingHttpServerRequest(RecordedRequest serverRequest, HttpScope sessionScope) throws URISyntaxException {
+ this(new String[0], new URI(serverRequest.getRequestUrl().toString()), serverRequest.getHeader("Cookie"), sessionScope);
+ this.requestMethod = serverRequest.getMethod();
+ this.body = serverRequest.getBody().readUtf8();
+ this.contentType = serverRequest.getHeader("Content-Type");
}
public Status getResult() {
@@ -292,7 +312,11 @@ public URI getRequestURI() {
}
public String getRequestPath() {
- throw new IllegalStateException();
+ try {
+ return requestURI.toURL().getPath();
+ } catch (MalformedURLException cause) {
+ throw new RuntimeException("Mal-formed request URL", cause);
+ }
}
public Map> getParameters() {
@@ -308,6 +332,19 @@ public List getParameterValues(String name) {
}
public String getFirstParameterValue(String name) {
+ if ("application/x-www-form-urlencoded".equals(contentType)) {
+ if (body == null) {
+ return null;
+ }
+
+ for (String keyValue : body.split("&")) {
+ String key = keyValue.substring(0, keyValue.indexOf('='));
+
+ if (key.equals(name)) {
+ return keyValue.substring(keyValue.indexOf('=') + 1);
+ }
+ }
+ }
throw new IllegalStateException();
}
@@ -334,7 +371,15 @@ public boolean resumeRequest() {
public HttpScope getScope(Scope scope) {
if (scope.equals(Scope.SSL_SESSION)) {
return null;
- } else {
+ }
+
+ if (Scope.SESSION.equals(scope) && sessionScope != null) {
+ return sessionScope;
+ }
+
+ HttpScope httpScope = scopes.get(scope);
+
+ if (httpScope == null) {
return new HttpScope() {
@Override
@@ -354,26 +399,24 @@ public boolean supportsAttachments() {
@Override
public boolean supportsInvalidation() {
- return false;
+ return true;
}
@Override
public void setAttachment(String key, Object value) {
- if (scope.equals(Scope.SESSION)) {
- sessionScopeAttachments.put(key, value);
- }
+ attachments.put(key, value);
}
@Override
public Object getAttachment(String key) {
- if (scope.equals(Scope.SESSION)) {
- return sessionScopeAttachments.get(key);
- } else {
- return null;
- }
+ return attachments.get(key);
}
+
};
}
+ scopes.put(scope, httpScope);
+
+ return httpScope;
}
public Collection getScopeIds(Scope scope) {
@@ -381,7 +424,10 @@ public Collection getScopeIds(Scope scope) {
}
public HttpScope getScope(Scope scope, String id) {
- throw new IllegalStateException();
+ if (Scope.SESSION.equals(scope) && sessionScope != null) {
+ return sessionScope;
+ }
+ return scopes.get(scope);
}
public void setRemoteUser(String remoteUser) {
@@ -393,8 +439,8 @@ public String getRemoteUser() {
return remoteUser;
}
- public Map getSessionScopeAttachments() {
- return sessionScopeAttachments;
+ public Map getAttachments() {
+ return attachments;
}
}