This commit is contained in:
Bill Burke 2014-10-28 11:55:52 -04:00
commit cb110d095f
39 changed files with 1690 additions and 392 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

@ -34,6 +34,7 @@
<!ENTITY ServerCache SYSTEM "modules/cache.xml">
<!ENTITY SecurityVulnerabilities SYSTEM "modules/security-vulnerabilities.xml">
<!ENTITY Clustering SYSTEM "modules/clustering.xml">
<!ENTITY ApplicationClustering SYSTEM "modules/application-clustering.xml">
]>
<book>
@ -125,6 +126,7 @@ This one is short
&SAML;
&SecurityVulnerabilities;
&Clustering;
&ApplicationClustering;
&Migration;
</book>

View file

@ -291,6 +291,51 @@
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>auth-server-url-for-backend-requests</term>
<listitem>
<para>
Alternative location of auth-server-url used just for backend requests. It must be absolute URI. Useful
especially in cluster (see <link linkend="relative-uri-optimization">Relative URI Optimization</link>) or if you would like to use <emphasis>https</emphasis> for browser requests
but stick with <emphasis>http</emphasis> for backend requests etc.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>always-refresh-token</term>
<listitem>
<para>
If <emphasis>true</emphasis>, Keycloak will refresh token in every request. More info in <link linkend="refresh-token-each-req">Refresh token in each request</link> .
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>register-node-at-startup</term>
<listitem>
<para>
If <emphasis>true</emphasis>, then adapter will send registration request to Keycloak. It's <emphasis>false</emphasis>
by default as useful just in cluster (See <link linkend="registration-app-nodes">Registration of application nodes to Keycloak</link>)
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>register-node-period</term>
<listitem>
<para>
Period for re-registration adapter to Keycloak. Useful in cluster. See <link linkend="registration-app-nodes">Registration of application nodes to Keycloak</link> for details.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>token-store</term>
<listitem>
<para>
Possible values are <emphasis>session</emphasis> and <emphasis>cookie</emphasis>. Default is <emphasis>session</emphasis>,
which means that adapter stores account info in HTTP Session. Alternative <emphasis>cookie</emphasis> means storage of info in cookie.
See <link linkend="stateless-token-store">Stateless token store</link> for details.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>

View file

