Merge pull request #801 from mposolda/master

KEYCLOAK-702 - Added AdapterTokenStore spi. Possibility to save account ...
This commit is contained in:
Marek Posolda 2014-10-27 17:06:49 +01:00
commit 4b87b8f7e3
35 changed files with 1351 additions and 340 deletions

View file

@ -26,4 +26,7 @@ public interface AdapterConstants {
// 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";
// 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";
}

View file

@ -0,0 +1,9 @@
package org.keycloak.enums;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public enum TokenStore {
SESSION,
COOKIE
}

View file

@ -18,7 +18,7 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder;
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
"client-keystore", "client-keystore-password", "client-key-password",
"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 {
@ -46,6 +46,8 @@ public class AdapterConfig extends BaseAdapterConfig {
protected boolean registerNodeAtStartup = false;
@JsonProperty("register-node-period")
protected int registerNodePeriod = -1;
@JsonProperty("token-store")
protected String tokenStore;
public boolean isAllowAnyHostname() {
return allowAnyHostname;
@ -142,4 +144,12 @@ public class AdapterConfig extends BaseAdapterConfig {
public void setRegisterNodePeriod(int registerNodePeriod) {
this.registerNodePeriod = registerNodePeriod;
}
public String getTokenStore() {
return tokenStore;
}
public void setTokenStore(String tokenStore) {
this.tokenStore = tokenStore;
}
}

View file

@ -7,6 +7,7 @@ import org.apache.http.client.methods.HttpGet;
import org.jboss.logging.Logger;
import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.enums.SslRequired;
import org.keycloak.enums.TokenStore;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.PublishedRealmRepresentation;
import org.keycloak.util.JsonSerialization;
@ -257,6 +258,16 @@ public class AdapterDeploymentContext {
delegate.setSslRequired(sslRequired);
}
@Override
public TokenStore getTokenStore() {
return delegate.getTokenStore();
}
@Override
public void setTokenStore(TokenStore tokenStore) {
delegate.setTokenStore(tokenStore);
}
@Override
public String getStateCookieName() {
return delegate.getStateCookieName();

View file

@ -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);
}

View file

@ -1,6 +1,11 @@
package org.keycloak.adapters;
import java.util.Collections;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.util.UriUtils;
/**
@ -8,6 +13,8 @@ import org.keycloak.util.UriUtils;
*/
public class AdapterUtils {
private static Logger log = Logger.getLogger(AdapterUtils.class);
public static String getOrigin(String browserRequestURL, KeycloakSecurityContext session) {
if (session instanceof RefreshableKeycloakSecurityContext) {
KeycloakDeployment deployment = ((RefreshableKeycloakSecurityContext)session).getDeployment();
@ -26,4 +33,30 @@ public class AdapterUtils {
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;
}
}

View file

@ -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);
}
}

View file

