Merge pull request #801 from mposolda/master
KEYCLOAK-702 - Added AdapterTokenStore spi. Possibility to save account ...
This commit is contained in:
commit
4b87b8f7e3
35 changed files with 1351 additions and 340 deletions
|
@ -26,4 +26,7 @@ public interface AdapterConstants {
|
||||||
|
|
||||||
// Attribute passed in registerNode request for register new application cluster node once he joined cluster
|
// Attribute passed in registerNode request for register new application cluster node once he joined cluster
|
||||||
public static final String APPLICATION_CLUSTER_HOST = "application_cluster_host";
|
public static final String APPLICATION_CLUSTER_HOST = "application_cluster_host";
|
||||||
|
|
||||||
|
// Cookie used on adapter side to store token info. Used only when tokenStore is 'COOKIE'
|
||||||
|
public static final String KEYCLOAK_ADAPTER_STATE_COOKIE = "KEYCLOAK_ADAPTER_STATE";
|
||||||
}
|
}
|
||||||
|
|
9
core/src/main/java/org/keycloak/enums/TokenStore.java
Normal file
9
core/src/main/java/org/keycloak/enums/TokenStore.java
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package org.keycloak.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public enum TokenStore {
|
||||||
|
SESSION,
|
||||||
|
COOKIE
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder;
|
||||||
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
|
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
|
||||||
"client-keystore", "client-keystore-password", "client-key-password",
|
"client-keystore", "client-keystore-password", "client-key-password",
|
||||||
"auth-server-url-for-backend-requests", "always-refresh-token",
|
"auth-server-url-for-backend-requests", "always-refresh-token",
|
||||||
"register-node-at-startup", "register-node-period"
|
"register-node-at-startup", "register-node-period", "token-store"
|
||||||
})
|
})
|
||||||
public class AdapterConfig extends BaseAdapterConfig {
|
public class AdapterConfig extends BaseAdapterConfig {
|
||||||
|
|
||||||
|
@ -46,6 +46,8 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
protected boolean registerNodeAtStartup = false;
|
protected boolean registerNodeAtStartup = false;
|
||||||
@JsonProperty("register-node-period")
|
@JsonProperty("register-node-period")
|
||||||
protected int registerNodePeriod = -1;
|
protected int registerNodePeriod = -1;
|
||||||
|
@JsonProperty("token-store")
|
||||||
|
protected String tokenStore;
|
||||||
|
|
||||||
public boolean isAllowAnyHostname() {
|
public boolean isAllowAnyHostname() {
|
||||||
return allowAnyHostname;
|
return allowAnyHostname;
|
||||||
|
@ -142,4 +144,12 @@ public class AdapterConfig extends BaseAdapterConfig {
|
||||||
public void setRegisterNodePeriod(int registerNodePeriod) {
|
public void setRegisterNodePeriod(int registerNodePeriod) {
|
||||||
this.registerNodePeriod = registerNodePeriod;
|
this.registerNodePeriod = registerNodePeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTokenStore() {
|
||||||
|
return tokenStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTokenStore(String tokenStore) {
|
||||||
|
this.tokenStore = tokenStore;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import org.apache.http.client.methods.HttpGet;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.enums.RelativeUrlsUsed;
|
import org.keycloak.enums.RelativeUrlsUsed;
|
||||||
import org.keycloak.enums.SslRequired;
|
import org.keycloak.enums.SslRequired;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
import org.keycloak.representations.idm.PublishedRealmRepresentation;
|
import org.keycloak.representations.idm.PublishedRealmRepresentation;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
@ -257,6 +258,16 @@ public class AdapterDeploymentContext {
|
||||||
delegate.setSslRequired(sslRequired);
|
delegate.setSslRequired(sslRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TokenStore getTokenStore() {
|
||||||
|
return delegate.getTokenStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTokenStore(TokenStore tokenStore) {
|
||||||
|
delegate.setTokenStore(tokenStore);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getStateCookieName() {
|
public String getStateCookieName() {
|
||||||
return delegate.getStateCookieName();
|
return delegate.getStateCookieName();
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstraction for storing token info on adapter side. Intended to be per-request object
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface AdapterTokenStore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Impl can validate if current token exists and perform refreshing if it exists and is expired
|
||||||
|
*/
|
||||||
|
void checkCurrentToken();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we are logged already (we have already valid and successfully refreshed accessToken). Establish security context if yes
|
||||||
|
*
|
||||||
|
* @param authenticator used for actual request authentication
|
||||||
|
* @return true if we are logged-in already
|
||||||
|
*/
|
||||||
|
boolean isCached(RequestAuthenticator authenticator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish successful OAuth2 login and store validated account
|
||||||
|
*
|
||||||
|
* @param account
|
||||||
|
*/
|
||||||
|
void saveAccountInfo(KeycloakAccount account);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle logout on store side and possibly propagate logout call to Keycloak
|
||||||
|
*/
|
||||||
|
void logout();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback invoked after successful token refresh
|
||||||
|
*
|
||||||
|
* @param securityContext context where refresh was performed
|
||||||
|
*/
|
||||||
|
void refreshCallback(RefreshableKeycloakSecurityContext securityContext);
|
||||||
|
}
|
|
@ -1,6 +1,11 @@
|
||||||
package org.keycloak.adapters;
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.util.UriUtils;
|
import org.keycloak.util.UriUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,6 +13,8 @@ import org.keycloak.util.UriUtils;
|
||||||
*/
|
*/
|
||||||
public class AdapterUtils {
|
public class AdapterUtils {
|
||||||
|
|
||||||
|
private static Logger log = Logger.getLogger(AdapterUtils.class);
|
||||||
|
|
||||||
public static String getOrigin(String browserRequestURL, KeycloakSecurityContext session) {
|
public static String getOrigin(String browserRequestURL, KeycloakSecurityContext session) {
|
||||||
if (session instanceof RefreshableKeycloakSecurityContext) {
|
if (session instanceof RefreshableKeycloakSecurityContext) {
|
||||||
KeycloakDeployment deployment = ((RefreshableKeycloakSecurityContext)session).getDeployment();
|
KeycloakDeployment deployment = ((RefreshableKeycloakSecurityContext)session).getDeployment();
|
||||||
|
@ -26,4 +33,30 @@ public class AdapterUtils {
|
||||||
return UriUtils.getOrigin(browserRequestURL);
|
return UriUtils.getOrigin(browserRequestURL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Set<String> getRolesFromSecurityContext(RefreshableKeycloakSecurityContext session) {
|
||||||
|
Set<String> roles = null;
|
||||||
|
AccessToken accessToken = session.getToken();
|
||||||
|
if (session.getDeployment().isUseResourceRoleMappings()) {
|
||||||
|
if (log.isTraceEnabled()) {
|
||||||
|
log.trace("useResourceRoleMappings");
|
||||||
|
}
|
||||||
|
AccessToken.Access access = accessToken.getResourceAccess(session.getDeployment().getResourceName());
|
||||||
|
if (access != null) roles = access.getRoles();
|
||||||
|
} else {
|
||||||
|
if (log.isTraceEnabled()) {
|
||||||
|
log.trace("use realm role mappings");
|
||||||
|
}
|
||||||
|
AccessToken.Access access = accessToken.getRealmAccess();
|
||||||
|
if (access != null) roles = access.getRoles();
|
||||||
|
}
|
||||||
|
if (roles == null) roles = Collections.emptySet();
|
||||||
|
if (log.isTraceEnabled()) {
|
||||||
|
log.trace("Setting roles: ");
|
||||||
|
for (String role : roles) {
|
||||||
|
log.trace(" role: " + role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package org.keycloak.adapters;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.RSATokenVerifier;
|
||||||
|
import org.keycloak.VerificationException;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.IDToken;
|
||||||
|
import org.keycloak.util.KeycloakUriBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class CookieTokenStore {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(CookieTokenStore.class);
|
||||||
|
private static final String DELIM = "@";
|
||||||
|
|
||||||
|
public static void setTokenCookie(KeycloakDeployment deployment, HttpFacade facade, RefreshableKeycloakSecurityContext session) {
|
||||||
|
log.infof("Set new %s cookie now", AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE);
|
||||||
|
String accessToken = session.getTokenString();
|
||||||
|
String idToken = session.getIdTokenString();
|
||||||
|
String refreshToken = session.getRefreshToken();
|
||||||
|
String cookie = new StringBuilder(accessToken).append(DELIM)
|
||||||
|
.append(idToken).append(DELIM)
|
||||||
|
.append(refreshToken).toString();
|
||||||
|
|
||||||
|
String cookiePath = getContextPath(facade);
|
||||||
|
facade.getResponse().setCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookie, cookiePath, null, -1, deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr()), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KeycloakPrincipal<RefreshableKeycloakSecurityContext> getPrincipalFromCookie(KeycloakDeployment deployment, HttpFacade facade, AdapterTokenStore tokenStore) {
|
||||||
|
HttpFacade.Cookie cookie = facade.getRequest().getCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE);
|
||||||
|
if (cookie == null) {
|
||||||
|
log.debug("Not found adapter state cookie in current request");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String cookieVal = cookie.getValue();
|
||||||
|
|
||||||
|
String[] tokens = cookieVal.split(DELIM);
|
||||||
|
if (tokens.length != 3) {
|
||||||
|
log.warnf("Invalid format of %s cookie. Count of tokens: %s, expected 3", AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, tokens.length);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String accessTokenString = tokens[0];
|
||||||
|
String idTokenString = tokens[1];
|
||||||
|
String refreshTokenString = tokens[2];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Skip check if token is active now. It's supposed to be done later by the caller
|
||||||
|
AccessToken accessToken = RSATokenVerifier.verifyToken(accessTokenString, deployment.getRealmKey(), deployment.getRealm(), false);
|
||||||
|
IDToken idToken;
|
||||||
|
if (idTokenString != null && idTokenString.length() > 0) {
|
||||||
|
JWSInput input = new JWSInput(idTokenString);
|
||||||
|
try {
|
||||||
|
idToken = input.readJsonContent(IDToken.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new VerificationException(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Token Verification succeeded!");
|
||||||
|
RefreshableKeycloakSecurityContext secContext = new RefreshableKeycloakSecurityContext(deployment, tokenStore, accessTokenString, accessToken, idTokenString, idToken, refreshTokenString);
|
||||||
|
return new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(accessToken.getSubject(), secContext);
|
||||||
|
} catch (VerificationException ve) {
|
||||||
|
log.warn("Failed verify token", ve);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeCookie(HttpFacade facade) {
|
||||||
|
String cookiePath = getContextPath(facade);
|
||||||
|
facade.getResponse().resetCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookiePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getContextPath(HttpFacade facade) {
|
||||||
|
String uri = facade.getRequest().getURI();
|
||||||
|
String path = KeycloakUriBuilder.fromUri(uri).getPath();
|
||||||
|
int index = path.indexOf("/", 1);
|
||||||
|
return index == -1 ? path : path.substring(0, index);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import org.jboss.logging.Logger;
|
||||||
import org.keycloak.ServiceUrlConstants;
|
import org.keycloak.ServiceUrlConstants;
|
||||||
import org.keycloak.enums.RelativeUrlsUsed;
|
import org.keycloak.enums.RelativeUrlsUsed;
|
||||||
import org.keycloak.enums.SslRequired;
|
import org.keycloak.enums.SslRequired;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
import org.keycloak.util.KeycloakUriBuilder;
|
import org.keycloak.util.KeycloakUriBuilder;
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ public class KeycloakDeployment {
|
||||||
|
|
||||||
protected String scope;
|
protected String scope;
|
||||||
protected SslRequired sslRequired = SslRequired.ALL;
|
protected SslRequired sslRequired = SslRequired.ALL;
|
||||||
|
protected TokenStore tokenStore = TokenStore.SESSION;
|
||||||
protected String stateCookieName = "OAuth_Token_Request_State";
|
protected String stateCookieName = "OAuth_Token_Request_State";
|
||||||
protected boolean useResourceRoleMappings;
|
protected boolean useResourceRoleMappings;
|
||||||
protected boolean cors;
|
protected boolean cors;
|
||||||
|
@ -236,6 +238,14 @@ public class KeycloakDeployment {
|
||||||
this.sslRequired = sslRequired;
|
this.sslRequired = sslRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TokenStore getTokenStore() {
|
||||||
|
return tokenStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTokenStore(TokenStore tokenStore) {
|
||||||
|
this.tokenStore = tokenStore;
|
||||||
|
}
|
||||||
|
|
||||||
public String getStateCookieName() {
|
public String getStateCookieName() {
|
||||||
return stateCookieName;
|
return stateCookieName;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import org.codehaus.jackson.map.ObjectMapper;
|
||||||
import org.codehaus.jackson.map.annotate.JsonSerialize;
|
import org.codehaus.jackson.map.annotate.JsonSerialize;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.enums.SslRequired;
|
import org.keycloak.enums.SslRequired;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||||
import org.keycloak.util.PemUtils;
|
import org.keycloak.util.PemUtils;
|
||||||
import org.keycloak.util.SystemPropertiesJsonParserFactory;
|
import org.keycloak.util.SystemPropertiesJsonParserFactory;
|
||||||
|
@ -48,6 +49,11 @@ public class KeycloakDeploymentBuilder {
|
||||||
} else {
|
} else {
|
||||||
deployment.setSslRequired(SslRequired.EXTERNAL);
|
deployment.setSslRequired(SslRequired.EXTERNAL);
|
||||||
}
|
}
|
||||||
|
if (adapterConfig.getTokenStore() != null) {
|
||||||
|
deployment.setTokenStore(TokenStore.valueOf(adapterConfig.getTokenStore().toUpperCase()));
|
||||||
|
} else {
|
||||||
|
deployment.setTokenStore(TokenStore.SESSION);
|
||||||
|
}
|
||||||
deployment.setResourceCredentials(adapterConfig.getCredentials());
|
deployment.setResourceCredentials(adapterConfig.getCredentials());
|
||||||
deployment.setPublicClient(adapterConfig.isPublicClient());
|
deployment.setPublicClient(adapterConfig.isPublicClient());
|
||||||
deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings());
|
deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings());
|
||||||
|
|
|
@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.RSATokenVerifier;
|
import org.keycloak.RSATokenVerifier;
|
||||||
import org.keycloak.VerificationException;
|
import org.keycloak.VerificationException;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
|
@ -255,7 +256,8 @@ public abstract class OAuthRequestAuthenticator {
|
||||||
AccessTokenResponse tokenResponse = null;
|
AccessTokenResponse tokenResponse = null;
|
||||||
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
|
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
|
||||||
try {
|
try {
|
||||||
String httpSessionId = reqAuthenticator.getHttpSessionId(true);
|
// For COOKIE store we don't have httpSessionId and single sign-out won't be available
|
||||||
|
String httpSessionId = deployment.getTokenStore() == TokenStore.SESSION ? reqAuthenticator.getHttpSessionId(true) : null;
|
||||||
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId);
|
tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, strippedOauthParametersRequestUri, httpSessionId);
|
||||||
} catch (ServerRequest.HttpFailure failure) {
|
} catch (ServerRequest.HttpFailure failure) {
|
||||||
log.error("failed to turn code into token");
|
log.error("failed to turn code into token");
|
||||||
|
|
|
@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.RSATokenVerifier;
|
import org.keycloak.RSATokenVerifier;
|
||||||
import org.keycloak.VerificationException;
|
import org.keycloak.VerificationException;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
@ -19,14 +20,16 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
|
||||||
protected static Logger log = Logger.getLogger(RefreshableKeycloakSecurityContext.class);
|
protected static Logger log = Logger.getLogger(RefreshableKeycloakSecurityContext.class);
|
||||||
|
|
||||||
protected transient KeycloakDeployment deployment;
|
protected transient KeycloakDeployment deployment;
|
||||||
|
protected transient AdapterTokenStore tokenStore;
|
||||||
protected String refreshToken;
|
protected String refreshToken;
|
||||||
|
|
||||||
public RefreshableKeycloakSecurityContext() {
|
public RefreshableKeycloakSecurityContext() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public RefreshableKeycloakSecurityContext(KeycloakDeployment deployment, String tokenString, AccessToken token, String idTokenString, IDToken idToken, String refreshToken) {
|
public RefreshableKeycloakSecurityContext(KeycloakDeployment deployment, AdapterTokenStore tokenStore, String tokenString, AccessToken token, String idTokenString, IDToken idToken, String refreshToken) {
|
||||||
super(tokenString, token, idTokenString, idToken);
|
super(tokenString, token, idTokenString, idToken);
|
||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
|
this.tokenStore = tokenStore;
|
||||||
this.refreshToken = refreshToken;
|
this.refreshToken = refreshToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +45,10 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
|
||||||
return super.getTokenString();
|
return super.getTokenString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getRefreshToken() {
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
public void logout(KeycloakDeployment deployment) {
|
public void logout(KeycloakDeployment deployment) {
|
||||||
try {
|
try {
|
||||||
ServerRequest.invokeLogout(deployment, refreshToken);
|
ServerRequest.invokeLogout(deployment, refreshToken);
|
||||||
|
@ -58,8 +65,9 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
|
||||||
return deployment;
|
return deployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeployment(KeycloakDeployment deployment) {
|
public void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
|
||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
|
this.tokenStore = tokenStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,8 +115,14 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.refreshToken = response.getRefreshToken();
|
this.refreshToken = response.getRefreshToken();
|
||||||
this.tokenString = tokenString;
|
this.tokenString = tokenString;
|
||||||
|
tokenStore.refreshCallback(this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void updateTokenCookie(KeycloakDeployment deployment, HttpFacade facade) {
|
||||||
|
if (deployment.getTokenStore() == TokenStore.COOKIE) {
|
||||||
|
CookieTokenStore.setTokenCookie(deployment, facade, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,14 @@ public abstract class RequestAuthenticator {
|
||||||
|
|
||||||
protected HttpFacade facade;
|
protected HttpFacade facade;
|
||||||
protected KeycloakDeployment deployment;
|
protected KeycloakDeployment deployment;
|
||||||
|
protected AdapterTokenStore tokenStore;
|
||||||
protected AuthChallenge challenge;
|
protected AuthChallenge challenge;
|
||||||
protected int sslRedirectPort;
|
protected int sslRedirectPort;
|
||||||
|
|
||||||
public RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort) {
|
public RequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort) {
|
||||||
this.facade = facade;
|
this.facade = facade;
|
||||||
this.deployment = deployment;
|
this.deployment = deployment;
|
||||||
|
this.tokenStore = tokenStore;
|
||||||
this.sslRedirectPort = sslRedirectPort;
|
this.sslRedirectPort = sslRedirectPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +60,7 @@ public abstract class RequestAuthenticator {
|
||||||
log.trace("try oauth");
|
log.trace("try oauth");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCached()) {
|
if (tokenStore.isCached(this)) {
|
||||||
if (verifySSL()) return AuthOutcome.FAILED;
|
if (verifySSL()) return AuthOutcome.FAILED;
|
||||||
log.debug("AUTHENTICATED: was cached");
|
log.debug("AUTHENTICATED: was cached");
|
||||||
return AuthOutcome.AUTHENTICATED;
|
return AuthOutcome.AUTHENTICATED;
|
||||||
|
@ -103,18 +105,17 @@ public abstract class RequestAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void completeAuthentication(OAuthRequestAuthenticator oauth) {
|
protected void completeAuthentication(OAuthRequestAuthenticator oauth) {
|
||||||
RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, oauth.getTokenString(), oauth.getToken(), oauth.getIdTokenString(), oauth.getIdToken(), oauth.getRefreshToken());
|
RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, tokenStore, oauth.getTokenString(), oauth.getToken(), oauth.getIdTokenString(), oauth.getIdToken(), oauth.getRefreshToken());
|
||||||
final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(oauth.getToken().getSubject(), session);
|
final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(oauth.getToken().getSubject(), session);
|
||||||
completeOAuthAuthentication(principal);
|
completeOAuthAuthentication(principal);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
|
protected abstract void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
|
||||||
protected abstract void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
|
protected abstract void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
|
||||||
protected abstract boolean isCached();
|
|
||||||
protected abstract String getHttpSessionId(boolean create);
|
protected abstract String getHttpSessionId(boolean create);
|
||||||
|
|
||||||
protected void completeAuthentication(BearerTokenRequestAuthenticator bearer) {
|
protected void completeAuthentication(BearerTokenRequestAuthenticator bearer) {
|
||||||
RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, bearer.getTokenString(), bearer.getToken(), null, null, null);
|
RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null);
|
||||||
final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(bearer.getToken().getSubject(), session);
|
final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(bearer.getToken().getSubject(), session);
|
||||||
completeBearerAuthentication(principal);
|
completeBearerAuthentication(principal);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
package org.keycloak.adapters.as7;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.catalina.connector.Request;
|
||||||
|
import org.apache.catalina.realm.GenericPrincipal;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.AdapterUtils;
|
||||||
|
import org.keycloak.adapters.CookieTokenStore;
|
||||||
|
import org.keycloak.adapters.HttpFacade;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* per-request object
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class CatalinaCookieTokenStore implements AdapterTokenStore {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(CatalinaCookieTokenStore.class);
|
||||||
|
|
||||||
|
private Request request;
|
||||||
|
private HttpFacade facade;
|
||||||
|
private KeycloakDeployment deployment;
|
||||||
|
|
||||||
|
private KeycloakPrincipal<RefreshableKeycloakSecurityContext> authenticatedPrincipal;
|
||||||
|
|
||||||
|
public CatalinaCookieTokenStore(Request request, HttpFacade facade, KeycloakDeployment deployment) {
|
||||||
|
this.request = request;
|
||||||
|
this.facade = facade;
|
||||||
|
this.deployment = deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCurrentToken() {
|
||||||
|
this.authenticatedPrincipal = checkPrincipalFromCookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCached(RequestAuthenticator authenticator) {
|
||||||
|
// Assuming authenticatedPrincipal set by previous call of checkCurrentToken() during this request
|
||||||
|
if (authenticatedPrincipal != null) {
|
||||||
|
log.debug("remote logged in already. Establish state from cookie");
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = authenticatedPrincipal.getKeycloakSecurityContext();
|
||||||
|
securityContext.setCurrentRequestInfo(deployment, this);
|
||||||
|
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
|
||||||
|
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), authenticatedPrincipal, roles, securityContext);
|
||||||
|
|
||||||
|
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
|
request.setUserPrincipal(principal);
|
||||||
|
request.setAuthType("KEYCLOAK");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAccountInfo(KeycloakAccount account) {
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext();
|
||||||
|
CookieTokenStore.setTokenCookie(deployment, facade, securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
CookieTokenStore.removeCookie(facade);
|
||||||
|
|
||||||
|
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
|
if (ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||||
|
((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(RefreshableKeycloakSecurityContext secContext) {
|
||||||
|
CookieTokenStore.setTokenCookie(deployment, facade, secContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if we already have authenticated and active principal in cookie. Perform refresh if it's not active
|
||||||
|
*
|
||||||
|
* @return valid principal
|
||||||
|
*/
|
||||||
|
protected KeycloakPrincipal<RefreshableKeycloakSecurityContext> checkPrincipalFromCookie() {
|
||||||
|
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this);
|
||||||
|
if (principal == null) {
|
||||||
|
log.debug("Account was not in cookie or was invalid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext();
|
||||||
|
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return principal;
|
||||||
|
boolean success = session.refreshExpiredToken(false);
|
||||||
|
if (success && session.isActive()) return principal;
|
||||||
|
|
||||||
|
log.debugf("Cleanup and expire cookie for user %s after failed refresh", principal.getName());
|
||||||
|
request.setUserPrincipal(null);
|
||||||
|
request.setAuthType(null);
|
||||||
|
CookieTokenStore.removeCookie(facade);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,21 @@
|
||||||
package org.keycloak.adapters.as7;
|
package org.keycloak.adapters.as7;
|
||||||
|
|
||||||
import org.apache.catalina.Session;
|
|
||||||
import org.apache.catalina.authenticator.Constants;
|
import org.apache.catalina.authenticator.Constants;
|
||||||
import org.apache.catalina.connector.Request;
|
import org.apache.catalina.connector.Request;
|
||||||
import org.apache.catalina.realm.GenericPrincipal;
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.AdapterUtils;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.OAuthRequestAuthenticator;
|
import org.keycloak.adapters.OAuthRequestAuthenticator;
|
||||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.RequestAuthenticator;
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.enums.TokenStore;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
|
@ -25,18 +25,17 @@ import javax.servlet.http.HttpSession;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(CatalinaRequestAuthenticator.class);
|
private static final Logger log = Logger.getLogger(CatalinaRequestAuthenticator.class);
|
||||||
protected KeycloakAuthenticatorValve valve;
|
protected KeycloakAuthenticatorValve valve;
|
||||||
protected CatalinaUserSessionManagement userSessionManagement;
|
|
||||||
protected Request request;
|
protected Request request;
|
||||||
|
|
||||||
public CatalinaRequestAuthenticator(KeycloakDeployment deployment,
|
public CatalinaRequestAuthenticator(KeycloakDeployment deployment,
|
||||||
KeycloakAuthenticatorValve valve, CatalinaUserSessionManagement userSessionManagement,
|
KeycloakAuthenticatorValve valve, AdapterTokenStore tokenStore,
|
||||||
CatalinaHttpFacade facade,
|
CatalinaHttpFacade facade,
|
||||||
Request request) {
|
Request request) {
|
||||||
super(facade, deployment, request.getConnector().getRedirectPort());
|
super(facade, deployment, tokenStore, request.getConnector().getRedirectPort());
|
||||||
this.valve = valve;
|
this.valve = valve;
|
||||||
this.userSessionManagement = userSessionManagement;
|
|
||||||
this.request = request;
|
this.request = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +45,10 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
||||||
@Override
|
@Override
|
||||||
protected void saveRequest() {
|
protected void saveRequest() {
|
||||||
try {
|
try {
|
||||||
|
// Support saving request just for TokenStore.SESSION TODO: Add to tokenStore spi?
|
||||||
|
if (deployment.getTokenStore() == TokenStore.SESSION) {
|
||||||
valve.keycloakSaveRequest(request);
|
valve.keycloakSaveRequest(request);
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
@ -55,24 +57,36 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
|
protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
|
||||||
RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext();
|
final RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext();
|
||||||
|
final Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
|
||||||
|
KeycloakAccount account = new KeycloakAccount() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Principal getPrincipal() {
|
||||||
|
return skp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getRoles() {
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakSecurityContext getKeycloakSecurityContext() {
|
||||||
|
return securityContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
Set<String> roles = getRolesFromToken(securityContext);
|
this.tokenStore.saveAccountInfo(account);
|
||||||
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext);
|
|
||||||
Session session = request.getSessionInternal(true);
|
|
||||||
session.setPrincipal(principal);
|
|
||||||
session.setAuthType("OAUTH");
|
|
||||||
session.setNote(KeycloakSecurityContext.class.getName(), securityContext);
|
|
||||||
String username = securityContext.getToken().getSubject();
|
|
||||||
log.debug("userSessionManage.login: " + username);
|
|
||||||
userSessionManagement.login(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
||||||
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
|
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
|
||||||
Set<String> roles = getRolesFromToken(securityContext);
|
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("Completing bearer authentication. Bearer roles: " + roles);
|
log.debug("Completing bearer authentication. Bearer roles: " + roles);
|
||||||
}
|
}
|
||||||
|
@ -82,39 +96,6 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
||||||
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Set<String> getRolesFromToken(RefreshableKeycloakSecurityContext session) {
|
|
||||||
Set<String> roles = null;
|
|
||||||
if (deployment.isUseResourceRoleMappings()) {
|
|
||||||
AccessToken.Access access = session.getToken().getResourceAccess(deployment.getResourceName());
|
|
||||||
if (access != null) roles = access.getRoles();
|
|
||||||
} else {
|
|
||||||
AccessToken.Access access = session.getToken().getRealmAccess();
|
|
||||||
if (access != null) roles = access.getRoles();
|
|
||||||
}
|
|
||||||
if (roles == null) roles = Collections.emptySet();
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isCached() {
|
|
||||||
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null)
|
|
||||||
return false;
|
|
||||||
log.debug("remote logged in already");
|
|
||||||
GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal();
|
|
||||||
request.setUserPrincipal(principal);
|
|
||||||
request.setAuthType("KEYCLOAK");
|
|
||||||
Session session = request.getSessionInternal();
|
|
||||||
if (session != null) {
|
|
||||||
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) session.getNote(KeycloakSecurityContext.class.getName());
|
|
||||||
if (securityContext != null) {
|
|
||||||
securityContext.setDeployment(deployment);
|
|
||||||
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
restoreRequest();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void restoreRequest() {
|
protected void restoreRequest() {
|
||||||
if (request.getSessionInternal().getNote(Constants.FORM_REQUEST_NOTE) != null) {
|
if (request.getSessionInternal().getNote(Constants.FORM_REQUEST_NOTE) != null) {
|
||||||
if (valve.keycloakRestoreRequest(request)) {
|
if (valve.keycloakRestoreRequest(request)) {
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package org.keycloak.adapters.as7;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.catalina.Session;
|
||||||
|
import org.apache.catalina.connector.Request;
|
||||||
|
import org.apache.catalina.realm.GenericPrincipal;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* per-request object
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class CatalinaSessionTokenStore implements AdapterTokenStore {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(CatalinaSessionTokenStore.class);
|
||||||
|
|
||||||
|
private Request request;
|
||||||
|
private KeycloakDeployment deployment;
|
||||||
|
private CatalinaUserSessionManagement sessionManagement;
|
||||||
|
|
||||||
|
public CatalinaSessionTokenStore(Request request, KeycloakDeployment deployment, CatalinaUserSessionManagement sessionManagement) {
|
||||||
|
this.request = request;
|
||||||
|
this.deployment = deployment;
|
||||||
|
this.sessionManagement = sessionManagement;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCurrentToken() {
|
||||||
|
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return;
|
||||||
|
RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
|
||||||
|
if (session == null) return;
|
||||||
|
// just in case session got serialized
|
||||||
|
if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this);
|
||||||
|
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return;
|
||||||
|
|
||||||
|
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
|
||||||
|
// not be updated
|
||||||
|
boolean success = session.refreshExpiredToken(false);
|
||||||
|
if (success && session.isActive()) return;
|
||||||
|
|
||||||
|
// Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session
|
||||||
|
Session catalinaSession = request.getSessionInternal();
|
||||||
|
log.debugf("Cleanup and expire session %s after failed refresh", catalinaSession.getId());
|
||||||
|
catalinaSession.removeNote(KeycloakSecurityContext.class.getName());
|
||||||
|
request.setUserPrincipal(null);
|
||||||
|
request.setAuthType(null);
|
||||||
|
catalinaSession.setPrincipal(null);
|
||||||
|
catalinaSession.setAuthType(null);
|
||||||
|
catalinaSession.expire();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCached(RequestAuthenticator authenticator) {
|
||||||
|
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null)
|
||||||
|
return false;
|
||||||
|
log.debug("remote logged in already. Establish state from session");
|
||||||
|
GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal();
|
||||||
|
request.setUserPrincipal(principal);
|
||||||
|
request.setAuthType("KEYCLOAK");
|
||||||
|
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
|
||||||
|
if (securityContext != null) {
|
||||||
|
securityContext.setCurrentRequestInfo(deployment, this);
|
||||||
|
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
((CatalinaRequestAuthenticator)authenticator).restoreRequest();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAccountInfo(KeycloakAccount account) {
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext();
|
||||||
|
Set<String> roles = account.getRoles();
|
||||||
|
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), account.getPrincipal(), roles, securityContext);
|
||||||
|
|
||||||
|
Session session = request.getSessionInternal(true);
|
||||||
|
session.setPrincipal(principal);
|
||||||
|
session.setAuthType("OAUTH");
|
||||||
|
session.setNote(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
|
String username = securityContext.getToken().getSubject();
|
||||||
|
log.debug("userSessionManagement.login: " + username);
|
||||||
|
this.sessionManagement.login(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
Session session = request.getSessionInternal(false);
|
||||||
|
if (session != null) {
|
||||||
|
session.removeNote(KeycloakSecurityContext.class.getName());
|
||||||
|
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
|
if (ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||||
|
((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,17 +13,21 @@ import org.apache.catalina.connector.Response;
|
||||||
import org.apache.catalina.core.StandardContext;
|
import org.apache.catalina.core.StandardContext;
|
||||||
import org.apache.catalina.deploy.LoginConfig;
|
import org.apache.catalina.deploy.LoginConfig;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.AdapterConstants;
|
import org.keycloak.adapters.AdapterConstants;
|
||||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
import org.keycloak.adapters.AuthChallenge;
|
import org.keycloak.adapters.AuthChallenge;
|
||||||
import org.keycloak.adapters.AuthOutcome;
|
import org.keycloak.adapters.AuthOutcome;
|
||||||
|
import org.keycloak.adapters.CookieTokenStore;
|
||||||
import org.keycloak.adapters.HttpFacade;
|
import org.keycloak.adapters.HttpFacade;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
import org.keycloak.adapters.NodesRegistrationManagement;
|
import org.keycloak.adapters.NodesRegistrationManagement;
|
||||||
import org.keycloak.adapters.PreAuthActionsHandler;
|
import org.keycloak.adapters.PreAuthActionsHandler;
|
||||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
|
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
|
@ -44,6 +48,9 @@ import java.io.InputStream;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class KeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
|
public class KeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
|
||||||
|
|
||||||
|
public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(KeycloakAuthenticatorValve.class);
|
private static final Logger log = Logger.getLogger(KeycloakAuthenticatorValve.class);
|
||||||
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
|
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
|
||||||
protected AdapterDeploymentContext deploymentContext;
|
protected AdapterDeploymentContext deploymentContext;
|
||||||
|
@ -63,14 +70,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
||||||
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
|
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
if (ksc != null) {
|
if (ksc != null) {
|
||||||
request.removeAttribute(KeycloakSecurityContext.class.getName());
|
request.removeAttribute(KeycloakSecurityContext.class.getName());
|
||||||
Session session = request.getSessionInternal(false);
|
|
||||||
if (session != null) {
|
|
||||||
session.removeNote(KeycloakSecurityContext.class.getName());
|
|
||||||
if (ksc instanceof RefreshableKeycloakSecurityContext) {
|
|
||||||
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null);
|
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null);
|
||||||
((RefreshableKeycloakSecurityContext)ksc).logout(deploymentContext.resolveDeployment(facade));
|
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||||
}
|
|
||||||
}
|
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
|
||||||
|
tokenStore.logout();
|
||||||
}
|
}
|
||||||
super.logout(request);
|
super.logout(request);
|
||||||
}
|
}
|
||||||
|
@ -164,10 +168,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
||||||
log.info("*** deployment isn't configured return false");
|
log.info("*** deployment isn't configured return false");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
|
||||||
|
|
||||||
nodesRegistrationManagement.tryRegister(deployment);
|
nodesRegistrationManagement.tryRegister(deployment);
|
||||||
|
|
||||||
CatalinaRequestAuthenticator authenticator = new CatalinaRequestAuthenticator(deployment, this, userSessionManagement, facade, request);
|
CatalinaRequestAuthenticator authenticator = new CatalinaRequestAuthenticator(deployment, this, tokenStore, facade, request);
|
||||||
AuthOutcome outcome = authenticator.authenticate();
|
AuthOutcome outcome = authenticator.authenticate();
|
||||||
if (outcome == AuthOutcome.AUTHENTICATED) {
|
if (outcome == AuthOutcome.AUTHENTICATED) {
|
||||||
if (facade.isEnded()) {
|
if (facade.isEnded()) {
|
||||||
|
@ -188,27 +193,9 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
||||||
* @param request
|
* @param request
|
||||||
*/
|
*/
|
||||||
protected void checkKeycloakSession(Request request, HttpFacade facade) {
|
protected void checkKeycloakSession(Request request, HttpFacade facade) {
|
||||||
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return;
|
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||||
RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
|
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
|
||||||
if (session == null) return;
|
tokenStore.checkCurrentToken();
|
||||||
// just in case session got serialized
|
|
||||||
if (session.getDeployment() == null) session.setDeployment(deploymentContext.resolveDeployment(facade));
|
|
||||||
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return;
|
|
||||||
|
|
||||||
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
|
|
||||||
// not be updated
|
|
||||||
boolean success = session.refreshExpiredToken(false);
|
|
||||||
if (success && session.isActive()) return;
|
|
||||||
|
|
||||||
// Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session
|
|
||||||
Session catalinaSession = request.getSessionInternal();
|
|
||||||
log.debugf("Cleanup and expire session %s after failed refresh", catalinaSession.getId());
|
|
||||||
catalinaSession.removeNote(KeycloakSecurityContext.class.getName());
|
|
||||||
request.setUserPrincipal(null);
|
|
||||||
request.setAuthType(null);
|
|
||||||
catalinaSession.setPrincipal(null);
|
|
||||||
catalinaSession.setAuthType(null);
|
|
||||||
catalinaSession.expire();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void keycloakSaveRequest(Request request) throws IOException {
|
public void keycloakSaveRequest(Request request) throws IOException {
|
||||||
|
@ -223,4 +210,20 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) {
|
||||||
|
AdapterTokenStore store = (AdapterTokenStore)request.getNote(TOKEN_STORE_NOTE);
|
||||||
|
if (store != null) {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) {
|
||||||
|
store = new CatalinaSessionTokenStore(request, resolvedDeployment, userSessionManagement);
|
||||||
|
} else {
|
||||||
|
store = new CatalinaCookieTokenStore(request, facade, resolvedDeployment);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.setNote(TOKEN_STORE_NOTE, store);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
package org.keycloak.adapters.tomcat7;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.apache.catalina.connector.Request;
|
||||||
|
import org.apache.catalina.realm.GenericPrincipal;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.AdapterUtils;
|
||||||
|
import org.keycloak.adapters.CookieTokenStore;
|
||||||
|
import org.keycloak.adapters.HttpFacade;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class CatalinaCookieTokenStore implements AdapterTokenStore {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(""+CatalinaCookieTokenStore.class);
|
||||||
|
|
||||||
|
private Request request;
|
||||||
|
private HttpFacade facade;
|
||||||
|
private KeycloakDeployment deployment;
|
||||||
|
|
||||||
|
private KeycloakPrincipal<RefreshableKeycloakSecurityContext> authenticatedPrincipal;
|
||||||
|
|
||||||
|
public CatalinaCookieTokenStore(Request request, HttpFacade facade, KeycloakDeployment deployment) {
|
||||||
|
this.request = request;
|
||||||
|
this.facade = facade;
|
||||||
|
this.deployment = deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCurrentToken() {
|
||||||
|
this.authenticatedPrincipal = checkPrincipalFromCookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCached(RequestAuthenticator authenticator) {
|
||||||
|
// Assuming authenticatedPrincipal set by previous call of checkCurrentToken() during this request
|
||||||
|
if (authenticatedPrincipal != null) {
|
||||||
|
log.fine("remote logged in already. Establish state from cookie");
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = authenticatedPrincipal.getKeycloakSecurityContext();
|
||||||
|
securityContext.setCurrentRequestInfo(deployment, this);
|
||||||
|
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
|
||||||
|
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), authenticatedPrincipal, roles, securityContext);
|
||||||
|
|
||||||
|
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
|
request.setUserPrincipal(principal);
|
||||||
|
request.setAuthType("KEYCLOAK");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAccountInfo(KeycloakAccount account) {
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext();
|
||||||
|
CookieTokenStore.setTokenCookie(deployment, facade, securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
CookieTokenStore.removeCookie(facade);
|
||||||
|
|
||||||
|
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
|
if (ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||||
|
((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(RefreshableKeycloakSecurityContext secContext) {
|
||||||
|
CookieTokenStore.setTokenCookie(deployment, facade, secContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if we already have authenticated and active principal in cookie. Perform refresh if it's not active
|
||||||
|
*
|
||||||
|
* @return valid principal
|
||||||
|
*/
|
||||||
|
protected KeycloakPrincipal<RefreshableKeycloakSecurityContext> checkPrincipalFromCookie() {
|
||||||
|
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this);
|
||||||
|
if (principal == null) {
|
||||||
|
log.fine("Account was not in cookie or was invalid");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext();
|
||||||
|
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return principal;
|
||||||
|
boolean success = session.refreshExpiredToken(false);
|
||||||
|
if (success && session.isActive()) return principal;
|
||||||
|
|
||||||
|
log.fine("Cleanup and expire cookie for user " + principal.getName() + " after failed refresh");
|
||||||
|
request.setUserPrincipal(null);
|
||||||
|
request.setAuthType(null);
|
||||||
|
CookieTokenStore.removeCookie(facade);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,20 @@
|
||||||
package org.keycloak.adapters.tomcat7;
|
package org.keycloak.adapters.tomcat7;
|
||||||
|
|
||||||
import org.apache.catalina.Session;
|
|
||||||
import org.apache.catalina.authenticator.Constants;
|
import org.apache.catalina.authenticator.Constants;
|
||||||
import org.apache.catalina.connector.Request;
|
import org.apache.catalina.connector.Request;
|
||||||
import org.apache.catalina.realm.GenericPrincipal;
|
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.AdapterUtils;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.OAuthRequestAuthenticator;
|
import org.keycloak.adapters.OAuthRequestAuthenticator;
|
||||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.RequestAuthenticator;
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.enums.TokenStore;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
@ -28,16 +28,14 @@ import javax.servlet.http.HttpSession;
|
||||||
public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
||||||
private static final Logger log = Logger.getLogger(""+CatalinaRequestAuthenticator.class);
|
private static final Logger log = Logger.getLogger(""+CatalinaRequestAuthenticator.class);
|
||||||
protected KeycloakAuthenticatorValve valve;
|
protected KeycloakAuthenticatorValve valve;
|
||||||
protected CatalinaUserSessionManagement userSessionManagement;
|
|
||||||
protected Request request;
|
protected Request request;
|
||||||
|
|
||||||
public CatalinaRequestAuthenticator(KeycloakDeployment deployment,
|
public CatalinaRequestAuthenticator(KeycloakDeployment deployment,
|
||||||
KeycloakAuthenticatorValve valve, CatalinaUserSessionManagement userSessionManagement,
|
KeycloakAuthenticatorValve valve, AdapterTokenStore tokenStore,
|
||||||
CatalinaHttpFacade facade,
|
CatalinaHttpFacade facade,
|
||||||
Request request) {
|
Request request) {
|
||||||
super(facade, deployment, request.getConnector().getRedirectPort());
|
super(facade, deployment, tokenStore, request.getConnector().getRedirectPort());
|
||||||
this.valve = valve;
|
this.valve = valve;
|
||||||
this.userSessionManagement = userSessionManagement;
|
|
||||||
this.request = request;
|
this.request = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +45,10 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
||||||
@Override
|
@Override
|
||||||
protected void saveRequest() {
|
protected void saveRequest() {
|
||||||
try {
|
try {
|
||||||
|
// Support saving request just for TokenStore.SESSION TODO: Add to tokenStore spi?
|
||||||
|
if (deployment.getTokenStore() == TokenStore.SESSION) {
|
||||||
valve.keycloakSaveRequest(request);
|
valve.keycloakSaveRequest(request);
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
@ -56,24 +57,36 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
|
protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
|
||||||
RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext();
|
final RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext();
|
||||||
|
final Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
|
||||||
|
KeycloakAccount account = new KeycloakAccount() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Principal getPrincipal() {
|
||||||
|
return skp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getRoles() {
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeycloakSecurityContext getKeycloakSecurityContext() {
|
||||||
|
return securityContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
Set<String> roles = getRolesFromToken(securityContext);
|
this.tokenStore.saveAccountInfo(account);
|
||||||
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext);
|
|
||||||
Session session = request.getSessionInternal(true);
|
|
||||||
session.setPrincipal(principal);
|
|
||||||
session.setAuthType("OAUTH");
|
|
||||||
session.setNote(KeycloakSecurityContext.class.getName(), securityContext);
|
|
||||||
String username = securityContext.getToken().getSubject();
|
|
||||||
log.finer("userSessionManagement.login: " + username);
|
|
||||||
userSessionManagement.login(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
||||||
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
|
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
|
||||||
Set<String> roles = getRolesFromToken(securityContext);
|
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
|
||||||
if (log.isLoggable(Level.FINE)) {
|
if (log.isLoggable(Level.FINE)) {
|
||||||
log.fine("Completing bearer authentication. Bearer roles: " + roles);
|
log.fine("Completing bearer authentication. Bearer roles: " + roles);
|
||||||
}
|
}
|
||||||
|
@ -83,39 +96,6 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
|
||||||
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Set<String> getRolesFromToken(RefreshableKeycloakSecurityContext session) {
|
|
||||||
Set<String> roles = null;
|
|
||||||
if (deployment.isUseResourceRoleMappings()) {
|
|
||||||
AccessToken.Access access = session.getToken().getResourceAccess(deployment.getResourceName());
|
|
||||||
if (access != null) roles = access.getRoles();
|
|
||||||
} else {
|
|
||||||
AccessToken.Access access = session.getToken().getRealmAccess();
|
|
||||||
if (access != null) roles = access.getRoles();
|
|
||||||
}
|
|
||||||
if (roles == null) roles = Collections.emptySet();
|
|
||||||
return roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isCached() {
|
|
||||||
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null)
|
|
||||||
return false;
|
|
||||||
log.finer("remote logged in already");
|
|
||||||
GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal();
|
|
||||||
request.setUserPrincipal(principal);
|
|
||||||
request.setAuthType("KEYCLOAK");
|
|
||||||
Session session = request.getSessionInternal();
|
|
||||||
if (session != null) {
|
|
||||||
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) session.getNote(KeycloakSecurityContext.class.getName());
|
|
||||||
if (securityContext != null) {
|
|
||||||
securityContext.setDeployment(deployment);
|
|
||||||
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
restoreRequest();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void restoreRequest() {
|
protected void restoreRequest() {
|
||||||
if (request.getSessionInternal().getNote(Constants.FORM_REQUEST_NOTE) != null) {
|
if (request.getSessionInternal().getNote(Constants.FORM_REQUEST_NOTE) != null) {
|
||||||
if (valve.keycloakRestoreRequest(request)) {
|
if (valve.keycloakRestoreRequest(request)) {
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
package org.keycloak.adapters.tomcat7;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.apache.catalina.Session;
|
||||||
|
import org.apache.catalina.connector.Request;
|
||||||
|
import org.apache.catalina.realm.GenericPrincipal;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class CatalinaSessionTokenStore implements AdapterTokenStore {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(""+CatalinaSessionTokenStore.class);
|
||||||
|
|
||||||
|
private Request request;
|
||||||
|
private KeycloakDeployment deployment;
|
||||||
|
private CatalinaUserSessionManagement sessionManagement;
|
||||||
|
|
||||||
|
public CatalinaSessionTokenStore(Request request, KeycloakDeployment deployment, CatalinaUserSessionManagement sessionManagement) {
|
||||||
|
this.request = request;
|
||||||
|
this.deployment = deployment;
|
||||||
|
this.sessionManagement = sessionManagement;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCurrentToken() {
|
||||||
|
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return;
|
||||||
|
RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
|
||||||
|
if (session == null) return;
|
||||||
|
// just in case session got serialized
|
||||||
|
if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this);
|
||||||
|
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return;
|
||||||
|
|
||||||
|
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
|
||||||
|
// not be updated
|
||||||
|
boolean success = session.refreshExpiredToken(false);
|
||||||
|
if (success && session.isActive()) return;
|
||||||
|
|
||||||
|
// Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session
|
||||||
|
Session catalinaSession = request.getSessionInternal();
|
||||||
|
log.fine("Cleanup and expire session " + catalinaSession.getId() + " after failed refresh");
|
||||||
|
catalinaSession.removeNote(KeycloakSecurityContext.class.getName());
|
||||||
|
request.setUserPrincipal(null);
|
||||||
|
request.setAuthType(null);
|
||||||
|
catalinaSession.setPrincipal(null);
|
||||||
|
catalinaSession.setAuthType(null);
|
||||||
|
catalinaSession.expire();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCached(RequestAuthenticator authenticator) {
|
||||||
|
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null)
|
||||||
|
return false;
|
||||||
|
log.fine("remote logged in already. Establish state from session");
|
||||||
|
GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal();
|
||||||
|
request.setUserPrincipal(principal);
|
||||||
|
request.setAuthType("KEYCLOAK");
|
||||||
|
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
|
||||||
|
if (securityContext != null) {
|
||||||
|
securityContext.setCurrentRequestInfo(deployment, this);
|
||||||
|
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
((CatalinaRequestAuthenticator)authenticator).restoreRequest();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAccountInfo(KeycloakAccount account) {
|
||||||
|
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext();
|
||||||
|
Set<String> roles = account.getRoles();
|
||||||
|
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), account.getPrincipal(), roles, securityContext);
|
||||||
|
|
||||||
|
Session session = request.getSessionInternal(true);
|
||||||
|
session.setPrincipal(principal);
|
||||||
|
session.setAuthType("OAUTH");
|
||||||
|
session.setNote(KeycloakSecurityContext.class.getName(), securityContext);
|
||||||
|
String username = securityContext.getToken().getSubject();
|
||||||
|
log.fine("userSessionManagement.login: " + username);
|
||||||
|
this.sessionManagement.login(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
Session session = request.getSessionInternal(false);
|
||||||
|
if (session != null) {
|
||||||
|
session.removeNote(KeycloakSecurityContext.class.getName());
|
||||||
|
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
|
if (ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||||
|
((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import org.apache.catalina.deploy.LoginConfig;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.AdapterConstants;
|
import org.keycloak.adapters.AdapterConstants;
|
||||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
import org.keycloak.adapters.AuthChallenge;
|
import org.keycloak.adapters.AuthChallenge;
|
||||||
import org.keycloak.adapters.AuthOutcome;
|
import org.keycloak.adapters.AuthOutcome;
|
||||||
import org.keycloak.adapters.HttpFacade;
|
import org.keycloak.adapters.HttpFacade;
|
||||||
|
@ -24,6 +25,7 @@ import org.keycloak.adapters.NodesRegistrationManagement;
|
||||||
import org.keycloak.adapters.PreAuthActionsHandler;
|
import org.keycloak.adapters.PreAuthActionsHandler;
|
||||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.ServerRequest;
|
import org.keycloak.adapters.ServerRequest;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
|
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
|
@ -45,6 +47,9 @@ import java.util.logging.Logger;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class KeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
|
public class KeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
|
||||||
|
|
||||||
|
public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
|
||||||
|
|
||||||
private final static Logger log = Logger.getLogger(""+KeycloakAuthenticatorValve.class);
|
private final static Logger log = Logger.getLogger(""+KeycloakAuthenticatorValve.class);
|
||||||
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
|
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
|
||||||
protected AdapterDeploymentContext deploymentContext;
|
protected AdapterDeploymentContext deploymentContext;
|
||||||
|
@ -70,16 +75,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
||||||
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
|
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
if (ksc != null) {
|
if (ksc != null) {
|
||||||
request.removeAttribute(KeycloakSecurityContext.class.getName());
|
request.removeAttribute(KeycloakSecurityContext.class.getName());
|
||||||
Session session = request.getSessionInternal(false);
|
|
||||||
if (session != null) {
|
|
||||||
session.removeNote(KeycloakSecurityContext.class.getName());
|
|
||||||
try {
|
|
||||||
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null);
|
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null);
|
||||||
ServerRequest.invokeLogout(deploymentContext.resolveDeployment(facade), ksc.getToken().getSessionState());
|
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||||
} catch (Exception e) {
|
|
||||||
log.severe("failed to invoke remote logout. " + e.getMessage());
|
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
|
||||||
}
|
tokenStore.logout();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
super.logout(request);
|
super.logout(request);
|
||||||
}
|
}
|
||||||
|
@ -164,10 +164,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
||||||
if (deployment == null || !deployment.isConfigured()) {
|
if (deployment == null || !deployment.isConfigured()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
|
||||||
|
|
||||||
nodesRegistrationManagement.tryRegister(deployment);
|
nodesRegistrationManagement.tryRegister(deployment);
|
||||||
|
|
||||||
CatalinaRequestAuthenticator authenticator = new CatalinaRequestAuthenticator(deployment, this, userSessionManagement, facade, request);
|
CatalinaRequestAuthenticator authenticator = new CatalinaRequestAuthenticator(deployment, this, tokenStore, facade, request);
|
||||||
AuthOutcome outcome = authenticator.authenticate();
|
AuthOutcome outcome = authenticator.authenticate();
|
||||||
if (outcome == AuthOutcome.AUTHENTICATED) {
|
if (outcome == AuthOutcome.AUTHENTICATED) {
|
||||||
if (facade.isEnded()) {
|
if (facade.isEnded()) {
|
||||||
|
@ -188,27 +189,9 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
||||||
* @param request
|
* @param request
|
||||||
*/
|
*/
|
||||||
protected void checkKeycloakSession(Request request, HttpFacade facade) {
|
protected void checkKeycloakSession(Request request, HttpFacade facade) {
|
||||||
if (request.getSessionInternal(false) == null || request.getPrincipal() == null) return;
|
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||||
RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
|
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
|
||||||
if (session == null) return;
|
tokenStore.checkCurrentToken();
|
||||||
// just in case session got serialized
|
|
||||||
if (session.getDeployment() == null) session.setDeployment(deploymentContext.resolveDeployment(facade));
|
|
||||||
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return;
|
|
||||||
|
|
||||||
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
|
|
||||||
// not be updated
|
|
||||||
boolean success = session.refreshExpiredToken(false);
|
|
||||||
if (success && session.isActive()) return;
|
|
||||||
|
|
||||||
// Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session
|
|
||||||
Session catalinaSession = request.getSessionInternal();
|
|
||||||
log.fine("Cleanup and expire session " + catalinaSession + " after failed refresh");
|
|
||||||
catalinaSession.removeNote(KeycloakSecurityContext.class.getName());
|
|
||||||
request.setUserPrincipal(null);
|
|
||||||
request.setAuthType(null);
|
|
||||||
catalinaSession.setPrincipal(null);
|
|
||||||
catalinaSession.setAuthType(null);
|
|
||||||
catalinaSession.expire();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void keycloakSaveRequest(Request request) throws IOException {
|
public void keycloakSaveRequest(Request request) throws IOException {
|
||||||
|
@ -223,4 +206,20 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) {
|
||||||
|
AdapterTokenStore store = (AdapterTokenStore)request.getNote(TOKEN_STORE_NOTE);
|
||||||
|
if (store != null) {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) {
|
||||||
|
store = new CatalinaSessionTokenStore(request, resolvedDeployment, userSessionManagement);
|
||||||
|
} else {
|
||||||
|
store = new CatalinaCookieTokenStore(request, facade, resolvedDeployment);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.setNote(TOKEN_STORE_NOTE, store);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,14 +19,15 @@ package org.keycloak.adapters.undertow;
|
||||||
import io.undertow.security.idm.Account;
|
import io.undertow.security.idm.Account;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.AdapterUtils;
|
||||||
|
import org.keycloak.adapters.HttpFacade;
|
||||||
import org.keycloak.adapters.KeycloakAccount;
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
import org.keycloak.representations.AccessToken;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,33 +41,11 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA
|
||||||
|
|
||||||
public KeycloakUndertowAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
public KeycloakUndertowAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
||||||
this.principal = principal;
|
this.principal = principal;
|
||||||
setRoles(principal.getKeycloakSecurityContext().getToken());
|
setRoles(principal.getKeycloakSecurityContext());
|
||||||
}
|
|
||||||
|
|
||||||
protected void setRoles(AccessToken accessToken) {
|
|
||||||
Set<String> roles = null;
|
|
||||||
RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext();
|
|
||||||
if (session.getDeployment().isUseResourceRoleMappings()) {
|
|
||||||
if (log.isTraceEnabled()) {
|
|
||||||
log.trace("useResourceRoleMappings");
|
|
||||||
}
|
|
||||||
AccessToken.Access access = accessToken.getResourceAccess(session.getDeployment().getResourceName());
|
|
||||||
if (access != null) roles = access.getRoles();
|
|
||||||
} else {
|
|
||||||
if (log.isTraceEnabled()) {
|
|
||||||
log.trace("use realm role mappings");
|
|
||||||
}
|
|
||||||
AccessToken.Access access = accessToken.getRealmAccess();
|
|
||||||
if (access != null) roles = access.getRoles();
|
|
||||||
}
|
|
||||||
if (roles == null) roles = Collections.emptySet();
|
|
||||||
if (log.isTraceEnabled()) {
|
|
||||||
log.trace("Setting roles: ");
|
|
||||||
for (String role : roles) {
|
|
||||||
log.trace(" role: " + role);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void setRoles(RefreshableKeycloakSecurityContext session) {
|
||||||
|
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(session);
|
||||||
this.accountRoles = roles;
|
this.accountRoles = roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,11 +64,12 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA
|
||||||
return principal.getKeycloakSecurityContext();
|
return principal.getKeycloakSecurityContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeployment(KeycloakDeployment deployment) {
|
public void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
|
||||||
principal.getKeycloakSecurityContext().setDeployment(deployment);
|
principal.getKeycloakSecurityContext().setCurrentRequestInfo(deployment, tokenStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isActive() {
|
// Check if accessToken is active and try to refresh if it's not
|
||||||
|
public boolean checkActive() {
|
||||||
// this object may have been serialized, so we need to reset realm config/metadata
|
// this object may have been serialized, so we need to reset realm config/metadata
|
||||||
RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext();
|
RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext();
|
||||||
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) {
|
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) {
|
||||||
|
@ -106,7 +86,7 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA
|
||||||
}
|
}
|
||||||
log.debug("refresh succeeded");
|
log.debug("refresh succeeded");
|
||||||
|
|
||||||
setRoles(session.getToken());
|
setRoles(session);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,12 @@ import io.undertow.servlet.handlers.ServletRequestContext;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.HttpFacade;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.NodesRegistrationManagement;
|
import org.keycloak.adapters.NodesRegistrationManagement;
|
||||||
import org.keycloak.adapters.RequestAuthenticator;
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
|
@ -40,13 +43,11 @@ import javax.servlet.http.HttpSession;
|
||||||
public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
|
public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
|
||||||
private static final Logger log = Logger.getLogger(ServletKeycloakAuthMech.class);
|
private static final Logger log = Logger.getLogger(ServletKeycloakAuthMech.class);
|
||||||
|
|
||||||
protected UndertowUserSessionManagement userSessionManagement;
|
|
||||||
protected NodesRegistrationManagement nodesRegistrationManagement;
|
protected NodesRegistrationManagement nodesRegistrationManagement;
|
||||||
protected ConfidentialPortManager portManager;
|
protected ConfidentialPortManager portManager;
|
||||||
|
|
||||||
public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, NodesRegistrationManagement nodesRegistrationManagement, ConfidentialPortManager portManager) {
|
public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, NodesRegistrationManagement nodesRegistrationManagement, ConfidentialPortManager portManager) {
|
||||||
super(deploymentContext);
|
super(deploymentContext, userSessionManagement);
|
||||||
this.userSessionManagement = userSessionManagement;
|
|
||||||
this.nodesRegistrationManagement = nodesRegistrationManagement;
|
this.nodesRegistrationManagement = nodesRegistrationManagement;
|
||||||
this.portManager = portManager;
|
this.portManager = portManager;
|
||||||
}
|
}
|
||||||
|
@ -66,40 +67,12 @@ public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
|
||||||
return keycloakAuthenticate(exchange, securityContext, authenticator);
|
return keycloakAuthenticate(exchange, securityContext, authenticator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void registerNotifications(SecurityContext securityContext) {
|
|
||||||
|
|
||||||
final NotificationReceiver logoutReceiver = new NotificationReceiver() {
|
|
||||||
@Override
|
|
||||||
public void handleNotification(SecurityNotification notification) {
|
|
||||||
if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return;
|
|
||||||
final ServletRequestContext servletRequestContext = notification.getExchange().getAttachment(ServletRequestContext.ATTACHMENT_KEY);
|
|
||||||
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
|
|
||||||
req.removeAttribute(KeycloakUndertowAccount.class.getName());
|
|
||||||
req.removeAttribute(KeycloakSecurityContext.class.getName());
|
|
||||||
HttpSession session = req.getSession(false);
|
|
||||||
if (session == null) return;
|
|
||||||
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
|
||||||
if (account == null) return;
|
|
||||||
session.removeAttribute(KeycloakSecurityContext.class.getName());
|
|
||||||
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
|
||||||
if (account.getKeycloakSecurityContext() != null) {
|
|
||||||
UndertowHttpFacade facade = new UndertowHttpFacade(notification.getExchange());
|
|
||||||
account.getKeycloakSecurityContext().logout(deploymentContext.resolveDeployment(facade));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
securityContext.registerNotificationReceiver(logoutReceiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected RequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) {
|
protected RequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) {
|
||||||
|
|
||||||
int confidentialPort = getConfidentilPort(exchange);
|
int confidentialPort = getConfidentilPort(exchange);
|
||||||
|
AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext);
|
||||||
return new ServletRequestAuthenticator(facade, deployment,
|
return new ServletRequestAuthenticator(facade, deployment,
|
||||||
confidentialPort, securityContext, exchange, userSessionManagement);
|
confidentialPort, securityContext, exchange, tokenStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getConfidentilPort(HttpServerExchange exchange) {
|
protected int getConfidentilPort(HttpServerExchange exchange) {
|
||||||
|
@ -112,4 +85,13 @@ public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
|
||||||
return confidentialPort;
|
return confidentialPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AdapterTokenStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, KeycloakDeployment deployment, SecurityContext securityContext) {
|
||||||
|
if (deployment.getTokenStore() == TokenStore.SESSION) {
|
||||||
|
return new ServletSessionTokenStore(exchange, deployment, sessionManagement, securityContext);
|
||||||
|
} else {
|
||||||
|
return new UndertowCookieTokenStore(facade, deployment, securityContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,13 +18,11 @@ package org.keycloak.adapters.undertow;
|
||||||
|
|
||||||
import io.undertow.security.api.SecurityContext;
|
import io.undertow.security.api.SecurityContext;
|
||||||
import io.undertow.server.HttpServerExchange;
|
import io.undertow.server.HttpServerExchange;
|
||||||
import io.undertow.server.session.Session;
|
|
||||||
import io.undertow.servlet.handlers.ServletRequestContext;
|
import io.undertow.servlet.handlers.ServletRequestContext;
|
||||||
import io.undertow.util.Sessions;
|
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.KeycloakSecurityContext;
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
import org.keycloak.adapters.HttpFacade;
|
import org.keycloak.adapters.HttpFacade;
|
||||||
import org.keycloak.adapters.KeycloakAccount;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
|
||||||
|
@ -41,34 +39,8 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
|
||||||
|
|
||||||
public ServletRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
|
public ServletRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
|
||||||
SecurityContext securityContext, HttpServerExchange exchange,
|
SecurityContext securityContext, HttpServerExchange exchange,
|
||||||
UndertowUserSessionManagement userSessionManagement) {
|
AdapterTokenStore tokenStore) {
|
||||||
super(facade, deployment, sslRedirectPort, securityContext, exchange, userSessionManagement);
|
super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isCached() {
|
|
||||||
HttpSession session = getSession(false);
|
|
||||||
if (session == null) {
|
|
||||||
log.debug("session was null, returning null");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
|
||||||
if (account == null) {
|
|
||||||
log.debug("Account was not in session, returning null");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
account.setDeployment(deployment);
|
|
||||||
if (account.isActive()) {
|
|
||||||
log.debug("Cached account found");
|
|
||||||
securityContext.authenticationComplete(account, "KEYCLOAK", false);
|
|
||||||
propagateKeycloakContext( account);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session");
|
|
||||||
session.setAttribute(KeycloakUndertowAccount.class.getName(), null);
|
|
||||||
session.invalidate();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -79,15 +51,6 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
|
||||||
req.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
|
req.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void login(KeycloakAccount account) {
|
|
||||||
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
|
|
||||||
HttpSession session = getSession(true);
|
|
||||||
session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
|
|
||||||
userSessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected KeycloakUndertowAccount createAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
protected KeycloakUndertowAccount createAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
||||||
return new KeycloakUndertowAccount(principal);
|
return new KeycloakUndertowAccount(principal);
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
package org.keycloak.adapters.undertow;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
import io.undertow.security.api.SecurityContext;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import io.undertow.servlet.handlers.ServletRequestContext;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-request object. Storage of tokens in servlet session.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class ServletSessionTokenStore implements AdapterTokenStore {
|
||||||
|
|
||||||
|
protected static Logger log = Logger.getLogger(ServletSessionTokenStore.class);
|
||||||
|
|
||||||
|
private final HttpServerExchange exchange;
|
||||||
|
private final KeycloakDeployment deployment;
|
||||||
|
private final UndertowUserSessionManagement sessionManagement;
|
||||||
|
private final SecurityContext securityContext;
|
||||||
|
|
||||||
|
public ServletSessionTokenStore(HttpServerExchange exchange, KeycloakDeployment deployment, UndertowUserSessionManagement sessionManagement,
|
||||||
|
SecurityContext securityContext) {
|
||||||
|
this.exchange = exchange;
|
||||||
|
this.deployment = deployment;
|
||||||
|
this.sessionManagement = sessionManagement;
|
||||||
|
this.securityContext = securityContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCurrentToken() {
|
||||||
|
// no-op on undertow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCached(RequestAuthenticator authenticator) {
|
||||||
|
HttpSession session = getSession(false);
|
||||||
|
if (session == null) {
|
||||||
|
log.debug("session was null, returning null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
|
if (account == null) {
|
||||||
|
log.debug("Account was not in session, returning null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
account.setCurrentRequestInfo(deployment, this);
|
||||||
|
if (account.checkActive()) {
|
||||||
|
log.debug("Cached account found");
|
||||||
|
securityContext.authenticationComplete(account, "KEYCLOAK", false);
|
||||||
|
((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session");
|
||||||
|
session.setAttribute(KeycloakUndertowAccount.class.getName(), null);
|
||||||
|
session.invalidate();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAccountInfo(KeycloakAccount account) {
|
||||||
|
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
|
||||||
|
HttpSession session = getSession(true);
|
||||||
|
session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
|
||||||
|
sessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
|
||||||
|
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
|
||||||
|
req.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
|
req.removeAttribute(KeycloakSecurityContext.class.getName());
|
||||||
|
HttpSession session = req.getSession(false);
|
||||||
|
if (session == null) return;
|
||||||
|
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
|
if (account == null) return;
|
||||||
|
session.removeAttribute(KeycloakSecurityContext.class.getName());
|
||||||
|
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
|
if (account.getKeycloakSecurityContext() != null) {
|
||||||
|
account.getKeycloakSecurityContext().logout(deployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpSession getSession(boolean create) {
|
||||||
|
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
|
||||||
|
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
|
||||||
|
return req.getSession(create);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package org.keycloak.adapters.undertow;
|
||||||
|
|
||||||
|
import io.undertow.security.api.SecurityContext;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.CookieTokenStore;
|
||||||
|
import org.keycloak.adapters.HttpFacade;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-request object. Storage of tokens in cookie
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class UndertowCookieTokenStore implements AdapterTokenStore {
|
||||||
|
|
||||||
|
protected static Logger log = Logger.getLogger(UndertowCookieTokenStore.class);
|
||||||
|
|
||||||
|
private final HttpFacade facade;
|
||||||
|
private final KeycloakDeployment deployment;
|
||||||
|
private final SecurityContext securityContext;
|
||||||
|
|
||||||
|
public UndertowCookieTokenStore(HttpFacade facade, KeycloakDeployment deployment,
|
||||||
|
SecurityContext securityContext) {
|
||||||
|
this.facade = facade;
|
||||||
|
this.deployment = deployment;
|
||||||
|
this.securityContext = securityContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCurrentToken() {
|
||||||
|
// no-op on undertow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCached(RequestAuthenticator authenticator) {
|
||||||
|
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this);
|
||||||
|
if (principal == null) {
|
||||||
|
log.debug("Account was not in cookie or was invalid, returning null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal);
|
||||||
|
|
||||||
|
if (account.checkActive()) {
|
||||||
|
log.debug("Cached account found");
|
||||||
|
securityContext.authenticationComplete(account, "KEYCLOAK", false);
|
||||||
|
((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.debug("Account was not active, removing cookie and returning false");
|
||||||
|
CookieTokenStore.removeCookie(facade);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAccountInfo(KeycloakAccount account) {
|
||||||
|
RefreshableKeycloakSecurityContext secContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext();
|
||||||
|
CookieTokenStore.setTokenCookie(deployment, facade, secContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this);
|
||||||
|
if (principal == null) return;
|
||||||
|
|
||||||
|
CookieTokenStore.removeCookie(facade);
|
||||||
|
principal.getKeycloakSecurityContext().logout(deployment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
|
||||||
|
CookieTokenStore.setTokenCookie(deployment, facade, securityContext);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,10 +24,17 @@ import io.undertow.server.HttpServerExchange;
|
||||||
import io.undertow.server.session.Session;
|
import io.undertow.server.session.Session;
|
||||||
import io.undertow.util.AttachmentKey;
|
import io.undertow.util.AttachmentKey;
|
||||||
import io.undertow.util.Sessions;
|
import io.undertow.util.Sessions;
|
||||||
|
import org.keycloak.KeycloakPrincipal;
|
||||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
import org.keycloak.adapters.AuthChallenge;
|
import org.keycloak.adapters.AuthChallenge;
|
||||||
import org.keycloak.adapters.AuthOutcome;
|
import org.keycloak.adapters.AuthOutcome;
|
||||||
|
import org.keycloak.adapters.CookieTokenStore;
|
||||||
|
import org.keycloak.adapters.HttpFacade;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
import org.keycloak.adapters.RequestAuthenticator;
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
import org.keycloak.enums.TokenStore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism.
|
* Abstract base class for a Keycloak-enabled Undertow AuthenticationMechanism.
|
||||||
|
@ -37,9 +44,11 @@ import org.keycloak.adapters.RequestAuthenticator;
|
||||||
public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanism {
|
public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanism {
|
||||||
public static final AttachmentKey<AuthChallenge> KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class);
|
public static final AttachmentKey<AuthChallenge> KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class);
|
||||||
protected AdapterDeploymentContext deploymentContext;
|
protected AdapterDeploymentContext deploymentContext;
|
||||||
|
protected UndertowUserSessionManagement sessionManagement;
|
||||||
|
|
||||||
public UndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext) {
|
public UndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement) {
|
||||||
this.deploymentContext = deploymentContext;
|
this.deploymentContext = deploymentContext;
|
||||||
|
this.sessionManagement = sessionManagement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -54,21 +63,17 @@ public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanis
|
||||||
return new ChallengeResult(false);
|
return new ChallengeResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void registerNotifications(SecurityContext securityContext) {
|
protected void registerNotifications(final SecurityContext securityContext) {
|
||||||
|
|
||||||
final NotificationReceiver logoutReceiver = new NotificationReceiver() {
|
final NotificationReceiver logoutReceiver = new NotificationReceiver() {
|
||||||
@Override
|
@Override
|
||||||
public void handleNotification(SecurityNotification notification) {
|
public void handleNotification(SecurityNotification notification) {
|
||||||
if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return;
|
if (notification.getEventType() != SecurityNotification.EventType.LOGGED_OUT) return;
|
||||||
Session session = Sessions.getSession(notification.getExchange());
|
|
||||||
if (session == null) return;
|
|
||||||
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
|
||||||
if (account == null) return;
|
|
||||||
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
|
||||||
if (account.getKeycloakSecurityContext() != null) {
|
|
||||||
UndertowHttpFacade facade = new UndertowHttpFacade(notification.getExchange());
|
UndertowHttpFacade facade = new UndertowHttpFacade(notification.getExchange());
|
||||||
account.getKeycloakSecurityContext().logout(deploymentContext.resolveDeployment(facade));
|
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
|
||||||
}
|
AdapterTokenStore tokenStore = getTokenStore(notification.getExchange(), facade, deployment, securityContext);
|
||||||
|
tokenStore.logout();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -96,4 +101,12 @@ public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanis
|
||||||
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
|
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected AdapterTokenStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, KeycloakDeployment deployment, SecurityContext securityContext) {
|
||||||
|
if (deployment.getTokenStore() == TokenStore.SESSION) {
|
||||||
|
return new UndertowSessionTokenStore(exchange, deployment, sessionManagement, securityContext);
|
||||||
|
} else {
|
||||||
|
return new UndertowCookieTokenStore(facade, deployment, securityContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -21,8 +21,8 @@ import io.undertow.server.HttpServerExchange;
|
||||||
import io.undertow.server.session.Session;
|
import io.undertow.server.session.Session;
|
||||||
import io.undertow.util.Sessions;
|
import io.undertow.util.Sessions;
|
||||||
import org.keycloak.KeycloakPrincipal;
|
import org.keycloak.KeycloakPrincipal;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
import org.keycloak.adapters.HttpFacade;
|
import org.keycloak.adapters.HttpFacade;
|
||||||
import org.keycloak.adapters.KeycloakAccount;
|
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.OAuthRequestAuthenticator;
|
import org.keycloak.adapters.OAuthRequestAuthenticator;
|
||||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
@ -36,16 +36,14 @@ import org.keycloak.adapters.RequestAuthenticator;
|
||||||
public abstract class UndertowRequestAuthenticator extends RequestAuthenticator {
|
public abstract class UndertowRequestAuthenticator extends RequestAuthenticator {
|
||||||
protected SecurityContext securityContext;
|
protected SecurityContext securityContext;
|
||||||
protected HttpServerExchange exchange;
|
protected HttpServerExchange exchange;
|
||||||
protected UndertowUserSessionManagement userSessionManagement;
|
|
||||||
|
|
||||||
|
|
||||||
public UndertowRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
|
public UndertowRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
|
||||||
SecurityContext securityContext, HttpServerExchange exchange,
|
SecurityContext securityContext, HttpServerExchange exchange,
|
||||||
UndertowUserSessionManagement userSessionManagement) {
|
AdapterTokenStore tokenStore) {
|
||||||
super(facade, deployment, sslRedirectPort);
|
super(facade, deployment, tokenStore, sslRedirectPort);
|
||||||
this.securityContext = securityContext;
|
this.securityContext = securityContext;
|
||||||
this.exchange = exchange;
|
this.exchange = exchange;
|
||||||
this.userSessionManagement = userSessionManagement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void propagateKeycloakContext(KeycloakUndertowAccount account) {
|
protected void propagateKeycloakContext(KeycloakUndertowAccount account) {
|
||||||
|
@ -67,16 +65,9 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator
|
||||||
KeycloakUndertowAccount account = createAccount(principal);
|
KeycloakUndertowAccount account = createAccount(principal);
|
||||||
securityContext.authenticationComplete(account, "KEYCLOAK", false);
|
securityContext.authenticationComplete(account, "KEYCLOAK", false);
|
||||||
propagateKeycloakContext(account);
|
propagateKeycloakContext(account);
|
||||||
login(account);
|
tokenStore.saveAccountInfo(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void login(KeycloakAccount account) {
|
|
||||||
Session session = Sessions.getOrCreateSession(exchange);
|
|
||||||
session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
|
|
||||||
userSessionManagement.login(session.getSessionManager());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
|
||||||
KeycloakUndertowAccount account = createAccount(principal);
|
KeycloakUndertowAccount account = createAccount(principal);
|
||||||
|
@ -84,32 +75,6 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator
|
||||||
propagateKeycloakContext(account);
|
propagateKeycloakContext(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isCached() {
|
|
||||||
Session session = Sessions.getSession(exchange);
|
|
||||||
if (session == null) {
|
|
||||||
log.debug("session was null, returning null");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
|
||||||
if (account == null) {
|
|
||||||
log.debug("Account was not in session, returning null");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
account.setDeployment(deployment);
|
|
||||||
if (account.isActive()) {
|
|
||||||
log.debug("Cached account found");
|
|
||||||
securityContext.authenticationComplete(account, "KEYCLOAK", false);
|
|
||||||
propagateKeycloakContext( account);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
log.debug("Account was not active, returning false");
|
|
||||||
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
|
||||||
session.invalidate(exchange);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getHttpSessionId(boolean create) {
|
protected String getHttpSessionId(boolean create) {
|
||||||
if (create) {
|
if (create) {
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
package org.keycloak.adapters.undertow;
|
||||||
|
|
||||||
|
import io.undertow.security.api.SecurityContext;
|
||||||
|
import io.undertow.server.HttpServerExchange;
|
||||||
|
import io.undertow.server.session.Session;
|
||||||
|
import io.undertow.util.Sessions;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
|
import org.keycloak.adapters.KeycloakAccount;
|
||||||
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
|
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||||
|
import org.keycloak.adapters.RequestAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-request object. Storage of tokens in undertow session.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class UndertowSessionTokenStore implements AdapterTokenStore {
|
||||||
|
|
||||||
|
protected static Logger log = Logger.getLogger(UndertowSessionTokenStore.class);
|
||||||
|
|
||||||
|
private final HttpServerExchange exchange;
|
||||||
|
private final KeycloakDeployment deployment;
|
||||||
|
private final UndertowUserSessionManagement sessionManagement;
|
||||||
|
private final SecurityContext securityContext;
|
||||||
|
|
||||||
|
public UndertowSessionTokenStore(HttpServerExchange exchange, KeycloakDeployment deployment, UndertowUserSessionManagement sessionManagement,
|
||||||
|
SecurityContext securityContext) {
|
||||||
|
this.exchange = exchange;
|
||||||
|
this.deployment = deployment;
|
||||||
|
this.sessionManagement = sessionManagement;
|
||||||
|
this.securityContext = securityContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkCurrentToken() {
|
||||||
|
// no-op on undertow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCached(RequestAuthenticator authenticator) {
|
||||||
|
Session session = Sessions.getSession(exchange);
|
||||||
|
if (session == null) {
|
||||||
|
log.debug("session was null, returning null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
|
if (account == null) {
|
||||||
|
log.debug("Account was not in session, returning null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
account.setCurrentRequestInfo(deployment, this);
|
||||||
|
if (account.checkActive()) {
|
||||||
|
log.debug("Cached account found");
|
||||||
|
securityContext.authenticationComplete(account, "KEYCLOAK", false);
|
||||||
|
((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.debug("Account was not active, returning false");
|
||||||
|
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
|
session.invalidate(exchange);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAccountInfo(KeycloakAccount account) {
|
||||||
|
Session session = Sessions.getOrCreateSession(exchange);
|
||||||
|
session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
|
||||||
|
sessionManagement.login(session.getSessionManager());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() {
|
||||||
|
Session session = Sessions.getSession(exchange);
|
||||||
|
if (session == null) return;
|
||||||
|
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
|
if (account == null) return;
|
||||||
|
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||||
|
if (account.getKeycloakSecurityContext() != null) {
|
||||||
|
account.getKeycloakSecurityContext().logout(deployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import io.undertow.security.api.SecurityContext;
|
||||||
import io.undertow.server.HttpServerExchange;
|
import io.undertow.server.HttpServerExchange;
|
||||||
import io.undertow.servlet.api.ConfidentialPortManager;
|
import io.undertow.servlet.api.ConfidentialPortManager;
|
||||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.NodesRegistrationManagement;
|
import org.keycloak.adapters.NodesRegistrationManagement;
|
||||||
import org.keycloak.adapters.undertow.ServletKeycloakAuthMech;
|
import org.keycloak.adapters.undertow.ServletKeycloakAuthMech;
|
||||||
|
@ -27,7 +28,8 @@ public class WildflyAuthenticationMechanism extends ServletKeycloakAuthMech {
|
||||||
@Override
|
@Override
|
||||||
protected ServletRequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) {
|
protected ServletRequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) {
|
||||||
int confidentialPort = getConfidentilPort(exchange);
|
int confidentialPort = getConfidentilPort(exchange);
|
||||||
|
AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext);
|
||||||
return new WildflyRequestAuthenticator(facade, deployment,
|
return new WildflyRequestAuthenticator(facade, deployment,
|
||||||
confidentialPort, securityContext, exchange, userSessionManagement);
|
confidentialPort, securityContext, exchange, tokenStore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import org.jboss.security.SecurityConstants;
|
||||||
import org.jboss.security.SecurityContextAssociation;
|
import org.jboss.security.SecurityContextAssociation;
|
||||||
import org.jboss.security.SimpleGroup;
|
import org.jboss.security.SimpleGroup;
|
||||||
import org.jboss.security.SimplePrincipal;
|
import org.jboss.security.SimplePrincipal;
|
||||||
|
import org.keycloak.adapters.AdapterTokenStore;
|
||||||
import org.keycloak.adapters.HttpFacade;
|
import org.keycloak.adapters.HttpFacade;
|
||||||
import org.keycloak.adapters.KeycloakDeployment;
|
import org.keycloak.adapters.KeycloakDeployment;
|
||||||
import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
|
import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
|
||||||
|
@ -31,8 +32,8 @@ public class WildflyRequestAuthenticator extends ServletRequestAuthenticator {
|
||||||
|
|
||||||
public WildflyRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
|
public WildflyRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
|
||||||
SecurityContext securityContext, HttpServerExchange exchange,
|
SecurityContext securityContext, HttpServerExchange exchange,
|
||||||
UndertowUserSessionManagement userSessionManagement) {
|
AdapterTokenStore tokenStore) {
|
||||||
super(facade, deployment, sslRedirectPort, securityContext, exchange, userSessionManagement);
|
super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -32,7 +32,6 @@ import org.keycloak.adapters.AdapterConstants;
|
||||||
import org.keycloak.models.ApplicationModel;
|
import org.keycloak.models.ApplicationModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.PasswordPolicy;
|
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
@ -112,7 +111,7 @@ public class AdapterTest {
|
||||||
TokenManager tm = new TokenManager();
|
TokenManager tm = new TokenManager();
|
||||||
UserModel admin = session.users().getUserByUsername("admin", adminRealm);
|
UserModel admin = session.users().getUserByUsername("admin", adminRealm);
|
||||||
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false);
|
UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false);
|
||||||
AccessToken token = tm.createClientAccessToken(tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession);
|
AccessToken token = tm.createClientAccessToken(TokenManager.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession);
|
||||||
return tm.encodeToken(adminRealm, token);
|
return tm.encodeToken(adminRealm, token);
|
||||||
} finally {
|
} finally {
|
||||||
keycloakRule.stopSession(session, true);
|
keycloakRule.stopSession(session, true);
|
||||||
|
@ -440,7 +439,7 @@ public class AdapterTest {
|
||||||
// Open browser2
|
// Open browser2
|
||||||
browser2.webRule.before();
|
browser2.webRule.before();
|
||||||
try {
|
try {
|
||||||
browser2.loginAndCheckSession(browser2.driver, browser2.loginPage);
|
loginAndCheckSession(browser2.driver, browser2.loginPage);
|
||||||
|
|
||||||
// Logout in browser1
|
// Logout in browser1
|
||||||
String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
|
String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
package org.keycloak.testsuite.adapter;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.adapters.AdapterConstants;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.protocol.oidc.OpenIDConnectService;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.services.managers.RealmManager;
|
||||||
|
import org.keycloak.testsuite.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
|
||||||
|
import org.keycloak.testsuite.rule.WebResource;
|
||||||
|
import org.keycloak.testsuite.rule.WebRule;
|
||||||
|
import org.keycloak.testutils.KeycloakServer;
|
||||||
|
import org.openqa.selenium.Cookie;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KEYCLOAK-702
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class CookieTokenStoreAdapterTest {
|
||||||
|
|
||||||
|
public static final String LOGIN_URL = OpenIDConnectService.loginPageUrl(UriBuilder.fromUri("http://localhost:8081/auth")).build("demo").toString();
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
|
||||||
|
RealmRepresentation representation = KeycloakServer.loadJson(getClass().getResourceAsStream("/adapter-test/demorealm.json"), RealmRepresentation.class);
|
||||||
|
manager.importRealm(representation);
|
||||||
|
|
||||||
|
URL url = getClass().getResource("/adapter-test/cust-app-keycloak.json");
|
||||||
|
deployApplication("customer-portal", "/customer-portal", CustomerServlet.class, url.getPath(), "user");
|
||||||
|
url = getClass().getResource("/adapter-test/cust-app-cookie-keycloak.json");
|
||||||
|
deployApplication("customer-cookie-portal", "/customer-cookie-portal", CustomerServlet.class, url.getPath(), "user");
|
||||||
|
url = getClass().getResource("/adapter-test/customer-db-keycloak.json");
|
||||||
|
deployApplication("customer-db", "/customer-db", CustomerDatabaseServlet.class, url.getPath(), "user");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public WebRule webRule = new WebRule(this);
|
||||||
|
|
||||||
|
@WebResource
|
||||||
|
protected WebDriver driver;
|
||||||
|
|
||||||
|
@WebResource
|
||||||
|
protected OAuthClient oauth;
|
||||||
|
|
||||||
|
@WebResource
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTokenInCookieSSO() throws Throwable {
|
||||||
|
// Login
|
||||||
|
String tokenCookie = loginToCustomerCookiePortal();
|
||||||
|
|
||||||
|
// SSO to second app
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||||
|
assertLogged();
|
||||||
|
|
||||||
|
// return to customer-cookie-portal and assert still same cookie (accessToken didn't expire)
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||||
|
assertLogged();
|
||||||
|
String tokenCookie2 = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue();
|
||||||
|
Assert.assertEquals(tokenCookie, tokenCookie2);
|
||||||
|
|
||||||
|
// Logout with httpServletRequest
|
||||||
|
logoutFromCustomerCookiePortal();
|
||||||
|
|
||||||
|
// Also should be logged-out from the second app
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTokenInCookieRefresh() throws Throwable {
|
||||||
|
// Set token timeout 1 sec
|
||||||
|
KeycloakSession session = keycloakRule.startSession();
|
||||||
|
RealmModel realm = session.realms().getRealmByName("demo");
|
||||||
|
int originalTokenTimeout = realm.getAccessTokenLifespan();
|
||||||
|
realm.setAccessTokenLifespan(1);
|
||||||
|
session.getTransaction().commit();
|
||||||
|
session.close();
|
||||||
|
|
||||||
|
// login to customer-cookie-portal
|
||||||
|
String tokenCookie1 = loginToCustomerCookiePortal();
|
||||||
|
|
||||||
|
// wait 2 secs
|
||||||
|
Thread.sleep(2000);
|
||||||
|
|
||||||
|
// assert cookie was refreshed
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||||
|
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-cookie-portal");
|
||||||
|
assertLogged();
|
||||||
|
String tokenCookie2 = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue();
|
||||||
|
Assert.assertNotEquals(tokenCookie1, tokenCookie2);
|
||||||
|
|
||||||
|
// login to 2nd app and logout from it
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||||
|
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||||
|
assertLogged();
|
||||||
|
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-portal/logout");
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||||
|
|
||||||
|
// wait 2 secs until accessToken expires for customer-cookie-portal too.
|
||||||
|
Thread.sleep(2000);
|
||||||
|
|
||||||
|
// assert not logged in customer-cookie-portal
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||||
|
|
||||||
|
// Change timeout back
|
||||||
|
session = keycloakRule.startSession();
|
||||||
|
realm = session.realms().getRealmByName("demo");
|
||||||
|
realm.setAccessTokenLifespan(originalTokenTimeout);
|
||||||
|
session.getTransaction().commit();
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidTokenCookie() throws Throwable {
|
||||||
|
// Login
|
||||||
|
String tokenCookie = loginToCustomerCookiePortal();
|
||||||
|
String changedTokenCookie = tokenCookie.replace("a", "b");
|
||||||
|
|
||||||
|
// change cookie to invalid value
|
||||||
|
driver.manage().addCookie(new Cookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, changedTokenCookie, "/customer-cookie-portal"));
|
||||||
|
|
||||||
|
// visit page and assert re-logged and cookie was refreshed
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||||
|
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-cookie-portal");
|
||||||
|
String currentCookie = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue();
|
||||||
|
Assert.assertNotEquals(currentCookie, tokenCookie);
|
||||||
|
Assert.assertNotEquals(currentCookie, changedTokenCookie);
|
||||||
|
|
||||||
|
// logout
|
||||||
|
logoutFromCustomerCookiePortal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// login to customer-cookie-portal and return the KEYCLOAK_ADAPTER_STATE cookie established on adapter
|
||||||
|
private String loginToCustomerCookiePortal() {
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||||
|
loginPage.login("bburke@redhat.com", "password");
|
||||||
|
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-cookie-portal");
|
||||||
|
assertLogged();
|
||||||
|
|
||||||
|
// Assert no JSESSIONID cookie
|
||||||
|
Assert.assertNull(driver.manage().getCookieNamed("JSESSIONID"));
|
||||||
|
|
||||||
|
return driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logoutFromCustomerCookiePortal() {
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-cookie-portal/logout");
|
||||||
|
Assert.assertTrue(driver.getPageSource().contains("ok"));
|
||||||
|
Assert.assertNull(driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE));
|
||||||
|
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||||
|
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertLogged() {
|
||||||
|
String pageSource = driver.getPageSource();
|
||||||
|
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,8 +29,10 @@ public class CustomerServlet extends HttpServlet {
|
||||||
if (req.getRequestURI().toString().endsWith("logout")) {
|
if (req.getRequestURI().toString().endsWith("logout")) {
|
||||||
resp.setStatus(200);
|
resp.setStatus(200);
|
||||||
pw.println("ok");
|
pw.println("ok");
|
||||||
pw.flush();
|
|
||||||
|
// Call logout before pw.flush
|
||||||
req.logout();
|
req.logout();
|
||||||
|
pw.flush();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
KeycloakSecurityContext context = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName());
|
KeycloakSecurityContext context = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName());
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"realm": "demo",
|
||||||
|
"resource": "customer-cookie-portal",
|
||||||
|
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||||
|
"auth-server-url": "http://localhost:8081/auth",
|
||||||
|
"ssl-required" : "external",
|
||||||
|
"expose-token": true,
|
||||||
|
"token-store": "cookie",
|
||||||
|
"credentials": {
|
||||||
|
"secret": "password"
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,6 +68,15 @@
|
||||||
],
|
],
|
||||||
"secret": "password"
|
"secret": "password"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "customer-cookie-portal",
|
||||||
|
"enabled": true,
|
||||||
|
"baseUrl": "http://localhost:8081/customer-cookie-portal",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8081/customer-cookie-portal/*"
|
||||||
|
],
|
||||||
|
"secret": "password"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "customer-portal-js",
|
"name": "customer-portal-js",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|
Loading…
Reference in a new issue