@ -0,0 +1,254 @@
<chapter id="applicationClustering">
<title>Application Clustering</title>
<para>This chapter is focused on clustering support for your own AS7, EAP6 or Wildfly applications, which are secured by Keycloak.
We support various deployment scenarios according if your application is:
<itemizedlist>
<listitem>
<para>
stateless or stateful
</para>
</listitem>
<listitem>
<para>
distributable (replicated http session) or non-distributable and just relying on sticky sessions provided by loadbalancer
</para>
</listitem>
<listitem>
<para>
deployed on same or different cluster hosts where keycloak servers are deployed
</para>
</listitem>
</itemizedlist>
</para>
<para>
The situation is a bit tricky as application communicates with Keycloak directly within user's browser (for example redirecting to login screen),
but there is also backend (out-of-bound) communication between keycloak and application, which is hidden from end-user
and his browser and hence can't rely on sticky sessions.
</para>
<section id="stateless-token-store">
<title>Stateless token store</title>
<para>
By default, the servlet web application secured by Keycloak uses HTTP session to store information about authenticated
user account. This means that this info could be replicated across cluster and your application will safely survive
failover of some cluster node.
</para>
<para>
However if you don't need or don't want to use HTTP Session, you may alternatively save all info about authenticated
account into cookie. This is useful especially if your application is:
<itemizedlist>
<listitem>
<para>
stateless application without need of HTTP Session, but with requirement to be safe to failover of some cluster node
</para>
</listitem>
<listitem>
<para>
stateful application, but you don't want sensitive token data to be saved in HTTP session
</para>
</listitem>
<listitem>
<para>
stateless application relying on loadbalancer, which is not aware of sticky sessions (in this case cookie is your only way)
</para>
</listitem>
</itemizedlist>
</para>
<para>
To configure this, you can add this line to configuration of your adapter in <literal>WEB-INF/keycloak.json</literal> of your application:
<programlisting>
<![CDATA[
"token-store": "cookie"
]]>
</programlisting>
</para>
<para>
Default value of <literal>token-store</literal> is <literal>session</literal>, hence saving data in HTTP session. One disadvantage of cookie store is,
that whole info about account is passed in cookie KEYCLOAK_ADAPTER_STATE in each HTTP request. Hence it's not the best for network performance.
</para>
</section>
<section id="relative-uri-optimization">
<title>Relative URI optimization</title>
<para>
In many deployment scenarios will be Keycloak and secured applications deployed on same cluster hosts. For this case Keycloak
already provides option to use relative URI as value of option <emphasis>auth-server-url</emphasis> in <literal>WEB-INF/keycloak.json</literal> .
In this case, the URI of Keycloak server is resolved from the URI of current request.
</para>
<para>
For example if your loadbalancer is on <emphasis>https://loadbalancer.com/myapp</emphasis> and auth-server-url is <emphasis>/auth</emphasis>,
then relative URI of Keycloak is resolved to be <emphasis>https://loadbalancer.com/myapp</emphasis> .
</para>
<para>
For cluster setup, it may be even better to use option <emphasis>auth-server-url-for-backend-request</emphasis> . This allows to configure
that backend requests between Keycloak and your application will be sent directly to same cluster host without additional
round-trip through loadbalancer. So for this, it's good to configure values in <literal>WEB-INF/keycloak.json</literal> like this:
<programlisting>
<![CDATA[
"auth-server-url": "/auth",
"auth-server-url-for-backend-requests": "http://${jboss.host.name}:8080/auth"
]]>
</programlisting>
</para>
<para>
This would mean that browser requests (like redirecting to Keycloak login screen) will be still resolved relatively
to current request URI like <emphasis>https://loadbalancer.com/myapp</emphasis>, but backend (out-of-bound) requests between keycloak
and your app are send always to same cluster host with application .
</para>
<para>
Note that additionally to network optimization,
you may not need "https" in this case as application and keycloak are communicating directly within same cluster host.
</para>
</section>
<section id="admin-url-configuration">
<title>Admin URL configuration</title>
<para>
Admin URL for particular application can be configured in Keycloak admin console. It's used by Keycloak server to
send backend requests to application for various tasks, like logout users or push revocation policies.
</para>
<para>
For example logout of user from Keycloak works like this:
<orderedlist>
<listitem>
<para>
User sends logout request from one of applications where he is logged.
</para>
</listitem>
<listitem>
<para>
Then application will send logout request to Keycloak
</para>
</listitem>
<listitem>
<para>
Keycloak server logout user in itself, and then it re-sends logout request by backend channel to all
applications where user is logged. Keycloak is using admin URL for this. So logout is propagated to all apps.
</para>
</listitem>
</orderedlist>
</para>
<para>
You may again use relative values for admin URL, but in cluster it may not be the best similarly like in <link linkend='relative-uri-optimization'>previous section</link> .
</para>
<para>
Some examples of possible values of admin URL are:
<variablelist>
<varlistentry>
<term>http://${jboss.host.name}:8080/myapp</term>
<listitem>
<para>
This is best choice if "myapp" is deployed on same cluster hosts like Keycloak and is distributable.
In this case Keycloak server sends logout request to itself, hence no communication with loadbalancer
or other cluster nodes and no additional network traffic.
</para>
<para>
Note that since the application is distributable,
the backend request sent by Keycloak could be served on any application cluster node as invalidation
of HTTP Session on <emphasis>node1</emphasis> will propagate the invalidation to other cluster nodes due to replicated HTTP sessions.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>http://${application.session.host}:8080/myapp</term>
<listitem>
<para>
Keycloak will track hosts where is particular HTTP Session served and it will send session
invalidation message to proper cluster node.
</para>
<para>
For example application is deployed on <emphasis>http://node1:8080/myapp</emphasis> and <emphasis>http://node2:8080/myapp</emphasis> .
Now HTTP Session <emphasis>session1</emphasis> is sticky-session served on cluster node <emphasis>node2</emphasis> .
When keycloak invalidates this session, it will send request directly to <emphasis>http://node2:8080/myapp</emphasis> .
</para>
<para>
This is ideal configuration for distributable applications deployed on different host than keycloak
or for non-distributable applications deployed either on same or different nodes than keycloak.
Good thing is that it doesn't send requests through load-balancer and hence helps to reduce network traffic.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
<section id="registration-app-nodes">
<title>Registration of application nodes to Keycloak</title>
<para>
Previous section describes how can Keycloak send logout request to proper application node. However in some cases admin
may want to propagate admin tasks to all registered cluster nodes, not just one of them. For example push new notBefore
for realm or application, or logout all users from all applications on all cluster nodes.
</para>
<para>
In this case Keycloak should
be aware of all application cluster nodes, so it could send event to all of them. To achieve this, we support auto-discovery mechanism:
<orderedlist>
<listitem>
<para>
Once new application node joins cluster, it sends registration request to Keycloak server
</para>
</listitem>
<listitem>
<para>
The request may be re-sent to Keycloak in configured periodic intervals
</para>
</listitem>
<listitem>
<para>
If Keycloak won't receive re-registration request within specified timeout (should be greater than period from point 2)
then it automatically unregister particular node
</para>
</listitem>
<listitem>
<para>
Node is also unregistered in Keycloak when it sends unregistration request, which is usually during node
shutdown or application undeployment. This may not work properly for forced shutdown when
undeployment listeners are not invoked, so here you need to rely on automatic unregistration from point 3 .
</para>
</listitem>
</orderedlist>
</para>
<para>
Sending startup registrations and periodic re-registration is disabled by default, as it's main usecase is just
cluster deployment. In <literal>WEB-INF/keycloak.json</literal> of your application, you can specify:
<programlisting>
<![CDATA[
"register-node-at-startup": true,
"register-node-period": 600,
]]>
</programlisting>
which means that registration is sent at startup (accurately when 1st request is served by the application node)
and then it's resent each 10 minutes.
</para>
<para>
In Keycloak admin console you can specify the maximum node re-registration timeout (makes sense to have it
bigger than <emphasis>register-node-period</emphasis> from adapter configuration for particular application). Also you
can manually add and remove cluster nodes in admin console, which is useful if you don't want to rely on adapter's
automatic registration or if you want to remove stale application nodes, which weren't unregistered
(for example due to forced shutdown).
</para>
</section>
<section id="refresh-token-each-req">
<title>Refresh token in each request</title>
<para>
By default, application adapter tries to refresh access token when it's expired (period can be specified as <link linkend='token-timeouts'>Access Token Lifespan</link>) .
However if you don't want to rely on the fact, that Keycloak is able to successfully propagate admin events like logout
to your application nodes, then you have possibility to configure adapter to refresh access token in each HTTP request.
</para>
<para>
In <literal>WEB-INF/keycloak.json</literal> you can configure:
<programlisting>
<![CDATA[
"always-refresh-token": true
]]>
</programlisting>
</para>
<para>
Note that this has big performance impact. It's useful just if performance is not priority, but security is critical
and you can't rely on logout and push notBefore propagation from Keycloak to applications.
</para>
</section>
</chapter>