@ -5,6 +5,7 @@ import org.jboss.logging.Logger;
import org.keycloak.ServiceUrlConstants;
import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.enums.SslRequired;
import org.keycloak.enums.TokenStore;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.KeycloakUriBuilder;
@ -42,6 +43,7 @@ public class KeycloakDeployment {
protected String scope;
protected SslRequired sslRequired = SslRequired.ALL;
protected TokenStore tokenStore = TokenStore.SESSION;
protected String stateCookieName = "OAuth_Token_Request_State";
protected boolean useResourceRoleMappings;
protected boolean cors;
@ -236,6 +238,14 @@ public class KeycloakDeployment {
this.sslRequired = sslRequired;
}
public TokenStore getTokenStore() {
return tokenStore;
}
public void setTokenStore(TokenStore tokenStore) {
this.tokenStore = tokenStore;
}
public String getStateCookieName() {
return stateCookieName;
}

View file

@ -4,6 +4,7 @@ import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.jboss.logging.Logger;
import org.keycloak.enums.SslRequired;
import org.keycloak.enums.TokenStore;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.PemUtils;
import org.keycloak.util.SystemPropertiesJsonParserFactory;
@ -48,6 +49,11 @@ public class KeycloakDeploymentBuilder {
} else {
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.setPublicClient(adapterConfig.isPublicClient());
deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings());

View file

@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.enums.TokenStore;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
@ -255,7 +256,8 @@ public abstract class OAuthRequestAuthenticator {
AccessTokenResponse tokenResponse = null;
strippedOauthParametersRequestUri = stripOauthParametersFromRedirect();
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);
} catch (ServerRequest.HttpFailure failure) {
log.error("failed to turn code into token");

View file

@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.enums.TokenStore;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
@ -19,14 +20,16 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
protected static Logger log = Logger.getLogger(RefreshableKeycloakSecurityContext.class);
protected transient KeycloakDeployment deployment;
protected transient AdapterTokenStore tokenStore;
protected String refreshToken;
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);
this.deployment = deployment;
this.tokenStore = tokenStore;
this.refreshToken = refreshToken;
}
@ -42,6 +45,10 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
return super.getTokenString();
}
public String getRefreshToken() {
return refreshToken;
}
public void logout(KeycloakDeployment deployment) {
try {
ServerRequest.invokeLogout(deployment, refreshToken);
@ -58,8 +65,9 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
return deployment;
}
public void setDeployment(KeycloakDeployment deployment) {
public void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
this.deployment = deployment;
this.tokenStore = tokenStore;
}
/**
@ -107,8 +115,14 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
this.token = token;
this.refreshToken = response.getRefreshToken();
this.tokenString = tokenString;
tokenStore.refreshCallback(this);
return true;
}
protected void updateTokenCookie(KeycloakDeployment deployment, HttpFacade facade) {
if (deployment.getTokenStore() == TokenStore.COOKIE) {
CookieTokenStore.setTokenCookie(deployment, facade, this);
}
}
}

View file

@ -12,12 +12,14 @@ public abstract class RequestAuthenticator {
protected HttpFacade facade;
protected KeycloakDeployment deployment;
protected AdapterTokenStore tokenStore;
protected AuthChallenge challenge;
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.deployment = deployment;
this.tokenStore = tokenStore;
this.sslRedirectPort = sslRedirectPort;
}
@ -58,7 +60,7 @@ public abstract class RequestAuthenticator {
log.trace("try oauth");
}
if (isCached()) {
if (tokenStore.isCached(this)) {
if (verifySSL()) return AuthOutcome.FAILED;
log.debug("AUTHENTICATED: was cached");
return AuthOutcome.AUTHENTICATED;
@ -103,18 +105,17 @@ public abstract class RequestAuthenticator {
}
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);
completeOAuthAuthentication(principal);
}
protected abstract void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
protected abstract void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
protected abstract boolean isCached();
protected abstract String getHttpSessionId(boolean create);
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);
completeBearerAuthentication(principal);
}

View file

@ -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;
}
}

View file

@ -1,21 +1,21 @@
package org.keycloak.adapters.as7;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.Constants;
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.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OAuthRequestAuthenticator;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.RequestAuthenticator;
import org.keycloak.representations.AccessToken;
import org.keycloak.enums.TokenStore;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import java.util.Set;
import javax.servlet.http.HttpSession;
@ -25,18 +25,17 @@ import javax.servlet.http.HttpSession;
* @version $Revision: 1 $
*/
public class CatalinaRequestAuthenticator extends RequestAuthenticator {
private static final Logger log = Logger.getLogger(CatalinaRequestAuthenticator.class);
protected KeycloakAuthenticatorValve valve;
protected CatalinaUserSessionManagement userSessionManagement;
protected Request request;
public CatalinaRequestAuthenticator(KeycloakDeployment deployment,
KeycloakAuthenticatorValve valve, CatalinaUserSessionManagement userSessionManagement,
KeycloakAuthenticatorValve valve, AdapterTokenStore tokenStore,
CatalinaHttpFacade facade,
Request request) {
super(facade, deployment, request.getConnector().getRedirectPort());
super(facade, deployment, tokenStore, request.getConnector().getRedirectPort());
this.valve = valve;
this.userSessionManagement = userSessionManagement;
this.request = request;
}
@ -46,7 +45,10 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
@Override
protected void saveRequest() {
try {
valve.keycloakSaveRequest(request);
// Support saving request just for TokenStore.SESSION TODO: Add to tokenStore spi?
if (deployment.getTokenStore() == TokenStore.SESSION) {
valve.keycloakSaveRequest(request);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
@ -55,24 +57,36 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
}
@Override
protected void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext();
protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
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);
Set<String> roles = getRolesFromToken(securityContext);
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);
this.tokenStore.saveAccountInfo(account);
}
@Override
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
Set<String> roles = getRolesFromToken(securityContext);
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
if (log.isDebugEnabled()) {
log.debug("Completing bearer authentication. Bearer roles: " + roles);
}
@ -82,39 +96,6 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
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() {
if (request.getSessionInternal().getNote(Constants.FORM_REQUEST_NOTE) != null) {
if (valve.keycloakRestoreRequest(request)) {

View file

@ -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
}
}

View file

@ -13,17 +13,21 @@ import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.LoginConfig;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.CookieTokenStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.NodesRegistrationManagement;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.enums.TokenStore;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@ -44,6 +48,9 @@ import java.io.InputStream;
* @version $Revision: 1 $
*/
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);
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
protected AdapterDeploymentContext deploymentContext;
@ -63,14 +70,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
if (ksc != null) {
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);
((RefreshableKeycloakSecurityContext)ksc).logout(deploymentContext.resolveDeployment(facade));
}
}
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
tokenStore.logout();
}
super.logout(request);
}
@ -164,10 +168,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
log.info("*** deployment isn't configured return false");
return false;
}
AdapterTokenStore tokenStore = getTokenStore(request, facade, 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();
if (outcome == AuthOutcome.AUTHENTICATED) {
if (facade.isEnded()) {
@ -188,27 +193,9 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
* @param request
*/
protected void checkKeycloakSession(Request request, HttpFacade facade) {
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.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();
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
tokenStore.checkCurrentToken();
}
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;
}
}

View file

@ -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;
}
}

