Merge pull request #283 from patriot1burke/master

adapter security context propagation
This commit is contained in:
Bill Burke 2014-03-11 18:08:23 -04:00
commit 53f671e8af
32 changed files with 907 additions and 33 deletions

View file

@ -38,6 +38,18 @@
</xsl:copy>
</xsl:template>
<xsl:template match="node()[name(.)='security-domains']">
<xsl:copy>
<xsl:apply-templates select="node()[name(.)='security-domain']"/>
<security-domain name="keycloak">
<authentication>
<login-module code="org.keycloak.adapters.jboss.KeycloakLoginModule" flag="required"/>
</authentication>
</security-domain>
</xsl:copy>
</xsl:template>
<!-- for some reason, Wildfly 8 final decided to turn off management-native which means jboss-as-maven-plugin no
longer works -->
<xsl:template match="node()[name(.)='management-interfaces']">

View file

@ -12,6 +12,7 @@
<excludes>
<exclude>org/keycloak/keycloak-undertow-adapter/**</exclude>
<exclude>org/keycloak/keycloak-wildfly-subsystem/**</exclude>
<exclude>org/keycloak/keycloak-wildfly-adapter/**</exclude>
</excludes>
<outputDirectory>modules</outputDirectory>
</fileSet>

View file

@ -59,6 +59,10 @@
<maven-resource group="org.keycloak" artifact="keycloak-adapter-core"/>
</module-def>
<module-def name="org.keycloak.keycloak-jboss-adapter-core">
<maven-resource group="org.keycloak" artifact="keycloak-jboss-adapter-core"/>
</module-def>
<module-def name="org.keycloak.keycloak-as7-adapter">
<maven-resource group="org.keycloak" artifact="keycloak-as7-adapter"/>
</module-def>
@ -67,6 +71,10 @@
<maven-resource group="org.keycloak" artifact="keycloak-undertow-adapter"/>
</module-def>
<module-def name="org.keycloak.keycloak-wildfly-adapter">
<maven-resource group="org.keycloak" artifact="keycloak-wildfly-adapter"/>
</module-def>
<module-def name="org.keycloak.keycloak-wildfly-subsystem">
<maven-resource group="org.keycloak" artifact="keycloak-wildfly-subsystem"/>
</module-def>

View file

@ -49,6 +49,11 @@
<artifactId>keycloak-adapter-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jboss-adapter-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-as7-adapter</artifactId>
@ -59,6 +64,11 @@
<artifactId>keycloak-undertow-adapter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-adapter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-subsystem</artifactId>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ JBoss, Home of Professional Open Source.
~ Copyright 2010, Red Hat, Inc., and individual contributors
~ as indicated by the @author tags. See the copyright.txt file in the
~ distribution for a full listing of individual contributors.
~
~ This is free software; you can redistribute it and/or modify it
~ under the terms of the GNU Lesser General Public License as
~ published by the Free Software Foundation; either version 2.1 of
~ the License, or (at your option) any later version.
~
~ This software is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
~ Lesser General Public License for more details.
~
~ You should have received a copy of the GNU Lesser General Public
~ License along with this software; if not, write to the Free
~ Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
~ 02110-1301 USA, or see the FSF site: http://www.fsf.org.
-->
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-jboss-adapter-core">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="javax.api"/>
<module name="org.jboss.logging"/>
<module name="org.picketbox"/>
<module name="org.keycloak.keycloak-adapter-core"/>
<module name="org.keycloak.keycloak-core"/>
</dependencies>
</module>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ JBoss, Home of Professional Open Source.
~ Copyright 2010, Red Hat, Inc., and individual contributors
~ as indicated by the @author tags. See the copyright.txt file in the
~ distribution for a full listing of individual contributors.
~
~ This is free software; you can redistribute it and/or modify it
~ under the terms of the GNU Lesser General Public License as
~ published by the Free Software Foundation; either version 2.1 of
~ the License, or (at your option) any later version.
~
~ This software is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
~ Lesser General Public License for more details.
~
~ You should have received a copy of the GNU Lesser General Public
~ License along with this software; if not, write to the Free
~ Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
~ 02110-1301 USA, or see the FSF site: http://www.fsf.org.
-->
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-wildfly-adapter">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="javax.api"/>
<module name="org.bouncycastle"/>
<module name="org.codehaus.jackson.jackson-core-asl"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.codehaus.jackson.jackson-xc"/>
<module name="org.apache.httpcomponents" />
<module name="javax.servlet.api"/>
<module name="org.jboss.logging"/>
<module name="io.undertow.core"/>
<module name="io.undertow.servlet"/>
<module name="org.picketbox"/>
<module name="org.keycloak.keycloak-undertow-adapter"/>
<module name="org.keycloak.keycloak-adapter-core"/>
<module name="org.keycloak.keycloak-core"/>
</dependencies>
</module>

View file

@ -73,6 +73,66 @@ $ unzip keycloak-as7-adapter-dist.zip
]]>
</programlisting>
</para>
<para>
Finally, for both AS7, EAP 6.x, and Wildfly installations you must specify a shared keycloak security domain.
This security domain should be used with EJBs and other components when you need the security context created
in the secured web tier to be propagated to the EJBs (other EE component) you are invoking. Otherwise
this configuration is optional.
</para>
<programlisting><![CDATA[
<server xmlns="urn:jboss:domain:1.4">
<subsystem xmlns="urn:jboss:domain:security:1.2">
<security-domains>
...
<security-domain name="keycloak">
<authentication>
<login-module code="org.keycloak.adapters.jboss.KeycloakLoginModule"
flag="required"/>
</authentication>
</security-domain>
</security-domains>
]]>
</programlisting>
<para>
For example, if you have a JAX-RS service that is an EJB within your WEB-INF/classes directory, you'll want
to annotate it with the @SecurityDomain annotation as follows:
</para>
<programlisting><![CDATA[
import org.jboss.ejb3.annotation.SecurityDomain;
import org.jboss.resteasy.annotations.cache.NoCache;
import javax.annotation.security.RolesAllowed;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import java.util.ArrayList;
import java.util.List;
@Path("customers")
@Stateless
@SecurityDomain("keycloak")
public class CustomerService {
@EJB
CustomerDB db;
@GET
@Produces("application/json")
@NoCache
@RolesAllowed("db_user")
public List<String> getCustomers() {
return db.getCustomers();
}
}
]]>
</programlisting>
<para>
We hope to improve our integration in the future so that you don't have to specify the @SecurityDomain
annotation when you want to propagate a keycloak security context to the EJB tier.
</para>
</section>
<section>
<title>Per WAR Configuration</title>

View file

@ -0,0 +1,19 @@
package org.keycloak.adapters;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.representations.AccessToken;
import java.security.Principal;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface KeycloakAccount {
Principal getPrincipal();
Set<String> getRoles();
KeycloakSecurityContext getKeycloakSecurityContext();
}

View file

@ -48,6 +48,7 @@ public class KeycloakDeploymentBuilder {
deployment.setSslRequired(!adapterConfig.isSslNotRequired());
deployment.setResourceCredentials(adapterConfig.getCredentials());
deployment.setPublicClient(adapterConfig.isPublicClient());
deployment.setUseResourceRoleMappings(adapterConfig.isUseResourceRoleMappings());
if (adapterConfig.isBearerOnly()) {
deployment.setBearerOnly(true);

View file

@ -46,6 +46,10 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
return this.token.isActive() && this.token.getIssuedAt() > deployment.getNotBefore();
}
public KeycloakDeployment getDeployment() {
return deployment;
}
public void setDeployment(KeycloakDeployment deployment) {
this.deployment = deployment;
}

View file

@ -8,7 +8,7 @@ import org.keycloak.KeycloakPrincipal;
* @version $Revision: 1 $
*/
public abstract class RequestAuthenticator {
protected Logger log = Logger.getLogger(RequestAuthenticator.class);
protected static Logger log = Logger.getLogger(RequestAuthenticator.class);
protected HttpFacade facade;
protected KeycloakDeployment deployment;
@ -33,19 +33,25 @@ public abstract class RequestAuthenticator {
public AuthOutcome authenticate() {
log.info("--> authenticate()");
BearerTokenRequestAuthenticator bearer = createBearerTokenAuthenticator();
log.info("try bearer");
AuthOutcome outcome = bearer.authenticate(facade);
if (outcome == AuthOutcome.FAILED) {
challenge = bearer.getChallenge();
log.info("Bearer FAILED");
return AuthOutcome.FAILED;
} else if (outcome == AuthOutcome.AUTHENTICATED) {
completeAuthentication(bearer);
log.info("Bearer AUTHENTICATED");
return AuthOutcome.AUTHENTICATED;
} else if (deployment.isBearerOnly()) {
challenge = bearer.getChallenge();
log.info("NOT_ATTEMPTED: bearer only");
return AuthOutcome.NOT_ATTEMPTED;
}
log.info("try oauth");
if (isCached()) {
log.info("AUTHENTICATED: was cached");
return AuthOutcome.AUTHENTICATED;
}

View file

@ -36,6 +36,7 @@ public class KeycloakDependencyProcessor implements DeploymentUnitProcessor {
private static final ModuleIdentifier KEYCLOAK_AS7_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-as7-adapter");
private static final ModuleIdentifier KEYCLOAK_CORE_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-adapter-core");
private static final ModuleIdentifier KEYCLOAK_JBOSS_CORE_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-jboss-adapter-core");
private static final ModuleIdentifier KEYCLOAK_CORE = ModuleIdentifier.create("org.keycloak.keycloak-core");
//private static final ModuleIdentifier APACHE_HTTPCOMPONENTS = ModuleIdentifier.create("org.apache.httpcomponents");
@ -51,6 +52,7 @@ public class KeycloakDependencyProcessor implements DeploymentUnitProcessor {
final ModuleLoader moduleLoader = Module.getBootModuleLoader();
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_AS7_ADAPTER, false, false, true, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_JBOSS_CORE_ADAPTER, false, false, false, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_CORE_ADAPTER, false, false, false, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_CORE, false, false, false, false));
//moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, APACHE_HTTPCOMPONENTS, false, false, true, false));

View file

@ -23,6 +23,11 @@
<artifactId>keycloak-adapter-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jboss-adapter-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>

View file

@ -4,6 +4,7 @@ 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.KeycloakDeployment;
@ -22,6 +23,7 @@ import java.util.Set;
* @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;
@ -53,7 +55,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
@Override
protected void completeOAuthAuthentication(KeycloakPrincipal skp, RefreshableKeycloakSecurityContext securityContext) {
Set<String> roles = getRolesFromToken(securityContext);
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles);
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext);
Session session = request.getSessionInternal(true);
session.setPrincipal(principal);
session.setAuthType("OAUTH");
@ -66,7 +68,10 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
@Override
protected void completeBearerAuthentication(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext securityContext) {
Set<String> roles = getRolesFromToken(securityContext);
Principal generalPrincipal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), principal, roles);
for (String role : roles) {
log.info("Bearer role: " + role);
}
Principal generalPrincipal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), principal, roles, securityContext);
request.setUserPrincipal(generalPrincipal);
request.setAuthType("KEYCLOAK");
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);