View file

@ -33,7 +33,7 @@
in that it allow you to force a relogin after a set timeframe.
</para>
</section>
<section>
<section id="token-timeouts">
<title>Token Timeouts</title>
<para>
The <literal>Access Token Lifespan</literal> is how long an access token is valid for. An access token contains everything

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,41 @@
package org.keycloak.adapters;
/**
* 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

@ -19,14 +19,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 +44,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 +64,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 +114,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
this.token = token;
this.refreshToken = response.getRefreshToken();
this.tokenString = tokenString;
tokenStore.refreshCallback(this);
return true;
}
}

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;
/**
* Handle storage of token info in cookie. 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 {
// 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;
/**
* Handle storage of token info in HTTP Session. 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));
}
}
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.debug("*** 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,42 +1,41 @@
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.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 java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:ungarida@gmail.com">Davide Ungari</a>
* @version $Revision: 1 $
*/
public class CatalinaRequestAuthenticator extends RequestAuthenticator {
private static final Logger log = Logger.getLogger(CatalinaRequestAuthenticator.class);
private static final Logger log = Logger.getLogger(""+CatalinaRequestAuthenticator.class);
protected KeycloakAuthenticatorValve valve;
protected 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 {
// 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,26 +57,38 @@ 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);
if (log.isDebugEnabled()) {
log.debug("Completing bearer authentication. Bearer roles: " + roles);
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
if (log.isLoggable(Level.FINE)) {
log.fine("Completing bearer authentication. Bearer roles: " + roles);
}
Principal generalPrincipal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), principal, roles, securityContext);
request.setUserPrincipal(generalPrincipal);
@ -82,45 +96,12 @@ 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)) {
log.debug("restoreRequest");
log.finer("restoreRequest");
} else {
log.debug("Restore of original request failed");
log.finer("Restore of original request failed");
throw new RuntimeException("Restore of original request failed");
}
}

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