View file

@ -1,20 +1,20 @@
package org.keycloak.adapters.tomcat7;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.Constants;
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.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OAuthRequestAuthenticator;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.RequestAuthenticator;
import org.keycloak.representations.AccessToken;
import org.keycloak.enums.TokenStore;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -28,16 +28,14 @@ import javax.servlet.http.HttpSession;
public class CatalinaRequestAuthenticator extends RequestAuthenticator {
private static final Logger log = Logger.getLogger(""+CatalinaRequestAuthenticator.class);
protected KeycloakAuthenticatorValve valve;
protected CatalinaUserSessionManagement userSessionManagement;
protected Request request;
public CatalinaRequestAuthenticator(KeycloakDeployment deployment,
KeycloakAuthenticatorValve valve, CatalinaUserSessionManagement userSessionManagement,
KeycloakAuthenticatorValve valve, AdapterTokenStore tokenStore,
CatalinaHttpFacade facade,
Request request) {
super(facade, deployment, request.getConnector().getRedirectPort());
super(facade, deployment, tokenStore, request.getConnector().getRedirectPort());
this.valve = valve;
this.userSessionManagement = userSessionManagement;
this.request = request;
}
@ -47,7 +45,10 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
@Override
protected void saveRequest() {
try {
valve.keycloakSaveRequest(request);
// Support saving request just for TokenStore.SESSION TODO: Add to tokenStore spi?
if (deployment.getTokenStore() == TokenStore.SESSION) {
valve.keycloakSaveRequest(request);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
@ -56,24 +57,36 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
}
@Override
protected void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext();
protected void completeOAuthAuthentication(final KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
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);
Set<String> roles = getRolesFromToken(securityContext);
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);
this.tokenStore.saveAccountInfo(account);
}
@Override
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
Set<String> roles = getRolesFromToken(securityContext);
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
if (log.isLoggable(Level.FINE)) {
log.fine("Completing bearer authentication. Bearer roles: " + roles);
}
@ -83,39 +96,6 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
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() {
if (request.getSessionInternal().getNote(Constants.FORM_REQUEST_NOTE) != null) {
if (valve.keycloakRestoreRequest(request)) {

View file

@ -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
}
}

View file

@ -15,6 +15,7 @@ import org.apache.catalina.deploy.LoginConfig;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.HttpFacade;
@ -24,6 +25,7 @@ import org.keycloak.adapters.NodesRegistrationManagement;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.ServerRequest;
import org.keycloak.enums.TokenStore;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@ -45,6 +47,9 @@ import java.util.logging.Logger;
* @version $Revision: 1 $
*/
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);
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
protected AdapterDeploymentContext deploymentContext;
@ -70,16 +75,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
if (ksc != null) {
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);
ServerRequest.invokeLogout(deploymentContext.resolveDeployment(facade), ksc.getToken().getSessionState());
} catch (Exception e) {
log.severe("failed to invoke remote logout. " + e.getMessage());
}
}
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
tokenStore.logout();
}
super.logout(request);
}
@ -164,10 +164,11 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
if (deployment == null || !deployment.isConfigured()) {
return false;
}
AdapterTokenStore tokenStore = getTokenStore(request, facade, 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();
if (outcome == AuthOutcome.AUTHENTICATED) {
if (facade.isEnded()) {
@ -188,27 +189,9 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
* @param request
*/
protected void checkKeycloakSession(Request request, HttpFacade facade) {
if (request.getSessionInternal(false) == null || request.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.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();
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
tokenStore.checkCurrentToken();
}
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;
}
}

View file

@ -19,14 +19,15 @@ package org.keycloak.adapters.undertow;
import io.undertow.security.idm.Account;
import org.jboss.logging.Logger;
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.KeycloakDeployment;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.representations.AccessToken;
import java.io.Serializable;
import java.security.Principal;
import java.util.Collections;
import java.util.Set;
/**
@ -40,33 +41,11 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA
public KeycloakUndertowAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> 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;
}
@ -85,11 +64,12 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA
return principal.getKeycloakSecurityContext();
}
public void setDeployment(KeycloakDeployment deployment) {
principal.getKeycloakSecurityContext().setDeployment(deployment);
public void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
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
RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext();
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) {
@ -106,7 +86,7 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA
}
log.debug("refresh succeeded");
setRoles(session.getToken());
setRoles(session);
return true;
}

View file

@ -25,9 +25,12 @@ import io.undertow.servlet.handlers.ServletRequestContext;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.NodesRegistrationManagement;
import org.keycloak.adapters.RequestAuthenticator;
import org.keycloak.enums.TokenStore;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@ -40,13 +43,11 @@ import javax.servlet.http.HttpSession;
public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
private static final Logger log = Logger.getLogger(ServletKeycloakAuthMech.class);
protected UndertowUserSessionManagement userSessionManagement;
protected NodesRegistrationManagement nodesRegistrationManagement;
protected ConfidentialPortManager portManager;
public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, NodesRegistrationManagement nodesRegistrationManagement, ConfidentialPortManager portManager) {
super(deploymentContext);
this.userSessionManagement = userSessionManagement;
super(deploymentContext, userSessionManagement);
this.nodesRegistrationManagement = nodesRegistrationManagement;
this.portManager = portManager;
}
@ -66,40 +67,12 @@ public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
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) {
int confidentialPort = getConfidentilPort(exchange);
AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext);
return new ServletRequestAuthenticator(facade, deployment,
confidentialPort, securityContext, exchange, userSessionManagement);
confidentialPort, securityContext, exchange, tokenStore);
}
protected int getConfidentilPort(HttpServerExchange exchange) {
@ -112,4 +85,13 @@ public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
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);
}
}
}

View file

@ -18,13 +18,11 @@ 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.servlet.handlers.ServletRequestContext;
import io.undertow.util.Sessions;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
@ -41,34 +39,8 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
public ServletRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
SecurityContext securityContext, HttpServerExchange exchange,
UndertowUserSessionManagement userSessionManagement) {
super(facade, deployment, sslRedirectPort, securityContext, exchange, userSessionManagement);
}
@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;
}
AdapterTokenStore tokenStore) {
super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore);
}
@Override
@ -79,15 +51,6 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
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
protected KeycloakUndertowAccount createAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
return new KeycloakUndertowAccount(principal);

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -24,10 +24,17 @@ import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session;
import io.undertow.util.AttachmentKey;
import io.undertow.util.Sessions;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.AuthChallenge;
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.enums.TokenStore;
/**
* 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 static final AttachmentKey<AuthChallenge> KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class);
protected AdapterDeploymentContext deploymentContext;
protected UndertowUserSessionManagement sessionManagement;
public UndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext) {
public UndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement) {
this.deploymentContext = deploymentContext;
this.sessionManagement = sessionManagement;
}
@Override
@ -54,21 +63,17 @@ public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanis
return new ChallengeResult(false);
}
protected void registerNotifications(SecurityContext securityContext) {
protected void registerNotifications(final SecurityContext securityContext) {
final NotificationReceiver logoutReceiver = new NotificationReceiver() {
@Override
public void handleNotification(SecurityNotification notification) {
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());
account.getKeycloakSecurityContext().logout(deploymentContext.resolveDeployment(facade));
}
UndertowHttpFacade facade = new UndertowHttpFacade(notification.getExchange());
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;
}
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);
}
}
}

View file

@ -21,8 +21,8 @@ import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session;
import io.undertow.util.Sessions;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.OAuthRequestAuthenticator;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
@ -36,16 +36,14 @@ import org.keycloak.adapters.RequestAuthenticator;
public abstract class UndertowRequestAuthenticator extends RequestAuthenticator {
protected SecurityContext securityContext;
protected HttpServerExchange exchange;
protected UndertowUserSessionManagement userSessionManagement;
public UndertowRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
SecurityContext securityContext, HttpServerExchange exchange,
UndertowUserSessionManagement userSessionManagement) {
super(facade, deployment, sslRedirectPort);
AdapterTokenStore tokenStore) {
super(facade, deployment, tokenStore, sslRedirectPort);
this.securityContext = securityContext;
this.exchange = exchange;
this.userSessionManagement = userSessionManagement;
}
protected void propagateKeycloakContext(KeycloakUndertowAccount account) {
@ -67,16 +65,9 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator
KeycloakUndertowAccount account = createAccount(principal);
securityContext.authenticationComplete(account, "KEYCLOAK", false);
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
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
KeycloakUndertowAccount account = createAccount(principal);
@ -84,32 +75,6 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator
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
protected String getHttpSessionId(boolean create) {
if (create) {

View file

@ -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
}
}

View file

@ -4,6 +4,7 @@ import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.api.ConfidentialPortManager;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.NodesRegistrationManagement;
import org.keycloak.adapters.undertow.ServletKeycloakAuthMech;
@ -27,7 +28,8 @@ public class WildflyAuthenticationMechanism extends ServletKeycloakAuthMech {
@Override
protected ServletRequestAuthenticator createRequestAuthenticator(KeycloakDeployment deployment, HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) {
int confidentialPort = getConfidentilPort(exchange);
AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext);
return new WildflyRequestAuthenticator(facade, deployment,
confidentialPort, securityContext, exchange, userSessionManagement);
confidentialPort, securityContext, exchange, tokenStore);
}
}

View file

@ -8,6 +8,7 @@ import org.jboss.security.SecurityConstants;
import org.jboss.security.SecurityContextAssociation;
import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
@ -31,8 +32,8 @@ public class WildflyRequestAuthenticator extends ServletRequestAuthenticator {
public WildflyRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
SecurityContext securityContext, HttpServerExchange exchange,
UndertowUserSessionManagement userSessionManagement) {
super(facade, deployment, sslRedirectPort, securityContext, exchange, userSessionManagement);
AdapterTokenStore tokenStore) {
super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore);
}
@Override

View file

@ -32,7 +32,6 @@ import org.keycloak.adapters.AdapterConstants;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@ -112,7 +111,7 @@ public class AdapterTest {
TokenManager tm = new TokenManager();
UserModel admin = session.users().getUserByUsername("admin", adminRealm);
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);
} finally {
keycloakRule.stopSession(session, true);
@ -440,7 +439,7 @@ public class AdapterTest {
// Open browser2
browser2.webRule.before();
try {
browser2.loginAndCheckSession(browser2.driver, browser2.loginPage);
loginAndCheckSession(browser2.driver, browser2.loginPage);
// Logout in browser1
String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))

View file

@ -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"));
}
}

View file

@ -29,8 +29,10 @@ public class CustomerServlet extends HttpServlet {
if (req.getRequestURI().toString().endsWith("logout")) {
resp.setStatus(200);
pw.println("ok");
pw.flush();
// Call logout before pw.flush
req.logout();
pw.flush();
return;
}
KeycloakSecurityContext context = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName());

View file

@ -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"
}
}

View file

@ -68,6 +68,15 @@
],
"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",
"enabled": true,