View file

@ -9,6 +9,8 @@ import org.jboss.security.SecurityContext;
import org.jboss.security.SecurityContextAssociation;
import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.KeycloakAccount;
import javax.security.auth.Subject;
import java.security.Principal;
@ -25,9 +27,24 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public class CatalinaSecurityContextHelper {
public GenericPrincipal createPrincipal(Realm realm, Principal identity, Collection<String> roleSet) {
public GenericPrincipal createPrincipal(Realm realm, final Principal identity, final Set<String> roleSet, final KeycloakSecurityContext securityContext) {
KeycloakAccount account = new KeycloakAccount() {
@Override
public Principal getPrincipal() {
return identity;
}
@Override
public Set<String> getRoles() {
return roleSet;
}
@Override
public KeycloakSecurityContext getKeycloakSecurityContext() {
return securityContext;
}
};
Subject subject = new Subject();
String credentials = "";
Set<Principal> principals = subject.getPrincipals();
principals.add(identity);
Group[] roleSets = getRoleSets(roleSet);
@ -56,11 +73,11 @@ public class CatalinaSecurityContextHelper {
principals.add(callerGroup);
SecurityContext sc = SecurityContextAssociation.getSecurityContext();
Principal userPrincipal = getPrincipal(subject);
sc.getUtil().createSubjectInfo(userPrincipal, credentials, subject);
sc.getUtil().createSubjectInfo(userPrincipal, account, subject);
List<String> rolesAsStringList = new ArrayList<String>();
rolesAsStringList.addAll(roleSet);
return new JBossGenericPrincipal(realm, userPrincipal.getName(), null, rolesAsStringList,
userPrincipal, null, credentials, null, subject);
userPrincipal, null, account, null, subject);
}

View file

@ -0,0 +1,83 @@
<?xml version="1.0"?>
<project>
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-alpha-3-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-jboss-adapter-core</artifactId>
<name>Common JBoss/Wildfly Core Classes</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.1.2.GA</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${keycloak.apache.httpcomponents.version}</version>
</dependency>
<dependency>
<groupId>net.iharder</groupId>
<artifactId>base64</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-xc</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.picketbox</groupId>
<artifactId>picketbox</artifactId>
<version>4.0.20.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,100 @@
package org.keycloak.adapters.jboss;
import org.jboss.logging.Logger;
import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.jboss.security.auth.callback.ObjectCallback;
import org.jboss.security.auth.spi.AbstractServerLoginModule;
import org.keycloak.adapters.KeycloakAccount;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KeycloakLoginModule extends AbstractServerLoginModule {
protected static Logger log = Logger.getLogger(KeycloakLoginModule.class);
protected Set<String> roleSet;
protected Principal identity;
@SuppressWarnings("unchecked")
@Override
public boolean login() throws LoginException {
log.info("KeycloakLoginModule.login()");
if (super.login() == true) {
log.info("super.login()==true");
return true;
}
Object credential = getCredential();
if (credential != null && (credential instanceof KeycloakAccount)) {
log.info("Found Account");
KeycloakAccount account = (KeycloakAccount)credential;
roleSet = account.getRoles();
identity = account.getPrincipal();
sharedState.put("javax.security.auth.login.name", identity);
sharedState.put("javax.security.auth.login.password", credential);
loginOk = true;
return true;
}
// We return false to allow the next module to attempt authentication, maybe a
// username and password has been supplied to a web auth.
return false;
}
@Override
protected Principal getIdentity() {
return identity;
}
/*
@Override
protected Group[] getRoleSets() throws LoginException {
return new Group[0];
}
*/
@Override
protected Group[] getRoleSets() throws LoginException {
//log.info("getRoleSets");
SimpleGroup roles = new SimpleGroup("Roles");
Group[] roleSets = {roles};
for (String role : roleSet) {
//log.info(" adding role: " + role);
roles.addMember(new SimplePrincipal(role));
}
return roleSets;
}
protected Object getCredential() throws LoginException {
NameCallback nc = new NameCallback("Alias: ");
ObjectCallback oc = new ObjectCallback("Credential: ");
Callback[] callbacks = { nc, oc };
try {
callbackHandler.handle(callbacks);
return oc.getCredential();
} catch (IOException ioe) {
LoginException le = new LoginException();
le.initCause(ioe);
throw le;
} catch (UnsupportedCallbackException uce) {
LoginException le = new LoginException();
le.initCause(uce);
throw le;
}
}
}

View file

@ -18,8 +18,10 @@
<module>adapter-core</module>
<module>jaxrs-oauth-client</module>
<module>servlet-oauth-client</module>
<module>jboss-adapter-core</module>
<module>as7-eap6/adapter</module>
<module>undertow</module>
<module>wildfly-adapter</module>
<module>wildfly-subsystem</module>
<module>as7-eap-subsystem</module>
<module>js</module>

View file

@ -65,7 +65,7 @@ public class KeycloakServletExtension implements ServletExtension {
if (is == null) throw new RuntimeException("Unable to find realm config in /WEB-INF/keycloak.json or in keycloak subsystem.");
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is);
UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement(deployment);
final ServletKeycloakAuthMech mech = new ServletKeycloakAuthMech(deployment, userSessionManagement, deploymentInfo.getConfidentialPortManager());
final ServletKeycloakAuthMech mech = createAuthenticationMechanism(deploymentInfo, deployment, userSessionManagement);
UndertowAuthenticatedActionsHandler.Wrapper actions = new UndertowAuthenticatedActionsHandler.Wrapper(deployment);
@ -102,4 +102,9 @@ public class KeycloakServletExtension implements ServletExtension {
cookieConfig.setPath(deploymentInfo.getContextPath());
deploymentInfo.setServletSessionConfig(cookieConfig);
}
protected ServletKeycloakAuthMech createAuthenticationMechanism(DeploymentInfo deploymentInfo, KeycloakDeployment deployment, UndertowUserSessionManagement userSessionManagement) {
log.info("creating ServletKeycloakAuthMech");
return new ServletKeycloakAuthMech(deployment, userSessionManagement, deploymentInfo.getConfidentialPortManager());
}
}

View file

@ -3,6 +3,7 @@ package org.keycloak.adapters.undertow;
import io.undertow.security.idm.Account;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.representations.AccessToken;
@ -16,7 +17,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KeycloakUndertowAccount implements Account, Serializable {
public class KeycloakUndertowAccount implements Account, Serializable, KeycloakAccount {
protected static Logger log = Logger.getLogger(KeycloakUndertowAccount.class);
protected RefreshableKeycloakSecurityContext session;
protected KeycloakPrincipal principal;
@ -25,19 +26,27 @@ public class KeycloakUndertowAccount implements Account, Serializable {
public KeycloakUndertowAccount(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext session, KeycloakDeployment deployment) {
this.principal = principal;
this.session = session;
setRoles(session.getToken(), deployment);
setRoles(session.getToken());
}
protected void setRoles(AccessToken accessToken, KeycloakDeployment deployment) {
protected void setRoles(AccessToken accessToken) {
Set<String> roles = null;
if (deployment.isUseResourceRoleMappings()) {
AccessToken.Access access = accessToken.getResourceAccess(deployment.getResourceName());
if (session.getDeployment().isUseResourceRoleMappings()) {
log.info("useResourceRoleMappings");
AccessToken.Access access = accessToken.getResourceAccess(session.getDeployment().getResourceName());
if (access != null) roles = access.getRoles();
} else {
log.info("use realm role mappings");
AccessToken.Access access = accessToken.getRealmAccess();
if (access != null) roles = access.getRoles();
}
if (roles == null) roles = Collections.emptySet();
/*
log.info("Setting roles: ");
for (String role : roles) {
log.info(" role: " + role);
}
*/
this.accountRoles = roles;
}
@ -51,22 +60,17 @@ public class KeycloakUndertowAccount implements Account, Serializable {
return accountRoles;
}
public AccessToken getAccessToken() {
return session.getToken();
}
public String getEncodedAccessToken() {
return session.getTokenString();
}
@Override
public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() {
return session;
}
public boolean isActive(KeycloakDeployment deployment) {
// this object may have been serialized, so we need to reset realm config/metadata
public void setDeployment(KeycloakDeployment deployment) {
session.setDeployment(deployment);
log.info("realmConfig notBefore: " + deployment.getNotBefore());
}
public boolean isActive() {
// this object may have been serialized, so we need to reset realm config/metadata
if (session.isActive()) {
log.info("session is active");
return true;
@ -81,7 +85,7 @@ public class KeycloakUndertowAccount implements Account, Serializable {
}
log.info("refresh succeeded");
setRoles(session.getToken(), deployment);
setRoles(session.getToken());
return true;
}

View file

@ -29,8 +29,7 @@ public class ServletKeycloakAuthMech implements AuthenticationMechanism {
@Override
public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
UndertowHttpFacade facade = new UndertowHttpFacade(exchange);
ServletRequestAuthenticator authenticator = new ServletRequestAuthenticator(facade, deployment,
portManager.getConfidentialPort(exchange), securityContext, exchange, userSessionManagement);
ServletRequestAuthenticator authenticator = createRequestAuthenticator(exchange, securityContext, facade);
AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) {
return AuthenticationMechanismOutcome.AUTHENTICATED;
@ -46,6 +45,11 @@ public class ServletKeycloakAuthMech implements AuthenticationMechanism {
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
protected ServletRequestAuthenticator createRequestAuthenticator(HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) {
return new ServletRequestAuthenticator(facade, deployment,
portManager.getConfidentialPort(exchange), securityContext, exchange, userSessionManagement);
}
@Override
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
AuthChallenge challenge = exchange.getAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY);

View file

@ -5,6 +5,7 @@ import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.handlers.ServletRequestContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakAccount;
import org.keycloak.adapters.KeycloakDeployment;
import javax.servlet.http.HttpServletRequest;
@ -39,7 +40,8 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
log.info("Account was not in session, returning null");
return false;
}
if (account.isActive(deployment)) {
account.setDeployment(deployment);
if (account.isActive()) {
log.info("Cached account found");
securityContext.authenticationComplete(account, "KEYCLOAK", false);
propagateKeycloakContext( account);
@ -59,7 +61,7 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
}
@Override
protected void login(KeycloakUndertowAccount account) {
protected void login(KeycloakAccount account) {
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpSession session = req.getSession(true);

View file

@ -4,6 +4,7 @@ import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
import org.keycloak.KeycloakPrincipal;
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;
@ -46,7 +47,7 @@ public class UndertowRequestAuthenticator extends RequestAuthenticator {
login(account);
}
protected void login(KeycloakUndertowAccount account) {
protected void login(KeycloakAccount account) {
}

View file

@ -0,0 +1,108 @@
<?xml version="1.0"?>
<project>
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-alpha-3-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-wildfly-adapter</artifactId>
<name>Keycloak Wildfly Integration</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.1.2.GA</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-undertow-adapter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jboss-adapter-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${keycloak.apache.httpcomponents.version}</version>
</dependency>
<dependency>
<groupId>net.iharder</groupId>
<artifactId>base64</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-xc</artifactId>
</dependency>
<dependency>
<groupId>org.picketbox</groupId>
<artifactId>picketbox</artifactId>
<version>4.0.20.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,116 @@
package org.keycloak.adapters.wildfly;
import org.jboss.security.NestableGroup;
import org.jboss.security.SecurityConstants;
import org.jboss.security.SecurityContextAssociation;
import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.keycloak.adapters.KeycloakAccount;
import javax.security.auth.Subject;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SecurityInfoHelper {
public static void propagateSessionInfo(KeycloakAccount account) {
Subject subject = new Subject();
Set<Principal> principals = subject.getPrincipals();
principals.add(account.getPrincipal());
Group[] roleSets = getRoleSets(account.getRoles());
for (int g = 0; g < roleSets.length; g++) {
Group group = roleSets[g];
String name = group.getName();
Group subjectGroup = createGroup(name, principals);
if (subjectGroup instanceof NestableGroup) {
/* A NestableGroup only allows Groups to be added to it so we
need to add a SimpleGroup to subjectRoles to contain the roles
*/
SimpleGroup tmp = new SimpleGroup("Roles");
subjectGroup.addMember(tmp);
subjectGroup = tmp;
}
// Copy the group members to the Subject group
Enumeration<? extends Principal> members = group.members();
while (members.hasMoreElements()) {
Principal role = (Principal) members.nextElement();
subjectGroup.addMember(role);
}
}
// add the CallerPrincipal group if none has been added in getRoleSets
Group callerGroup = new SimpleGroup(SecurityConstants.CALLER_PRINCIPAL_GROUP);
callerGroup.addMember(account.getPrincipal());
principals.add(callerGroup);
org.jboss.security.SecurityContext sc = SecurityContextAssociation.getSecurityContext();
Principal userPrincipal = getPrincipal(subject);
sc.getUtil().createSubjectInfo(userPrincipal, account, subject);
}
/**
* Get the Principal given the authenticated Subject. Currently the first subject that is not of type {@code Group} is
* considered or the single subject inside the CallerPrincipal group.
*
* @param subject
* @return the authenticated subject
*/
protected static Principal getPrincipal(Subject subject) {
Principal principal = null;
Principal callerPrincipal = null;
if (subject != null) {
Set<Principal> principals = subject.getPrincipals();
if (principals != null && !principals.isEmpty()) {
for (Principal p : principals) {
if (!(p instanceof Group) && principal == null) {
principal = p;
}
if (p instanceof Group) {
Group g = Group.class.cast(p);
if (g.getName().equals(SecurityConstants.CALLER_PRINCIPAL_GROUP) && callerPrincipal == null) {
Enumeration<? extends Principal> e = g.members();
if (e.hasMoreElements())
callerPrincipal = e.nextElement();
}
}
}
}
}
return callerPrincipal == null ? principal : callerPrincipal;
}
protected static Group createGroup(String name, Set<Principal> principals) {
Group roles = null;
Iterator<Principal> iter = principals.iterator();
while (iter.hasNext()) {
Object next = iter.next();
if ((next instanceof Group) == false)
continue;
Group grp = (Group) next;
if (grp.getName().equals(name)) {
roles = grp;
break;
}
}
// If we did not find a group create one
if (roles == null) {
roles = new SimpleGroup(name);
principals.add(roles);
}
return roles;
}
protected static Group[] getRoleSets(Collection<String> roleSet) {
SimpleGroup roles = new SimpleGroup("Roles");
Group[] roleSets = {roles};
for (String role : roleSet) {
roles.addMember(new SimplePrincipal(role));
}
return roleSets;
}
}

View file

@ -0,0 +1,29 @@
package org.keycloak.adapters.wildfly;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.api.ConfidentialPortManager;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.undertow.ServletKeycloakAuthMech;
import org.keycloak.adapters.undertow.ServletRequestAuthenticator;
import org.keycloak.adapters.undertow.UndertowHttpFacade;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class WildflyAuthenticationMechanism extends ServletKeycloakAuthMech {
public WildflyAuthenticationMechanism(KeycloakDeployment deployment,
UndertowUserSessionManagement userSessionManagement,
ConfidentialPortManager portManager) {
super(deployment, userSessionManagement, portManager);
}
@Override
protected ServletRequestAuthenticator createRequestAuthenticator(HttpServerExchange exchange, SecurityContext securityContext, UndertowHttpFacade facade) {
return new WildflyRequestAuthenticator(facade, deployment,
portManager.getConfidentialPort(exchange), securityContext, exchange, userSessionManagement);
}
}

View file

@ -0,0 +1,24 @@
package org.keycloak.adapters.wildfly;
import io.undertow.servlet.api.DeploymentInfo;
import org.jboss.logging.Logger;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.undertow.KeycloakServletExtension;
import org.keycloak.adapters.undertow.ServletKeycloakAuthMech;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class WildflyKeycloakServletExtension extends KeycloakServletExtension {
protected static Logger log = Logger.getLogger(WildflyKeycloakServletExtension.class);
@Override
protected ServletKeycloakAuthMech createAuthenticationMechanism(DeploymentInfo deploymentInfo, KeycloakDeployment deployment,
UndertowUserSessionManagement userSessionManagement) {
log.info("creating WildflyAuthenticationMechanism");
return new WildflyAuthenticationMechanism(deployment, userSessionManagement, deploymentInfo.getConfidentialPortManager());
}
}

View file

@ -0,0 +1,136 @@
package org.keycloak.adapters.wildfly;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
import org.jboss.logging.Logger;
import org.jboss.security.NestableGroup;
import org.jboss.security.SecurityConstants;
import org.jboss.security.SecurityContextAssociation;
import org.jboss.security.SimpleGroup;
import org.jboss.security.SimplePrincipal;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
import org.keycloak.adapters.undertow.ServletRequestAuthenticator;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import javax.security.auth.Subject;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class WildflyRequestAuthenticator extends ServletRequestAuthenticator {
protected static Logger log = Logger.getLogger(WildflyRequestAuthenticator.class);
public WildflyRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
SecurityContext securityContext, HttpServerExchange exchange,
UndertowUserSessionManagement userSessionManagement) {
super(facade, deployment, sslRedirectPort, securityContext, exchange, userSessionManagement);
}
@Override
protected void propagateKeycloakContext(KeycloakUndertowAccount account) {
super.propagateKeycloakContext(account);
SecurityInfoHelper.propagateSessionInfo(account);
log.info("propagate security context to wildfly");
Subject subject = new Subject();
Set<Principal> principals = subject.getPrincipals();
principals.add(account.getPrincipal());
Group[] roleSets = getRoleSets(account.getRoles());
for (int g = 0; g < roleSets.length; g++) {
Group group = roleSets[g];
String name = group.getName();
Group subjectGroup = createGroup(name, principals);
if (subjectGroup instanceof NestableGroup) {
/* A NestableGroup only allows Groups to be added to it so we
need to add a SimpleGroup to subjectRoles to contain the roles
*/
SimpleGroup tmp = new SimpleGroup("Roles");
subjectGroup.addMember(tmp);
subjectGroup = tmp;
}
// Copy the group members to the Subject group
Enumeration<? extends Principal> members = group.members();
while (members.hasMoreElements()) {
Principal role = (Principal) members.nextElement();
subjectGroup.addMember(role);
}
}
// add the CallerPrincipal group if none has been added in getRoleSets
Group callerGroup = new SimpleGroup(SecurityConstants.CALLER_PRINCIPAL_GROUP);
callerGroup.addMember(account.getPrincipal());
principals.add(callerGroup);
org.jboss.security.SecurityContext sc = SecurityContextAssociation.getSecurityContext();
Principal userPrincipal = getPrincipal(subject);
sc.getUtil().createSubjectInfo(userPrincipal, account, subject);
}
/**
* Get the Principal given the authenticated Subject. Currently the first subject that is not of type {@code Group} is
* considered or the single subject inside the CallerPrincipal group.
*
* @param subject
* @return the authenticated subject
*/
protected Principal getPrincipal(Subject subject) {
Principal principal = null;
Principal callerPrincipal = null;
if (subject != null) {
Set<Principal> principals = subject.getPrincipals();
if (principals != null && !principals.isEmpty()) {
for (Principal p : principals) {
if (!(p instanceof Group) && principal == null) {
principal = p;
}
if (p instanceof Group) {
Group g = Group.class.cast(p);
if (g.getName().equals(SecurityConstants.CALLER_PRINCIPAL_GROUP) && callerPrincipal == null) {
Enumeration<? extends Principal> e = g.members();
if (e.hasMoreElements())
callerPrincipal = e.nextElement();
}
}
}
}
}
return callerPrincipal == null ? principal : callerPrincipal;
}
protected Group createGroup(String name, Set<Principal> principals) {
Group roles = null;
Iterator<Principal> iter = principals.iterator();
while (iter.hasNext()) {
Object next = iter.next();
if ((next instanceof Group) == false)
continue;
Group grp = (Group) next;
if (grp.getName().equals(name)) {
roles = grp;
break;
}
}
// If we did not find a group create one
if (roles == null) {
roles = new SimpleGroup(name);
principals.add(roles);
}
return roles;
}
protected Group[] getRoleSets(Collection<String> roleSet) {
SimpleGroup roles = new SimpleGroup("Roles");
Group[] roleSets = {roles};
for (String role : roleSet) {
roles.addMember(new SimplePrincipal(role));
}
return roleSets;
}
}

View file

@ -0,0 +1 @@
org.keycloak.adapters.wildfly.WildflyKeycloakServletExtension

View file

@ -48,11 +48,30 @@ public class KeycloakAdapterConfigDeploymentProcessor implements DeploymentUnitP
// Seems wise to have this run after INSTALL_WAR_DEPLOYMENT
public static final int PRIORITY = Phase.INSTALL_WAR_DEPLOYMENT - 1;
// not sure if we need this yet, keeping here just in case
protected void addSecurityDomain(DeploymentUnit deploymentUnit, KeycloakAdapterConfigService service) {
String deploymentName = deploymentUnit.getName();
if (!service.isKeycloakDeployment(deploymentName)) {
return;
}
WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY);
if (warMetaData == null) return;
JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData();
if (webMetaData == null) return;
LoginConfigMetaData loginConfig = webMetaData.getLoginConfig();
if (loginConfig == null || !loginConfig.getAuthMethod().equalsIgnoreCase("KEYCLOAK")) {
return;
}
webMetaData.setSecurityDomain("keycloak");
}
@Override
public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit();
String deploymentName = deploymentUnit.getName();
String deploymentName = deploymentUnit.getName();
KeycloakAdapterConfigService service = KeycloakAdapterConfigService.find(phaseContext.getServiceRegistry());
//log.info("********* CHECK KEYCLOAK DEPLOYMENT: " + deploymentName);
if (service.isKeycloakDeployment(deploymentName)) {
@ -61,6 +80,9 @@ public class KeycloakAdapterConfigDeploymentProcessor implements DeploymentUnitP
}
// FYI, Undertow Extension will find deployments that have auth-method set to KEYCLOAK
// todo notsure if we need this
// addSecurityDomain(deploymentUnit, service);
}
private void addKeycloakAuthData(DeploymentPhaseContext phaseContext, String deploymentName, KeycloakAdapterConfigService service) {

View file

@ -36,7 +36,9 @@ import org.keycloak.subsystem.logging.KeycloakLogger;
*/
public class KeycloakDependencyProcessor implements DeploymentUnitProcessor {
private static final ModuleIdentifier KEYCLOAK_WILDFLY_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-wildfly-adapter");
private static final ModuleIdentifier KEYCLOAK_UNDERTOW_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-undertow-adapter");
private static final ModuleIdentifier KEYCLOAK_JBOSS_CORE_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-jboss-adapter-core");
private static final ModuleIdentifier KEYCLOAK_CORE_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-adapter-core");
private static final ModuleIdentifier KEYCLOAK_CORE = ModuleIdentifier.create("org.keycloak.keycloak-core");
//private static final ModuleIdentifier APACHE_HTTPCOMPONENTS = ModuleIdentifier.create("org.apache.httpcomponents");
@ -51,7 +53,9 @@ public class KeycloakDependencyProcessor implements DeploymentUnitProcessor {
final ModuleSpecification moduleSpecification = deploymentUnit.getAttachment(Attachments.MODULE_SPECIFICATION);
final ModuleLoader moduleLoader = Module.getBootModuleLoader();
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_UNDERTOW_ADAPTER, false, false, true, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_WILDFLY_ADAPTER, false, false, true, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_UNDERTOW_ADAPTER, false, false, false, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_JBOSS_CORE_ADAPTER, false, false, false, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_CORE_ADAPTER, false, false, false, false));
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_CORE, false, false, false, false));
//moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, APACHE_HTTPCOMPONENTS, false, false, true, false));

View file

@ -218,7 +218,7 @@ public class AdminService {
}
}
@Path("isLoggedIn.js")
@Path("isLoggedIn.js")
@GET
@Produces("application/javascript")
@NoCache