@ -12,10 +12,10 @@ import org.apache.catalina.connector.Request;
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.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 +24,8 @@ import org.keycloak.adapters.KeycloakDeploymentBuilder;
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;
@ -33,6 +35,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
/**
* Web deployment whose security is managed by a remote OAuth Skeleton Key authentication server
@ -40,11 +43,14 @@ import java.io.InputStream;
* Redirects browser to remote authentication server if not logged in. Also allows OAuth Bearer Token requests
* that contain a Skeleton Key bearer tokens.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author <a href="mailto:ungarida@gmail.com">Davide Ungari</a>
* @version $Revision: 1 $
*/
public class KeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
private static final Logger log = Logger.getLogger(KeycloakAuthenticatorValve.class);
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;
protected NodesRegistrationManagement nodesRegistrationManagement;
@ -55,7 +61,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
try {
startDeployment();
} catch (LifecycleException e) {
log.error("Error starting deployment. " + e.getMessage());
log.severe("Error starting deployment. " + e.getMessage());
}
} else if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
initInternal();
@ -64,6 +70,19 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
}
}
@Override
public void logout(Request request) throws ServletException {
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
if (ksc != null) {
request.removeAttribute(KeycloakSecurityContext.class.getName());
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
tokenStore.logout();
}
super.logout(request);
}
public void startDeployment() throws LifecycleException {
super.start();
@ -72,21 +91,25 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
cache = false;
}
@Override
public void logout(Request request) throws ServletException {
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));
public void initInternal() {
InputStream configInputStream = getConfigInputStream(context);
KeycloakDeployment kd = null;
if (configInputStream == null) {
log.warning("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
kd = new KeycloakDeployment();
} else {
kd = KeycloakDeploymentBuilder.build(configInputStream);
}
deploymentContext = new AdapterDeploymentContext(kd);
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getObjectName());
setNext(actions);
nodesRegistrationManagement = new NodesRegistrationManagement();
}
}
super.logout(request);
protected void beforeStop() {
nodesRegistrationManagement.stop();
}
private static InputStream getJSONFromServletContext(ServletContext servletContext) {
@ -104,12 +127,13 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
if (is == null) {
String path = context.getServletContext().getInitParameter("keycloak.config.file");
if (path == null) {
log.debug("**** using /WEB-INF/keycloak.json");
log.info("**** using /WEB-INF/keycloak.json");
is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json");
} else {
try {
is = new FileInputStream(path);
} catch (FileNotFoundException e) {
log.severe("NOT FOUND /WEB-INF/keycloak.json");
throw new RuntimeException(e);
}
}
@ -117,34 +141,9 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
return is;
}
public void initInternal() {
InputStream configInputStream = getConfigInputStream(context);
KeycloakDeployment kd = null;
if (configInputStream == null) {
log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
kd = new KeycloakDeployment();
} else {
kd = KeycloakDeploymentBuilder.build(configInputStream);
}
deploymentContext = new AdapterDeploymentContext(kd);
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer());
setNext(actions);
nodesRegistrationManagement = new NodesRegistrationManagement();
}
protected void beforeStop() {
nodesRegistrationManagement.stop();
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
try {
if (log.isTraceEnabled()) {
log.trace("invoke");
}
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, response);
Manager sessionManager = request.getContext().getManager();
CatalinaUserSessionManagementWrapper sessionManagementWrapper = new CatalinaUserSessionManagementWrapper(userSessionManagement, sessionManager);
@ -160,19 +159,16 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
@Override
public boolean authenticate(Request request, HttpServletResponse response, LoginConfig config) throws IOException {
if (log.isTraceEnabled()) {
log.trace("*** authenticate");
}
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, response);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (deployment == null || !deployment.isConfigured()) {
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()) {
@ -193,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.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 {
@ -228,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());
}
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);
}
setRoles(principal.getKeycloakSecurityContext());
}
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 HTTP 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));
}
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,