conflict
This commit is contained in:
commit
cb110d095f
39 changed files with 1690 additions and 392 deletions
|
@ -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";
|
||||
}
|
||||
|
|
9
core/src/main/java/org/keycloak/enums/TokenStore.java
Normal file
9
core/src/main/java/org/keycloak/enums/TokenStore.java
Normal file
|
@ -0,0 +1,9 @@
|
|||
package org.keycloak.enums;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public enum TokenStore {
|
||||
SESSION,
|
||||
COOKIE
|
||||
}
|
|
@ -18,7 +18,7 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder;
|
|||
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
254
docbook/reference/en/en-US/modules/application-clustering.xml
Normal file
254
docbook/reference/en/en-US/modules/application-clustering.xml
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package org.keycloak.adapters;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.KeycloakPrincipal;
|
||||
import org.keycloak.RSATokenVerifier;
|
||||
import org.keycloak.VerificationException;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.util.KeycloakUriBuilder;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class CookieTokenStore {
|
||||
|
||||
private static final Logger log = Logger.getLogger(CookieTokenStore.class);
|
||||
private static final String DELIM = "@";
|
||||
|
||||
public static void setTokenCookie(KeycloakDeployment deployment, HttpFacade facade, RefreshableKeycloakSecurityContext session) {
|
||||
log.infof("Set new %s cookie now", AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE);
|
||||
String accessToken = session.getTokenString();
|
||||
String idToken = session.getIdTokenString();
|
||||
String refreshToken = session.getRefreshToken();
|
||||
String cookie = new StringBuilder(accessToken).append(DELIM)
|
||||
.append(idToken).append(DELIM)
|
||||
.append(refreshToken).toString();
|
||||
|
||||
String cookiePath = getContextPath(facade);
|
||||
facade.getResponse().setCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookie, cookiePath, null, -1, deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr()), true);
|
||||
}
|
||||
|
||||
public static KeycloakPrincipal<RefreshableKeycloakSecurityContext> getPrincipalFromCookie(KeycloakDeployment deployment, HttpFacade facade, AdapterTokenStore tokenStore) {
|
||||
HttpFacade.Cookie cookie = facade.getRequest().getCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE);
|
||||
if (cookie == null) {
|
||||
log.debug("Not found adapter state cookie in current request");
|
||||
return null;
|
||||
}
|
||||
|
||||
String cookieVal = cookie.getValue();
|
||||
|
||||
String[] tokens = cookieVal.split(DELIM);
|
||||
if (tokens.length != 3) {
|
||||
log.warnf("Invalid format of %s cookie. Count of tokens: %s, expected 3", AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, tokens.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
String accessTokenString = tokens[0];
|
||||
String idTokenString = tokens[1];
|
||||
String refreshTokenString = tokens[2];
|
||||
|
||||
try {
|
||||
// Skip check if token is active now. It's supposed to be done later by the caller
|
||||
AccessToken accessToken = RSATokenVerifier.verifyToken(accessTokenString, deployment.getRealmKey(), deployment.getRealm(), false);
|
||||
IDToken idToken;
|
||||
if (idTokenString != null && idTokenString.length() > 0) {
|
||||
JWSInput input = new JWSInput(idTokenString);
|
||||
try {
|
||||
idToken = input.readJsonContent(IDToken.class);
|
||||
} catch (IOException e) {
|
||||
throw new VerificationException(e);
|
||||
}
|
||||
} else {
|
||||
idToken = null;
|
||||
}
|
||||
|
||||
log.debug("Token Verification succeeded!");
|
||||
RefreshableKeycloakSecurityContext secContext = new RefreshableKeycloakSecurityContext(deployment, tokenStore, accessTokenString, accessToken, idTokenString, idToken, refreshTokenString);
|
||||
return new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(accessToken.getSubject(), secContext);
|
||||
} catch (VerificationException ve) {
|
||||
log.warn("Failed verify token", ve);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeCookie(HttpFacade facade) {
|
||||
String cookiePath = getContextPath(facade);
|
||||
facade.getResponse().resetCookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, cookiePath);
|
||||
}
|
||||
|
||||
private static String getContextPath(HttpFacade facade) {
|
||||
String uri = facade.getRequest().getURI();
|
||||
String path = KeycloakUriBuilder.fromUri(uri).getPath();
|
||||
int index = path.indexOf("/", 1);
|
||||
return index == -1 ? path : path.substring(0, index);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.ServiceUrlConstants;
|
||||
import org.keycloak.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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package org.keycloak.adapters.tomcat7;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.apache.catalina.connector.Request;
|
||||
import org.apache.catalina.realm.GenericPrincipal;
|
||||
import org.keycloak.KeycloakPrincipal;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.adapters.AdapterTokenStore;
|
||||
import org.keycloak.adapters.AdapterUtils;
|
||||
import org.keycloak.adapters.CookieTokenStore;
|
||||
import org.keycloak.adapters.HttpFacade;
|
||||
import org.keycloak.adapters.KeycloakAccount;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||
import org.keycloak.adapters.RequestAuthenticator;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class CatalinaCookieTokenStore implements AdapterTokenStore {
|
||||
|
||||
private static final Logger log = Logger.getLogger(""+CatalinaCookieTokenStore.class);
|
||||
|
||||
private Request request;
|
||||
private HttpFacade facade;
|
||||
private KeycloakDeployment deployment;
|
||||
|
||||
private KeycloakPrincipal<RefreshableKeycloakSecurityContext> authenticatedPrincipal;
|
||||
|
||||
public CatalinaCookieTokenStore(Request request, HttpFacade facade, KeycloakDeployment deployment) {
|
||||
this.request = request;
|
||||
this.facade = facade;
|
||||
this.deployment = deployment;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void checkCurrentToken() {
|
||||
this.authenticatedPrincipal = checkPrincipalFromCookie();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCached(RequestAuthenticator authenticator) {
|
||||
// Assuming authenticatedPrincipal set by previous call of checkCurrentToken() during this request
|
||||
if (authenticatedPrincipal != null) {
|
||||
log.fine("remote logged in already. Establish state from cookie");
|
||||
RefreshableKeycloakSecurityContext securityContext = authenticatedPrincipal.getKeycloakSecurityContext();
|
||||
securityContext.setCurrentRequestInfo(deployment, this);
|
||||
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
|
||||
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), authenticatedPrincipal, roles, securityContext);
|
||||
|
||||
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
|
||||
request.setUserPrincipal(principal);
|
||||
request.setAuthType("KEYCLOAK");
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAccountInfo(KeycloakAccount account) {
|
||||
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext();
|
||||
CookieTokenStore.setTokenCookie(deployment, facade, securityContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout() {
|
||||
CookieTokenStore.removeCookie(facade);
|
||||
|
||||
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
|
||||
if (ksc instanceof RefreshableKeycloakSecurityContext) {
|
||||
((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshCallback(RefreshableKeycloakSecurityContext secContext) {
|
||||
CookieTokenStore.setTokenCookie(deployment, facade, secContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if we already have authenticated and active principal in cookie. Perform refresh if it's not active
|
||||
*
|
||||
* @return valid principal
|
||||
*/
|
||||
protected KeycloakPrincipal<RefreshableKeycloakSecurityContext> checkPrincipalFromCookie() {
|
||||
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this);
|
||||
if (principal == null) {
|
||||
log.fine("Account was not in cookie or was invalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext();
|
||||
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return principal;
|
||||
boolean success = session.refreshExpiredToken(false);
|
||||
if (success && session.isActive()) return principal;
|
||||
|
||||
log.fine("Cleanup and expire cookie for user " + principal.getName() + " after failed refresh");
|
||||
request.setUserPrincipal(null);
|
||||
request.setAuthType(null);
|
||||
CookieTokenStore.removeCookie(facade);
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package org.keycloak.adapters.undertow;
|
||||
|
||||
import io.undertow.security.api.SecurityContext;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.KeycloakPrincipal;
|
||||
import org.keycloak.adapters.AdapterTokenStore;
|
||||
import org.keycloak.adapters.CookieTokenStore;
|
||||
import org.keycloak.adapters.HttpFacade;
|
||||
import org.keycloak.adapters.KeycloakAccount;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||
import org.keycloak.adapters.RequestAuthenticator;
|
||||
|
||||
/**
|
||||
* Per-request object. Storage of tokens in cookie
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class UndertowCookieTokenStore implements AdapterTokenStore {
|
||||
|
||||
protected static Logger log = Logger.getLogger(UndertowCookieTokenStore.class);
|
||||
|
||||
private final HttpFacade facade;
|
||||
private final KeycloakDeployment deployment;
|
||||
private final SecurityContext securityContext;
|
||||
|
||||
public UndertowCookieTokenStore(HttpFacade facade, KeycloakDeployment deployment,
|
||||
SecurityContext securityContext) {
|
||||
this.facade = facade;
|
||||
this.deployment = deployment;
|
||||
this.securityContext = securityContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkCurrentToken() {
|
||||
// no-op on undertow
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCached(RequestAuthenticator authenticator) {
|
||||
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this);
|
||||
if (principal == null) {
|
||||
log.debug("Account was not in cookie or was invalid, returning null");
|
||||
return false;
|
||||
}
|
||||
KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal);
|
||||
|
||||
if (account.checkActive()) {
|
||||
log.debug("Cached account found");
|
||||
securityContext.authenticationComplete(account, "KEYCLOAK", false);
|
||||
((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
|
||||
return true;
|
||||
} else {
|
||||
log.debug("Account was not active, removing cookie and returning false");
|
||||
CookieTokenStore.removeCookie(facade);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAccountInfo(KeycloakAccount account) {
|
||||
RefreshableKeycloakSecurityContext secContext = (RefreshableKeycloakSecurityContext)account.getKeycloakSecurityContext();
|
||||
CookieTokenStore.setTokenCookie(deployment, facade, secContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout() {
|
||||
KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = CookieTokenStore.getPrincipalFromCookie(deployment, facade, this);
|
||||
if (principal == null) return;
|
||||
|
||||
CookieTokenStore.removeCookie(facade);
|
||||
principal.getKeycloakSecurityContext().logout(deployment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
|
||||
CookieTokenStore.setTokenCookie(deployment, facade, securityContext);
|
||||
}
|
||||
}
|
|
@ -24,10 +24,17 @@ import io.undertow.server.HttpServerExchange;
|
|||
import io.undertow.server.session.Session;
|
||||
import io.undertow.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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package org.keycloak.adapters.undertow;
|
||||
|
||||
import io.undertow.security.api.SecurityContext;
|
||||
import io.undertow.server.HttpServerExchange;
|
||||
import io.undertow.server.session.Session;
|
||||
import io.undertow.util.Sessions;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.adapters.AdapterTokenStore;
|
||||
import org.keycloak.adapters.KeycloakAccount;
|
||||
import org.keycloak.adapters.KeycloakDeployment;
|
||||
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
|
||||
import org.keycloak.adapters.RequestAuthenticator;
|
||||
|
||||
/**
|
||||
* Per-request object. Storage of tokens in undertow session.
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class UndertowSessionTokenStore implements AdapterTokenStore {
|
||||
|
||||
protected static Logger log = Logger.getLogger(UndertowSessionTokenStore.class);
|
||||
|
||||
private final HttpServerExchange exchange;
|
||||
private final KeycloakDeployment deployment;
|
||||
private final UndertowUserSessionManagement sessionManagement;
|
||||
private final SecurityContext securityContext;
|
||||
|
||||
public UndertowSessionTokenStore(HttpServerExchange exchange, KeycloakDeployment deployment, UndertowUserSessionManagement sessionManagement,
|
||||
SecurityContext securityContext) {
|
||||
this.exchange = exchange;
|
||||
this.deployment = deployment;
|
||||
this.sessionManagement = sessionManagement;
|
||||
this.securityContext = securityContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkCurrentToken() {
|
||||
// no-op on undertow
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCached(RequestAuthenticator authenticator) {
|
||||
Session session = Sessions.getSession(exchange);
|
||||
if (session == null) {
|
||||
log.debug("session was null, returning null");
|
||||
return false;
|
||||
}
|
||||
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
||||
if (account == null) {
|
||||
log.debug("Account was not in session, returning null");
|
||||
return false;
|
||||
}
|
||||
account.setCurrentRequestInfo(deployment, this);
|
||||
if (account.checkActive()) {
|
||||
log.debug("Cached account found");
|
||||
securityContext.authenticationComplete(account, "KEYCLOAK", false);
|
||||
((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
|
||||
return true;
|
||||
} else {
|
||||
log.debug("Account was not active, returning false");
|
||||
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||
session.invalidate(exchange);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAccountInfo(KeycloakAccount account) {
|
||||
Session session = Sessions.getOrCreateSession(exchange);
|
||||
session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
|
||||
sessionManagement.login(session.getSessionManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logout() {
|
||||
Session session = Sessions.getSession(exchange);
|
||||
if (session == null) return;
|
||||
KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
|
||||
if (account == null) return;
|
||||
session.removeAttribute(KeycloakUndertowAccount.class.getName());
|
||||
if (account.getKeycloakSecurityContext() != null) {
|
||||
account.getKeycloakSecurityContext().logout(deployment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) {
|
||||
// no-op
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import io.undertow.security.api.SecurityContext;
|
|||
import io.undertow.server.HttpServerExchange;
|
||||
import io.undertow.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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
package org.keycloak.testsuite.adapter;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.adapters.AdapterConstants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.OpenIDConnectService;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.testsuite.OAuthClient;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
|
||||
import org.keycloak.testsuite.rule.WebResource;
|
||||
import org.keycloak.testsuite.rule.WebRule;
|
||||
import org.keycloak.testutils.KeycloakServer;
|
||||
import org.openqa.selenium.Cookie;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
/**
|
||||
* KEYCLOAK-702
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class CookieTokenStoreAdapterTest {
|
||||
|
||||
public static final String LOGIN_URL = OpenIDConnectService.loginPageUrl(UriBuilder.fromUri("http://localhost:8081/auth")).build("demo").toString();
|
||||
|
||||
@ClassRule
|
||||
public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() {
|
||||
|
||||
@Override
|
||||
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
|
||||
RealmRepresentation representation = KeycloakServer.loadJson(getClass().getResourceAsStream("/adapter-test/demorealm.json"), RealmRepresentation.class);
|
||||
manager.importRealm(representation);
|
||||
|
||||
URL url = getClass().getResource("/adapter-test/cust-app-keycloak.json");
|
||||
deployApplication("customer-portal", "/customer-portal", CustomerServlet.class, url.getPath(), "user");
|
||||
url = getClass().getResource("/adapter-test/cust-app-cookie-keycloak.json");
|
||||
deployApplication("customer-cookie-portal", "/customer-cookie-portal", CustomerServlet.class, url.getPath(), "user");
|
||||
url = getClass().getResource("/adapter-test/customer-db-keycloak.json");
|
||||
deployApplication("customer-db", "/customer-db", CustomerDatabaseServlet.class, url.getPath(), "user");
|
||||
}
|
||||
};
|
||||
|
||||
@Rule
|
||||
public WebRule webRule = new WebRule(this);
|
||||
|
||||
@WebResource
|
||||
protected WebDriver driver;
|
||||
|
||||
@WebResource
|
||||
protected OAuthClient oauth;
|
||||
|
||||
@WebResource
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Test
|
||||
public void testTokenInCookieSSO() throws Throwable {
|
||||
// Login
|
||||
String tokenCookie = loginToCustomerCookiePortal();
|
||||
|
||||
// SSO to second app
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
assertLogged();
|
||||
|
||||
// return to customer-cookie-portal and assert still same cookie (accessToken didn't expire)
|
||||
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||
assertLogged();
|
||||
String tokenCookie2 = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue();
|
||||
Assert.assertEquals(tokenCookie, tokenCookie2);
|
||||
|
||||
// Logout with httpServletRequest
|
||||
logoutFromCustomerCookiePortal();
|
||||
|
||||
// Also should be logged-out from the second app
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTokenInCookieRefresh() throws Throwable {
|
||||
// Set token timeout 1 sec
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
RealmModel realm = session.realms().getRealmByName("demo");
|
||||
int originalTokenTimeout = realm.getAccessTokenLifespan();
|
||||
realm.setAccessTokenLifespan(1);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
|
||||
// login to customer-cookie-portal
|
||||
String tokenCookie1 = loginToCustomerCookiePortal();
|
||||
|
||||
// wait 2 secs
|
||||
Thread.sleep(2000);
|
||||
|
||||
// assert cookie was refreshed
|
||||
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-cookie-portal");
|
||||
assertLogged();
|
||||
String tokenCookie2 = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue();
|
||||
Assert.assertNotEquals(tokenCookie1, tokenCookie2);
|
||||
|
||||
// login to 2nd app and logout from it
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
|
||||
assertLogged();
|
||||
|
||||
driver.navigate().to("http://localhost:8081/customer-portal/logout");
|
||||
driver.navigate().to("http://localhost:8081/customer-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
// wait 2 secs until accessToken expires for customer-cookie-portal too.
|
||||
Thread.sleep(2000);
|
||||
|
||||
// assert not logged in customer-cookie-portal
|
||||
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
|
||||
// Change timeout back
|
||||
session = keycloakRule.startSession();
|
||||
realm = session.realms().getRealmByName("demo");
|
||||
realm.setAccessTokenLifespan(originalTokenTimeout);
|
||||
session.getTransaction().commit();
|
||||
session.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidTokenCookie() throws Throwable {
|
||||
// Login
|
||||
String tokenCookie = loginToCustomerCookiePortal();
|
||||
String changedTokenCookie = tokenCookie.replace("a", "b");
|
||||
|
||||
// change cookie to invalid value
|
||||
driver.manage().addCookie(new Cookie(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE, changedTokenCookie, "/customer-cookie-portal"));
|
||||
|
||||
// visit page and assert re-logged and cookie was refreshed
|
||||
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-cookie-portal");
|
||||
String currentCookie = driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue();
|
||||
Assert.assertNotEquals(currentCookie, tokenCookie);
|
||||
Assert.assertNotEquals(currentCookie, changedTokenCookie);
|
||||
|
||||
// logout
|
||||
logoutFromCustomerCookiePortal();
|
||||
}
|
||||
|
||||
// login to customer-cookie-portal and return the KEYCLOAK_ADAPTER_STATE cookie established on adapter
|
||||
private String loginToCustomerCookiePortal() {
|
||||
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
loginPage.login("bburke@redhat.com", "password");
|
||||
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-cookie-portal");
|
||||
assertLogged();
|
||||
|
||||
// Assert no JSESSIONID cookie
|
||||
Assert.assertNull(driver.manage().getCookieNamed("JSESSIONID"));
|
||||
|
||||
return driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE).getValue();
|
||||
}
|
||||
|
||||
private void logoutFromCustomerCookiePortal() {
|
||||
driver.navigate().to("http://localhost:8081/customer-cookie-portal/logout");
|
||||
Assert.assertTrue(driver.getPageSource().contains("ok"));
|
||||
Assert.assertNull(driver.manage().getCookieNamed(AdapterConstants.KEYCLOAK_ADAPTER_STATE_COOKIE));
|
||||
driver.navigate().to("http://localhost:8081/customer-cookie-portal");
|
||||
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
|
||||
}
|
||||
|
||||
private void assertLogged() {
|
||||
String pageSource = driver.getPageSource();
|
||||
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
|
||||
}
|
||||
}
|
|
@ -29,8 +29,10 @@ public class CustomerServlet extends HttpServlet {
|
|||
if (req.getRequestURI().toString().endsWith("logout")) {
|
||||
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());
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"realm": "demo",
|
||||
"resource": "customer-cookie-portal",
|
||||
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"auth-server-url": "http://localhost:8081/auth",
|
||||
"ssl-required" : "external",
|
||||
"expose-token": true,
|
||||
"token-store": "cookie",
|
||||
"credentials": {
|
||||
"secret": "password"
|
||||
}
|
||||
}
|
|
@ -68,6 +68,15 @@
|
|||
],
|
||||
"secret": "password"
|
||||
},
|
||||
{
|
||||
"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,
|
||||
|
|
Loading…
Reference in a new issue