diff --git a/integration/as7-eap6/adapter/pom.xml b/integration/as7-eap6/adapter/pom.xml new file mode 100755 index 0000000000..b4ad78f447 --- /dev/null +++ b/integration/as7-eap6/adapter/pom.xml @@ -0,0 +1,85 @@ + + + + keycloak-parent + org.keycloak + 1.0-alpha-1 + ../../../pom.xml + + 4.0.0 + + keycloak-as7-adapter + Keycloak AS7 Integration + + + + + org.jboss.logging + jboss-logging + 3.1.2.GA + provided + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.jboss.resteasy + jose-jwt + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + 1.0.0.Final + + + org.jboss.resteasy + resteasy-jaxrs + provided + + + org.jboss.resteasy + resteasy-client + provided + + + + org.jboss.web + jbossweb + 7.0.17.Final + provided + + + org.jboss.as + jboss-as-web + 7.1.2.Final + + + org.picketbox + picketbox + provided + 4.0.7.Final + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/Actions.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/Actions.java new file mode 100755 index 0000000000..65c9ddbc9d --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/Actions.java @@ -0,0 +1,14 @@ +package org.keycloak.adapters.as7; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface Actions +{ + public static final String J_OAUTH_ADMIN_FORCED_LOGOUT = "j_oauth_admin_forced_logout"; + public static final String J_OAUTH_LOGOUT = "j_oauth_logout"; + public static final String J_OAUTH_RESOLVE_ACCESS_CODE = "j_oauth_resolve_access_code"; + public static final String J_OAUTH_REMOTE_LOGOUT = "j_oauth_remote_logout"; + public static final String J_OAUTH_TOKEN_GRANT = "j_oauth_token_grant"; +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/BearerTokenAuthenticatorValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/BearerTokenAuthenticatorValve.java new file mode 100755 index 0000000000..2f7e95cc91 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/BearerTokenAuthenticatorValve.java @@ -0,0 +1,87 @@ +package org.keycloak.adapters.as7; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.authenticator.AuthenticatorBase; +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.ResourceMetadata; +import org.keycloak.adapters.as7.config.ManagedResourceConfig; +import org.keycloak.adapters.as7.config.ManagedResourceConfigLoader; +import org.jboss.resteasy.spi.ResteasyProviderFactory; + +import javax.security.auth.login.LoginException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Uses a configured remote auth server to do Bearer token authentication only. SkeletonKeyTokens are used + * to provide user data and role mappings. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class BearerTokenAuthenticatorValve extends AuthenticatorBase implements LifecycleListener +{ + private static final Logger log = Logger.getLogger(BearerTokenAuthenticatorValve.class); + protected ManagedResourceConfig remoteSkeletonKeyConfig; + protected ResourceMetadata resourceMetadata; + + @Override + public void start() throws LifecycleException + { + super.start(); + StandardContext standardContext = (StandardContext)context; + standardContext.addLifecycleListener(this); + } + + @Override + public void lifecycleEvent(LifecycleEvent event) + { + if (event.getType() == Lifecycle.AFTER_START_EVENT) init(); + } + + protected void init() + { + ManagedResourceConfigLoader managedResourceConfigLoader = new ManagedResourceConfigLoader(context); + resourceMetadata = managedResourceConfigLoader.getResourceMetadata(); + remoteSkeletonKeyConfig = managedResourceConfigLoader.getRemoteSkeletonKeyConfig(); + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException + { + try + { + super.invoke(request, response); + } + finally + { + ResteasyProviderFactory.clearContextData(); // to clear push of SkeletonKeySession + } + } + + @Override + protected boolean authenticate(Request request, HttpServletResponse response, LoginConfig config) throws IOException + { + try + { + CatalinaBearerTokenAuthenticator bearer = new CatalinaBearerTokenAuthenticator(resourceMetadata, !remoteSkeletonKeyConfig.isCancelPropagation(), true); + if (bearer.login(request, response)) + { + return true; + } + return false; + } + catch (LoginException e) + { + } + return false; + } +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaBearerTokenAuthenticator.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaBearerTokenAuthenticator.java new file mode 100755 index 0000000000..7d14c1d307 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaBearerTokenAuthenticator.java @@ -0,0 +1,163 @@ +package org.keycloak.adapters.as7; + +import org.apache.catalina.connector.Request; +import org.jboss.logging.Logger; +import org.keycloak.RSATokenVerifier; +import org.keycloak.ResourceMetadata; +import org.keycloak.SkeletonKeyPrincipal; +import org.keycloak.SkeletonKeySession; +import org.keycloak.VerificationException; +import org.keycloak.representations.SkeletonKeyToken; +import org.jboss.resteasy.spi.ResteasyProviderFactory; + +import javax.security.auth.login.LoginException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class CatalinaBearerTokenAuthenticator +{ + protected ResourceMetadata resourceMetadata; + protected boolean challenge; + protected Logger log = Logger.getLogger(CatalinaBearerTokenAuthenticator.class); + protected String tokenString; + protected SkeletonKeyToken token; + private Principal principal; + protected boolean propagateToken; + + public CatalinaBearerTokenAuthenticator(ResourceMetadata resourceMetadata, boolean propagateToken, boolean challenge) + { + this.resourceMetadata = resourceMetadata; + this.challenge = challenge; + this.propagateToken = propagateToken; + } + + public ResourceMetadata getResourceMetadata() + { + return resourceMetadata; + } + + public String getTokenString() + { + return tokenString; + } + + public SkeletonKeyToken getToken() + { + return token; + } + + public Principal getPrincipal() + { + return principal; + } + + public boolean login(Request request, HttpServletResponse response) throws LoginException, IOException + { + String authHeader = request.getHeader("Authorization"); + if (authHeader == null) + { + if (challenge) + { + challengeResponse(response, null, null); + return false; + } + else + { + return false; + } + } + + String[] split = authHeader.trim().split("\\s+"); + if (split == null || split.length != 2) challengeResponse(response, null, null); + if (!split[0].equalsIgnoreCase("Bearer")) challengeResponse(response, null, null); + + + tokenString = split[1]; + + try + { + token = RSATokenVerifier.verifyToken(tokenString, resourceMetadata); + } + catch (VerificationException e) + { + log.error("Failed to verify token", e); + challengeResponse(response, "invalid_token", e.getMessage()); + } + boolean verifyCaller = false; + Set roles = null; + if (resourceMetadata.getResourceName() != null) + { + SkeletonKeyToken.Access access = token.getResourceAccess(resourceMetadata.getResourceName()); + if (access != null) roles = access.getRoles(); + verifyCaller = token.isVerifyCaller(resourceMetadata.getResourceName()); + } + else + { + verifyCaller = token.isVerifyCaller(); + SkeletonKeyToken.Access access = token.getRealmAccess(); + if (access != null) roles = access.getRoles(); + } + String surrogate = null; + if (verifyCaller) + { + if (token.getTrustedCertificates() == null || token.getTrustedCertificates().size() == 0) + { + response.sendError(400); + throw new LoginException("No trusted certificates in token"); + } + // for now, we just make sure JBoss Web did two-way SSL + // assume JBoss Web verifies the client cert + X509Certificate[] chain = request.getCertificateChain(); + if (chain == null || chain.length == 0) + { + response.sendError(400); + throw new LoginException("No certificates provided by jboss web to verify the caller"); + } + surrogate = chain[0].getSubjectX500Principal().getName(); + } + SkeletonKeyPrincipal skeletonKeyPrincipal = new SkeletonKeyPrincipal(token.getPrincipal(), surrogate); + principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skeletonKeyPrincipal, roles); + request.setUserPrincipal(principal); + request.setAuthType("OAUTH_BEARER"); + if (propagateToken) + { + SkeletonKeySession skSession = new SkeletonKeySession(tokenString, resourceMetadata); + request.setAttribute(SkeletonKeySession.class.getName(), skSession); + ResteasyProviderFactory.pushContext(SkeletonKeySession.class, skSession); + } + + return true; + } + + + protected void challengeResponse(HttpServletResponse response, String error, String description) throws LoginException + { + StringBuilder header = new StringBuilder("Bearer realm=\""); + header.append(resourceMetadata.getRealm()).append("\""); + if (error != null) + { + header.append(", error=\"").append(error).append("\""); + } + if (description != null) + { + header.append(", error_description=\"").append(description).append("\""); + } + response.setHeader("WWW-Authenticate", header.toString()); + try + { + response.sendError(401); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + throw new LoginException("Challenged"); + } +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSecurityContextHelper.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSecurityContextHelper.java new file mode 100755 index 0000000000..bf783bd295 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSecurityContextHelper.java @@ -0,0 +1,138 @@ +package org.keycloak.adapters.as7; + +import org.apache.catalina.Realm; +import org.apache.catalina.realm.GenericPrincipal; +import org.jboss.as.web.security.JBossGenericPrincipal; +import org.jboss.security.NestableGroup; +import org.jboss.security.SecurityConstants; +import org.jboss.security.SecurityContext; +import org.jboss.security.SecurityContextAssociation; +import org.jboss.security.SimpleGroup; +import org.jboss.security.SimplePrincipal; + +import javax.security.auth.Subject; +import java.security.Principal; +import java.security.acl.Group; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class CatalinaSecurityContextHelper +{ + public GenericPrincipal createPrincipal(Realm realm, Principal identity, Collection roleSet) + { + Subject subject = new Subject(); + String credentials = ""; + Set principals = subject.getPrincipals(); + principals.add(identity); + Group[] roleSets = getRoleSets(roleSet); + 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 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(identity); + principals.add(callerGroup); + SecurityContext sc = SecurityContextAssociation.getSecurityContext(); + Principal userPrincipal = getPrincipal(subject); + sc.getUtil().createSubjectInfo(userPrincipal, credentials, subject); + List rolesAsStringList = new ArrayList(); + rolesAsStringList.addAll(roleSet); + return new JBossGenericPrincipal(realm, userPrincipal.getName(), null, rolesAsStringList, + userPrincipal, null, credentials, null, subject); + + } + /** + * Get the Principal given the authenticated Subject. Currently the first principal that is not of type {@code Group} is + * considered or the single principal inside the CallerPrincipal group. + * + * @param subject + * @return the authenticated principal + */ + protected Principal getPrincipal(Subject subject) { + Principal principal = null; + Principal callerPrincipal = null; + if (subject != null) { + Set 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 e = g.members(); + if (e.hasMoreElements()) + callerPrincipal = e.nextElement(); + } + } + } + } + } + return callerPrincipal == null ? principal : callerPrincipal; + } + + protected Group createGroup(String name, Set principals) + { + Group roles = null; + Iterator 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 roleSet) + { + SimpleGroup roles = new SimpleGroup("Roles"); + Group[] roleSets = {roles}; + for (String role : roleSet) + { + roles.addMember(new SimplePrincipal(role)); + } + return roleSets; + } + +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/OAuthAuthenticationServerValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/OAuthAuthenticationServerValve.java new file mode 100755 index 0000000000..abc1dca240 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/OAuthAuthenticationServerValve.java @@ -0,0 +1,1050 @@ +package org.keycloak.adapters.as7; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.authenticator.Constants; +import org.apache.catalina.authenticator.FormAuthenticator; +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.apache.catalina.realm.GenericPrincipal; +import org.bouncycastle.openssl.PEMWriter; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.ObjectWriter; +import org.codehaus.jackson.map.SerializationConfig; +import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.jboss.logging.Logger; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.jose.jws.JWSBuilder; +import org.jboss.resteasy.jose.jws.JWSInput; +import org.jboss.resteasy.jose.jws.crypto.RSAProvider; +import org.jboss.resteasy.jwt.JsonSerialization; +import org.jboss.resteasy.plugins.providers.RegisterBuiltin; +import org.jboss.resteasy.plugins.server.servlet.ServletUtil; +import org.keycloak.EnvUtil; +import org.keycloak.PemUtils; +import org.keycloak.ResourceMetadata; +import org.keycloak.SkeletonKeySession; +import org.keycloak.adapters.as7.config.AuthServerConfig; +import org.keycloak.adapters.as7.config.ManagedResourceConfig; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.SkeletonKeyToken; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.jboss.resteasy.spi.ResteasyUriInfo; +import org.jboss.resteasy.util.BasicAuthHelper; + +import javax.security.auth.login.LoginException; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.UriBuilder; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.security.KeyStore; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Turns a web deployment into an authentication server that follwos the OAuth 2 protocol and Skeleton Key bearer tokens. + * Authentication store is backed by a JBoss security domain. + *

+ * Servlet FORM authentication that uses the local security domain to authenticate and for role mappings. + *

+ * Supports bearer token creation and authentication. The client asking for access must be set up as a valid user + * within the security domain. + *

+ * If no an OAuth access request, this works like normal FORM authentication and authorization. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class OAuthAuthenticationServerValve extends FormAuthenticator implements LifecycleListener +{ + + + public static class AccessCode + { + protected String id = UUID.randomUUID().toString() + System.currentTimeMillis(); + protected long expiration; + protected SkeletonKeyToken token; + protected String client; + protected boolean sso; + protected String redirect; + + public boolean isExpired() + { + return expiration != 0 && (System.currentTimeMillis() / 1000) > expiration; + } + + public String getId() + { + return id; + } + + public long getExpiration() + { + return expiration; + } + + public void setExpiration(long expiration) + { + this.expiration = expiration; + } + + public SkeletonKeyToken getToken() + { + return token; + } + + public void setToken(SkeletonKeyToken token) + { + this.token = token; + } + + public String getClient() + { + return client; + } + + public void setClient(String client) + { + this.client = client; + } + + public boolean isSso() + { + return sso; + } + + public void setSso(boolean sso) + { + this.sso = sso; + } + + public String getRedirect() + { + return redirect; + } + + public void setRedirect(String redirect) + { + this.redirect = redirect; + } + } + + protected ConcurrentHashMap accessCodeMap = new ConcurrentHashMap(); + private static final Logger log = Logger.getLogger(OAuthAuthenticationServerValve.class); + + private static AtomicLong counter = new AtomicLong(1); + + private static String generateId() + { + return counter.getAndIncrement() + "." + UUID.randomUUID().toString(); + } + + protected AuthServerConfig skeletonKeyConfig; + protected PrivateKey realmPrivateKey; + protected PublicKey realmPublicKey; + protected String realmPublicKeyPem; + protected ResteasyProviderFactory providers; + protected ResourceMetadata resourceMetadata; + protected UserSessionManagement userSessionManagement = new UserSessionManagement(); + protected ObjectMapper mapper; + protected ObjectWriter accessTokenResponseWriter; + protected ObjectWriter mapWriter; + + private static KeyStore loadKeyStore(String filename, String password) throws Exception + { + KeyStore trustStore = KeyStore.getInstance(KeyStore + .getDefaultType()); + File truststoreFile = new File(filename); + FileInputStream trustStream = new FileInputStream(truststoreFile); + trustStore.load(trustStream, password.toCharArray()); + trustStream.close(); + return trustStore; + } + + @Override + public void start() throws LifecycleException + { + super.start(); + StandardContext standardContext = (StandardContext) context; + standardContext.addLifecycleListener(this); + } + + @Override + public void lifecycleEvent(LifecycleEvent event) + { + if (event.getType() == Lifecycle.AFTER_START_EVENT) init(); + } + + protected void init() + { + mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT); + accessTokenResponseWriter = mapper.writerWithType(AccessTokenResponse.class); + mapWriter = mapper.writerWithType(mapper.getTypeFactory().constructMapType(Map.class, String.class, String.class)); + + InputStream is = null; + String path = context.getServletContext().getInitParameter("skeleton.key.config.file"); + if (path == null) + { + is = context.getServletContext().getResourceAsStream("/WEB-INF/resteasy-oauth.json"); + } + else + { + try + { + is = new FileInputStream(path); + } + catch (FileNotFoundException e) + { + throw new RuntimeException(e); + } + } + try + { + skeletonKeyConfig = mapper.readValue(is, AuthServerConfig.class); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + if (skeletonKeyConfig.getLoginRole() == null) + { + throw new RuntimeException("You must define the login-role in your config file"); + } + if (skeletonKeyConfig.getClientRole() == null) + { + throw new RuntimeException("You must define the oauth-client-role in your config file"); + } + if (skeletonKeyConfig.getRealmPrivateKey() != null) + { + try + { + realmPrivateKey = PemUtils.decodePrivateKey(skeletonKeyConfig.getRealmPrivateKey()); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + if (skeletonKeyConfig.getRealmPublicKey() != null) + { + try + { + realmPublicKey = PemUtils.decodePublicKey(skeletonKeyConfig.getRealmPublicKey()); + realmPublicKeyPem = skeletonKeyConfig.getRealmPublicKey(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + if (skeletonKeyConfig.getRealmKeyStore() != null) + { + if (skeletonKeyConfig.getRealmKeyAlias() == null) throw new RuntimeException("Must define realm-key-alias"); + String keystorePath = EnvUtil.replace(skeletonKeyConfig.getRealmKeyStore()); + try + { + KeyStore ks = loadKeyStore(keystorePath, skeletonKeyConfig.getRealmKeystorePassword()); + if (realmPrivateKey == null) + { + realmPrivateKey = (PrivateKey) ks.getKey(skeletonKeyConfig.getRealmKeyAlias(), skeletonKeyConfig.getRealmPrivateKeyPassword().toCharArray()); + } + if (realmPublicKey == null) + { + Certificate cert = ks.getCertificate(skeletonKeyConfig.getRealmKeyAlias()); + realmPublicKey = cert.getPublicKey(); + } + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + if (realmPublicKey == null) throw new RuntimeException("You have not declared a keystore or public key"); + if (realmPrivateKey == null) throw new RuntimeException("You have not declared a keystore or private key"); + if (realmPublicKeyPem == null) + { + StringWriter sw = new StringWriter(); + PEMWriter writer = new PEMWriter(sw); + try + { + writer.writeObject(realmPublicKey); + writer.flush(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + realmPublicKeyPem = sw.toString(); + realmPublicKeyPem = PemUtils.removeBeginEnd(realmPublicKeyPem); + } + providers = new ResteasyProviderFactory(); + ClassLoader old = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(OAuthAuthenticationServerValve.class.getClassLoader()); + try + { + ResteasyProviderFactory.getInstance(); // initialize builtins + RegisterBuiltin.register(providers); + } + finally + { + Thread.currentThread().setContextClassLoader(old); + } + resourceMetadata = new ResourceMetadata(); + resourceMetadata.setRealm(skeletonKeyConfig.getRealm()); + resourceMetadata.setRealmKey(realmPublicKey); + String truststore = skeletonKeyConfig.getTruststore(); + if (truststore != null) + { + truststore = EnvUtil.replace(truststore); + String truststorePassword = skeletonKeyConfig.getTruststorePassword(); + KeyStore trust = null; + try + { + trust = loadKeyStore(truststore, truststorePassword); + } + catch (Exception e) + { + throw new RuntimeException("Failed to load truststore", e); + } + resourceMetadata.setTruststore(trust); + } + String clientKeystore = skeletonKeyConfig.getClientKeystore(); + String clientKeyPassword = null; + if (clientKeystore != null) + { + clientKeystore = EnvUtil.replace(clientKeystore); + String clientKeystorePassword = skeletonKeyConfig.getClientKeystorePassword(); + KeyStore serverKS = null; + try + { + serverKS = loadKeyStore(clientKeystore, clientKeystorePassword); + } + catch (Exception e) + { + throw new RuntimeException("Failed to load keystore", e); + } + resourceMetadata.setClientKeystore(serverKS); + clientKeyPassword = skeletonKeyConfig.getClientKeyPassword(); + resourceMetadata.setClientKeyPassword(clientKeyPassword); + } + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException + { + try + { + String contextPath = request.getContextPath(); + String requestURI = request.getDecodedRequestURI(); + log.debug("--- invoke: " + requestURI); + if (request.getMethod().equalsIgnoreCase("GET") + && context.getLoginConfig().getLoginPage().equals(request.getRequestPathMB().toString())) + { + if (handleLoginPage(request, response)) return; + } + else if (request.getMethod().equalsIgnoreCase("GET") + && requestURI.endsWith(Actions.J_OAUTH_LOGOUT)) + { + logoutCurrentUser(request, response); + return; + } + else if (request.getMethod().equalsIgnoreCase("POST") + && requestURI.endsWith(Actions.J_OAUTH_ADMIN_FORCED_LOGOUT)) + { + adminLogout(request, response); + return; + } + else if (request.getMethod().equalsIgnoreCase("POST") + && requestURI.startsWith(contextPath) && + requestURI.endsWith(Constants.FORM_ACTION) + && request.getParameter("client_id") != null) + { + handleOAuth(request, response); + return; + } + else if (request.getMethod().equalsIgnoreCase("POST") + && requestURI.endsWith(Actions.J_OAUTH_TOKEN_GRANT)) + { + tokenGrant(request, response); + return; + } + else if (request.getMethod().equalsIgnoreCase("POST") + && requestURI.startsWith(contextPath) && + requestURI.endsWith(Actions.J_OAUTH_RESOLVE_ACCESS_CODE)) + { + resolveAccessCode(request, response); + return; + } + else if (request.getMethod().equalsIgnoreCase("GET") + && requestURI.startsWith(contextPath) && + requestURI.endsWith("j_oauth_realm_info.html")) + { + publishRealmInfoHtml(request, response); + return; + } + // propagate the skeleton key token string? + if (!skeletonKeyConfig.isCancelPropagation()) + { + if (request.getAttribute(SkeletonKeySession.class.getName()) == null && request.getSessionInternal() != null) + { + SkeletonKeySession skSession = (SkeletonKeySession) request.getSessionInternal().getNote(SkeletonKeySession.class.getName()); + if (skSession != null) + { + request.setAttribute(SkeletonKeySession.class.getName(), skSession); + ResteasyProviderFactory.pushContext(SkeletonKeySession.class, skSession); + } + } + } + request.setAttribute("OAUTH_FORM_ACTION", "j_security_check"); + super.invoke(request, response); + } + finally + { + ResteasyProviderFactory.clearContextData(); // to clear push of SkeletonKeySession + } + } + + protected boolean handleLoginPage(Request request, Response response) throws IOException, ServletException + { + String client_id = request.getParameter("client_id"); + // if this is not an OAUTH redirect, just return and let the default flow happen + if (client_id == null) return false; + + String redirect_uri = request.getParameter("redirect_uri"); + String state = request.getParameter("state"); + + if (redirect_uri == null) + { + response.sendError(400, "No oauth redirect query parameter set"); + return true; + } + // only bypass authentication if our session is authenticated, + // the login query parameter is on request URL, + // and we have configured the login-role + else if (!skeletonKeyConfig.isSsoDisabled() + && request.getSessionInternal() != null + && request.getSessionInternal().getPrincipal() != null + && request.getParameter("login") != null) + { + log.debug("We're ALREADY LOGGED IN!!!"); + GenericPrincipal gp = (GenericPrincipal) request.getSessionInternal().getPrincipal(); + redirectAccessCode(true, response, redirect_uri, client_id, state, gp); + } + else + { + UriBuilder builder = UriBuilder.fromUri("j_security_check") + .queryParam("redirect_uri", redirect_uri) + .queryParam("client_id", client_id); + if (state != null) builder.queryParam("state", state); + String loginAction = builder.build().toString(); + request.setAttribute("OAUTH_FORM_ACTION", loginAction); + getNext().invoke(request, response); + } + return true; + } + + protected GenericPrincipal checkLoggedIn(Request request, HttpServletResponse response) + { + if (request.getPrincipal() != null) + { + return (GenericPrincipal) request.getPrincipal(); + } + else if (request.getSessionInternal() != null && request.getSessionInternal().getPrincipal() != null) + { + return (GenericPrincipal) request.getSessionInternal().getPrincipal(); + } + return null; + } + + + protected void adminLogout(Request request, HttpServletResponse response) throws IOException + { + log.debug("<< adminLogout"); + GenericPrincipal gp = checkLoggedIn(request, response); + if (gp == null) + { + if (bearer(request, response, false)) + { + gp = (GenericPrincipal) request.getPrincipal(); + } + else + { + response.sendError(403); + return; + } + } + if (!gp.hasRole(skeletonKeyConfig.getAdminRole())) + { + response.sendError(403); + return; + } + String logoutUser = request.getParameter("user"); + if (logoutUser != null) + { + userSessionManagement.logout(logoutUser); + logoutResources(logoutUser, gp.getName()); + } + else + { + userSessionManagement.logoutAllBut(gp.getName()); + logoutResources(null, gp.getName()); + } + String forwardTo = request.getParameter("forward"); + if (forwardTo == null) + { + response.setStatus(204); + return; + } + RequestDispatcher disp = + context.getServletContext().getRequestDispatcher(forwardTo); + try + { + disp.forward(request.getRequest(), response); + } + catch (Throwable t) + { + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "failed to forward"); + } + + + } + + + protected void logoutCurrentUser(Request request, HttpServletResponse response) throws IOException + { + if (request.getSessionInternal() == null || request.getSessionInternal().getPrincipal() == null) + { + redirectToWelcomePage(request, response); + return; + } + GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal(); + String username = principal.getName(); + String admin = username; + userSessionManagement.logout(username); + request.setUserPrincipal(null); + request.setAuthType(null); + // logout user on all declared authenticated resources + logoutResources(username, admin); + redirectToWelcomePage(request, response); + } + + protected void logoutResources(String username, String admin) + { + if (skeletonKeyConfig.getResources().size() != 0) + { + SkeletonKeyToken token = new SkeletonKeyToken(); + token.id(generateId()); + token.principal(admin); + token.audience(skeletonKeyConfig.getRealm()); + SkeletonKeyToken.Access realmAccess = new SkeletonKeyToken.Access(); + realmAccess.addRole(skeletonKeyConfig.getAdminRole()); + token.setRealmAccess(realmAccess); + String tokenString = buildTokenString(realmPrivateKey, token); + ResteasyClient client = new ResteasyClientBuilder() + .providerFactory(providers) + .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY) + .trustStore(resourceMetadata.getTruststore()) + .keyStore(resourceMetadata.getClientKeystore(), resourceMetadata.getClientKeyPassword()) + .build(); + try + { + for (String resource : skeletonKeyConfig.getResources()) + { + try + { + log.debug("logging out: " + resource); + WebTarget target = client.target(resource).path(Actions.J_OAUTH_REMOTE_LOGOUT); + if (username != null) target = target.queryParam("user", username); + javax.ws.rs.core.Response response = target.request() + .header("Authorization", "Bearer " + tokenString) + .put(null); + if (response.getStatus() != 204) log.error("Failed to log out"); + response.close(); + } + catch (Exception ignored) + { + log.error("Failed to log out", ignored); + } + } + } + finally + { + client.close(); + } + } + } + + protected void redirectToWelcomePage(Request request, HttpServletResponse response) throws IOException + { + ResteasyUriInfo uriInfo = ServletUtil.extractUriInfo(request, null); + String[] welcomes = context.findWelcomeFiles(); + if (welcomes.length > 0) + { + UriBuilder welcome = uriInfo.getBaseUriBuilder().path(welcomes[0]); + response.sendRedirect(welcome.toTemplate()); + } + else + { + response.setStatus(204); + } + } + + + protected void publishRealmInfoHtml(Request request, HttpServletResponse response) throws IOException + { + ManagedResourceConfig rep = getRealmRepresentation(request); + StringWriter writer; + String json; + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT); + mapper.enable(SerializationConfig.Feature.INDENT_OUTPUT); + + StringBuffer html = new StringBuffer(); + html.append(""); + html.append("

Realm: ").append(rep.getRealm()).append("

"); + + ManagedResourceConfig bearer = new ManagedResourceConfig(); + bearer.setRealm(rep.getRealm()); + bearer.setRealmKey(rep.getRealmKey()); + writer = new StringWriter(); + mapper.writeValue(writer, bearer); + json = writer.toString(); + + html.append("

BearerTokenAuthValve Json Config

"); + html.append("
"); + + html.append("
"); + + writer = new StringWriter(); + rep.getClientCredentials().put("password", "REQUIRED"); + rep.setClientId("REQUIRED"); + rep.setTruststore("REQUIRED"); + rep.setTruststorePassword("REQUIRED"); + mapper.writeValue(writer, rep); + json = writer.toString(); + html.append("

OAuthManagedResourceValve Json Config

"); + html.append("
"); + + html.append(""); + + response.setStatus(200); + response.setContentType("text/html"); + response.getOutputStream().println(html.toString()); + response.getOutputStream().flush(); + + } + + + protected ManagedResourceConfig getRealmRepresentation(Request request) + { + ManagedResourceConfig rep = new ManagedResourceConfig(); + ResteasyUriInfo uriInfo = ServletUtil.extractUriInfo(request, null); + UriBuilder authUrl = uriInfo.getBaseUriBuilder().path(context.getLoginConfig().getLoginPage()); + UriBuilder codeUrl = uriInfo.getBaseUriBuilder().path(Actions.J_OAUTH_RESOLVE_ACCESS_CODE); + rep.setRealm(skeletonKeyConfig.getRealm()); + rep.setRealmKey(realmPublicKeyPem); + rep.setAuthUrl(authUrl.toTemplate()); + rep.setCodeUrl(codeUrl.toTemplate()); + rep.setAdminRole(skeletonKeyConfig.getAdminRole()); + return rep; + } + + public boolean bearer(Request request, HttpServletResponse response, boolean propagate) throws IOException + { + if (request.getHeader("Authorization") != null) + { + CatalinaBearerTokenAuthenticator bearer = new CatalinaBearerTokenAuthenticator(resourceMetadata, true, false); + try + { + if (bearer.login(request, response)) + { + return true; + } + } + catch (LoginException e) + { + } + } + return false; + } + + @Override + protected void register(Request request, HttpServletResponse response, Principal principal, String authType, String username, String password) + { + super.register(request, response, principal, authType, username, password); + log.debug("authenticate userSessionManage.login(): " + principal.getName()); + userSessionManagement.login(request.getSessionInternal(), principal.getName()); + if (!skeletonKeyConfig.isCancelPropagation()) + { + GenericPrincipal gp = (GenericPrincipal) request.getPrincipal(); + if (gp != null) + { + SkeletonKeyToken token = buildToken(gp); + String stringToken = buildTokenString(realmPrivateKey, token); + SkeletonKeySession skSession = new SkeletonKeySession(stringToken, resourceMetadata); + request.setAttribute(SkeletonKeySession.class.getName(), skSession); + ResteasyProviderFactory.pushContext(SkeletonKeySession.class, skSession); + request.getSessionInternal(true).setNote(SkeletonKeySession.class.getName(), skSession); + } + } + } + + @Override + public boolean authenticate(Request request, HttpServletResponse response, LoginConfig config) throws IOException + { + if (bearer(request, response, true)) + { + return true; + } + return super.authenticate(request, response, config); + } + + + protected void resolveAccessCode(Request request, Response response) throws IOException + { + if (!request.isSecure()) + { + response.sendError(400); + return; + } + // always verify code and remove access code from map before authenticating user + // if user authentication fails, we want the code to be removed irreguardless just in case we're under attack + String code = request.getParameter("code"); + JWSInput input = new JWSInput(code, providers); + boolean verifiedCode = false; + try + { + verifiedCode = RSAProvider.verify(input, realmPublicKey); + } + catch (Exception ignored) + { + log.error("Failed to verify signature", ignored); + } + if (!verifiedCode) + { + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Unable to verify code signature"); + response.sendError(400); + response.setContentType("application/json"); + mapWriter.writeValue(response.getOutputStream(), res); + response.getOutputStream().flush(); + return; + } + String key = input.readContent(String.class); + AccessCode accessCode = accessCodeMap.remove(key); + String redirect = request.getParameter("redirect_uri"); + + GenericPrincipal gp = basicAuth(request, response); + if (gp == null) + { + log.error("Failed to authenticate client_id"); + return; + } + if (accessCode == null) + { + log.error("No access code: " + code); + response.sendError(400); + return; + } + if (accessCode.isExpired()) + { + log.debug("Access code expired"); + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Code is expired"); + response.setStatus(400); + response.setContentType("application/json"); + mapWriter.writeValue(response.getOutputStream(), res); + response.getOutputStream().flush(); + return; + } + if (!accessCode.getToken().isActive()) + { + log.debug("token not active"); + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Token expired"); + response.setStatus(400); + response.setContentType("application/json"); + mapWriter.writeValue(response.getOutputStream(), res); + response.getOutputStream().flush(); + return; + } + if (!gp.getName().equals(accessCode.getClient())) + { + log.debug("not equal client"); + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Auth error"); + response.setStatus(400); + response.setContentType("application/json"); + mapWriter.writeValue(response.getOutputStream(), res); + response.getOutputStream().flush(); + return; + } + if (!accessCode.getRedirect().equals(redirect)) + { + log.debug("not equal redirect"); + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Auth error"); + response.setStatus(400); + response.setContentType("application/json"); + mapWriter.writeValue(response.getOutputStream(), res); + response.getOutputStream().flush(); + return; + } + if (accessCode.isSso() && !gp.hasRole(skeletonKeyConfig.getLoginRole())) + { + // we did not authenticate user on an access code request because a session was already established + // but, the client_id does not have permission to bypass this on a simple grant. We want + // to always ask for credentials from a simple oath request + + log.debug("does not have login permission"); + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Auth error"); + response.setStatus(400); + response.setContentType("application/json"); + mapWriter.writeValue(response.getOutputStream(), res); + response.getOutputStream().flush(); + return; + } + else if (!gp.hasRole(skeletonKeyConfig.getClientRole()) && !gp.hasRole(skeletonKeyConfig.getLoginRole())) + { + log.debug("does not have login or client role permission for access token request"); + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Auth error"); + response.setStatus(400); + response.setContentType("application/json"); + mapWriter.writeValue(response.getOutputStream(), res); + response.getOutputStream().flush(); + return; + + } + String wildcard = skeletonKeyConfig.getWildcardRole() == null ? "*" : skeletonKeyConfig.getWildcardRole(); + Set codeRoles = accessCode.getToken().getRealmAccess().getRoles(); + if (codeRoles != null && + (codeRoles.contains(skeletonKeyConfig.getClientRole()) || codeRoles.contains(skeletonKeyConfig.getLoginRole()))) + { + // we store roles a oauth client is granted in the user role mapping, remove those roles as we don't want those clients with those + // permissions if they are logging in. + Set newRoles = new HashSet(); + if (codeRoles.contains(skeletonKeyConfig.getClientRole())) newRoles.add(skeletonKeyConfig.getClientRole()); + if (codeRoles.contains(skeletonKeyConfig.getLoginRole())) newRoles.add(skeletonKeyConfig.getLoginRole()); + if (codeRoles.contains(wildcard)) newRoles.add(wildcard); + codeRoles.clear(); + codeRoles.addAll(newRoles); + } + + // is we have a login role, then we don't need to filter out roles, just grant all the roles the user has + // Also, if the client has the "wildcard" role, then we don't need to filter out roles + if (codeRoles != null + && !gp.hasRole(wildcard) + && !gp.hasRole(skeletonKeyConfig.getLoginRole())) + { + Set clientAllowed = new HashSet(); + for (String role : gp.getRoles()) + { + clientAllowed.add(role); + } + Set newRoles = new HashSet(); + newRoles.addAll(codeRoles); + for (String role : newRoles) + { + if (!clientAllowed.contains(role)) + { + codeRoles.remove(role); + } + } + } + AccessTokenResponse res = accessTokenResponse(realmPrivateKey, accessCode.getToken()); + response.setStatus(200); + response.setContentType("application/json"); + accessTokenResponseWriter.writeValue(response.getOutputStream(), res); + response.getOutputStream().flush(); + } + + protected AccessTokenResponse accessTokenResponse(PrivateKey privateKey, SkeletonKeyToken token) + { + String encodedToken = buildTokenString(privateKey, token); + + AccessTokenResponse res = new AccessTokenResponse(); + res.setToken(encodedToken); + res.setTokenType("bearer"); + if (token.getExpiration() != 0) + { + long time = token.getExpiration() - (System.currentTimeMillis() / 1000); + res.setExpiresIn(time); + } + return res; + } + + protected String buildTokenString(PrivateKey privateKey, SkeletonKeyToken token) + { + byte[] tokenBytes = null; + try + { + tokenBytes = JsonSerialization.toByteArray(token, false); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + return new JWSBuilder() + .content(tokenBytes) + .rsa256(privateKey); + } + + + protected void handleOAuth(Request request, Response response) throws IOException + { + log.debug("<--- Begin oauthAuthenticate"); + String redirect_uri = request.getParameter("redirect_uri"); + String client_id = request.getParameter("client_id"); + String state = request.getParameter("state"); + String username = request.getParameter(Constants.FORM_USERNAME); + String password = request.getParameter(Constants.FORM_PASSWORD); + Principal principal = context.getRealm().authenticate(username, password); + if (principal == null) + { + UriBuilder builder = UriBuilder.fromUri(redirect_uri).queryParam("error", "unauthorized_client"); + if (state != null) builder.queryParam("state", state); + response.sendRedirect(builder.toTemplate()); + return; + } + GenericPrincipal gp = (GenericPrincipal) principal; + register(request, response, principal, HttpServletRequest.FORM_AUTH, username, password); + userSessionManagement.login(request.getSessionInternal(), username); + redirectAccessCode(false, response, redirect_uri, client_id, state, gp); + + return; + } + + protected void tokenGrant(Request request, Response response) throws IOException + { + if (!request.isSecure()) + { + response.sendError(400); + return; + } + GenericPrincipal gp = basicAuth(request, response); + if (gp == null) return; + SkeletonKeyToken token = buildToken(gp); + AccessTokenResponse res = accessTokenResponse(realmPrivateKey, token); + response.setStatus(200); + response.setContentType("application/json"); + accessTokenResponseWriter.writeValue(response.getOutputStream(), res); + response.getOutputStream().flush(); + } + + protected GenericPrincipal basicAuth(Request request, Response response) throws IOException + { + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authHeader == null) + { + basicAuthError(response); + return null; + } + String[] creds = BasicAuthHelper.parseHeader(authHeader); + if (creds == null) + { + basicAuthError(response); + return null; + } + String username = creds[0]; + String password = creds[1]; + GenericPrincipal gp = (GenericPrincipal) context.getRealm().authenticate(username, password); + if (gp == null) + { + basicAuthError(response); + return null; + } + return gp; + } + + protected void basicAuthError(Response response) throws IOException + { + response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + context.getLoginConfig().getRealmName() + "\""); + response.sendError(401); + } + + protected void redirectAccessCode(boolean sso, Response response, String redirect_uri, String client_id, String state, GenericPrincipal gp) throws IOException + { + SkeletonKeyToken token = buildToken(gp); + AccessCode code = new AccessCode(); + code.setToken(token); + code.setClient(client_id); + code.setSso(sso); + code.setRedirect(redirect_uri); + int expiration = skeletonKeyConfig.getAccessCodeLifetime() == 0 ? 300 : skeletonKeyConfig.getAccessCodeLifetime(); + code.setExpiration((System.currentTimeMillis() / 1000) + expiration); + accessCodeMap.put(code.getId(), code); + log.debug("--- sign access code"); + String accessCode = null; + try + { + accessCode = new JWSBuilder().content(code.getId().getBytes("UTF-8")).rsa256(realmPrivateKey); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e); + } + log.debug("--- build redirect"); + UriBuilder redirectUri = UriBuilder.fromUri(redirect_uri).queryParam("code", accessCode); + if (state != null) redirectUri.queryParam("state", state); + response.sendRedirect(redirectUri.toTemplate()); + log.debug("<--- end oauthAuthenticate"); + } + + protected SkeletonKeyToken buildToken(GenericPrincipal gp) + { + SkeletonKeyToken token = new SkeletonKeyToken(); + token.id(generateId()); + token.principal(gp.getName()); + token.audience(skeletonKeyConfig.getRealm()); + int expiration = skeletonKeyConfig.getAccessCodeLifetime() == 0 ? 3600 : skeletonKeyConfig.getAccessCodeLifetime(); + if (skeletonKeyConfig.getTokenLifetime() > 0) + { + token.expiration((System.currentTimeMillis() / 1000) + expiration); + } + SkeletonKeyToken.Access realmAccess = new SkeletonKeyToken.Access(); + for (String role : gp.getRoles()) + { + realmAccess.addRole(role); + } + token.setRealmAccess(realmAccess); + return token; + } + +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/OAuthManagedResourceValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/OAuthManagedResourceValve.java new file mode 100755 index 0000000000..772607c550 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/OAuthManagedResourceValve.java @@ -0,0 +1,303 @@ +package org.keycloak.adapters.as7; + +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.Session; +import org.apache.catalina.authenticator.Constants; +import org.apache.catalina.authenticator.FormAuthenticator; +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.apache.catalina.realm.GenericPrincipal; +import org.jboss.logging.Logger; +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.resteasy.plugins.providers.RegisterBuiltin; +import org.keycloak.RealmConfiguration; +import org.keycloak.ResourceMetadata; +import org.keycloak.SkeletonKeyPrincipal; +import org.keycloak.SkeletonKeySession; +import org.keycloak.adapters.as7.config.ManagedResourceConfig; +import org.keycloak.adapters.as7.config.ManagedResourceConfigLoader; +import org.keycloak.representations.SkeletonKeyToken; +import org.jboss.resteasy.spi.ResteasyProviderFactory; + +import javax.security.auth.login.LoginException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +/** + * Web deployment whose security is managed by a remote OAuth Skeleton Key authentication server + *

+ * Redirects browser to remote authentication server if not logged in. Also allows OAuth Bearer Token requests + * that contain a Skeleton Key bearer tokens. + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class OAuthManagedResourceValve extends FormAuthenticator implements LifecycleListener +{ + protected RealmConfiguration realmConfiguration; + private static final Logger log = Logger.getLogger(OAuthManagedResourceValve.class); + protected UserSessionManagement userSessionManagement = new UserSessionManagement(); + protected ManagedResourceConfig remoteSkeletonKeyConfig; + protected ResourceMetadata resourceMetadata; + + + @Override + public void start() throws LifecycleException + { + super.start(); + StandardContext standardContext = (StandardContext) context; + standardContext.addLifecycleListener(this); + } + + @Override + public void lifecycleEvent(LifecycleEvent event) + { + if (event.getType() == Lifecycle.AFTER_START_EVENT) init(); + } + + protected void init() + { + ManagedResourceConfigLoader managedResourceConfigLoader = new ManagedResourceConfigLoader(context); + resourceMetadata = managedResourceConfigLoader.getResourceMetadata(); + remoteSkeletonKeyConfig = managedResourceConfigLoader.getRemoteSkeletonKeyConfig(); + String client_id = remoteSkeletonKeyConfig.getClientId(); + if (client_id == null) + { + throw new IllegalArgumentException("Must set client-id to use with auth server"); + } + realmConfiguration = new RealmConfiguration(); + String authUrl = remoteSkeletonKeyConfig.getAuthUrl(); + if (authUrl == null) + { + throw new RuntimeException("You must specify auth-url"); + } + String tokenUrl = remoteSkeletonKeyConfig.getCodeUrl(); + if (tokenUrl == null) + { + throw new RuntimeException("You mut specify code-url"); + } + realmConfiguration.setMetadata(resourceMetadata); + realmConfiguration.setClientId(client_id); + + for (Map.Entry entry : managedResourceConfigLoader.getRemoteSkeletonKeyConfig().getClientCredentials().entrySet()) + { + realmConfiguration.getCredentials().param(entry.getKey(), entry.getValue()); + } + int size = 10; + if (managedResourceConfigLoader.getRemoteSkeletonKeyConfig().getConnectionPoolSize() > 0) + size = managedResourceConfigLoader.getRemoteSkeletonKeyConfig().getConnectionPoolSize(); + ResteasyClientBuilder.HostnameVerificationPolicy policy = ResteasyClientBuilder.HostnameVerificationPolicy.WILDCARD; + if (managedResourceConfigLoader.getRemoteSkeletonKeyConfig().isAllowAnyHostname()) + policy = ResteasyClientBuilder.HostnameVerificationPolicy.ANY; + ResteasyProviderFactory providerFactory = new ResteasyProviderFactory(); + ClassLoader old = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(OAuthManagedResourceValve.class.getClassLoader()); + try + { + ResteasyProviderFactory.getInstance(); // initialize builtins + RegisterBuiltin.register(providerFactory); + } + finally + { + Thread.currentThread().setContextClassLoader(old); + } + ResteasyClient client = new ResteasyClientBuilder() + .providerFactory(providerFactory) + .connectionPoolSize(size) + .hostnameVerification(policy) + .trustStore(resourceMetadata.getTruststore()) + .keyStore(resourceMetadata.getClientKeystore(), resourceMetadata.getClientKeyPassword()) + .build(); + realmConfiguration.setClient(client); + realmConfiguration.setAuthUrl(UriBuilder.fromUri(authUrl).queryParam("client_id", client_id)); + realmConfiguration.setCodeUrl(client.target(tokenUrl)); + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException + { + try + { + String requestURI = request.getDecodedRequestURI(); + if (requestURI.endsWith("j_oauth_remote_logout")) + { + remoteLogout(request, response); + return; + } + super.invoke(request, response); + } + finally + { + ResteasyProviderFactory.clearContextData(); // to clear push of SkeletonKeySession + } + } + + @Override + public boolean authenticate(Request request, HttpServletResponse response, LoginConfig config) throws IOException + { + try + { + if (bearer(false, request, response)) return true; + else if (checkLoggedIn(request, response)) + { + if (request.getSessionInternal().getNote(Constants.FORM_REQUEST_NOTE) != null) + { + if (restoreRequest(request, request.getSessionInternal())) + { + log.debug("restoreRequest"); + return (true); + } + else + { + log.debug("Restore of original request failed"); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return (false); + } + } + else + { + return true; + } + } + + // initiate or continue oauth2 protocol + oauth(request, response); + } + catch (LoginException e) + { + } + return false; + } + + protected void remoteLogout(Request request, HttpServletResponse response) throws IOException + { + try + { + log.debug("->> remoteLogout: "); + if (!bearer(true, request, response)) + { + log.debug("remoteLogout: bearer auth failed"); + return; + } + GenericPrincipal gp = (GenericPrincipal) request.getPrincipal(); + if (!gp.hasRole(remoteSkeletonKeyConfig.getAdminRole())) + { + log.debug("remoteLogout: role failure"); + response.sendError(403); + return; + } + String user = request.getParameter("user"); + if (user != null) + { + userSessionManagement.logout(user); + } + else + { + userSessionManagement.logoutAll(); + } + } + catch (Exception e) + { + log.error("failed to logout", e); + } + response.setStatus(204); + } + + protected boolean bearer(boolean challenge, Request request, HttpServletResponse response) throws LoginException, IOException + { + CatalinaBearerTokenAuthenticator bearer = new CatalinaBearerTokenAuthenticator(realmConfiguration.getMetadata(), !remoteSkeletonKeyConfig.isCancelPropagation(), challenge); + if (bearer.login(request, response)) + { + return true; + } + return false; + } + + protected boolean checkLoggedIn(Request request, HttpServletResponse response) + { + if (request.getSessionInternal() == null || request.getSessionInternal().getPrincipal() == null) + return false; + log.debug("remote logged in already"); + GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal(); + request.setUserPrincipal(principal); + request.setAuthType("OAUTH"); + Session session = request.getSessionInternal(); + if (session != null && !remoteSkeletonKeyConfig.isCancelPropagation()) + { + SkeletonKeySession skSession = (SkeletonKeySession) session.getNote(SkeletonKeySession.class.getName()); + if (skSession != null) + { + request.setAttribute(SkeletonKeySession.class.getName(), skSession); + ResteasyProviderFactory.pushContext(SkeletonKeySession.class, skSession); + + } + } + return true; + } + + /** + * This method always set the HTTP response, so do not continue after invoking + */ + protected void oauth(Request request, HttpServletResponse response) throws IOException + { + ServletOAuthLogin oauth = new ServletOAuthLogin(realmConfiguration, request, response, request.getConnector().getRedirectPort()); + String code = oauth.getCode(); + if (code == null) + { + String error = oauth.getError(); + if (error != null) + { + response.sendError(400, "OAuth " + error); + return; + } + else + { + saveRequest(request, request.getSessionInternal(true)); + oauth.loginRedirect(); + } + return; + } + else + { + if (!oauth.resolveCode(code)) return; + + SkeletonKeyToken token = oauth.getToken(); + Set roles = null; + if (resourceMetadata.getResourceName() != null) + { + SkeletonKeyToken.Access access = token.getResourceAccess(resourceMetadata.getResourceName()); + if (access != null) roles = access.getRoles(); + } + else + { + SkeletonKeyToken.Access access = token.getRealmAccess(); + if (access != null) roles = access.getRoles(); + } + SkeletonKeyPrincipal skp = new SkeletonKeyPrincipal(token.getPrincipal(), null); + GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(context.getRealm(), skp, roles); + Session session = request.getSessionInternal(true); + session.setPrincipal(principal); + session.setAuthType("OAUTH"); + if (!remoteSkeletonKeyConfig.isCancelPropagation()) + { + SkeletonKeySession skSession = new SkeletonKeySession(oauth.getTokenString(), realmConfiguration.getMetadata()); + session.setNote(SkeletonKeySession.class.getName(), skSession); + } + + String username = token.getPrincipal(); + log.debug("userSessionManage.login: " + username); + userSessionManagement.login(session, username); + } + } + +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java new file mode 100755 index 0000000000..4a7d8c19b5 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/ServletOAuthLogin.java @@ -0,0 +1,320 @@ +package org.keycloak.adapters.as7; + +import org.jboss.logging.Logger; +import org.keycloak.RSATokenVerifier; +import org.keycloak.RealmConfiguration; +import org.keycloak.VerificationException; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.SkeletonKeyToken; +import org.jboss.resteasy.util.BasicAuthHelper; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +/** + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ServletOAuthLogin +{ + private static final Logger log = Logger.getLogger(ServletOAuthLogin.class); + protected HttpServletRequest request; + protected HttpServletResponse response; + protected boolean codePresent; + protected RealmConfiguration realmInfo; + protected int redirectPort; + protected String tokenString; + protected SkeletonKeyToken token; + + public ServletOAuthLogin(RealmConfiguration realmInfo, HttpServletRequest request, HttpServletResponse response, int redirectPort) + { + this.request = request; + this.response = response; + this.realmInfo = realmInfo; + this.redirectPort = redirectPort; + } + + public String getTokenString() + { + return tokenString; + } + + public SkeletonKeyToken getToken() + { + return token; + } + + public RealmConfiguration getRealmInfo() + { + return realmInfo; + } + + protected String getDefaultCookiePath() + { + String path = request.getContextPath(); + if ("".equals(path) || path == null) path = "/"; + return path; + } + + protected String getRequestUrl() + { + return request.getRequestURL().toString(); + } + + protected boolean isRequestSecure() + { + return request.isSecure(); + } + + protected void sendError(int code) + { + try + { + response.sendError(code); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + protected void sendRedirect(String url) + { + try + { + response.sendRedirect(url); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + protected Cookie getCookie(String cookieName) + { + if (request.getCookies() == null) return null; + for (Cookie cookie : request.getCookies()) + { + if (cookie.getName().equals(cookieName)) + { + return cookie; + } + } + return null; + } + + protected String getCookieValue(String cookieName) + { + Cookie cookie = getCookie(cookieName); + if (cookie == null) return null; + return cookie.getValue(); + } + + protected String getQueryParamValue(String paramName) + { + String query = request.getQueryString(); + if (query == null) return null; + String[] params = query.split("&"); + for (String param : params) + { + int eq = param.indexOf('='); + if (eq == -1) continue; + String name = param.substring(0, eq); + if (!name.equals(paramName)) continue; + return param.substring(eq + 1); + } + return null; + } + + public String getError() + { + return getQueryParamValue("error"); + } + + public String getCode() + { + return getQueryParamValue("code"); + } + + protected void setCookie(String name, String value, String domain, String path, boolean secure) + { + Cookie cookie = new Cookie(name, value); + if (domain != null) cookie.setDomain(domain); + if (path != null) cookie.setPath(path); + if (secure) cookie.setSecure(true); + response.addCookie(cookie); + } + + protected String getRedirectUri(String state) + { + String url = getRequestUrl(); + if (!isRequestSecure() && realmInfo.isSslRequired()) + { + int port = redirectPort; + if (port < 0) + { + // disabled? + return null; + } + UriBuilder secureUrl = UriBuilder.fromUri(url).scheme("https").port(-1); + if (port != 443) secureUrl.port(port); + url = secureUrl.build().toString(); + } + return realmInfo.getAuthUrl().clone() + .queryParam("client_id", realmInfo.getClientId()) + .queryParam("redirect_uri", url) + .queryParam("state", state) + .queryParam("login", "true") + .build().toString(); + } + + protected static final AtomicLong counter = new AtomicLong(); + + protected String getStateCode() + { + return counter.getAndIncrement() + "/" + UUID.randomUUID().toString(); + } + + public void loginRedirect() + { + String state = getStateCode(); + String redirect = getRedirectUri(state); + if (redirect == null) + { + sendError(Response.Status.FORBIDDEN.getStatusCode()); + return; + } + setCookie(realmInfo.getStateCookieName(), state, null, getDefaultCookiePath(), realmInfo.isSslRequired()); + sendRedirect(redirect); + } + + public boolean checkStateCookie() + { + Cookie stateCookie = getCookie(realmInfo.getStateCookieName()); + + if (stateCookie == null) + { + sendError(400); + log.warn("No state cookie"); + return false; + } + // reset the cookie + Cookie reset = new Cookie(stateCookie.getName(), stateCookie.getValue()); + reset.setPath(stateCookie.getPath()); + reset.setMaxAge(0); + response.addCookie(reset); + + String stateCookieValue = getCookieValue(realmInfo.getStateCookieName()); + // its ok to call request.getParameter() because this should be a redirect + String state = request.getParameter("state"); + if (state == null) + { + sendError(400); + log.warn("state parameter was null"); + return false; + } + if (!state.equals(stateCookieValue)) + { + sendError(400); + log.warn("state parameter invalid"); + log.warn("cookie: " + stateCookieValue); + log.warn("queryParam: " + state); + return false; + } + return true; + + } + + /** + * Start or continue the oauth login process. + * + * if code query parameter is not present, then browser is redirected to authUrl. The redirect URL will be + * the URL of the current request. + * + * If code query parameter is present, then an access token is obtained by invoking a secure request to the codeUrl. + * If the access token is obtained, the browser is again redirected to the current request URL, but any OAuth + * protocol specific query parameters are removed. + * + * @return true if an access token was obtained + */ + public boolean resolveCode(String code) + { + // abort if not HTTPS + if (realmInfo.isSslRequired() && !isRequestSecure()) + { + log.error("SSL is required"); + sendError(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + + if (!checkStateCookie()) return false; + + String client_id = realmInfo.getClientId(); + String password = realmInfo.getCredentials().asMap().getFirst("password"); + String authHeader = BasicAuthHelper.createHeader(client_id, password); + String redirectUri = stripOauthParametersFromRedirect(); + Form form = new Form(); + form.param("grant_type", "authorization_code") + .param("code", code) + .param("redirect_uri", redirectUri); + + Response res = realmInfo.getCodeUrl().request().header(HttpHeaders.AUTHORIZATION, authHeader).post(Entity.form(form)); + AccessTokenResponse tokenResponse; + try + { + if (res.getStatus() != 200) + { + log.error("failed to turn code into token"); + sendError(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + log.debug("media type: " + res.getMediaType()); + log.debug("Content-Type header: " + res.getHeaderString("Content-Type")); + tokenResponse = res.readEntity(AccessTokenResponse.class); + } + finally + { + res.close(); + } + + tokenString = tokenResponse.getToken(); + try + { + token = RSATokenVerifier.verifyToken(tokenString, realmInfo.getMetadata()); + log.debug("Verification succeeded!"); + } + catch (VerificationException e) + { + log.error("failed verification of token"); + sendError(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + // redirect to URL without oauth query parameters + sendRedirect(redirectUri); + return true; + } + + /** + * strip out unwanted query parameters and redirect so bookmarks don't retain oauth protocol bits + */ + protected String stripOauthParametersFromRedirect() + { + StringBuffer buf = request.getRequestURL().append("?").append(request.getQueryString()); + UriBuilder builder = UriBuilder.fromUri(buf.toString()) + .replaceQueryParam("code", null) + .replaceQueryParam("state", null); + return builder.build().toString(); + } + + +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/UserSessionManagement.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/UserSessionManagement.java new file mode 100755 index 0000000000..081df776c6 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/UserSessionManagement.java @@ -0,0 +1,111 @@ +package org.keycloak.adapters.as7; + +import org.apache.catalina.Session; +import org.apache.catalina.SessionEvent; +import org.apache.catalina.SessionListener; +import org.apache.catalina.realm.GenericPrincipal; +import org.jboss.logging.Logger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages relationship to users and sessions so that forced admin logout can be implemented + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserSessionManagement implements SessionListener +{ + private static final Logger log = Logger.getLogger(UserSessionManagement.class); + protected ConcurrentHashMap> userSessionMap = new ConcurrentHashMap>(); + + protected void login(Session session, String username) + { + Map map = userSessionMap.get(username); + if (map == null) + { + final Map value = new HashMap(); + map = userSessionMap.putIfAbsent(username, value); + if (map == null) + { + map = value; + } + } + synchronized (map) + { + map.put(session.getId(), session); + } + session.addSessionListener(this); + } + + public void logoutAll() + { + List users = new ArrayList(); + users.addAll(userSessionMap.keySet()); + for (String user : users) logout(user); + } + + public void logoutAllBut(String but) + { + List users = new ArrayList(); + users.addAll(userSessionMap.keySet()); + for (String user : users) + { + if (!but.equals(user)) logout(user); + } + } + + + public void logout(String user) + { + log.debug("logoutUser: " + user); + Map map = userSessionMap.remove(user); + if (map == null) + { + log.debug("no session for user: " + user); + return; + } + log.debug("found session for user"); + synchronized (map) + { + for (Session session : map.values()) + { + log.debug("invalidating session for user: " + user); + session.setPrincipal(null); + session.setAuthType(null); + session.getSession().invalidate(); + } + } + + } + + public void sessionEvent(SessionEvent event) + { + // We only care about session destroyed events + if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()) + && (!Session.SESSION_PASSIVATED_EVENT.equals(event.getType()))) + return; + + // Look up the single session id associated with this session (if any) + Session session = event.getSession(); + GenericPrincipal principal = (GenericPrincipal) session.getPrincipal(); + if (principal == null) return; + session.setPrincipal(null); + session.setAuthType(null); + + String username = principal.getUserPrincipal().getName(); + Map map = userSessionMap.get(username); + if (map == null) return; + synchronized (map) + { + map.remove(session.getId()); + if (map.isEmpty()) userSessionMap.remove(username); + } + + + } +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/AuthServerConfig.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/AuthServerConfig.java new file mode 100755 index 0000000000..73372e3c83 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/AuthServerConfig.java @@ -0,0 +1,279 @@ +package org.keycloak.adapters.as7.config; + +import org.codehaus.jackson.annotate.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class AuthServerConfig +{ + @JsonProperty("realm") + protected String realm; + + @JsonProperty("realm-private-key") + protected String realmPrivateKey; + + @JsonProperty("realm-public-key") + protected String realmPublicKey; + + @JsonProperty("realm-keystore") + protected String realmKeyStore; + + @JsonProperty("realm-keystore-password") + protected String realmKeystorePassword; + + @JsonProperty("realm-key-alias") + protected String realmKeyAlias; + + @JsonProperty("realm-private-key-password") + protected String realmPrivateKeyPassword; + + @JsonProperty("access-code-lifetime") + protected int accessCodeLifetime; + + @JsonProperty("token-lifetime") + protected int tokenLifetime; + + @JsonProperty("admin-role") + protected String adminRole; + + @JsonProperty("login-role") + protected String loginRole; + + @JsonProperty("oauth-client-role") + protected String clientRole; + + @JsonProperty("wildcard-role") + protected String wildcardRole; + + @JsonProperty("cancel-propagation") + protected boolean cancelPropagation; + + @JsonProperty("sso-disabled") + protected boolean ssoDisabled; + + // these properties are optional and used to provide connection metadata when the server wants to make + // remote SSL connections + + protected String truststore; + @JsonProperty("truststore-password") + protected String truststorePassword; + @JsonProperty("client-keystore") + protected String clientKeystore; + @JsonProperty("client-keystore-password") + protected String clientKeystorePassword; + @JsonProperty("client-key-password") + protected String clientKeyPassword; + + protected List resources = new ArrayList(); + + + public String getRealm() + { + return realm; + } + + public void setRealm(String realm) + { + this.realm = realm; + } + + public String getRealmPrivateKey() + { + return realmPrivateKey; + } + + public void setRealmPrivateKey(String realmPrivateKey) + { + this.realmPrivateKey = realmPrivateKey; + } + + public String getRealmPublicKey() + { + return realmPublicKey; + } + + public void setRealmPublicKey(String realmPublicKey) + { + this.realmPublicKey = realmPublicKey; + } + + public int getAccessCodeLifetime() + { + return accessCodeLifetime; + } + + public void setAccessCodeLifetime(int accessCodeLifetime) + { + this.accessCodeLifetime = accessCodeLifetime; + } + + public String getTruststore() + { + return truststore; + } + + public void setTruststore(String truststore) + { + this.truststore = truststore; + } + + public String getTruststorePassword() + { + return truststorePassword; + } + + public void setTruststorePassword(String truststorePassword) + { + this.truststorePassword = truststorePassword; + } + + public String getClientKeystore() + { + return clientKeystore; + } + + public void setClientKeystore(String clientKeystore) + { + this.clientKeystore = clientKeystore; + } + + public String getClientKeystorePassword() + { + return clientKeystorePassword; + } + + public void setClientKeystorePassword(String clientKeystorePassword) + { + this.clientKeystorePassword = clientKeystorePassword; + } + + public String getClientKeyPassword() + { + return clientKeyPassword; + } + + public void setClientKeyPassword(String clientKeyPassword) + { + this.clientKeyPassword = clientKeyPassword; + } + + public boolean isCancelPropagation() + { + return cancelPropagation; + } + + public void setCancelPropagation(boolean cancelPropagation) + { + this.cancelPropagation = cancelPropagation; + } + + public boolean isSsoDisabled() + { + return ssoDisabled; + } + + public void setSsoDisabled(boolean ssoDisabled) + { + this.ssoDisabled = ssoDisabled; + } + + public List getResources() + { + return resources; + } + + public String getAdminRole() + { + return adminRole; + } + + public void setAdminRole(String adminRole) + { + this.adminRole = adminRole; + } + + public String getLoginRole() + { + return loginRole; + } + + public void setLoginRole(String loginRole) + { + this.loginRole = loginRole; + } + + public String getClientRole() + { + return clientRole; + } + + public void setClientRole(String clientRole) + { + this.clientRole = clientRole; + } + + public String getWildcardRole() + { + return wildcardRole; + } + + public void setWildcardRole(String wildcardRole) + { + this.wildcardRole = wildcardRole; + } + + public String getRealmKeyStore() + { + return realmKeyStore; + } + + public void setRealmKeyStore(String realmKeyStore) + { + this.realmKeyStore = realmKeyStore; + } + + public String getRealmKeystorePassword() + { + return realmKeystorePassword; + } + + public void setRealmKeystorePassword(String realmKeystorePassword) + { + this.realmKeystorePassword = realmKeystorePassword; + } + + public String getRealmKeyAlias() + { + return realmKeyAlias; + } + + public void setRealmKeyAlias(String realmKeyAlias) + { + this.realmKeyAlias = realmKeyAlias; + } + + public String getRealmPrivateKeyPassword() + { + return realmPrivateKeyPassword; + } + + public void setRealmPrivateKeyPassword(String realmPrivateKeyPassword) + { + this.realmPrivateKeyPassword = realmPrivateKeyPassword; + } + + public int getTokenLifetime() + { + return tokenLifetime; + } + + public void setTokenLifetime(int tokenLifetime) + { + this.tokenLifetime = tokenLifetime; + } +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/ManagedResourceConfig.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/ManagedResourceConfig.java new file mode 100755 index 0000000000..e1bef4bbec --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/ManagedResourceConfig.java @@ -0,0 +1,213 @@ +package org.keycloak.adapters.as7.config; + +import org.codehaus.jackson.annotate.JsonProperty; +import org.codehaus.jackson.annotate.JsonPropertyOrder; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@JsonPropertyOrder({"realm", "resource", "realm-public-key", "admin-role", "auth-url", "code-url", "truststore", "truststore-password", "client-id", "client-credentials"}) +public class ManagedResourceConfig +{ + @JsonProperty("realm") + protected String realm; + @JsonProperty("resource") + protected String resource; + + @JsonProperty("realm-public-key") + protected String realmKey; + + @JsonProperty("admin-role") + protected String adminRole; + + @JsonProperty("auth-url") + protected String authUrl; + @JsonProperty("code-url") + protected String codeUrl; + + @JsonProperty("allow-any-hostname") + protected boolean allowAnyHostname; + + @JsonProperty("truststore") + protected String truststore; + + @JsonProperty("truststore-password") + protected String truststorePassword; + @JsonProperty("client-id") + protected String clientId; + @JsonProperty("client-keystore") + protected String clientKeystore; + @JsonProperty("client-keystore-password") + protected String clientKeystorePassword; + @JsonProperty("client-key-password") + protected String clientKeyPassword; + + @JsonProperty("client-credentials") + protected Map clientCredentials = new HashMap(); + + @JsonProperty("connection-pool-size") + protected int connectionPoolSize; + + @JsonProperty("cancel-propagation") + protected boolean cancelPropagation; + + + public String getRealm() + { + return realm; + } + + public void setRealm(String realm) + { + this.realm = realm; + } + + public String getResource() + { + return resource; + } + + public void setResource(String resource) + { + this.resource = resource; + } + + public String getRealmKey() + { + return realmKey; + } + + public void setRealmKey(String realmKey) + { + this.realmKey = realmKey; + } + + public String getAuthUrl() + { + return authUrl; + } + + public void setAuthUrl(String authUrl) + { + this.authUrl = authUrl; + } + + public String getCodeUrl() + { + return codeUrl; + } + + public void setCodeUrl(String codeUrl) + { + this.codeUrl = codeUrl; + } + + public boolean isAllowAnyHostname() + { + return allowAnyHostname; + } + + public void setAllowAnyHostname(boolean allowAnyHostname) + { + this.allowAnyHostname = allowAnyHostname; + } + + public String getTruststore() + { + return truststore; + } + + public void setTruststore(String truststore) + { + this.truststore = truststore; + } + + public String getTruststorePassword() + { + return truststorePassword; + } + + public void setTruststorePassword(String truststorePassword) + { + this.truststorePassword = truststorePassword; + } + + public String getClientId() + { + return clientId; + } + + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + public Map getClientCredentials() + { + return clientCredentials; + } + + public String getClientKeystore() + { + return clientKeystore; + } + + public void setClientKeystore(String clientKeystore) + { + this.clientKeystore = clientKeystore; + } + + public String getClientKeystorePassword() + { + return clientKeystorePassword; + } + + public void setClientKeystorePassword(String clientKeystorePassword) + { + this.clientKeystorePassword = clientKeystorePassword; + } + + public String getClientKeyPassword() + { + return clientKeyPassword; + } + + public void setClientKeyPassword(String clientKeyPassword) + { + this.clientKeyPassword = clientKeyPassword; + } + + public int getConnectionPoolSize() + { + return connectionPoolSize; + } + + public void setConnectionPoolSize(int connectionPoolSize) + { + this.connectionPoolSize = connectionPoolSize; + } + + public boolean isCancelPropagation() + { + return cancelPropagation; + } + + public void setCancelPropagation(boolean cancelPropagation) + { + this.cancelPropagation = cancelPropagation; + } + + public String getAdminRole() + { + return adminRole; + } + + public void setAdminRole(String adminRole) + { + this.adminRole = adminRole; + } +} diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/ManagedResourceConfigLoader.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/ManagedResourceConfigLoader.java new file mode 100755 index 0000000000..75339d1054 --- /dev/null +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/config/ManagedResourceConfigLoader.java @@ -0,0 +1,139 @@ +package org.keycloak.adapters.as7.config; + +import org.apache.catalina.Context; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.jboss.logging.Logger; +import org.keycloak.EnvUtil; +import org.keycloak.PemUtils; +import org.keycloak.ResourceMetadata; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.PublicKey; + +public class ManagedResourceConfigLoader +{ + static final Logger log = Logger.getLogger(ManagedResourceConfigLoader.class); + protected ManagedResourceConfig remoteSkeletonKeyConfig; + protected ResourceMetadata resourceMetadata; + + public ManagedResourceConfigLoader(Context context) + { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT); + InputStream is = null; + String path = context.getServletContext().getInitParameter("skeleton.key.config.file"); + if (path == null) + { + is = context.getServletContext().getResourceAsStream("/WEB-INF/resteasy-oauth.json"); + } + else + { + try + { + is = new FileInputStream(path); + } + catch (FileNotFoundException e) + { + throw new RuntimeException(e); + } + } + remoteSkeletonKeyConfig = null; + try + { + remoteSkeletonKeyConfig = mapper.readValue(is, ManagedResourceConfig.class); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + + String name = remoteSkeletonKeyConfig.getResource(); + String realm = remoteSkeletonKeyConfig.getRealm(); + if (realm == null) throw new RuntimeException("Must set 'realm' in config"); + + String realmKeyPem = remoteSkeletonKeyConfig.getRealmKey(); + if (realmKeyPem == null) + { + throw new IllegalArgumentException("You must set the realm-public-key"); + } + + PublicKey realmKey = null; + try + { + realmKey = PemUtils.decodePublicKey(realmKeyPem); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + resourceMetadata = new ResourceMetadata(); + resourceMetadata.setRealm(realm); + resourceMetadata.setResourceName(name); + resourceMetadata.setRealmKey(realmKey); + + + String truststore = remoteSkeletonKeyConfig.getTruststore(); + if (truststore != null) + { + truststore = EnvUtil.replace(truststore); + String truststorePassword = remoteSkeletonKeyConfig.getTruststorePassword(); + KeyStore trust = null; + try + { + trust = loadKeyStore(truststore, truststorePassword); + } + catch (Exception e) + { + throw new RuntimeException("Failed to load truststore", e); + } + resourceMetadata.setTruststore(trust); + } + String clientKeystore = remoteSkeletonKeyConfig.getClientKeystore(); + String clientKeyPassword = null; + if (clientKeystore != null) + { + clientKeystore = EnvUtil.replace(clientKeystore); + String clientKeystorePassword = remoteSkeletonKeyConfig.getClientKeystorePassword(); + KeyStore serverKS = null; + try + { + serverKS = loadKeyStore(clientKeystore, clientKeystorePassword); + } + catch (Exception e) + { + throw new RuntimeException("Failed to load keystore", e); + } + resourceMetadata.setClientKeystore(serverKS); + clientKeyPassword = remoteSkeletonKeyConfig.getClientKeyPassword(); + resourceMetadata.setClientKeyPassword(clientKeyPassword); + } + + } + public static KeyStore loadKeyStore(String filename, String password) throws Exception + { + KeyStore trustStore = KeyStore.getInstance(KeyStore + .getDefaultType()); + File truststoreFile = new File(filename); + FileInputStream trustStream = new FileInputStream(truststoreFile); + trustStore.load(trustStream, password.toCharArray()); + trustStream.close(); + return trustStore; + } + + public ManagedResourceConfig getRemoteSkeletonKeyConfig() + { + return remoteSkeletonKeyConfig; + } + + public ResourceMetadata getResourceMetadata() + { + return resourceMetadata; + } + +} \ No newline at end of file diff --git a/integration/pom.xml b/integration/pom.xml new file mode 100755 index 0000000000..5028c1d59e --- /dev/null +++ b/integration/pom.xml @@ -0,0 +1,21 @@ + + + keycloak-parent + org.keycloak + 1.0-alpha-1 + ../pom.xml + + Keycloak Integration + + 4.0.0 + + org.keycloak + integration-pom + pom + + + as7-eap6/adapter + + + diff --git a/pom.xml b/pom.xml index 53b9656504..bcb889c1f5 100755 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ core services + integration diff --git a/services/src/main/java/org/keycloak/services/managers/InstallationManager.java b/services/src/main/java/org/keycloak/services/managers/InstallationManager.java index 4550ce6ec7..61545475a8 100755 --- a/services/src/main/java/org/keycloak/services/managers/InstallationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/InstallationManager.java @@ -20,10 +20,14 @@ public class InstallationManager { defaultRealm.setAccessCodeLifespan(60); defaultRealm.setSslNotRequired(false); defaultRealm.setCookieLoginAllowed(true); + defaultRealm.setRegistrationAllowed(true); manager.generateRealmKeys(defaultRealm); defaultRealm.updateRealm(); defaultRealm.addRequiredCredential(RequiredCredentialModel.PASSWORD); defaultRealm.getIdm().add(new SimpleRole(RegistrationService.REALM_CREATOR_ROLE)); + } + public boolean isInstalled(RealmManager manager) { + return manager.defaultRealm() != null; } } diff --git a/services/src/main/java/org/keycloak/services/models/RealmModel.java b/services/src/main/java/org/keycloak/services/models/RealmModel.java index 235bba19cc..0cdf45ca01 100755 --- a/services/src/main/java/org/keycloak/services/models/RealmModel.java +++ b/services/src/main/java/org/keycloak/services/models/RealmModel.java @@ -17,7 +17,6 @@ import org.picketlink.idm.model.Attribute; import org.picketlink.idm.model.Grant; import org.picketlink.idm.model.Realm; import org.picketlink.idm.model.Role; -import org.picketlink.idm.model.SimpleAgent; import org.picketlink.idm.model.Tier; import org.picketlink.idm.model.User; import org.picketlink.idm.query.IdentityQuery; @@ -48,21 +47,22 @@ public class RealmModel { public static final String REALM_PUBLIC_KEY = "publicKey"; public static final String REALM_IS_SSL_NOT_REQUIRED = "isSSLNotRequired"; public static final String REALM_IS_COOKIE_LOGIN_ALLOWED = "isCookieLoginAllowed"; + public static final String REALM_IS_REGISTRATION_ALLOWED = "isRegistrationAllowed"; protected Realm realm; protected Agent realmAgent; - protected IdentitySession IdentitySession; + protected IdentitySession identitySession; protected volatile transient PublicKey publicKey; protected volatile transient PrivateKey privateKey; - public RealmModel(Realm realm, IdentitySession factory) { + public RealmModel(Realm realm, IdentitySession session) { this.realm = realm; - this.IdentitySession = factory; + this.identitySession = session; realmAgent = getIdm().getAgent(REALM_AGENT_ID); } public IdentityManager getIdm() { - return IdentitySession.createIdentityManager(realm); + return identitySession.createIdentityManager(realm); } public void updateRealm() { @@ -105,6 +105,14 @@ public class RealmModel { realmAgent.setAttribute(new Attribute(REALM_IS_COOKIE_LOGIN_ALLOWED, cookieLoginAllowed)); } + public boolean isRegistrationAllowed() { + return (Boolean) realmAgent.getAttribute(REALM_IS_REGISTRATION_ALLOWED).getValue(); + } + + public void setRegistrationAllowed(boolean registrationAllowed) { + realmAgent.setAttribute(new Attribute(REALM_IS_REGISTRATION_ALLOWED, registrationAllowed)); + } + public long getTokenLifespan() { return (Long) realmAgent.getAttribute(REALM_TOKEN_LIFESPAN).getValue(); } @@ -269,8 +277,8 @@ public class RealmModel { List results = query.getResultList(); List resources = new ArrayList(); for (ResourceRelationship relationship : results) { - Tier resourceTier = IdentitySession.findTier(relationship.getResourceId()); - ResourceModel model = new ResourceModel(resourceTier,relationship, this, IdentitySession); + Tier resourceTier = identitySession.findTier(relationship.getResourceId()); + ResourceModel model = new ResourceModel(resourceTier,relationship, this, identitySession); resources.add(model); } @@ -278,14 +286,14 @@ public class RealmModel { } public ResourceModel addResource(String name) { - Tier newTier = IdentitySession.createTier(RealmManager.generateId()); + Tier newTier = identitySession.createTier(RealmManager.generateId()); IdentityManager idm = getIdm(); ResourceRelationship relationship = new ResourceRelationship(); relationship.setResourceName(name); relationship.setRealmAgent(realmAgent); relationship.setResourceId(newTier.getId()); idm.add(relationship); - return new ResourceModel(newTier, relationship, this, IdentitySession); + return new ResourceModel(newTier, relationship, this, identitySession); } public Set getRoleMappings(User user) { @@ -322,7 +330,7 @@ public class RealmModel { } public boolean isRealmAdmin(Agent agent) { - IdentityManager idm = new RealmManager(IdentitySession).defaultRealm().getIdm(); + IdentityManager idm = new RealmManager(identitySession).defaultRealm().getIdm(); RelationshipQuery query = idm.createRelationshipQuery(RealmAdminRelationship.class); query.setParameter(RealmAdminRelationship.REALM, realm.getId()); query.setParameter(RealmAdminRelationship.ADMIN, agent); @@ -331,7 +339,7 @@ public class RealmModel { } public void addRealmAdmin(Agent agent) { - IdentityManager idm = new RealmManager(IdentitySession).defaultRealm().getIdm(); + IdentityManager idm = new RealmManager(identitySession).defaultRealm().getIdm(); RealmAdminRelationship relationship = new RealmAdminRelationship(); relationship.setAdmin(agent); relationship.setRealm(realm.getId()); diff --git a/services/src/main/java/org/keycloak/services/resources/RegistrationService.java b/services/src/main/java/org/keycloak/services/resources/RegistrationService.java index 3718f76aa5..55c576c3f8 100755 --- a/services/src/main/java/org/keycloak/services/resources/RegistrationService.java +++ b/services/src/main/java/org/keycloak/services/resources/RegistrationService.java @@ -11,6 +11,7 @@ import org.picketlink.idm.model.SimpleUser; import org.picketlink.idm.model.User; import javax.ws.rs.Consumes; +import javax.ws.rs.ForbiddenException; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.core.Context; @@ -32,15 +33,21 @@ public class RegistrationService { protected UriInfo uriInfo; @Context - protected IdentitySession IdentitySession; + protected IdentitySession identitySession; @POST @Consumes(MediaType.APPLICATION_JSON) public Response register(UserRepresentation newUser) { - IdentitySession.getTransaction().begin(); + identitySession.getTransaction().begin(); try { - RealmManager realmManager = new RealmManager(IdentitySession); + RealmManager realmManager = new RealmManager(identitySession); RealmModel defaultRealm = realmManager.defaultRealm(); + if (!defaultRealm.isEnabled()) { + throw new ForbiddenException(); + } + if (!defaultRealm.isRegistrationAllowed()) { + throw new ForbiddenException(); + } User user = defaultRealm.getIdm().getUser(newUser.getUsername()); if (user != null) { return Response.status(400).type("text/plain").entity("user exists").build(); @@ -56,12 +63,12 @@ public class RegistrationService { } Role realmCreator = defaultRealm.getIdm().getRole(REALM_CREATOR_ROLE); defaultRealm.getIdm().grantRole(user, realmCreator); - IdentitySession.getTransaction().commit(); + identitySession.getTransaction().commit(); URI uri = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(user.getLoginName()).build(); return Response.created(uri).build(); } catch (RuntimeException e) { logger.error("Failed to register", e); - IdentitySession.getTransaction().rollback(); + identitySession.getTransaction().rollback(); throw e; } }