From 7a1c825dcbeaa939a744f8b77facef25782501da Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 2 Jul 2013 10:05:33 -0400 Subject: [PATCH] initial --- .gitignore | 32 + core/pom.xml | 60 ++ .../org/keycloak/AbstractOAuthClient.java | 162 +++++ .../java/org/keycloak/BouncyIntegration.java | 22 + core/src/main/java/org/keycloak/DerUtils.java | 68 ++ core/src/main/java/org/keycloak/EnvUtil.java | 36 ++ core/src/main/java/org/keycloak/PemUtils.java | 117 ++++ .../java/org/keycloak/RSATokenVerifier.java | 59 ++ .../java/org/keycloak/RealmConfiguration.java | 98 +++ .../java/org/keycloak/ResourceMetadata.java | 94 +++ .../keycloak/SkeletonKeyContextResolver.java | 42 ++ .../org/keycloak/SkeletonKeyPrincipal.java | 58 ++ .../java/org/keycloak/SkeletonKeySession.java | 34 + .../org/keycloak/VerificationException.java | 27 + .../jaxrs/JaxrsBearerTokenFilter.java | 129 ++++ .../org/keycloak/jaxrs/JaxrsOAuthClient.java | 55 ++ .../representations/AccessTokenResponse.java | 64 ++ .../representations/SkeletonKeyScope.java | 13 + .../representations/SkeletonKeyToken.java | 202 ++++++ .../idm/PublishedRealmRepresentation.java | 136 ++++ .../idm/RealmRepresentation.java | 171 +++++ .../idm/RequiredCredentialRepresentation.java | 46 ++ .../idm/ResourceRepresentation.java | 97 +++ .../idm/RoleMappingRepresentation.java | 74 +++ .../idm/ScopeMappingRepresentation.java | 55 ++ .../idm/UserRepresentation.java | 124 ++++ .../keycloak/servlet/ServletOAuthClient.java | 127 ++++ .../java/org/keycloak/RSAVerifierTest.java | 282 +++++++++ .../org/keycloak/SkeletonKeyTokenTest.java | 78 +++ pom.xml | 301 +++++++++ services/pom.xml | 111 ++++ .../services/IdentityManagerAdapter.java | 84 +++ .../keycloak/services/model/RealmManager.java | 49 ++ .../keycloak/services/model/RealmModel.java | 389 ++++++++++++ .../model/RealmResourceRelationship.java | 57 ++ .../model/RequiredCredentialModel.java | 42 ++ .../model/RequiredCredentialRelationship.java | 79 +++ .../services/model/ResourceModel.java | 134 ++++ .../services/model/ScopeRelationship.java | 50 ++ .../services/model/UserCredentialModel.java | 32 + .../services/model/data/RealmModel.java | 194 ++++++ .../model/data/RequiredCredentialModel.java | 58 ++ .../services/model/data/ResourceModel.java | 46 ++ .../services/model/data/RoleMappingModel.java | 59 ++ .../services/model/data/RoleModel.java | 35 ++ .../model/data/ScopeMappingModel.java | 49 ++ .../model/data/UserAttributeModel.java | 46 ++ .../model/data/UserCredentialModel.java | 58 ++ .../services/model/data/UserModel.java | 46 ++ .../service/AuthenticationManager.java | 87 +++ .../services/service/RealmFactory.java | 354 +++++++++++ .../services/service/RealmResource.java | 158 +++++ .../services/service/RegistrationService.java | 65 ++ .../service/SkeletonKeyApplication.java | 57 ++ .../services/service/TokenManager.java | 176 ++++++ .../services/service/TokenService.java | 593 ++++++++++++++++++ .../java/org/keycloak/test/AdapterTest.java | 159 +++++ .../test/resources/META-INF/persistence.xml | 29 + services/src/test/resources/testrealm.json | 101 +++ 59 files changed, 6260 insertions(+) create mode 100644 .gitignore create mode 100755 core/pom.xml create mode 100755 core/src/main/java/org/keycloak/AbstractOAuthClient.java create mode 100755 core/src/main/java/org/keycloak/BouncyIntegration.java create mode 100755 core/src/main/java/org/keycloak/DerUtils.java create mode 100755 core/src/main/java/org/keycloak/EnvUtil.java create mode 100755 core/src/main/java/org/keycloak/PemUtils.java create mode 100755 core/src/main/java/org/keycloak/RSATokenVerifier.java create mode 100755 core/src/main/java/org/keycloak/RealmConfiguration.java create mode 100755 core/src/main/java/org/keycloak/ResourceMetadata.java create mode 100755 core/src/main/java/org/keycloak/SkeletonKeyContextResolver.java create mode 100755 core/src/main/java/org/keycloak/SkeletonKeyPrincipal.java create mode 100755 core/src/main/java/org/keycloak/SkeletonKeySession.java create mode 100755 core/src/main/java/org/keycloak/VerificationException.java create mode 100755 core/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java create mode 100755 core/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java create mode 100755 core/src/main/java/org/keycloak/representations/AccessTokenResponse.java create mode 100755 core/src/main/java/org/keycloak/representations/SkeletonKeyScope.java create mode 100755 core/src/main/java/org/keycloak/representations/SkeletonKeyToken.java create mode 100755 core/src/main/java/org/keycloak/representations/idm/PublishedRealmRepresentation.java create mode 100755 core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java create mode 100755 core/src/main/java/org/keycloak/representations/idm/RequiredCredentialRepresentation.java create mode 100755 core/src/main/java/org/keycloak/representations/idm/ResourceRepresentation.java create mode 100755 core/src/main/java/org/keycloak/representations/idm/RoleMappingRepresentation.java create mode 100755 core/src/main/java/org/keycloak/representations/idm/ScopeMappingRepresentation.java create mode 100755 core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java create mode 100755 core/src/main/java/org/keycloak/servlet/ServletOAuthClient.java create mode 100755 core/src/test/java/org/keycloak/RSAVerifierTest.java create mode 100755 core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java create mode 100755 pom.xml create mode 100755 services/pom.xml create mode 100755 services/src/main/java/org/keycloak/services/IdentityManagerAdapter.java create mode 100755 services/src/main/java/org/keycloak/services/model/RealmManager.java create mode 100755 services/src/main/java/org/keycloak/services/model/RealmModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/RealmResourceRelationship.java create mode 100755 services/src/main/java/org/keycloak/services/model/RequiredCredentialModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/RequiredCredentialRelationship.java create mode 100755 services/src/main/java/org/keycloak/services/model/ResourceModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/ScopeRelationship.java create mode 100755 services/src/main/java/org/keycloak/services/model/UserCredentialModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/data/RealmModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/data/RequiredCredentialModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/data/ResourceModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/data/RoleMappingModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/data/RoleModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/data/ScopeMappingModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/data/UserAttributeModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/data/UserCredentialModel.java create mode 100755 services/src/main/java/org/keycloak/services/model/data/UserModel.java create mode 100755 services/src/main/java/org/keycloak/services/service/AuthenticationManager.java create mode 100755 services/src/main/java/org/keycloak/services/service/RealmFactory.java create mode 100755 services/src/main/java/org/keycloak/services/service/RealmResource.java create mode 100755 services/src/main/java/org/keycloak/services/service/RegistrationService.java create mode 100755 services/src/main/java/org/keycloak/services/service/SkeletonKeyApplication.java create mode 100755 services/src/main/java/org/keycloak/services/service/TokenManager.java create mode 100755 services/src/main/java/org/keycloak/services/service/TokenService.java create mode 100755 services/src/test/java/org/keycloak/test/AdapterTest.java create mode 100755 services/src/test/resources/META-INF/persistence.xml create mode 100644 services/src/test/resources/testrealm.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..54602e48bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Intellij +################### +.idea +*.iml + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log + + diff --git a/core/pom.xml b/core/pom.xml new file mode 100755 index 0000000000..56f2396f4f --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,60 @@ + + + + keycloak-parent + org.keycloak + 1.0-alpha-1 + ../pom.xml + + 4.0.0 + + keycloak-core + Keycloak Core + + + + + org.jboss.resteasy + resteasy-jaxrs + provided + + + org.jboss.resteasy + resteasy-client + provided + + + org.jboss.resteasy + jose-jwt + provided + + + javax.servlet + servlet-api + provided + + + org.jboss.resteasy + tjws + test + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + diff --git a/core/src/main/java/org/keycloak/AbstractOAuthClient.java b/core/src/main/java/org/keycloak/AbstractOAuthClient.java new file mode 100755 index 0000000000..180bd5f310 --- /dev/null +++ b/core/src/main/java/org/keycloak/AbstractOAuthClient.java @@ -0,0 +1,162 @@ +package org.keycloak; + +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.keycloak.representations.AccessTokenResponse; +import org.jboss.resteasy.util.BasicAuthHelper; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.client.Client; +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.security.KeyStore; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class AbstractOAuthClient +{ + protected String clientId; + protected String password; + protected KeyStore truststore; + protected String authUrl; + protected String codeUrl; + protected String stateCookieName = "OAuth_Token_Request_State"; + protected Client client; + protected final AtomicLong counter = new AtomicLong(); + + protected String getStateCode() + { + return counter.getAndIncrement() + "/" + UUID.randomUUID().toString(); + } + + public void start() + { + if (client == null) + { + client = new ResteasyClientBuilder().trustStore(truststore) + .hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY) + .connectionPoolSize(10) + .build(); + } + } + + public void stop() + { + client.close(); + } + + public String getClientId() + { + return clientId; + } + + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public KeyStore getTruststore() + { + return truststore; + } + + public void setTruststore(KeyStore truststore) + { + this.truststore = truststore; + } + + 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 String getStateCookieName() + { + return stateCookieName; + } + + public void setStateCookieName(String stateCookieName) + { + this.stateCookieName = stateCookieName; + } + + public Client getClient() + { + return client; + } + + public void setClient(Client client) + { + this.client = client; + } + + public String resolveBearerToken(String redirectUri, String code) + { + redirectUri = stripOauthParametersFromRedirect(redirectUri); + String authHeader = BasicAuthHelper.createHeader(clientId, password); + Form codeForm = new Form() + .param("grant_type", "authorization_code") + .param("code", code) + .param("redirect_uri", redirectUri); + Response res = client.target(codeUrl).request().header(HttpHeaders.AUTHORIZATION, authHeader).post(Entity.form(codeForm)); + try + { + if (res.getStatus() == 400) + { + throw new BadRequestException(); + } + else if (res.getStatus() != 200) + { + throw new InternalServerErrorException(new Exception("Unknown error when getting acess token")); + } + AccessTokenResponse tokenResponse = res.readEntity(AccessTokenResponse.class); + return tokenResponse.getToken(); + } + finally + { + res.close(); + } + } + + protected String stripOauthParametersFromRedirect(String uri) + { + System.out.println("******************** redirect_uri: " + uri); + UriBuilder builder = UriBuilder.fromUri(uri) + .replaceQueryParam("code", null) + .replaceQueryParam("state", null); + return builder.build().toString(); + } + +} diff --git a/core/src/main/java/org/keycloak/BouncyIntegration.java b/core/src/main/java/org/keycloak/BouncyIntegration.java new file mode 100755 index 0000000000..7d1635b109 --- /dev/null +++ b/core/src/main/java/org/keycloak/BouncyIntegration.java @@ -0,0 +1,22 @@ +package org.keycloak; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.Security; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class BouncyIntegration +{ + static + { + if (Security.getProvider("BC") == null) Security.addProvider(new BouncyCastleProvider()); + } + + public static void init() + { + // empty, the static class does it + } +} diff --git a/core/src/main/java/org/keycloak/DerUtils.java b/core/src/main/java/org/keycloak/DerUtils.java new file mode 100755 index 0000000000..ca92418c67 --- /dev/null +++ b/core/src/main/java/org/keycloak/DerUtils.java @@ -0,0 +1,68 @@ +package org.keycloak; + +import java.io.DataInputStream; +import java.io.InputStream; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * Extract PrivateKey, PublicKey, and X509Certificate from a DER encoded byte array or file. Usually + * generated from openssl + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class DerUtils +{ + static + { + BouncyIntegration.init(); + } + + public static PrivateKey decodePrivateKey(InputStream is) + throws Exception + { + + DataInputStream dis = new DataInputStream(is); + byte[] keyBytes = new byte[dis.available()]; + dis.readFully(keyBytes); + dis.close(); + + PKCS8EncodedKeySpec spec = + new PKCS8EncodedKeySpec(keyBytes); + KeyFactory kf = KeyFactory.getInstance("RSA", "BC"); + return kf.generatePrivate(spec); + } + + public static PublicKey decodePublicKey(byte[] der) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException + { + X509EncodedKeySpec spec = + new X509EncodedKeySpec(der); + KeyFactory kf = KeyFactory.getInstance("RSA", "BC"); + return kf.generatePublic(spec); + } + + public static X509Certificate decodeCertificate(InputStream is) throws Exception + { + CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC"); + X509Certificate cert = (X509Certificate) cf.generateCertificate(is); + is.close(); + return cert; + } + + public static PrivateKey decodePrivateKey(byte[] der) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException + { + PKCS8EncodedKeySpec spec = + new PKCS8EncodedKeySpec(der); + KeyFactory kf = KeyFactory.getInstance("RSA", "BC"); + return kf.generatePrivate(spec); + } +} diff --git a/core/src/main/java/org/keycloak/EnvUtil.java b/core/src/main/java/org/keycloak/EnvUtil.java new file mode 100755 index 0000000000..66917f0cb0 --- /dev/null +++ b/core/src/main/java/org/keycloak/EnvUtil.java @@ -0,0 +1,36 @@ +package org.keycloak; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class EnvUtil +{ + private static final Pattern p = Pattern.compile("[$][{]([^}]+)[}]"); + + /** + * Replaces any ${} strings with their corresponding environent variable. + * + * @param val + * @return + */ + public static String replace(String val) + { + Matcher matcher = p.matcher(val); + StringBuffer buf = new StringBuffer(); + while (matcher.find()) + { + String envVar = matcher.group(1); + String envVal = System.getProperty(envVar); + if (envVal == null) envVal = "NOT-SPECIFIED"; + matcher.appendReplacement(buf, envVal.replace("\\", "\\\\")); + } + matcher.appendTail(buf); + return buf.toString(); + } +} + + diff --git a/core/src/main/java/org/keycloak/PemUtils.java b/core/src/main/java/org/keycloak/PemUtils.java new file mode 100755 index 0000000000..5f2fcebbe1 --- /dev/null +++ b/core/src/main/java/org/keycloak/PemUtils.java @@ -0,0 +1,117 @@ +package org.keycloak; + +import org.jboss.resteasy.util.Base64; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; + +/** + * Utility classes to extract PublicKey, PrivateKey, and X509Certificate from openssl generated PEM files + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class PemUtils +{ + static + { + BouncyIntegration.init(); + } + public static X509Certificate decodeCertificate(InputStream is) throws Exception + { + byte[] der = pemToDer(is); + ByteArrayInputStream bis = new ByteArrayInputStream(der); + return DerUtils.decodeCertificate(bis); + } + + public static X509Certificate decodeCertificate(String cert) throws Exception + { + byte[] der = pemToDer(cert); + ByteArrayInputStream bis = new ByteArrayInputStream(der); + return DerUtils.decodeCertificate(bis); + } + + + /** + * Extract a public key from a PEM string + * + * @param pem + * @return + * @throws Exception + */ + public static PublicKey decodePublicKey(String pem) throws Exception + { + byte[] der = pemToDer(pem); + return DerUtils.decodePublicKey(der); + } + + /** + * Extract a private key that is a PKCS8 pem string (base64 encoded PKCS8) + * + * @param pem + * @return + * @throws Exception + */ + public static PrivateKey decodePrivateKey(String pem) throws Exception + { + byte[] der = pemToDer(pem); + return DerUtils.decodePrivateKey(der); + } + + public static PrivateKey decodePrivateKey(InputStream is) throws Exception + { + String pem = pemFromStream(is); + return decodePrivateKey(pem); + } + + /** + * Decode a PEM file to DER format + * + * @param is + * @return + * @throws java.io.IOException + */ + public static byte[] pemToDer(InputStream is) throws IOException + { + String pem = pemFromStream(is); + byte[] der = pemToDer(pem); + return der; + } + + /** + * Decode a PEM string to DER format + * + * @param pem + * @return + * @throws java.io.IOException + */ + public static byte[] pemToDer(String pem) throws IOException + { + pem = removeBeginEnd(pem); + return Base64.decode(pem); + } + + public static String removeBeginEnd(String pem) + { + pem = pem.replaceAll("-----BEGIN (.*)-----", ""); + pem = pem.replaceAll("-----END (.*)----", ""); + pem = pem.replaceAll("\r\n", ""); + pem = pem.replaceAll("\n", ""); + return pem.trim(); + } + + + public static String pemFromStream(InputStream is) throws IOException + { + DataInputStream dis = new DataInputStream(is); + byte[] keyBytes = new byte[dis.available()]; + dis.readFully(keyBytes); + dis.close(); + return new String(keyBytes, "utf-8"); + } +} diff --git a/core/src/main/java/org/keycloak/RSATokenVerifier.java b/core/src/main/java/org/keycloak/RSATokenVerifier.java new file mode 100755 index 0000000000..92c23c8704 --- /dev/null +++ b/core/src/main/java/org/keycloak/RSATokenVerifier.java @@ -0,0 +1,59 @@ +package org.keycloak; + +import org.jboss.resteasy.jose.jws.JWSInput; +import org.jboss.resteasy.jose.jws.crypto.RSAProvider; +import org.jboss.resteasy.jwt.JsonSerialization; +import org.keycloak.representations.SkeletonKeyToken; + +import java.io.IOException; +import java.security.PublicKey; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RSATokenVerifier +{ + public static SkeletonKeyToken verifyToken(String tokenString, ResourceMetadata metadata) throws VerificationException + { + PublicKey realmKey = metadata.getRealmKey(); + String realm = metadata.getRealm(); + String resource = metadata.getResourceName(); + JWSInput input = new JWSInput(tokenString); + boolean verified = false; + try + { + verified = RSAProvider.verify(input, realmKey); + } + catch (Exception ignore) + { + + } + if (!verified) throw new VerificationException("Token signature not validated"); + + SkeletonKeyToken token = null; + try + { + token = JsonSerialization.fromBytes(SkeletonKeyToken.class, input.getContent()); + } + catch (IOException e) + { + throw new VerificationException(e); + } + if (!token.isActive()) + { + throw new VerificationException("Token is not active."); + } + String user = token.getPrincipal(); + if (user == null) + { + throw new VerificationException("Token user was null"); + } + if (!realm.equals(token.getAudience())) + { + throw new VerificationException("Token audience doesn't match domain"); + + } + return token; + } +} diff --git a/core/src/main/java/org/keycloak/RealmConfiguration.java b/core/src/main/java/org/keycloak/RealmConfiguration.java new file mode 100755 index 0000000000..c19c21fd71 --- /dev/null +++ b/core/src/main/java/org/keycloak/RealmConfiguration.java @@ -0,0 +1,98 @@ +package org.keycloak; + +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; + +import javax.ws.rs.core.Form; +import javax.ws.rs.core.UriBuilder; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RealmConfiguration +{ + protected ResourceMetadata metadata; + protected ResteasyClient client; + protected UriBuilder authUrl; + protected ResteasyWebTarget codeUrl; + protected String clientId; + protected Form credentials = new Form(); + protected boolean sslRequired = true; + protected String stateCookieName = "OAuth_Token_Request_State"; + + public ResourceMetadata getMetadata() + { + return metadata; + } + + public void setMetadata(ResourceMetadata metadata) + { + this.metadata = metadata; + } + + public ResteasyClient getClient() + { + return client; + } + + public void setClient(ResteasyClient client) + { + this.client = client; + } + + public UriBuilder getAuthUrl() + { + return authUrl; + } + + public void setAuthUrl(UriBuilder authUrl) + { + this.authUrl = authUrl; + } + + public String getClientId() + { + return clientId; + } + + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + public Form getCredentials() + { + return credentials; + } + + public ResteasyWebTarget getCodeUrl() + { + return codeUrl; + } + + public void setCodeUrl(ResteasyWebTarget codeUrl) + { + this.codeUrl = codeUrl; + } + + public boolean isSslRequired() + { + return sslRequired; + } + + public void setSslRequired(boolean sslRequired) + { + this.sslRequired = sslRequired; + } + + public String getStateCookieName() + { + return stateCookieName; + } + + public void setStateCookieName(String stateCookieName) + { + this.stateCookieName = stateCookieName; + } +} diff --git a/core/src/main/java/org/keycloak/ResourceMetadata.java b/core/src/main/java/org/keycloak/ResourceMetadata.java new file mode 100755 index 0000000000..02e75dbb51 --- /dev/null +++ b/core/src/main/java/org/keycloak/ResourceMetadata.java @@ -0,0 +1,94 @@ +package org.keycloak; + +import java.security.KeyStore; +import java.security.PublicKey; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ResourceMetadata +{ + protected String resourceName; + protected String realm; + protected KeyStore clientKeystore; + protected String clientKeyPassword; + protected KeyStore truststore; + protected PublicKey realmKey; + + public String getResourceName() + { + return resourceName; + } + + public String getRealm() + { + return realm; + } + + /** + * keystore that contains service's private key and certificate. + * Used when making invocations on remote HTTPS endpoints that require client-cert authentication + * + * @return + */ + public KeyStore getClientKeystore() + { + return clientKeystore; + } + + public String getClientKeyPassword() + { + return clientKeyPassword; + } + + public void setClientKeyPassword(String clientKeyPassword) + { + this.clientKeyPassword = clientKeyPassword; + } + + /** + * Truststore to use if this service makes client invocations on remote HTTPS endpoints. + * + * @return + */ + public KeyStore getTruststore() + { + return truststore; + } + + /** + * Public key of the realm. Used to verify access tokens + * + * @return + */ + public PublicKey getRealmKey() + { + return realmKey; + } + + public void setResourceName(String resourceName) + { + this.resourceName = resourceName; + } + + public void setRealm(String realm) + { + this.realm = realm; + } + + public void setClientKeystore(KeyStore clientKeystore) + { + this.clientKeystore = clientKeystore; + } + + public void setTruststore(KeyStore truststore) + { + this.truststore = truststore; + } + + public void setRealmKey(PublicKey realmKey) + { + this.realmKey = realmKey; + } +} diff --git a/core/src/main/java/org/keycloak/SkeletonKeyContextResolver.java b/core/src/main/java/org/keycloak/SkeletonKeyContextResolver.java new file mode 100755 index 0000000000..3bca480c5d --- /dev/null +++ b/core/src/main/java/org/keycloak/SkeletonKeyContextResolver.java @@ -0,0 +1,42 @@ +package org.keycloak; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; +import org.codehaus.jackson.map.annotate.JsonSerialize; + +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +/** + * Any class with package org.jboss.resteasy.skeleton.key will use NON_DEFAULT inclusion + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Provider +public class SkeletonKeyContextResolver implements ContextResolver +{ + protected ObjectMapper mapper = new ObjectMapper(); + + public SkeletonKeyContextResolver() + { + mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT); + } + + public SkeletonKeyContextResolver(boolean indent) + { + mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT); + if (indent) + { + mapper.enable(SerializationConfig.Feature.INDENT_OUTPUT); + } + } + + + @Override + public ObjectMapper getContext(Class type) + { + if (type.getPackage().getName().startsWith("org.jboss.resteasy.skeleton.key")) return mapper; + return null; + } +} diff --git a/core/src/main/java/org/keycloak/SkeletonKeyPrincipal.java b/core/src/main/java/org/keycloak/SkeletonKeyPrincipal.java new file mode 100755 index 0000000000..bdba805c5e --- /dev/null +++ b/core/src/main/java/org/keycloak/SkeletonKeyPrincipal.java @@ -0,0 +1,58 @@ +package org.keycloak; + +import java.security.Principal; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SkeletonKeyPrincipal implements Principal +{ + protected String name; + protected String surrogate; + + public SkeletonKeyPrincipal(String name, String surrogate) + { + this.name = name; + this.surrogate = surrogate; + } + + @Override + public String getName() + { + return name; + } + + public String getSurrogate() + { + return surrogate; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SkeletonKeyPrincipal that = (SkeletonKeyPrincipal) o; + + if (!name.equals(that.name)) return false; + if (surrogate != null ? !surrogate.equals(that.surrogate) : that.surrogate != null) return false; + + return true; + } + + @Override + public int hashCode() + { + int result = name.hashCode(); + result = 31 * result + (surrogate != null ? surrogate.hashCode() : 0); + return result; + } + + @Override + public String toString() + { + return name; + } +} diff --git a/core/src/main/java/org/keycloak/SkeletonKeySession.java b/core/src/main/java/org/keycloak/SkeletonKeySession.java new file mode 100755 index 0000000000..e3a2f5548d --- /dev/null +++ b/core/src/main/java/org/keycloak/SkeletonKeySession.java @@ -0,0 +1,34 @@ +package org.keycloak; + +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SkeletonKeySession implements Serializable +{ + protected String token; + protected transient ResourceMetadata metadata; + + public SkeletonKeySession() + { + } + + public SkeletonKeySession(String token, ResourceMetadata metadata) + { + this.token = token; + this.metadata = metadata; + } + + public String getToken() + { + return token; + } + + public ResourceMetadata getMetadata() + { + return metadata; + } + +} diff --git a/core/src/main/java/org/keycloak/VerificationException.java b/core/src/main/java/org/keycloak/VerificationException.java new file mode 100755 index 0000000000..6ef877d479 --- /dev/null +++ b/core/src/main/java/org/keycloak/VerificationException.java @@ -0,0 +1,27 @@ +package org.keycloak; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class VerificationException extends Exception +{ + public VerificationException() + { + } + + public VerificationException(String message) + { + super(message); + } + + public VerificationException(String message, Throwable cause) + { + super(message, cause); + } + + public VerificationException(Throwable cause) + { + super(cause); + } +} diff --git a/core/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java b/core/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java new file mode 100755 index 0000000000..69c7b0f96c --- /dev/null +++ b/core/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java @@ -0,0 +1,129 @@ +package org.keycloak.jaxrs; + +import org.jboss.resteasy.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.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.io.IOException; +import java.security.Principal; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Priority(Priorities.AUTHENTICATION) +public class JaxrsBearerTokenFilter implements ContainerRequestFilter +{ + protected ResourceMetadata resourceMetadata; + private static Logger log = Logger.getLogger(JaxrsBearerTokenFilter.class); + + public JaxrsBearerTokenFilter(ResourceMetadata resourceMetadata) + { + this.resourceMetadata = resourceMetadata; + } + + protected void challengeResponse(ContainerRequestContext request, String error, String description) + { + 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("\""); + } + request.abortWith(Response.status(401).header("WWW-Authenticate", header.toString()).build()); + return; + } + + @Context + protected SecurityContext securityContext; + + @Override + public void filter(ContainerRequestContext request) throws IOException + { + String authHeader = request.getHeaderString(HttpHeaders.AUTHORIZATION); + if (authHeader == null) + { + challengeResponse(request, null, null); + return; + } + + String[] split = authHeader.trim().split("\\s+"); + if (split == null || split.length != 2) challengeResponse(request, null, null); + if (!split[0].equalsIgnoreCase("Bearer")) challengeResponse(request, null, null); + + + String tokenString = split[1]; + + + try + { + SkeletonKeyToken token = RSATokenVerifier.verifyToken(tokenString, resourceMetadata); + SkeletonKeySession skSession = new SkeletonKeySession(tokenString, resourceMetadata); + ResteasyProviderFactory.pushContext(SkeletonKeySession.class, skSession); + String callerPrincipal = securityContext.getUserPrincipal() != null ? securityContext.getUserPrincipal().getName() : null; + + final SkeletonKeyPrincipal principal = new SkeletonKeyPrincipal(token.getPrincipal(), callerPrincipal); + final boolean isSecure = securityContext.isSecure(); + final SkeletonKeyToken.Access access; + if (resourceMetadata.getResourceName() != null) + { + access = token.getResourceAccess(resourceMetadata.getResourceName()); + } + else + { + access = token.getRealmAccess(); + } + SecurityContext ctx = new SecurityContext() + { + @Override + public Principal getUserPrincipal() + { + return principal; + } + + @Override + public boolean isUserInRole(String role) + { + if (access.getRoles() == null) return false; + return access.getRoles().contains(role); + } + + @Override + public boolean isSecure() + { + return isSecure; + } + + @Override + public String getAuthenticationScheme() + { + return "OAUTH_BEARER"; + } + }; + request.setSecurityContext(ctx); + } + catch (VerificationException e) + { + log.error("Failed to verify token", e); + challengeResponse(request, "invalid_token", e.getMessage()); + } + } + +} diff --git a/core/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java b/core/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java new file mode 100755 index 0000000000..acc99a59f2 --- /dev/null +++ b/core/src/main/java/org/keycloak/jaxrs/JaxrsOAuthClient.java @@ -0,0 +1,55 @@ +package org.keycloak.jaxrs; + +import org.keycloak.AbstractOAuthClient; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.net.URI; + +/** + * Helper code to obtain oauth access tokens via browser redirects + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class JaxrsOAuthClient extends AbstractOAuthClient +{ + public Response redirect(UriInfo uriInfo, String redirectUri) + { + String state = getStateCode(); + + URI url = UriBuilder.fromUri(authUrl) + .queryParam("client_id", clientId) + .queryParam("redirect_uri", redirectUri) + .queryParam("state", state) + .build(); + NewCookie cookie = new NewCookie(stateCookieName, state, uriInfo.getBaseUri().getPath(), null, null, -1, true); + return Response.status(302) + .location(url) + .cookie(cookie).build(); + } + + public String getBearerToken(UriInfo uriInfo, HttpHeaders headers) throws BadRequestException, InternalServerErrorException + { + String error = uriInfo.getQueryParameters().getFirst("error"); + if (error != null) throw new BadRequestException(new Exception("OAuth error: " + error)); + Cookie stateCookie = headers.getCookies().get(stateCookieName); + if (stateCookie == null) throw new BadRequestException(new Exception("state cookie not set"));; + + String state = uriInfo.getQueryParameters().getFirst("state"); + if (state == null) throw new BadRequestException(new Exception("state parameter was null")); + if (!state.equals(stateCookie.getValue())) + { + throw new BadRequestException(new Exception("state parameter invalid")); + } + String code = uriInfo.getQueryParameters().getFirst("code"); + if (code == null) throw new BadRequestException(new Exception("code parameter was null")); + return resolveBearerToken(uriInfo.getRequestUri().toString(), code); + } +} diff --git a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java new file mode 100755 index 0000000000..a5eec7af5f --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java @@ -0,0 +1,64 @@ +package org.keycloak.representations; + +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * OAuth 2.0 Access Token Response json + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class AccessTokenResponse +{ + @JsonProperty("access_token") + protected String token; + + @JsonProperty("expires_in") + protected long expiresIn; + + @JsonProperty("refresh_token") + protected String refreshToken; + + @JsonProperty("token_type") + protected String tokenType; + + public String getToken() + { + return token; + } + + public void setToken(String token) + { + this.token = token; + } + + public long getExpiresIn() + { + return expiresIn; + } + + public void setExpiresIn(long expiresIn) + { + this.expiresIn = expiresIn; + } + + public String getRefreshToken() + { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) + { + this.refreshToken = refreshToken; + } + + public String getTokenType() + { + return tokenType; + } + + public void setTokenType(String tokenType) + { + this.tokenType = tokenType; + } +} diff --git a/core/src/main/java/org/keycloak/representations/SkeletonKeyScope.java b/core/src/main/java/org/keycloak/representations/SkeletonKeyScope.java new file mode 100755 index 0000000000..8f48d966c1 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/SkeletonKeyScope.java @@ -0,0 +1,13 @@ +package org.keycloak.representations; + +import javax.ws.rs.core.MultivaluedHashMap; + +/** + * Key is resource desired. Values are roles desired for that resource + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SkeletonKeyScope extends MultivaluedHashMap +{ +} diff --git a/core/src/main/java/org/keycloak/representations/SkeletonKeyToken.java b/core/src/main/java/org/keycloak/representations/SkeletonKeyToken.java new file mode 100755 index 0000000000..b73478180d --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/SkeletonKeyToken.java @@ -0,0 +1,202 @@ +package org.keycloak.representations; + +import org.codehaus.jackson.annotate.JsonIgnore; +import org.codehaus.jackson.annotate.JsonProperty; +import org.jboss.resteasy.jwt.JsonWebToken; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SkeletonKeyToken extends JsonWebToken +{ + public static class Access + { + @JsonProperty("roles") + protected Set roles; + @JsonProperty("verify_caller") + protected Boolean verifyCaller; + + public Set getRoles() + { + return roles; + } + + public Access roles(Set roles) + { + this.roles = roles; + return this; + } + + @JsonIgnore + public boolean isUserInRole(String role) + { + if (roles == null) return false; + return roles.contains(role); + } + + public Access addRole(String role) + { + if (roles == null) roles = new HashSet(); + roles.add(role); + return this; + } + + public Boolean getVerifyCaller() + { + return verifyCaller; + } + + public Access verifyCaller(Boolean required) + { + this.verifyCaller = required; + return this; + } + } + + @JsonProperty("issuedFor") + public String issuedFor; + + @JsonProperty("trusted-certs") + protected Set trustedCertificates; + + + @JsonProperty("realm_access") + protected Access realmAccess; + + @JsonProperty("resource_access") + protected Map resourceAccess = new HashMap(); + + public Map getResourceAccess() + { + return resourceAccess; + } + + /** + * Does the realm require verifying the caller? + * + * @return + */ + @JsonIgnore + public boolean isVerifyCaller() + { + if (getRealmAccess() != null && getRealmAccess().getVerifyCaller() != null) return getRealmAccess().getVerifyCaller().booleanValue(); + return false; + } + + /** + * Does the resource override the requirement of verifying the caller? + * + * @param resource + * @return + */ + @JsonIgnore + public boolean isVerifyCaller(String resource) + { + Access access = getResourceAccess(resource); + if (access != null && access.getVerifyCaller() != null) return access.getVerifyCaller().booleanValue(); + return false; + } + + @JsonIgnore + public Access getResourceAccess(String resource) + { + return resourceAccess.get(resource); + } + + public Access addAccess(String service) + { + Access token = new Access(); + resourceAccess.put(service, token); + return token; + } + + @Override + public SkeletonKeyToken id(String id) + { + return (SkeletonKeyToken)super.id(id); + } + + @Override + public SkeletonKeyToken expiration(long expiration) + { + return (SkeletonKeyToken)super.expiration(expiration); + } + + @Override + public SkeletonKeyToken notBefore(long notBefore) + { + return (SkeletonKeyToken)super.notBefore(notBefore); + } + + @Override + public SkeletonKeyToken issuedAt(long issuedAt) + { + return (SkeletonKeyToken)super.issuedAt(issuedAt); + } + + @Override + public SkeletonKeyToken issuer(String issuer) + { + return (SkeletonKeyToken)super.issuer(issuer); + } + + @Override + public SkeletonKeyToken audience(String audience) + { + return (SkeletonKeyToken)super.audience(audience); + } + + @Override + public SkeletonKeyToken principal(String principal) + { + return (SkeletonKeyToken)super.principal(principal); + } + + @Override + public SkeletonKeyToken type(String type) + { + return (SkeletonKeyToken)super.type(type); + } + + public Access getRealmAccess() + { + return realmAccess; + } + + public void setRealmAccess(Access realmAccess) + { + this.realmAccess = realmAccess; + } + + public Set getTrustedCertificates() + { + return trustedCertificates; + } + + public void setTrustedCertificates(Set trustedCertificates) + { + this.trustedCertificates = trustedCertificates; + } + + /** + * OAuth client the token was issued for. + * + * @return + */ + public String getIssuedFor() + { + return issuedFor; + } + + public SkeletonKeyToken issuedFor(String issuedFor) + { + this.issuedFor = issuedFor; + return this; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/PublishedRealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/PublishedRealmRepresentation.java new file mode 100755 index 0000000000..76933637c9 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/PublishedRealmRepresentation.java @@ -0,0 +1,136 @@ +package org.keycloak.representations.idm; + +import org.bouncycastle.openssl.PEMWriter; +import org.codehaus.jackson.annotate.JsonIgnore; +import org.codehaus.jackson.annotate.JsonProperty; +import org.keycloak.PemUtils; + +import java.io.IOException; +import java.io.StringWriter; +import java.security.PublicKey; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class PublishedRealmRepresentation +{ + protected String realm; + protected String self; + + @JsonProperty("public_key") + protected String publicKeyPem; + + @JsonProperty("authorization") + protected String authorizationUrl; + + @JsonProperty("codes") + protected String codeUrl; + + @JsonProperty("grants") + protected String grantUrl; + + @JsonIgnore + protected volatile transient PublicKey publicKey; + + + public String getRealm() + { + return realm; + } + + public void setRealm(String realm) + { + this.realm = realm; + } + + public String getSelf() + { + return self; + } + + public void setSelf(String self) + { + this.self = self; + } + + public String getPublicKeyPem() + { + return publicKeyPem; + } + + public void setPublicKeyPem(String publicKeyPem) + { + this.publicKeyPem = publicKeyPem; + this.publicKey = null; + } + + + @JsonIgnore + public PublicKey getPublicKey() + { + if (publicKey != null) return publicKey; + if (publicKeyPem != null) + { + try + { + publicKey = PemUtils.decodePublicKey(publicKeyPem); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + return publicKey; + } + + @JsonIgnore + public void setPublicKey(PublicKey publicKey) + { + this.publicKey = publicKey; + StringWriter writer = new StringWriter(); + PEMWriter pemWriter = new PEMWriter(writer); + try + { + pemWriter.writeObject(publicKey); + pemWriter.flush(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + String s = writer.toString(); + this.publicKeyPem = PemUtils.removeBeginEnd(s); + } + + + public String getAuthorizationUrl() + { + return authorizationUrl; + } + + public void setAuthorizationUrl(String authorizationUrl) + { + this.authorizationUrl = authorizationUrl; + } + + public String getCodeUrl() + { + return codeUrl; + } + + public void setCodeUrl(String codeUrl) + { + this.codeUrl = codeUrl; + } + + public String getGrantUrl() + { + return grantUrl; + } + + public void setGrantUrl(String grantUrl) + { + this.grantUrl = grantUrl; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java new file mode 100755 index 0000000000..17a1bb2dde --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -0,0 +1,171 @@ +package org.keycloak.representations.idm; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RealmRepresentation +{ + protected String self; // link + protected String realm; + protected long tokenLifespan; + protected long accessCodeLifespan; + protected boolean enabled; + protected boolean sslNotRequired; + protected boolean cookieLoginAllowed; + protected List requiredCredentials; + protected List users; + protected List roleMappings; + protected List scopeMappings; + protected List resources; + + + public String getSelf() + { + return self; + } + + public void setSelf(String self) + { + this.self = self; + } + + public String getRealm() + { + return realm; + } + + public void setRealm(String realm) + { + this.realm = realm; + } + + public List getUsers() + { + return users; + } + + public List getResources() + { + return resources; + } + + public ResourceRepresentation resource(String name) + { + ResourceRepresentation resource = new ResourceRepresentation(); + if (resources == null) resources = new ArrayList(); + resources.add(resource); + resource.setName(name); + return resource; + } + + public void setUsers(List users) + { + this.users = users; + } + + public UserRepresentation user(String username) + { + UserRepresentation user = new UserRepresentation(); + user.setUsername(username); + if (users == null) users = new ArrayList(); + users.add(user); + return user; + } + + public void setResources(List resources) + { + this.resources = resources; + } + + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public boolean isSslNotRequired() + { + return sslNotRequired; + } + + public void setSslNotRequired(boolean sslNotRequired) + { + this.sslNotRequired = sslNotRequired; + } + + public boolean isCookieLoginAllowed() + { + return cookieLoginAllowed; + } + + public void setCookieLoginAllowed(boolean cookieLoginAllowed) + { + this.cookieLoginAllowed = cookieLoginAllowed; + } + + public long getTokenLifespan() + { + return tokenLifespan; + } + + public void setTokenLifespan(long tokenLifespan) + { + this.tokenLifespan = tokenLifespan; + } + + public List getRoleMappings() + { + return roleMappings; + } + + public RoleMappingRepresentation roleMapping(String username) + { + RoleMappingRepresentation mapping = new RoleMappingRepresentation(); + mapping.setUsername(username); + if (roleMappings == null) roleMappings = new ArrayList(); + roleMappings.add(mapping); + return mapping; + } + + public List getScopeMappings() + { + return scopeMappings; + } + + public ScopeMappingRepresentation scopeMapping(String username) + { + ScopeMappingRepresentation mapping = new ScopeMappingRepresentation(); + mapping.setUsername(username); + if (scopeMappings == null) scopeMappings = new ArrayList(); + scopeMappings.add(mapping); + return mapping; + } + + public List getRequiredCredentials() + { + return requiredCredentials; + } + + public void setRequiredCredentials(List requiredCredentials) + { + this.requiredCredentials = requiredCredentials; + } + + public long getAccessCodeLifespan() + { + return accessCodeLifespan; + } + + public void setAccessCodeLifespan(long accessCodeLifespan) + { + this.accessCodeLifespan = accessCodeLifespan; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/RequiredCredentialRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RequiredCredentialRepresentation.java new file mode 100755 index 0000000000..763e29ca80 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/RequiredCredentialRepresentation.java @@ -0,0 +1,46 @@ +package org.keycloak.representations.idm; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RequiredCredentialRepresentation +{ + public static final String PASSWORD = "Password"; + public static final String TOTP = "TOTP"; + public static final String CLIENT_CERT = "CLIENT_CERT"; + public static final String CALLER_PRINCIPAL = "CALLER_PRINCIPAL"; + protected String type; + protected boolean input; + protected boolean secret; + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } + + public boolean isInput() + { + return input; + } + + public void setInput(boolean input) + { + this.input = input; + } + + public boolean isSecret() + { + return secret; + } + + public void setSecret(boolean secret) + { + this.secret = secret; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/ResourceRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ResourceRepresentation.java new file mode 100755 index 0000000000..d3d2c5f1d8 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/ResourceRepresentation.java @@ -0,0 +1,97 @@ +package org.keycloak.representations.idm; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** +* @author Bill Burke +* @version $Revision: 1 $ +*/ +public class ResourceRepresentation +{ + protected String self; // link + protected String name; + protected boolean surrogateAuthRequired; + protected Set roles; + protected List roleMappings; + protected List scopeMappings; + + public String getSelf() + { + return self; + } + + public void setSelf(String self) + { + this.self = self; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public boolean isSurrogateAuthRequired() + { + return surrogateAuthRequired; + } + + public void setSurrogateAuthRequired(boolean surrogateAuthRequired) + { + this.surrogateAuthRequired = surrogateAuthRequired; + } + + public Set getRoles() + { + return roles; + } + + public void setRoles(Set roles) + { + this.roles = roles; + } + + public ResourceRepresentation role(String role) + { + if (this.roles == null) this.roles = new HashSet(); + this.roles.add(role); + return this; + } + + public List getRoleMappings() + { + return roleMappings; + } + + public RoleMappingRepresentation roleMapping(String username) + { + RoleMappingRepresentation mapping = new RoleMappingRepresentation(); + mapping.setUsername(username); + if (roleMappings == null) roleMappings = new ArrayList(); + roleMappings.add(mapping); + return mapping; + } + + public List getScopeMappings() + { + return scopeMappings; + } + + public ScopeMappingRepresentation scopeMapping(String username) + { + ScopeMappingRepresentation mapping = new ScopeMappingRepresentation(); + mapping.setUsername(username); + if (scopeMappings == null) scopeMappings = new ArrayList(); + scopeMappings.add(mapping); + return mapping; + } + + +} diff --git a/core/src/main/java/org/keycloak/representations/idm/RoleMappingRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RoleMappingRepresentation.java new file mode 100755 index 0000000000..05915c1909 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/RoleMappingRepresentation.java @@ -0,0 +1,74 @@ +package org.keycloak.representations.idm; + +import java.util.HashSet; +import java.util.Set; + +/** + * + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RoleMappingRepresentation +{ + protected String self; // link + protected String username; + protected Set roles; + protected Set surrogates; + + public String getSelf() + { + return self; + } + + public void setSelf(String self) + { + this.self = self; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public Set getRoles() + { + return roles; + } + + public Set getSurrogates() + { + return surrogates; + } + + public void setSurrogates(Set surrogates) + { + this.surrogates = surrogates; + } + + public RoleMappingRepresentation surrogate(String surrogate) + { + if (this.surrogates == null) this.surrogates = new HashSet(); + this.surrogates.add(surrogate); + return this; + } + + + public void setRoles(Set roles) + { + this.roles = roles; + } + + public RoleMappingRepresentation role(String role) + { + if (this.roles == null) this.roles = new HashSet(); + this.roles.add(role); + return this; + } + +} diff --git a/core/src/main/java/org/keycloak/representations/idm/ScopeMappingRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ScopeMappingRepresentation.java new file mode 100755 index 0000000000..b86e2e2239 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/ScopeMappingRepresentation.java @@ -0,0 +1,55 @@ +package org.keycloak.representations.idm; + +import java.util.HashSet; +import java.util.Set; + +/** + * + * + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ScopeMappingRepresentation +{ + protected String self; // link + protected String username; + protected Set roles; + + public String getSelf() + { + return self; + } + + public void setSelf(String self) + { + this.self = self; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public Set getRoles() + { + return roles; + } + + public void setRoles(Set roles) + { + this.roles = roles; + } + + public ScopeMappingRepresentation role(String role) + { + if (this.roles == null) this.roles = new HashSet(); + this.roles.add(role); + return this; + } + +} diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java new file mode 100755 index 0000000000..4bd0de41e1 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -0,0 +1,124 @@ +package org.keycloak.representations.idm; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** +* @author Bill Burke +* @version $Revision: 1 $ +*/ +public class UserRepresentation +{ + public static class Credential + { + protected String type; + protected String value; + protected boolean hashed; + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } + + public String getValue() + { + return value; + } + + public void setValue(String value) + { + this.value = value; + } + + public boolean isHashed() + { + return hashed; + } + + public void setHashed(boolean hashed) + { + this.hashed = hashed; + } + } + + protected String self; // link + protected String username; + protected boolean enabled; + protected Map attributes; + protected List credentials; + + public String getSelf() + { + return self; + } + + public void setSelf(String self) + { + this.self = self; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public Map getAttributes() + { + return attributes; + } + + public void setAttributes(Map attributes) + { + this.attributes = attributes; + } + + public List getCredentials() + { + return credentials; + } + + public void setCredentials(List credentials) + { + this.credentials = credentials; + } + + public UserRepresentation attribute(String name, String value) + { + if (this.attributes == null) attributes = new HashMap(); + attributes.put(name, value); + return this; + } + + public UserRepresentation credential(String type, String value, boolean hashed) + { + if (this.credentials == null) credentials = new ArrayList(); + Credential cred = new Credential(); + cred.setType(type); + cred.setValue(value); + cred.setHashed(hashed); + credentials.add( cred); + return this; + } + + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } +} diff --git a/core/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/core/src/main/java/org/keycloak/servlet/ServletOAuthClient.java new file mode 100755 index 0000000000..27d4372106 --- /dev/null +++ b/core/src/main/java/org/keycloak/servlet/ServletOAuthClient.java @@ -0,0 +1,127 @@ +package org.keycloak.servlet; + +import org.jboss.resteasy.plugins.server.servlet.ServletUtil; +import org.keycloak.AbstractOAuthClient; +import org.jboss.resteasy.spi.ResteasyUriInfo; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.net.URI; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ServletOAuthClient extends AbstractOAuthClient +{ + /** + * Start the process of obtaining an access token by redirecting the browser + * to the authentication server + * + * + * + * @param relativePath path relative to context root you want auth server to redirect back to + * @param request + * @param response + * @throws IOException + */ + public void redirectRelative(String relativePath, HttpServletRequest request, HttpServletResponse response) throws IOException + { + ResteasyUriInfo uriInfo = ServletUtil.extractUriInfo(request, null); + String redirect = uriInfo.getBaseUriBuilder().path(relativePath).toTemplate(); + redirect(redirect, request, response); + } + + + /** + * Start the process of obtaining an access token by redirecting the browser + * to the authentication server + * + * @param redirectUri full URI you want auth server to redirect back to + * @param request + * @param response + * @throws IOException + */ + public void redirect(String redirectUri, HttpServletRequest request, HttpServletResponse response) throws IOException + { + String state = getStateCode(); + + URI url = UriBuilder.fromUri(authUrl) + .queryParam("client_id", clientId) + .queryParam("redirect_uri", redirectUri) + .queryParam("state", state) + .build(); + String cookiePath = request.getContextPath(); + if (cookiePath.equals("")) cookiePath = "/"; + + Cookie cookie = new Cookie(stateCookieName, state); + cookie.setSecure(true); + cookie.setPath(cookiePath); + response.addCookie(cookie); + response.sendRedirect(url.toString()); + } + + protected String getCookieValue(String name, HttpServletRequest request) + { + if (request.getCookies() == null) return null; + + for (Cookie cookie : request.getCookies()) + { + if (cookie.getName().equals(name)) return cookie.getValue(); + } + return null; + } + + protected String getCode(HttpServletRequest request) + { + 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("code")) continue; + return param.substring(eq + 1); + } + return null; + } + + + /** + * Obtain the code parameter from the url after being redirected back from the auth-server. Then + * do an authenticated request back to the auth-server to turn the access code into an access token. + * + * @param request + * @return + * @throws BadRequestException + * @throws InternalServerErrorException + */ + public String getBearerToken(HttpServletRequest request) throws BadRequestException, InternalServerErrorException + { + String error = request.getParameter("error"); + if (error != null) throw new BadRequestException(new Exception("OAuth error: " + error)); + String redirectUri = request.getRequestURL().append("?").append(request.getQueryString()).toString(); + String stateCookie = getCookieValue(stateCookieName, request); + if (stateCookie == null) throw new BadRequestException(new Exception("state cookie not set")); + // we can call get parameter as this should be a redirect + String state = request.getParameter("state"); + String code = request.getParameter("code"); + + if (state == null) throw new BadRequestException(new Exception("state parameter was null")); + if (!state.equals(stateCookie)) + { + throw new BadRequestException(new Exception("state parameter invalid")); + } + if (code == null) throw new BadRequestException(new Exception("code parameter was null")); + return resolveBearerToken(redirectUri, code); + } + + +} diff --git a/core/src/test/java/org/keycloak/RSAVerifierTest.java b/core/src/test/java/org/keycloak/RSAVerifierTest.java new file mode 100755 index 0000000000..8841954222 --- /dev/null +++ b/core/src/test/java/org/keycloak/RSAVerifierTest.java @@ -0,0 +1,282 @@ +package org.keycloak; + +import junit.framework.Assert; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMWriter; +import org.bouncycastle.x509.X509V1CertificateGenerator; +import org.jboss.resteasy.jose.jws.JWSBuilder; +import org.jboss.resteasy.jwt.JsonSerialization; +import org.keycloak.RSATokenVerifier; +import org.keycloak.ResourceMetadata; +import org.keycloak.VerificationException; +import org.keycloak.representations.SkeletonKeyToken; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.io.StringWriter; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PublicKey; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.util.Date; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RSAVerifierTest +{ + private static X509Certificate[] idpCertificates; + private static KeyPair idpPair; + private static KeyPair badPair; + private static KeyPair clientPair; + private static X509Certificate[] clientCertificateChain; + private ResourceMetadata metadata; + private SkeletonKeyToken token; + + static + { + if (Security.getProvider("BC") == null) Security.addProvider(new BouncyCastleProvider()); + } + + public static X509Certificate generateTestCertificate(String subject, String issuer, KeyPair pair) throws InvalidKeyException, + NoSuchProviderException, SignatureException + { + + X509V1CertificateGenerator certGen = new X509V1CertificateGenerator(); + + certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis())); + certGen.setIssuerDN(new X500Principal(issuer)); + certGen.setNotBefore(new Date(System.currentTimeMillis() - 10000)); + certGen.setNotAfter(new Date(System.currentTimeMillis() + 10000)); + certGen.setSubjectDN(new X500Principal(subject)); + certGen.setPublicKey(pair.getPublic()); + certGen.setSignatureAlgorithm("SHA256WithRSAEncryption"); + + return certGen.generateX509Certificate(pair.getPrivate(), "BC"); + } + + @BeforeClass + public static void setupCerts() throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException + { + badPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + idpPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + idpCertificates = new X509Certificate[]{generateTestCertificate("CN=IDP", "CN=IDP", idpPair)}; + clientPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + clientCertificateChain = new X509Certificate[]{generateTestCertificate("CN=Client", "CN=IDP", idpPair)}; + } + + @Before + public void initTest() + { + metadata = new ResourceMetadata(); + metadata.setResourceName("service"); + metadata.setRealm("domain"); + metadata.setRealmKey(idpPair.getPublic()); + + token = new SkeletonKeyToken(); + token.principal("CN=Client") + .audience("domain") + .addAccess("service").addRole("admin"); + } + + @Test + public void testPemWriter() throws Exception + { + PublicKey realmPublicKey = idpPair.getPublic(); + StringWriter sw = new StringWriter(); + PEMWriter writer = new PEMWriter(sw); + try + { + writer.writeObject(realmPublicKey); + writer.flush(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + System.out.println(sw.toString()); + } + + + @Test + public void testSimpleVerification() throws Exception + { + + byte[] tokenBytes = JsonSerialization.toByteArray(token, false); + + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(idpPair.getPrivate()); + SkeletonKeyToken token = RSATokenVerifier.verifyToken(encoded, metadata); + Assert.assertTrue(token.getResourceAccess("service").getRoles().contains("admin")); + Assert.assertEquals("CN=Client", token.getPrincipal()); + } + + /* + @Test + public void testSpeed() throws Exception + { + + byte[] tokenBytes = JsonSerialization.toByteArray(token, false); + + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(idpPair.getPrivate()); + + long start = System.currentTimeMillis(); + int count = 10000; + for (int i = 0; i < count; i++) + { + SkeletonKeyTokenVerification v = RSATokenVerifier.verify(null, encoded, metadata); + + } + long end = System.currentTimeMillis() - start; + System.out.println("rate: " + ((double)end/(double)count)); + } + */ + + + @Test + public void testBadSignature() throws Exception + { + + byte[] tokenBytes = JsonSerialization.toByteArray(token, false); + + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(badPair.getPrivate()); + + SkeletonKeyToken v = null; + try + { + v = RSATokenVerifier.verifyToken(encoded, metadata); + Assert.fail(); + } + catch (VerificationException ignored) + { + } + } + + @Test + public void testNotBeforeGood() throws Exception + { + token.notBefore((System.currentTimeMillis()/1000) - 100); + byte[] tokenBytes = JsonSerialization.toByteArray(token, false); + + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(idpPair.getPrivate()); + + SkeletonKeyToken v = null; + try + { + v = RSATokenVerifier.verifyToken(encoded, metadata); + } + catch (VerificationException ignored) + { + throw ignored; + } + } + + @Test + public void testNotBeforeBad() throws Exception + { + token.notBefore((System.currentTimeMillis()/1000) + 100); + byte[] tokenBytes = JsonSerialization.toByteArray(token, false); + + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(idpPair.getPrivate()); + + SkeletonKeyToken v = null; + try + { + v = RSATokenVerifier.verifyToken(encoded, metadata); + Assert.fail(); + } + catch (VerificationException ignored) + { + System.out.println(ignored.getMessage()); + } + } + + @Test + public void testExpirationGood() throws Exception + { + token.expiration((System.currentTimeMillis()/1000) + 100); + byte[] tokenBytes = JsonSerialization.toByteArray(token, false); + + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(idpPair.getPrivate()); + + SkeletonKeyToken v = null; + try + { + v = RSATokenVerifier.verifyToken(encoded, metadata); + } + catch (VerificationException ignored) + { + throw ignored; + } + } + + @Test + public void testExpirationBad() throws Exception + { + token.expiration((System.currentTimeMillis()/1000) - 100); + byte[] tokenBytes = JsonSerialization.toByteArray(token, false); + + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(idpPair.getPrivate()); + + SkeletonKeyToken v = null; + try + { + v = RSATokenVerifier.verifyToken(encoded, metadata); + Assert.fail(); + } + catch (VerificationException ignored) + { + System.out.println(ignored.getMessage()); + } + } + + @Test + public void testTokenAuth() throws Exception + { + token = new SkeletonKeyToken(); + token.principal("CN=Client") + .audience("domain") + .addAccess("service").addRole("admin").verifyCaller(true); + byte[] tokenBytes = JsonSerialization.toByteArray(token, false); + + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(idpPair.getPrivate()); + + SkeletonKeyToken v = null; + try + { + v = RSATokenVerifier.verifyToken(encoded, metadata); + } + catch (VerificationException ignored) + { + System.out.println(ignored.getMessage()); + } + } + + + +} diff --git a/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java b/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java new file mode 100755 index 0000000000..cf5e459d54 --- /dev/null +++ b/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java @@ -0,0 +1,78 @@ +package org.keycloak; + +import junit.framework.Assert; +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.keycloak.representations.SkeletonKeyScope; +import org.keycloak.representations.SkeletonKeyToken; +import org.junit.Test; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SkeletonKeyTokenTest +{ + @Test + public void testScope() throws Exception + { + SkeletonKeyScope scope2 = new SkeletonKeyScope(); + + scope2.add("one", "admin"); + scope2.add("one", "buyer"); + scope2.add("two", "seller"); + String json = JsonSerialization.toString(scope2, true); + System.out.println(json); + + + } + + @Test + public void testToken() throws Exception + { + SkeletonKeyToken token = new SkeletonKeyToken(); + token.id("111"); + token.addAccess("foo").addRole("admin"); + token.addAccess("bar").addRole("user"); + + String json = JsonSerialization.toString(token, true); + System.out.println(json); + + token = JsonSerialization.fromString(SkeletonKeyToken.class, json); + Assert.assertEquals("111", token.getId()); + SkeletonKeyToken.Access foo = token.getResourceAccess("foo"); + Assert.assertNotNull(foo); + Assert.assertTrue(foo.isUserInRole("admin")); + + } + + @Test + public void testRSA() throws Exception + { + SkeletonKeyToken token = new SkeletonKeyToken(); + token.id("111"); + token.addAccess("foo").addRole("admin"); + token.addAccess("bar").addRole("user"); + + KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + byte[] tokenBytes = JsonSerialization.toByteArray(token, true); + + String encoded = new JWSBuilder() + .content(tokenBytes) + .rsa256(keyPair.getPrivate()); + + System.out.println(encoded); + + JWSInput input = new JWSInput(encoded); + byte[] content = input.getContent(); + + token = JsonSerialization.fromBytes(SkeletonKeyToken.class, content); + Assert.assertEquals("111", token.getId()); + Assert.assertTrue(RSAProvider.verify(input, keyPair.getPublic())); + } +} diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000000..2225e9be9c --- /dev/null +++ b/pom.xml @@ -0,0 +1,301 @@ + + 4.0.0 + + Identity Guardener + org.keycloak + keycloak-parent + 1.0-alpha-1 + pom + + + 3.0.1.Final + + + http://keycloak.org + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + + + jboss-releases-repository + JBoss Releases Repository + https://repository.jboss.org/nexus/service/local/staging/deploy/maven2/ + + + + + JIRA + http://jira.jboss.com/jira/browse/KEYCLOAK + + + + + patriot1burke + Bill Burke + bburke@redhat.co + Red Hat + + project-owner + + -5 + + + + + + + + core + services + + + + + + org.bouncycastle + bcprov-jdk16 + 1.46 + + + org.bouncycastle + bcmail-jdk16 + 1.46 + + + org.infinispan + infinispan-core + 5.1.6.FINAL + + + org.infinispan + infinispan-tree + 5.1.6.FINAL + + + org.jboss.resteasy + jaxrs-api + ${resteasy.version} + + + org.jboss.resteasy + resteasy-jaxrs + ${resteasy.version} + + + org.jboss.resteasy + resteasy-client + ${resteasy.version} + + + org.jboss.resteasy + jose-jwt + ${resteasy.version} + + + org.jboss.resteasy + resteasy-crypto + ${resteasy.version} + + + org.jboss.resteasy + jose-jwt + ${resteasy.version} + + + org.codehaus.jackson + jackson-core-asl + 1.9.12 + + + org.codehaus.jackson + jackson-mapper-asl + 1.9.12 + + + org.codehaus.jackson + jackson-xc + 1.9.12 + + + org.codehaus.jackson + jackson-jaxrs + 1.9.12 + + + javax.servlet + servlet-api + 2.5 + + + org.jboss.resteasy + tjws + ${resteasy.version} + + + org.picketlink + picketlink-idm-api + 2.5.0-SNAPSHOT + + + org.picketlink + picketlink-idm-impl + 2.5.0-SNAPSHOT + + + org.picketlink + picketlink-idm-schema + 2.5.0-SNAPSHOT + + + org.picketlink + picketlink-config + 2.5.0-SNAPSHOT + + + junit + junit + 4.11 + + + + + + + jboss + http://repository.jboss.org/nexus/content/groups/public/ + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + once + -Xms512m -Xmx512m + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.1 + + 1.6 + 1.6 + utf-8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.8 + + 128m + 1024m + false + true + + com.restfully.*:org.jboss.resteasy.examples.*:se.unlogic.*:org.jboss.resteasy.tests.* + + + + + org.apache.maven.plugins + maven-install-plugin + 2.3.1 + + true + + + + org.apache.maven.plugins + maven-source-plugin + 2.1.2 + + + verify + + jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.5 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.1 + + 1.6 + 1.6 + utf-8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.8 + + 128m + 1024m + false + true + + se.unlogic.*:com.restfully.*:org.jboss.resteasy.examples.*:org.jboss.resteasy.tests.* + + + + + org.apache.maven.plugins + maven-install-plugin + 2.3.1 + + true + + + + org.apache.maven.plugins + maven-source-plugin + 2.1.2 + + + verify + + jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.5 + + + com.atlassian.maven.plugins + maven-clover2-plugin + 3.1.6 + + + + + diff --git a/services/pom.xml b/services/pom.xml new file mode 100755 index 0000000000..8c097aeb36 --- /dev/null +++ b/services/pom.xml @@ -0,0 +1,111 @@ + + + + keycloak-parent + org.keycloak + 1.0-alpha-1 + ../pom.xml + + 4.0.0 + + keycloak-services + Keycloak REST Services + + + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.picketlink + picketlink-idm-api + + + org.picketlink + picketlink-idm-impl + + + org.picketlink + picketlink-idm-schema + + + org.picketlink + picketlink-config + + + org.infinispan + infinispan-core + + + org.jboss.resteasy + resteasy-jaxrs + provided + + + org.jboss.resteasy + jaxrs-api + provided + + + org.jboss.resteasy + resteasy-client + provided + + + org.jboss.resteasy + resteasy-crypto + provided + + + org.jboss.resteasy + jose-jwt + provided + + + javax.servlet + servlet-api + test + + + org.jboss.resteasy + tjws + test + + + org.codehaus.jackson + jackson-core-asl + provided + + + org.codehaus.jackson + jackson-mapper-asl + provided + + + org.codehaus.jackson + jackson-xc + provided + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + diff --git a/services/src/main/java/org/keycloak/services/IdentityManagerAdapter.java b/services/src/main/java/org/keycloak/services/IdentityManagerAdapter.java new file mode 100755 index 0000000000..0df74ffab1 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/IdentityManagerAdapter.java @@ -0,0 +1,84 @@ +package org.keycloak.services; + +import org.keycloak.services.model.data.RealmModel; +import org.keycloak.services.model.data.RequiredCredentialModel; +import org.keycloak.services.model.data.ResourceModel; +import org.keycloak.services.model.data.RoleMappingModel; +import org.keycloak.services.model.data.RoleModel; +import org.keycloak.services.model.data.ScopeMappingModel; +import org.keycloak.services.model.data.UserAttributeModel; +import org.keycloak.services.model.data.UserCredentialModel; +import org.keycloak.services.model.data.UserModel; + +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public interface IdentityManagerAdapter +{ + RealmModel getRealm(String id); + RealmModel create(RealmModel realm); + void update(RealmModel realm); + void delete(RealmModel realm); + List getRequiredCredentials(RealmModel realm); + RequiredCredentialModel getRealmCredential(String id); + RequiredCredentialModel create(RealmModel realm, RequiredCredentialModel cred); + void update(RequiredCredentialModel cred); + void delete(RequiredCredentialModel cred); + + UserModel getUser(RealmModel realm, String username); + UserModel create(RealmModel realm, UserModel user); + void update(RealmModel realm, UserModel user); + void delete(RealmModel realm,UserModel user); + + List getCredentials(UserModel user); + UserCredentialModel getCredential(String id); + UserCredentialModel create(UserModel user, UserCredentialModel cred); + void update(UserCredentialModel cred); + void delete(UserCredentialModel cred); + + UserAttributeModel getUserAttribute(String id); + UserAttributeModel create(UserModel user, UserAttributeModel attribute); + void update(UserAttributeModel attribute); + void delete(UserAttributeModel attribute); + + ResourceModel getResource(String resourceid); + List getResources(RealmModel realm); + ResourceModel create(RealmModel realm, ResourceModel resource); + void update(ResourceModel resource); + void delete(ResourceModel resource); + + + RoleModel getRoleByName(RealmModel realm, String roleName); + RoleModel getRoleByName(ResourceModel resource, String roleName); + List getRoles(RealmModel realm, ResourceModel resource); + List getRoles(RealmModel realm); + RoleModel getRole(String id); + RoleModel create(RealmModel realm, ResourceModel resource, String role); + RoleModel create(RealmModel realm, String role); + void delete(RoleModel role); + + List getRoleMappings(RealmModel realm); + List getRoleMappings(RealmModel realm, ResourceModel resource); + RoleMappingModel getRoleMapping(RealmModel realm, UserModel user); + RoleMappingModel getRoleMapping(RealmModel realm, ResourceModel resource, UserModel user); + RoleMappingModel getRoleMapping(String id); + RoleMappingModel create(RealmModel realm, UserModel user, RoleMappingModel mapping); + RoleMappingModel create(RealmModel realm, ResourceModel resource, UserModel user, RoleMappingModel mapping); + void delete(RoleMappingModel role); + + List getScopeMappings(RealmModel realm); + List getScopeMappings(RealmModel realm, ResourceModel resource); + ScopeMappingModel getScopeMapping(RealmModel realm, UserModel user); + ScopeMappingModel getScopeMapping(RealmModel realm, ResourceModel resource, UserModel user); + ScopeMappingModel getScopeMapping(String id); + ScopeMappingModel create(RealmModel realm, UserModel user, ScopeMappingModel mapping); + ScopeMappingModel create(RealmModel realm, ResourceModel resource, UserModel user, ScopeMappingModel mapping); + void delete(ScopeMappingModel scope); + + + List getRealmsByName(String name); +} diff --git a/services/src/main/java/org/keycloak/services/model/RealmManager.java b/services/src/main/java/org/keycloak/services/model/RealmManager.java new file mode 100755 index 0000000000..270a86c499 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/RealmManager.java @@ -0,0 +1,49 @@ +package org.keycloak.services.model; + +import org.picketlink.idm.IdentityManager; +import org.picketlink.idm.internal.IdentityManagerFactory; +import org.picketlink.idm.model.Realm; +import org.picketlink.idm.model.SimpleAgent; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RealmManager +{ + private static AtomicLong counter = new AtomicLong(1); + + public static String generateId() + { + return counter.getAndIncrement() + "-" + System.currentTimeMillis(); + } + + protected IdentityManagerFactory factory; + + public RealmManager(IdentityManagerFactory factory) + { + this.factory = factory; + } + + public RealmModel getRealm(String id) + { + Realm existing = factory.findRealm(id); + if (existing == null) + { + return null; + } + return new RealmModel(existing, factory); + } + + public RealmModel create(String name) + { + Realm newRealm = factory.createRealm(generateId()); + IdentityManager idm = factory.createIdentityManager(newRealm); + SimpleAgent agent = new SimpleAgent(RealmModel.REALM_AGENT_ID); + idm.add(agent); + return new RealmModel(newRealm, factory); + } + +} diff --git a/services/src/main/java/org/keycloak/services/model/RealmModel.java b/services/src/main/java/org/keycloak/services/model/RealmModel.java new file mode 100755 index 0000000000..1b48554f59 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/RealmModel.java @@ -0,0 +1,389 @@ +package org.keycloak.services.model; + +import org.bouncycastle.openssl.PEMWriter; +import org.jboss.resteasy.security.PemUtils; +import org.keycloak.representations.idm.RequiredCredentialRepresentation; +import org.picketlink.idm.IdentityManager; +import org.picketlink.idm.credential.Password; +import org.picketlink.idm.credential.TOTPCredential; +import org.picketlink.idm.credential.X509CertificateCredentials; +import org.picketlink.idm.internal.IdentityManagerFactory; +import org.picketlink.idm.model.Agent; +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; +import org.picketlink.idm.query.RelationshipQuery; + +import java.io.IOException; +import java.io.StringWriter; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RealmModel +{ + public static final String REALM_AGENT_ID = "_realm_"; + public static final String REALM_NAME = "name"; + public static final String REALM_ACCESS_CODE_LIFESPAN = "accessCodeLifespan"; + public static final String REALM_TOKEN_LIFESPAN = "tokenLifespan"; + public static final String REALM_PRIVATE_KEY = "privateKey"; + 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"; + + protected Realm realm; + protected Agent realmAgent; + protected IdentityManagerFactory factory; + protected volatile transient PublicKey publicKey; + protected volatile transient PrivateKey privateKey; + + public RealmModel(Realm realm, IdentityManagerFactory factory) + { + this.realm = realm; + this.factory = factory; + realmAgent = getIdm().getAgent(REALM_AGENT_ID); + } + + public IdentityManager getIdm() + { + return factory.createIdentityManager(realm); + } + + public void updateRealm() + { + getIdm().update(realmAgent); + } + + public String getId() + { + return realm.getId(); + } + + public String getName() + { + return (String)realmAgent.getAttribute(REALM_NAME).getValue(); + } + + public void setName(String name) + { + realmAgent.setAttribute(new Attribute(REALM_NAME, name)); + } + + public boolean isEnabled() + { + return realmAgent.isEnabled(); + } + + public void setEnabled(boolean enabled) + { + realmAgent.setEnabled(enabled); + } + + public boolean isSslNotRequired() + { + return (Boolean)realmAgent.getAttribute(REALM_IS_SSL_NOT_REQUIRED).getValue(); + } + + public void setSslNotRequired(boolean sslNotRequired) + { + realmAgent.setAttribute(new Attribute(REALM_IS_SSL_NOT_REQUIRED, sslNotRequired)); + } + + public boolean isCookieLoginAllowed() + { + return (Boolean)realmAgent.getAttribute(REALM_IS_COOKIE_LOGIN_ALLOWED).getValue(); + } + + public void setCookieLoginAllowed(boolean cookieLoginAllowed) + { + realmAgent.setAttribute(new Attribute(REALM_IS_COOKIE_LOGIN_ALLOWED, cookieLoginAllowed)); + } + + public long getTokenLifespan() + { + return (Long) realmAgent.getAttribute(REALM_TOKEN_LIFESPAN).getValue(); + } + + public void setTokenLifespan(long tokenLifespan) + { + realmAgent.setAttribute(new Attribute(REALM_TOKEN_LIFESPAN,tokenLifespan)); + } + + public long getAccessCodeLifespan() + { + return (Long) realmAgent.getAttribute(REALM_ACCESS_CODE_LIFESPAN).getValue(); + } + + public void setAccessCodeLifespan(long accessCodeLifespan) + { + realmAgent.setAttribute(new Attribute(REALM_ACCESS_CODE_LIFESPAN, accessCodeLifespan)); + } + + public String getPublicKeyPem() + { + return (String) realmAgent.getAttribute(REALM_PUBLIC_KEY).getValue(); + } + + public void setPublicKeyPem(String publicKeyPem) + { + realmAgent.setAttribute(new Attribute(REALM_PUBLIC_KEY, publicKeyPem)); + this.publicKey = null; + } + + public String getPrivateKeyPem() + { + return (String) realmAgent.getAttribute(REALM_PRIVATE_KEY).getValue(); + } + + public void setPrivateKeyPem(String privateKeyPem) + { + realmAgent.setAttribute(new Attribute(REALM_PRIVATE_KEY, privateKeyPem)); + this.privateKey = null; + } + + public PublicKey getPublicKey() + { + if (publicKey != null) return publicKey; + String pem = getPublicKeyPem(); + if (pem != null) + { + try + { + publicKey = PemUtils.decodePublicKey(pem); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + return publicKey; + } + + public void setPublicKey(PublicKey publicKey) + { + this.publicKey = publicKey; + StringWriter writer = new StringWriter(); + PEMWriter pemWriter = new PEMWriter(writer); + try + { + pemWriter.writeObject(publicKey); + pemWriter.flush(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + String s = writer.toString(); + setPublicKeyPem(PemUtils.removeBeginEnd(s)); + } + + public PrivateKey getPrivateKey() + { + if (privateKey != null) return privateKey; + String pem = getPrivateKeyPem(); + if (pem != null) + { + try + { + privateKey = PemUtils.decodePrivateKey(pem); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + return privateKey; + } + + public void setPrivateKey(PrivateKey privateKey) + { + this.privateKey = privateKey; + StringWriter writer = new StringWriter(); + PEMWriter pemWriter = new PEMWriter(writer); + try + { + pemWriter.writeObject(privateKey); + pemWriter.flush(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + String s = writer.toString(); + setPrivateKeyPem(PemUtils.removeBeginEnd(s)); + } + + public List getRequiredCredentials() + { + IdentityManager idm = getIdm(); + Agent realmAgent = idm.getAgent(REALM_AGENT_ID); + RelationshipQuery query = idm.createRelationshipQuery(RequiredCredentialRelationship.class); + query.setParameter(RequiredCredentialRelationship.REALM_AGENT, realmAgent); + List results = query.getResultList(); + List rtn = new ArrayList(); + for (RequiredCredentialRelationship relationship : results) + { + RequiredCredentialModel model = new RequiredCredentialModel(); + model.setInput(relationship.isInput()); + model.setSecret(relationship.isSecret()); + model.setType(relationship.getCredentialType()); + rtn.add(model); + } + return rtn; + } + + public void addRequiredCredential(RequiredCredentialModel cred) + { + IdentityManager idm = getIdm(); + Agent realmAgent = idm.getAgent(REALM_AGENT_ID); + RequiredCredentialRelationship relationship = new RequiredCredentialRelationship(); + relationship.setCredentialType(cred.getType()); + relationship.setInput(cred.isInput()); + relationship.setSecret(cred.isSecret()); + relationship.setRealmAgent(realmAgent); + idm.add(relationship); + } + + public void updateCredential(User user, UserCredentialModel cred) + { + IdentityManager idm = getIdm(); + if (cred.getType().equals(RequiredCredentialRepresentation.PASSWORD)) + { + Password password = new Password(cred.getValue()); + idm.updateCredential(user, password); + } + else if (cred.getType().equals(RequiredCredentialRepresentation.TOTP)) + { + TOTPCredential totp = new TOTPCredential(cred.getValue()); + idm.updateCredential(user, totp); + } + else if (cred.getType().equals(RequiredCredentialRepresentation.CLIENT_CERT)) + { + X509Certificate cert = null; + try + { + cert = org.keycloak.PemUtils.decodeCertificate(cred.getValue()); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + X509CertificateCredentials creds = new X509CertificateCredentials(cert); + idm.updateCredential(user, creds); + } + } + + public List getRoles() + { + IdentityManager idm = getIdm(); + IdentityQuery query = idm.createIdentityQuery(Role.class); + query.setParameter(Role.PARTITION, realm); + return query.getResultList(); + } + + + protected ResourceModel loadResource(Agent resource) + { + Tier tier = factory.findTier(resource.getPartition().getId()); + return new ResourceModel(tier, resource, this, factory); + } + + /** + * Key name, value resource + * + * @return + */ + public Map getResourceMap() + { + Map resourceMap = new HashMap(); + for (ResourceModel resource : getResources()) + { + resourceMap.put(resource.getName(), resource); + } + return resourceMap; + } + + public List getResources() + { + IdentityManager idm = getIdm(); + RelationshipQuery query = idm.createRelationshipQuery(RealmResourceRelationship.class); + query.setParameter(RealmResourceRelationship.REALM_AGENT, realmAgent); + List results = query.getResultList(); + List resources = new ArrayList(); + for (RealmResourceRelationship relationship : results) + { + ResourceModel model = loadResource(relationship.getResourceAgent()); + resources.add(model); + } + + return resources; + } + + public ResourceModel addResource(String name) + { + Tier newTier = factory.createTier(RealmManager.generateId()); + IdentityManager idm = factory.createIdentityManager(newTier); + SimpleAgent resourceAgent = new SimpleAgent(ResourceModel.RESOURCE_AGENT_ID); + resourceAgent.setAttribute(new Attribute(ResourceModel.RESOURCE_NAME, name)); + idm.add(resourceAgent); + idm = getIdm(); + RealmResourceRelationship relationship = new RealmResourceRelationship(); + relationship.setRealmAgent(realmAgent); + relationship.setResourceAgent(resourceAgent); + idm.add(relationship); + return new ResourceModel(newTier, resourceAgent, this, factory); + } + + public Set getRoleMappings(User user) + { + RelationshipQuery query = getIdm().createRelationshipQuery(Grant.class); + query.setParameter(Grant.ASSIGNEE, user); + List grants = query.getResultList(); + HashSet set = new HashSet(); + for (Grant grant : grants) + { + if (grant.getRole().getPartition().getId().equals(realm.getId()))set.add(grant.getRole().getName()); + } + return set; + } + + public void addScope(Agent agent, String roleName) + { + IdentityManager idm = getIdm(); + Role role = idm.getRole(roleName); + if (role == null) throw new RuntimeException("role not found"); + ScopeRelationship scope = new ScopeRelationship(); + scope.setClient(agent); + scope.setScope(role); + + } + + + public Set getScope(Agent agent) + { + RelationshipQuery query = getIdm().createRelationshipQuery(ScopeRelationship.class); + query.setParameter(ScopeRelationship.CLIENT, agent); + List scope = query.getResultList(); + HashSet set = new HashSet(); + for (ScopeRelationship rel : scope) + { + if (rel.getScope().getPartition().getId().equals(realm.getId())) set.add(rel.getScope().getName()); + } + return set; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/RealmResourceRelationship.java b/services/src/main/java/org/keycloak/services/model/RealmResourceRelationship.java new file mode 100755 index 0000000000..b6011602ff --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/RealmResourceRelationship.java @@ -0,0 +1,57 @@ +package org.keycloak.services.model; + +import org.picketlink.idm.model.AbstractAttributedType; +import org.picketlink.idm.model.Agent; +import org.picketlink.idm.model.Relationship; +import org.picketlink.idm.model.annotation.IdentityProperty; +import org.picketlink.idm.query.RelationshipQueryParameter; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RealmResourceRelationship extends AbstractAttributedType implements Relationship +{ + private static final long serialVersionUID = 1L; + + public static final RelationshipQueryParameter REALM_AGENT = new RelationshipQueryParameter() { + + @Override + public String getName() { + return "realmAgent"; + } + }; + + public static final RelationshipQueryParameter RESOURCE_AGENT = new RelationshipQueryParameter() { + + @Override + public String getName() { + return "resourceAgent"; + } + }; + + protected Agent realmAgent; + protected Agent resourceAgent; + + @IdentityProperty + public Agent getRealmAgent() + { + return realmAgent; + } + + public void setRealmAgent(Agent realmAgent) + { + this.realmAgent = realmAgent; + } + + @IdentityProperty + public Agent getResourceAgent() + { + return resourceAgent; + } + + public void setResourceAgent(Agent resourceAgent) + { + this.resourceAgent = resourceAgent; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/RequiredCredentialModel.java b/services/src/main/java/org/keycloak/services/model/RequiredCredentialModel.java new file mode 100755 index 0000000000..8506e440dd --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/RequiredCredentialModel.java @@ -0,0 +1,42 @@ +package org.keycloak.services.model; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RequiredCredentialModel +{ + protected String type; + protected boolean input; + protected boolean secret; + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } + + public boolean isInput() + { + return input; + } + + public void setInput(boolean input) + { + this.input = input; + } + + public boolean isSecret() + { + return secret; + } + + public void setSecret(boolean secret) + { + this.secret = secret; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/RequiredCredentialRelationship.java b/services/src/main/java/org/keycloak/services/model/RequiredCredentialRelationship.java new file mode 100755 index 0000000000..c2e94db5a2 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/RequiredCredentialRelationship.java @@ -0,0 +1,79 @@ +package org.keycloak.services.model; + +import org.picketlink.idm.model.AbstractAttributedType; +import org.picketlink.idm.model.Agent; +import org.picketlink.idm.model.Relationship; +import org.picketlink.idm.model.annotation.AttributeProperty; +import org.picketlink.idm.model.annotation.IdentityProperty; +import org.picketlink.idm.query.RelationshipQueryParameter; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class RequiredCredentialRelationship extends AbstractAttributedType implements Relationship +{ + private static final long serialVersionUID = 1L; + + public static final RelationshipQueryParameter REALM_AGENT = new RelationshipQueryParameter() { + + @Override + public String getName() { + return "realmAgent"; + } + }; + + + protected Agent realmAgent; + protected String credentialType; + protected boolean input; + protected boolean secret; + + public RequiredCredentialRelationship() + { + } + + @IdentityProperty + public Agent getRealmAgent() + { + return realmAgent; + } + + public void setRealmAgent(Agent realmAgent) + { + this.realmAgent = realmAgent; + } + + @AttributeProperty + public String getCredentialType() + { + return credentialType; + } + + public void setCredentialType(String credentialType) + { + this.credentialType = credentialType; + } + + @AttributeProperty + public boolean isInput() + { + return input; + } + + public void setInput(boolean input) + { + this.input = input; + } + + @AttributeProperty + public boolean isSecret() + { + return secret; + } + + public void setSecret(boolean secret) + { + this.secret = secret; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/ResourceModel.java b/services/src/main/java/org/keycloak/services/model/ResourceModel.java new file mode 100755 index 0000000000..a50dcdade7 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/ResourceModel.java @@ -0,0 +1,134 @@ +package org.keycloak.services.model; + +import org.picketlink.idm.IdentityManager; +import org.picketlink.idm.internal.IdentityManagerFactory; +import org.picketlink.idm.model.Agent; +import org.picketlink.idm.model.Attribute; +import org.picketlink.idm.model.Grant; +import org.picketlink.idm.model.Role; +import org.picketlink.idm.model.Tier; +import org.picketlink.idm.model.User; +import org.picketlink.idm.query.IdentityQuery; +import org.picketlink.idm.query.RelationshipQuery; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ResourceModel +{ + public static final String RESOURCE_AGENT_ID = "_resource_"; + public static final String RESOURCE_NAME = "name"; + public static final String RESOURCE_SURROGATE_AUTH = "surrogate_auth"; + + protected Tier tier; + protected Agent agent; + protected RealmModel realm; + protected IdentityManagerFactory factory; + + public ResourceModel(Tier tier, Agent agent, RealmModel realm, IdentityManagerFactory factory) + { + this.tier = tier; + this.agent = agent; + this.realm = realm; + this.factory = factory; + } + + public IdentityManager getIdm() + { + return factory.createIdentityManager(tier); + } + + public void updateResource() + { + getIdm().update(agent); + } + + public String getId() + { + return tier.getId(); + } + + public String getName() + { + return (String)agent.getAttribute(RESOURCE_NAME).getValue(); + } + + public void setName(String name) + { + agent.setAttribute(new Attribute(RESOURCE_NAME, name)); + getIdm().update(agent); + } + + public boolean isEnabled() + { + return agent.isEnabled(); + } + + public void setEnabled(boolean enabled) + { + agent.setEnabled(enabled); + } + + public boolean isSurrogateAuthRequired() + { + return (Boolean)agent.getAttribute(RESOURCE_SURROGATE_AUTH).getValue(); + } + + public void setSurrogateAuthRequired(boolean surrogateAuthRequired) + { + agent.setAttribute(new Attribute(RESOURCE_SURROGATE_AUTH, surrogateAuthRequired)); + } + + public List getRoles() + { + IdentityQuery query = getIdm().createIdentityQuery(Role.class); + query.setParameter(Role.PARTITION, tier); + return query.getResultList(); + } + + public Set getRoleMappings(User user) + { + RelationshipQuery query = getIdm().createRelationshipQuery(Grant.class); + query.setParameter(Grant.ASSIGNEE, user); + List grants = query.getResultList(); + HashSet set = new HashSet(); + for (Grant grant : grants) + { + if (grant.getRole().getPartition().getId().equals(tier.getId()))set.add(grant.getRole().getName()); + } + return set; + } + + public void addScope(Agent agent, String roleName) + { + IdentityManager idm = getIdm(); + Role role = idm.getRole(roleName); + if (role == null) throw new RuntimeException("role not found"); + ScopeRelationship scope = new ScopeRelationship(); + scope.setClient(agent); + scope.setScope(role); + + } + + + public Set getScope(Agent agent) + { + RelationshipQuery query = getIdm().createRelationshipQuery(ScopeRelationship.class); + query.setParameter(ScopeRelationship.CLIENT, agent); + List scope = query.getResultList(); + HashSet set = new HashSet(); + for (ScopeRelationship rel : scope) + { + if (rel.getScope().getPartition().getId().equals(tier.getId())) set.add(rel.getScope().getName()); + } + return set; + } + + + +} diff --git a/services/src/main/java/org/keycloak/services/model/ScopeRelationship.java b/services/src/main/java/org/keycloak/services/model/ScopeRelationship.java new file mode 100755 index 0000000000..0f01be7423 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/ScopeRelationship.java @@ -0,0 +1,50 @@ +package org.keycloak.services.model; + +import org.picketlink.idm.model.AbstractAttributedType; +import org.picketlink.idm.model.Agent; +import org.picketlink.idm.model.Relationship; +import org.picketlink.idm.model.Role; +import org.picketlink.idm.model.annotation.IdentityProperty; +import org.picketlink.idm.query.RelationshipQueryParameter; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class ScopeRelationship extends AbstractAttributedType implements Relationship +{ + private static final long serialVersionUID = 1L; + + public static final RelationshipQueryParameter CLIENT = new RelationshipQueryParameter() { + + @Override + public String getName() { + return "client"; + } + }; + + protected Agent client; + protected Role scope; + + @IdentityProperty + public Agent getClient() + { + return client; + } + + public void setClient(Agent client) + { + this.client = client; + } + + @IdentityProperty + public Role getScope() + { + return scope; + } + + public void setScope(Role scope) + { + this.scope = scope; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/UserCredentialModel.java b/services/src/main/java/org/keycloak/services/model/UserCredentialModel.java new file mode 100755 index 0000000000..ee8c7e7251 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/UserCredentialModel.java @@ -0,0 +1,32 @@ +package org.keycloak.services.model; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UserCredentialModel +{ + + protected String type; + protected String value; + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } + + public String getValue() + { + return value; + } + + public void setValue(String value) + { + this.value = value; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/data/RealmModel.java b/services/src/main/java/org/keycloak/services/model/data/RealmModel.java new file mode 100755 index 0000000000..ab3711a943 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/data/RealmModel.java @@ -0,0 +1,194 @@ +package org.keycloak.services.model.data; + +import org.bouncycastle.openssl.PEMWriter; +import org.jboss.resteasy.security.PemUtils; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringWriter; +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public class RealmModel implements Serializable +{ + private static final long serialVersionUID = 1L; + + protected String id; + protected String name; + protected long tokenLifespan = 3600 * 24; // one day + protected long accessCodeLifespan = 300; // 5 minutes + protected boolean enabled; + protected boolean sslNotRequired; + protected boolean cookieLoginAllowed; + protected String publicKeyPem; + protected String privateKeyPem; + protected volatile transient PublicKey publicKey; + protected volatile transient PrivateKey privateKey; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public boolean isSslNotRequired() + { + return sslNotRequired; + } + + public void setSslNotRequired(boolean sslNotRequired) + { + this.sslNotRequired = sslNotRequired; + } + + public boolean isCookieLoginAllowed() + { + return cookieLoginAllowed; + } + + public void setCookieLoginAllowed(boolean cookieLoginAllowed) + { + this.cookieLoginAllowed = cookieLoginAllowed; + } + + public long getTokenLifespan() + { + return tokenLifespan; + } + + public void setTokenLifespan(long tokenLifespan) + { + this.tokenLifespan = tokenLifespan; + } + + public long getAccessCodeLifespan() + { + return accessCodeLifespan; + } + + public void setAccessCodeLifespan(long accessCodeLifespan) + { + this.accessCodeLifespan = accessCodeLifespan; + } + + public String getPublicKeyPem() + { + return publicKeyPem; + } + + public void setPublicKeyPem(String publicKeyPem) + { + this.publicKeyPem = publicKeyPem; + this.publicKey = null; + } + + public String getPrivateKeyPem() + { + return privateKeyPem; + } + + public void setPrivateKeyPem(String privateKeyPem) + { + this.privateKeyPem = privateKeyPem; + this.privateKey = null; + } + + public PublicKey getPublicKey() + { + if (publicKey != null) return publicKey; + if (publicKeyPem != null) + { + try + { + publicKey = PemUtils.decodePublicKey(publicKeyPem); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + return publicKey; + } + + public void setPublicKey(PublicKey publicKey) + { + this.publicKey = publicKey; + StringWriter writer = new StringWriter(); + PEMWriter pemWriter = new PEMWriter(writer); + try + { + pemWriter.writeObject(publicKey); + pemWriter.flush(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + String s = writer.toString(); + this.publicKeyPem = PemUtils.removeBeginEnd(s); + } + + public PrivateKey getPrivateKey() + { + if (privateKey != null) return privateKey; + if (privateKeyPem != null) + { + try + { + privateKey = PemUtils.decodePrivateKey(privateKeyPem); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + return privateKey; + } + + public void setPrivateKey(PrivateKey privateKey) + { + this.privateKey = privateKey; + StringWriter writer = new StringWriter(); + PEMWriter pemWriter = new PEMWriter(writer); + try + { + pemWriter.writeObject(privateKey); + pemWriter.flush(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + String s = writer.toString(); + this.privateKeyPem = PemUtils.removeBeginEnd(s); + } +} diff --git a/services/src/main/java/org/keycloak/services/model/data/RequiredCredentialModel.java b/services/src/main/java/org/keycloak/services/model/data/RequiredCredentialModel.java new file mode 100755 index 0000000000..5ab0159142 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/data/RequiredCredentialModel.java @@ -0,0 +1,58 @@ +package org.keycloak.services.model.data; + +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public class RequiredCredentialModel implements Serializable +{ + private static final long serialVersionUID = 1L; + + protected String id; + protected String type; + protected boolean input; + protected boolean secret; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } + + public boolean isInput() + { + return input; + } + + public void setInput(boolean input) + { + this.input = input; + } + + public boolean isSecret() + { + return secret; + } + + public void setSecret(boolean secret) + { + this.secret = secret; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/data/ResourceModel.java b/services/src/main/java/org/keycloak/services/model/data/ResourceModel.java new file mode 100755 index 0000000000..e8f2421124 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/data/ResourceModel.java @@ -0,0 +1,46 @@ +package org.keycloak.services.model.data; + +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public class ResourceModel implements Serializable +{ + private static final long serialVersionUID = 1L; + protected String id; + protected String name; + protected boolean surrogateAuthRequired; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public boolean isSurrogateAuthRequired() + { + return surrogateAuthRequired; + } + + public void setSurrogateAuthRequired(boolean surrogateAuthRequired) + { + this.surrogateAuthRequired = surrogateAuthRequired; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/data/RoleMappingModel.java b/services/src/main/java/org/keycloak/services/model/data/RoleMappingModel.java new file mode 100755 index 0000000000..06e96d86e4 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/data/RoleMappingModel.java @@ -0,0 +1,59 @@ +package org.keycloak.services.model.data; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public class RoleMappingModel implements Serializable +{ + private static final long serialVersionUID = 1L; + protected String id; + protected String userid; + protected Set roles = new HashSet(); + protected Set surrogateIds = new HashSet(); + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getUserid() + { + return userid; + } + + public void setUserid(String userid) + { + this.userid = userid; + } + + public Set getRoles() + { + return roles; + } + + public void setRoles(Set roles) + { + this.roles = roles; + } + + public Set getSurrogateIds() + { + return surrogateIds; + } + + public void setSurrogateIds(Set surrogateIds) + { + this.surrogateIds = surrogateIds; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/data/RoleModel.java b/services/src/main/java/org/keycloak/services/model/data/RoleModel.java new file mode 100755 index 0000000000..a0740d1679 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/data/RoleModel.java @@ -0,0 +1,35 @@ +package org.keycloak.services.model.data; + +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public class RoleModel implements Serializable +{ + private static final long serialVersionUID = 1L; + protected String id; + protected String name; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/data/ScopeMappingModel.java b/services/src/main/java/org/keycloak/services/model/data/ScopeMappingModel.java new file mode 100755 index 0000000000..3e3bc01726 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/data/ScopeMappingModel.java @@ -0,0 +1,49 @@ +package org.keycloak.services.model.data; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public class ScopeMappingModel implements Serializable +{ + private static final long serialVersionUID = 1L; + protected String id; + protected String userid; + protected Set roles = new HashSet(); + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getUserid() + { + return userid; + } + + public void setUserid(String userid) + { + this.userid = userid; + } + + public Set getRoles() + { + return roles; + } + + public void setRoles(Set roles) + { + this.roles = roles; + } + +} diff --git a/services/src/main/java/org/keycloak/services/model/data/UserAttributeModel.java b/services/src/main/java/org/keycloak/services/model/data/UserAttributeModel.java new file mode 100755 index 0000000000..1f7e2ba465 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/data/UserAttributeModel.java @@ -0,0 +1,46 @@ +package org.keycloak.services.model.data; + +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public class UserAttributeModel implements Serializable +{ + private static final long serialVersionUID = 1L; + protected String id; + protected String name; + protected String value; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getValue() + { + return value; + } + + public void setValue(String value) + { + this.value = value; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/data/UserCredentialModel.java b/services/src/main/java/org/keycloak/services/model/data/UserCredentialModel.java new file mode 100755 index 0000000000..b1cf95a079 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/data/UserCredentialModel.java @@ -0,0 +1,58 @@ +package org.keycloak.services.model.data; + +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public class UserCredentialModel implements Serializable +{ + private static final long serialVersionUID = 1L; + + protected String id; + protected String type; + protected String value; + protected boolean hashed; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getType() + { + return type; + } + + public void setType(String type) + { + this.type = type; + } + + public String getValue() + { + return value; + } + + public void setValue(String value) + { + this.value = value; + } + + public boolean isHashed() + { + return hashed; + } + + public void setHashed(boolean hashed) + { + this.hashed = hashed; + } +} diff --git a/services/src/main/java/org/keycloak/services/model/data/UserModel.java b/services/src/main/java/org/keycloak/services/model/data/UserModel.java new file mode 100755 index 0000000000..d878348a84 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/model/data/UserModel.java @@ -0,0 +1,46 @@ +package org.keycloak.services.model.data; + +import java.io.Serializable; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Deprecated +public class UserModel implements Serializable +{ + private static final long serialVersionUID = 1L; + protected String id; + protected String username; + protected boolean enabled; + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } +} diff --git a/services/src/main/java/org/keycloak/services/service/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/service/AuthenticationManager.java new file mode 100755 index 0000000000..6412afd8cf --- /dev/null +++ b/services/src/main/java/org/keycloak/services/service/AuthenticationManager.java @@ -0,0 +1,87 @@ +package org.keycloak.services.service; + +import org.jboss.resteasy.logging.Logger; +import org.keycloak.representations.idm.RequiredCredentialRepresentation; +import org.keycloak.services.model.RealmManager; +import org.keycloak.services.model.RealmModel; +import org.keycloak.services.model.RequiredCredentialModel; +import org.picketlink.idm.credential.Credentials; +import org.picketlink.idm.credential.Password; +import org.picketlink.idm.credential.TOTPCredentials; +import org.picketlink.idm.credential.UsernamePasswordCredentials; +import org.picketlink.idm.model.User; + +import javax.ws.rs.core.MultivaluedMap; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class AuthenticationManager +{ + protected Logger logger = Logger.getLogger(AuthenticationManager.class); + public static final String FORM_USERNAME = "username"; + protected RealmManager adapter; + + public AuthenticationManager(RealmManager adapter) + { + this.adapter = adapter; + } + + public boolean authenticate(RealmModel realm, User user, MultivaluedMap formData) + { + String username = user.getLoginName(); + Set types = new HashSet(); + + for (RequiredCredentialModel credential : realm.getRequiredCredentials()) + { + types.add(credential.getType()); + } + + if (types.contains(RequiredCredentialRepresentation.PASSWORD)) + { + String password = formData.getFirst(RequiredCredentialRepresentation.PASSWORD); + if (password == null) + { + logger.warn("Password not provided"); + return false; + } + + if (types.contains(RequiredCredentialRepresentation.TOTP)) + { + String token = formData.getFirst(RequiredCredentialRepresentation.TOTP); + if (token == null) + { + logger.warn("TOTP token not provided"); + return false; + } + TOTPCredentials creds = new TOTPCredentials(); + creds.setToken(token); + creds.setUsername(username); + creds.setPassword(new Password(password)); + realm.getIdm().validateCredentials(creds); + if (creds.getStatus() != Credentials.Status.VALID) + { + return false; + } + } + else + { + UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, new Password(password)); + realm.getIdm().validateCredentials(creds); + if (creds.getStatus() != Credentials.Status.VALID) + { + return false; + } + } + } + else + { + logger.warn("Do not know how to authenticate user"); + return false; + } + return true; + } +} diff --git a/services/src/main/java/org/keycloak/services/service/RealmFactory.java b/services/src/main/java/org/keycloak/services/service/RealmFactory.java new file mode 100755 index 0000000000..821acc5f87 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/service/RealmFactory.java @@ -0,0 +1,354 @@ +package org.keycloak.services.service; + +import org.keycloak.services.model.RealmManager; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.model.RealmModel; +import org.picketlink.idm.model.User; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Path("/realmfactory") +public class RealmFactory +{ + protected RealmManager adapter; + + @Context + protected UriInfo uriInfo; + + public RealmFactory(RealmManager adapter) + { + this.adapter = adapter; + } + + + @POST + @Consumes("application/json") + public Response importDomain(RealmRepresentation rep) + { + RealmModel realm = createRealm(rep); + UriBuilder builder = uriInfo.getRequestUriBuilder().path(realm.getId()); + return Response.created(builder.build()) + //.entity(RealmResource.realmRep(realm, uriInfo)) + .type(MediaType.APPLICATION_JSON_TYPE).build(); + } + + protected RealmModel createRealm(RealmRepresentation rep) + { + //verifyRealmRepresentation(rep); + + RealmModel realm = adapter.create(rep.getRealm()); + KeyPair keyPair = null; + try + { + keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + } + catch (NoSuchAlgorithmException e) + { + throw new RuntimeException(e); + } + realm.setPrivateKey(keyPair.getPrivate()); + realm.setPublicKey(keyPair.getPublic()); + realm.setName(rep.getRealm()); + realm.setEnabled(rep.isEnabled()); + realm.setTokenLifespan(rep.getTokenLifespan()); + realm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); + realm.setSslNotRequired(rep.isSslNotRequired()); + realm.updateRealm(); + + + Map userMap = new HashMap(); + return null; + }/* + RoleModel adminRole = identityManager.create(realm, "admin"); + + for (RequiredCredentialRepresentation requiredCred : rep.getRequiredCredentials()) + { + RequiredCredentialModel credential = new RequiredCredentialModel(); + credential.setType(requiredCred.getType()); + credential.setInput(requiredCred.isInput()); + credential.setSecret(requiredCred.isSecret()); + identityManager.create(realm, credential); + } + + for (UserRepresentation userRep : rep.getUsers()) + { + UserModel user = new UserModel(); + user.setUsername(userRep.getUsername()); + user.setEnabled(userRep.isEnabled()); + user = identityManager.create(realm, user); + userMap.put(user.getUsername(), user); + if (userRep.getCredentials() != null) + { + for (UserRepresentation.Credential cred : userRep.getCredentials()) + { + UserCredentialModel credential = new UserCredentialModel(); + credential.setType(cred.getType()); + credential.setValue(cred.getValue()); + credential.setHashed(cred.isHashed()); + identityManager.create(user, credential); + } + } + + if (userRep.getAttributes() != null) + { + for (Map.Entry entry : userRep.getAttributes().entrySet()) + { + UserAttributeModel attribute = new UserAttributeModel(); + attribute.setName(entry.getKey()); + attribute.setValue(entry.getValue()); + identityManager.create(user, attribute); + } + } + } + + for (RoleMappingRepresentation mapping : rep.getRoleMappings()) + { + RoleMappingModel roleMapping = createRoleMapping(userMap, mapping); + UserModel user = userMap.get(mapping.getUsername()); + identityManager.create(realm, user, roleMapping); + } + + if (rep.getScopeMappings() != null) + { + for (ScopeMappingRepresentation scope : rep.getScopeMappings()) + { + ScopeMappingModel scopeMapping = createScopeMapping(userMap, scope); + UserModel user = userMap.get(scope.getUsername()); + identityManager.create(realm, user, scopeMapping); + + } + } + + if (rep.getResources() != null) + { + for (ResourceRepresentation resourceRep : rep.getResources()) + { + ResourceModel resource = new ResourceModel(); + resource.setName(resourceRep.getName()); + resource.setSurrogateAuthRequired(resourceRep.isSurrogateAuthRequired()); + resource = identityManager.create(realm, resource); + if (resourceRep.getRoles() != null) + { + for (String role : resourceRep.getRoles()) + { + RoleModel r = identityManager.create(realm, resource, role); + } + } + if (resourceRep.getRoleMappings() != null) + { + for (RoleMappingRepresentation mapping : resourceRep.getRoleMappings()) + { + RoleMappingModel roleMapping = createRoleMapping(userMap, mapping); + UserModel user = userMap.get(mapping.getUsername()); + identityManager.create(realm, resource, user, roleMapping); + } + } + if (resourceRep.getScopeMappings() != null) + { + for (ScopeMappingRepresentation mapping : resourceRep.getScopeMappings()) + { + ScopeMappingModel scopeMapping = createScopeMapping(userMap, mapping); + UserModel user = userMap.get(mapping.getUsername()); + identityManager.create(realm, resource, user, scopeMapping); + } + } + + } + } + return realm; + } + + protected RoleMappingModel createRoleMapping(Map userMap, RoleMappingRepresentation mapping) + { + RoleMappingModel roleMapping = new RoleMappingModel(); + UserModel user = userMap.get(mapping.getUsername()); + roleMapping.setUserid(user.getId()); + if (mapping.getSurrogates() != null) + { + for (String s : mapping.getSurrogates()) + { + UserModel surrogate = userMap.get(s); + roleMapping.getSurrogateIds().add(surrogate.getId()); + + } + } + for (String role : mapping.getRoles()) + { + roleMapping.getRoles().add(role); + } + return roleMapping; + } + + protected ScopeMappingModel createScopeMapping(Map userMap, ScopeMappingRepresentation mapping) + { + ScopeMappingModel scopeMapping = new ScopeMappingModel(); + UserModel user = userMap.get(mapping.getUsername()); + scopeMapping.setUserid(user.getId()); + for (String role : mapping.getRoles()) + { + scopeMapping.getRoles().add(role); + } + return scopeMapping; + } + + + protected void verifyRealmRepresentation(RealmRepresentation rep) + { + if (rep.getUsers() == null) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("No realm admin users defined for realm").type("text/plain").build()); + } + + if (rep.getRequiredCredentials() == null) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("Realm credential requirements not defined").type("text/plain").build()); + + } + + if (rep.getRoleMappings() == null) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("No realm admin users defined for realm").type("text/plain").build()); + } + + HashMap userReps = new HashMap(); + for (UserRepresentation userRep : rep.getUsers()) userReps.put(userRep.getUsername(), userRep); + + // make sure there is a user that has admin privileges for the realm + Set admins = new HashSet(); + for (RoleMappingRepresentation mapping : rep.getRoleMappings()) + { + if (!userReps.containsKey(mapping.getUsername())) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("No users declared for role mapping").type("text/plain").build()); + + } + for (String role : mapping.getRoles()) + { + if (!role.equals("admin")) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("There is only an 'admin' role for realms").type("text/plain").build()); + + } + else + { + admins.add(userReps.get(mapping.getUsername())); + } + } + } + if (admins.size() == 0) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("No realm admin users defined for realm").type("text/plain").build()); + } + + // override enabled to false if user does not have at least all of browser or client credentials + for (UserRepresentation userRep : rep.getUsers()) + { + if (!userRep.isEnabled()) + { + admins.remove(userRep); + continue; + } + if (userRep.getCredentials() == null) + { + admins.remove(userRep); + userRep.setEnabled(false); + } + else + { + boolean hasBrowserCredentials = true; + for (RequiredCredentialRepresentation credential : rep.getRequiredCredentials()) + { + boolean hasCredential = false; + for (UserRepresentation.Credential cred : userRep.getCredentials()) + { + if (cred.getType().equals(credential.getType())) + { + hasCredential = true; + break; + } + } + if (!hasCredential) + { + hasBrowserCredentials = false; + break; + } + } + if (!hasBrowserCredentials) + { + userRep.setEnabled(false); + admins.remove(userRep); + } + + } + } + + if (admins.size() == 0) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("No realm admin users are enabled or have appropriate credentials").type("text/plain").build()); + } + + if (rep.getResources() != null) + { + // check mappings + for (ResourceRepresentation resourceRep : rep.getResources()) + { + if (resourceRep.getRoleMappings() != null) + { + for (RoleMappingRepresentation mapping : resourceRep.getRoleMappings()) + { + if (!userReps.containsKey(mapping.getUsername())) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("No users declared for role mapping").type("text/plain").build()); + + } + if (mapping.getSurrogates() != null) + { + for (String surrogate : mapping.getSurrogates()) + { + if (!userReps.containsKey(surrogate)) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("No users declared for role mapping surrogate").type("text/plain").build()); + } + } + } + for (String role : mapping.getRoles()) + { + if (!resourceRep.getRoles().contains(role)) + { + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("No resource role for role mapping").type("text/plain").build()); + } + } + } + } + } + } + } + */ + +} diff --git a/services/src/main/java/org/keycloak/services/service/RealmResource.java b/services/src/main/java/org/keycloak/services/service/RealmResource.java new file mode 100755 index 0000000000..a5baf404fc --- /dev/null +++ b/services/src/main/java/org/keycloak/services/service/RealmResource.java @@ -0,0 +1,158 @@ +package org.keycloak.services.service; + +import org.keycloak.services.IdentityManagerAdapter; +import org.keycloak.services.model.data.RealmModel; +import org.jboss.resteasy.logging.Logger; +import org.keycloak.representations.idm.PublishedRealmRepresentation; + +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Path("/") +public class RealmResource +{ + protected Logger logger = Logger.getLogger(RealmResource.class); + protected IdentityManagerAdapter identityManager; + @Context + protected UriInfo uriInfo; + + public RealmResource(IdentityManagerAdapter identityManager) + { + this.identityManager = identityManager; + } + + @GET + @Path("realms/{realm}") + @Produces("application/json") + public PublishedRealmRepresentation getRealm(@PathParam("realm") String id) + { + RealmModel realm = identityManager.getRealm(id); + if (realm == null) + { + logger.debug("realm not found"); + throw new NotFoundException(); + } + return realmRep(realm, uriInfo); + } + + @GET + @Path("realms/{realm}.html") + @Produces("text/html") + public String getRealmHtml(@PathParam("realm") String id) + { + RealmModel realm = identityManager.getRealm(id); + if (realm == null) + { + logger.debug("realm not found"); + throw new NotFoundException(); + } + return realmHtml(realm); + } + + private String realmHtml(RealmModel realm) + { + StringBuffer html = new StringBuffer(); + + UriBuilder auth = uriInfo.getBaseUriBuilder(); + auth.path(TokenService.class) + .path(TokenService.class, "requestAccessCode"); + String authUri = auth.build(realm.getId()).toString(); + + UriBuilder code = uriInfo.getBaseUriBuilder(); + code.path(TokenService.class).path(TokenService.class, "accessRequest"); + String codeUri = code.build(realm.getId()).toString(); + + UriBuilder grant = uriInfo.getBaseUriBuilder(); + grant.path(TokenService.class).path(TokenService.class, "accessTokenGrant"); + String grantUrl = grant.build(realm.getId()).toString(); + + html.append("

Realm: ").append(realm.getName()).append("

"); + html.append("

auth: ").append(authUri).append("

"); + html.append("

code: ").append(codeUri).append("

"); + html.append("

grant: ").append(grantUrl).append("

"); + html.append("

public key: ").append(realm.getPublicKeyPem()).append("

"); + html.append(""); + + return html.toString(); + } + + + @GET + @Path("realms") + @Produces("application/json") + public Response getRealmsByName(@QueryParam("name") String name) + { + if (name == null) return Response.noContent().build(); + List realms = identityManager.getRealmsByName(name); + if (realms.size() == 0) return Response.noContent().build(); + + List list = new ArrayList(); + for (RealmModel realm : realms) + { + list.add(realmRep(realm, uriInfo)); + } + GenericEntity> entity = new GenericEntity>(list){}; + return Response.ok(entity).type(MediaType.APPLICATION_JSON_TYPE).build(); + } + + @GET + @Path("realms.html") + @Produces("text/html") + public String getRealmsByNameHtml(@QueryParam("name") String name) + { + if (name == null) return "

No realms with that name

"; + List realms = identityManager.getRealmsByName(name); + if (realms.size() == 0) return "

No realms with that name

"; + if (realms.size() == 1) return realmHtml(realms.get(0)); + + StringBuffer html = new StringBuffer(); + html.append("

Realms

"); + for (RealmModel realm : realms) + { + html.append("

").append(realm.getId()).append("

"); + } + html.append(""); + return html.toString(); + } + + + public static PublishedRealmRepresentation realmRep(RealmModel realm, UriInfo uriInfo) + { + PublishedRealmRepresentation rep = new PublishedRealmRepresentation(); + rep.setRealm(realm.getName()); + rep.setSelf(uriInfo.getRequestUri().toString()); + rep.setPublicKeyPem(realm.getPublicKeyPem()); + + UriBuilder auth = uriInfo.getBaseUriBuilder(); + auth.path(TokenService.class) + .path(TokenService.class, "requestAccessCode"); + rep.setAuthorizationUrl(auth.build(realm.getId()).toString()); + + UriBuilder code = uriInfo.getBaseUriBuilder(); + code.path(TokenService.class).path(TokenService.class, "accessRequest"); + rep.setCodeUrl(code.build(realm.getId()).toString()); + + UriBuilder grant = uriInfo.getBaseUriBuilder(); + grant.path(TokenService.class).path(TokenService.class, "accessTokenGrant"); + String grantUrl = grant.build(realm.getId()).toString(); + rep.setGrantUrl(grantUrl); + return rep; + } +} diff --git a/services/src/main/java/org/keycloak/services/service/RegistrationService.java b/services/src/main/java/org/keycloak/services/service/RegistrationService.java new file mode 100755 index 0000000000..e8ff93deb2 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/service/RegistrationService.java @@ -0,0 +1,65 @@ +package org.keycloak.services.service; + +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.model.RealmManager; +import org.keycloak.services.model.RealmModel; +import org.keycloak.services.model.UserCredentialModel; +import org.picketlink.idm.model.Realm; +import org.picketlink.idm.model.SimpleUser; +import org.picketlink.idm.model.User; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.net.URI; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Path("/registrations") +public class RegistrationService +{ + protected RealmManager adapter; + protected RealmModel defaultRealm; + + @Context + protected UriInfo uriInfo; + + public RegistrationService(RealmManager adapter) + { + this.adapter = adapter; + defaultRealm = adapter.getRealm(Realm.DEFAULT_REALM); + } + + + + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response register(UserRepresentation newUser) + { + User user = defaultRealm.getIdm().getUser(newUser.getUsername()); + if (user != null) + { + return Response.status(400).type("text/plain").entity("user exists").build(); + } + + user = new SimpleUser(newUser.getUsername()); + defaultRealm.getIdm().add(user); + for (UserRepresentation.Credential cred : newUser.getCredentials()) + { + UserCredentialModel credModel = new UserCredentialModel(); + credModel.setType(cred.getType()); + credModel.setValue(cred.getValue()); + defaultRealm.updateCredential(user, credModel); + } + URI uri = uriInfo.getBaseUriBuilder().path(RealmFactory.class).path(user.getLoginName()).build(); + return Response.created(uri).build(); + } + + +} diff --git a/services/src/main/java/org/keycloak/services/service/SkeletonKeyApplication.java b/services/src/main/java/org/keycloak/services/service/SkeletonKeyApplication.java new file mode 100755 index 0000000000..96ead5ea1f --- /dev/null +++ b/services/src/main/java/org/keycloak/services/service/SkeletonKeyApplication.java @@ -0,0 +1,57 @@ +package org.keycloak.services.service; + +import org.infinispan.Cache; +import org.infinispan.manager.DefaultCacheManager; +import org.keycloak.SkeletonKeyContextResolver; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@ApplicationPath("/") +public class SkeletonKeyApplication extends Application +{ + protected Set singletons = new HashSet(); + protected Set> classes = new HashSet>(); + + public SkeletonKeyApplication() + { + Cache cache = getCache(); + singletons.add(new TokenService(null)); + singletons.add(new RealmFactory(null)); + singletons.add(new RealmResource(null)); + classes.add(SkeletonKeyContextResolver.class); + } + + @Override + public Set> getClasses() + { + return classes; + } + + @Override + public Set getSingletons() + { + return singletons; + } + + protected Cache getCache() + { + try + { + InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("skeleton-key.xml"); + return new DefaultCacheManager(is).getCache("skeleton-key"); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/services/src/main/java/org/keycloak/services/service/TokenManager.java b/services/src/main/java/org/keycloak/services/service/TokenManager.java new file mode 100755 index 0000000000..b2580319ef --- /dev/null +++ b/services/src/main/java/org/keycloak/services/service/TokenManager.java @@ -0,0 +1,176 @@ +package org.keycloak.services.service; + +import org.jboss.resteasy.jose.Base64Url; +import org.jboss.resteasy.jose.jws.JWSBuilder; +import org.jboss.resteasy.jwt.JsonSerialization; +import org.keycloak.representations.SkeletonKeyScope; +import org.keycloak.representations.SkeletonKeyToken; +import org.keycloak.services.model.RealmManager; +import org.keycloak.services.model.RealmModel; +import org.keycloak.services.model.ResourceModel; +import org.picketlink.idm.model.User; + +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class TokenManager +{ + protected RealmManager adapter; + + public TokenManager(RealmManager adapter) + { + this.adapter = adapter; + } + + public SkeletonKeyToken createScopedToken(SkeletonKeyScope scope, RealmModel realm, User client, User user) + { + SkeletonKeyToken token = new SkeletonKeyToken(); + token.id(adapter.generateId()); + token.principal(user.getLoginName()); + token.audience(realm.getName()); + token.issuedNow(); + token.issuedFor(client.getLoginName()); + if (realm.getTokenLifespan() > 0) + { + token.expiration((System.currentTimeMillis() / 1000) + realm.getTokenLifespan()); + } + Map resourceMap = realm.getResourceMap(); + + for (String res : scope.keySet()) + { + ResourceModel resource = resourceMap.get(res); + Set scopeMapping = resource.getScope(client); + Set roleMapping = resource.getRoleMappings(user); + SkeletonKeyToken.Access access = token.addAccess(resource.getName()); + for (String role : scope.get(res)) + { + if (!scopeMapping.contains("*") && !scopeMapping.contains(role)) + { + throw new ForbiddenException(Response.status(403).entity("

Security Alert

Known client not authorized for the requested scope.

").type("text/html").build()); + } + if (!roleMapping.contains(role)) + { + throw new ForbiddenException(Response.status(403).entity("

Security Alert

Known client not authorized for the requested scope.

").type("text/html").build()); + + } + access.addRole(role); + } + } + return token; + } + + public SkeletonKeyToken createScopedToken(String scopeParam, RealmModel realm, User client, User user) + { + SkeletonKeyScope scope = decodeScope(scopeParam); + return createScopedToken(scope, realm, client, user); + } + + public SkeletonKeyToken createLoginToken(RealmModel realm, User client, User user) + { + Set mapping = realm.getScope(client); + if (!mapping.contains("*")) + { + throw new ForbiddenException(Response.status(403).entity("

Security Alert

Known client not authorized to request a user login.

").type("text/html").build()); + } + SkeletonKeyToken token = createAccessToken(realm, user); + token.issuedFor(client.getLoginName()); + return token; + + } + + public SkeletonKeyScope decodeScope(String scopeParam) + { + SkeletonKeyScope scope = null; + byte[] bytes = Base64Url.decode(scopeParam); + try + { + scope = JsonSerialization.fromBytes(SkeletonKeyScope.class, bytes); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + return scope; + } + + + public SkeletonKeyToken createAccessToken(RealmModel realm, User user) + { + List resources = realm.getResources(); + SkeletonKeyToken token = new SkeletonKeyToken(); + token.id(adapter.generateId()); + token.issuedNow(); + token.principal(user.getLoginName()); + token.audience(realm.getId()); + if (realm.getTokenLifespan() > 0) + { + token.expiration((System.currentTimeMillis() / 1000) + realm.getTokenLifespan()); + } + + Set realmMapping = realm.getRoleMappings(user); + + if (realmMapping != null && realmMapping.size() > 0) + { + SkeletonKeyToken.Access access = new SkeletonKeyToken.Access(); + for (String role : realmMapping) + { + access.addRole(role); + } + token.setRealmAccess(access); + } + if (resources != null) + { + for (ResourceModel resource : resources) + { + Set mapping = resource.getRoleMappings(user); + if (mapping == null) continue; + SkeletonKeyToken.Access access = token.addAccess(resource.getName()) + .verifyCaller(resource.isSurrogateAuthRequired()); + for (String role : mapping) + { + access.addRole(role); + } + } + } + return token; + } + + public SkeletonKeyToken createIdentityToken(RealmModel realm, String username) + { + SkeletonKeyToken token = new SkeletonKeyToken(); + token.id(adapter.generateId()); + token.issuedNow(); + token.principal(username); + token.audience(realm.getId()); + if (realm.getTokenLifespan() > 0) + { + token.expiration((System.currentTimeMillis() / 1000) + realm.getTokenLifespan()); + } + return token; + } + + public String encodeToken(RealmModel realm, SkeletonKeyToken token) + { + byte[] tokenBytes = null; + try + { + tokenBytes = JsonSerialization.toByteArray(token, false); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + String encodedToken = new JWSBuilder() + .content(tokenBytes) + .rsa256(realm.getPrivateKey()); + return encodedToken; + } +} diff --git a/services/src/main/java/org/keycloak/services/service/TokenService.java b/services/src/main/java/org/keycloak/services/service/TokenService.java new file mode 100755 index 0000000000..6edd0afe15 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/service/TokenService.java @@ -0,0 +1,593 @@ +package org.keycloak.services.service; + +import org.jboss.resteasy.jose.Base64Url; +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.logging.Logger; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.SkeletonKeyScope; +import org.keycloak.representations.SkeletonKeyToken; +import org.keycloak.services.model.RealmManager; +import org.keycloak.services.model.RealmModel; +import org.keycloak.services.model.RequiredCredentialModel; +import org.keycloak.services.model.ResourceModel; +import org.picketlink.idm.model.User; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.Providers; +import java.io.UnsupportedEncodingException; +import java.security.PrivateKey; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Path("/realms") +public class TokenService +{ + public static class AccessCode + { + protected String id = UUID.randomUUID().toString() + System.currentTimeMillis(); + protected long expiration; + protected SkeletonKeyToken token; + protected User client; + + 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 User getClient() + { + return client; + } + + public void setClient(User client) + { + this.client = client; + } + } + + protected RealmManager adapter; + protected TokenManager tokenManager; + protected AuthenticationManager authManager; + protected Logger logger = Logger.getLogger(TokenService.class); + protected Map accessCodeMap = new HashMap(); + @Context + protected UriInfo uriInfo; + @Context + protected Providers providers; + @Context + protected SecurityContext securityContext; + @Context + protected HttpHeaders headers; + + private static AtomicLong counter = new AtomicLong(1); + private static String generateId() + { + return counter.getAndIncrement() + "." + UUID.randomUUID().toString(); + } + + public TokenService(RealmManager adapter) + { + this.adapter = adapter; + this.tokenManager = new TokenManager(adapter); + this.authManager = new AuthenticationManager(adapter); + } + + @Path("{realm}/grants/identity-token") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.APPLICATION_JSON) + public Response identityTokenGrant(@PathParam("realm") String realmId, MultivaluedMap form) + { + String username = form.getFirst(AuthenticationManager.FORM_USERNAME); + if (username == null) + { + throw new NotAuthorizedException("No user"); + } + RealmModel realm = adapter.getRealm(realmId); + if (realm == null) + { + throw new NotFoundException("Realm not found"); + } + if (!realm.isEnabled()) + { + throw new NotAuthorizedException("Disabled realm"); + } + User user = realm.getIdm().getUser(username); + if (user == null) + { + throw new NotAuthorizedException("No user"); + } + if (!user.isEnabled()) + { + throw new NotAuthorizedException("Disabled user."); + } + SkeletonKeyToken token = tokenManager.createIdentityToken(realm, username); + String encoded = tokenManager.encodeToken(realm, token); + AccessTokenResponse res = accessTokenResponse(token, encoded); + return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build(); + } + + @Path("{realm}/grants/access") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.APPLICATION_JSON) + public Response accessTokenGrant(@PathParam("realm") String realmId, MultivaluedMap form) + { + String username = form.getFirst(AuthenticationManager.FORM_USERNAME); + if (username == null) + { + throw new NotAuthorizedException("No user"); + } + RealmModel realm = adapter.getRealm(realmId); + if (realm == null) + { + throw new NotFoundException("Realm not found"); + } + if (!realm.isEnabled()) + { + throw new NotAuthorizedException("Disabled realm"); + } + User user = realm.getIdm().getUser(username); + if (user == null) + { + throw new NotAuthorizedException("No user"); + } + if (!user.isEnabled()) + { + throw new NotAuthorizedException("Disabled user."); + } + if (authManager.authenticate(realm, user, form)) + { + throw new NotAuthorizedException("Auth failed"); + } + SkeletonKeyToken token = tokenManager.createAccessToken(realm, user); + String encoded = tokenManager.encodeToken(realm, token); + AccessTokenResponse res = accessTokenResponse(token, encoded); + return Response.ok(res, MediaType.APPLICATION_JSON_TYPE).build(); + } + + @Path("{realm}/auth/request/login") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response login(@PathParam("realm") String realmId, + MultivaluedMap formData) + { + String clientId = formData.getFirst("client_id"); + String scopeParam = formData.getFirst("scope"); + String state = formData.getFirst("state"); + String redirect = formData.getFirst("redirect_uri"); + + RealmModel realm = adapter.getRealm(realmId); + if (realm == null) + { + throw new NotFoundException("Realm not found"); + } + if (!realm.isEnabled()) + { + return Response.ok("Realm not enabled").type("text/html").build(); + } + User client = realm.getIdm().getUser(clientId); + if (client == null) + { + throw new NotAuthorizedException("No client"); + } + if (!client.isEnabled()) + { + return Response.ok("Requester not enabled").type("text/html").build(); + } + String username = formData.getFirst("username"); + User user = realm.getIdm().getUser(username); + if (user == null) + { + logger.debug("user not found"); + return loginForm("Not valid user", redirect, clientId, scopeParam, state, realm, client); + } + if (!user.isEnabled()) + { + return Response.ok("Your account is not enabled").type("text/html").build(); + + } + boolean authenticated = authManager.authenticate(realm, user, formData); + if (!authenticated) return loginForm("Unable to authenticate, try again", redirect, clientId, scopeParam, state, realm, client); + + SkeletonKeyToken token = null; + if (scopeParam != null) token = tokenManager.createScopedToken(scopeParam, realm, client, user); + else token = tokenManager.createLoginToken(realm, client, user); + + AccessCode code = new AccessCode(); + code.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan()); + code.setToken(token); + code.setClient(client); + synchronized (accessCodeMap) + { + accessCodeMap.put(code.getId(), code); + } + String accessCode = null; + try + { + accessCode = new JWSBuilder().content(code.getId().getBytes("UTF-8")).rsa256(realm.getPrivateKey()); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException(e); + } + UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", accessCode); + if (state != null) redirectUri.queryParam("state", state); + return Response.status(302).location(redirectUri.build()).build(); + } + + @Path("{realm}/access/codes") + @POST + @Produces("application/json") + public Response accessRequest(@PathParam("realm") String realmId, + MultivaluedMap formData) + { + RealmModel realm = adapter.getRealm(realmId); + if (realm == null) + { + throw new NotFoundException("Realm not found"); + } + if (!realm.isEnabled()) + { + throw new NotAuthorizedException("Realm not enabled"); + } + + String code = formData.getFirst("code"); + if (code == null) + { + logger.debug("code not specified"); + Map error = new HashMap(); + error.put("error", "invalid_request"); + error.put("error_description", "code not specified"); + return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); + + } + String client_id = formData.getFirst("client_id"); + if (client_id == null) + { + logger.debug("client_id not specified"); + Map error = new HashMap(); + error.put("error", "invalid_request"); + error.put("error_description", "client_id not specified"); + return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); + } + User client = realm.getIdm().getUser(client_id); + if (client == null) + { + logger.debug("Could not find user"); + Map error = new HashMap(); + error.put("error", "invalid_client"); + error.put("error_description", "Could not find user"); + return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); + } + + if (!client.isEnabled()) + { + logger.debug("user is not enabled"); + Map error = new HashMap(); + error.put("error", "invalid_client"); + error.put("error_description", "User is not enabled"); + return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); + } + + boolean authenticated = authManager.authenticate(realm, client, formData); + if (!authenticated) + { + Map error = new HashMap(); + error.put("error", "unauthorized_client"); + return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build(); + } + + + + JWSInput input = new JWSInput(code, providers); + boolean verifiedCode = false; + try + { + verifiedCode = RSAProvider.verify(input, realm.getPublicKey()); + } + catch (Exception ignored) + { + logger.debug("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"); + return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); + } + String key = input.readContent(String.class); + AccessCode accessCode = null; + synchronized (accessCodeMap) + { + accessCode = accessCodeMap.remove(key); + } + if (accessCode == null) + { + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Code not found"); + return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); + } + if (accessCode.isExpired()) + { + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Code is expired"); + return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); + } + if (!accessCode.getToken().isActive()) + { + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Token expired"); + return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); + } + if (!client.getId().equals(accessCode.getClient().getId())) + { + Map res = new HashMap(); + res.put("error", "invalid_grant"); + res.put("error_description", "Auth error"); + return Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON_TYPE).entity(res).build(); + } + AccessTokenResponse res = accessTokenResponse(realm.getPrivateKey(), accessCode.getToken()); + return Response.ok(res).build(); + + } + + protected AccessTokenResponse accessTokenResponse(PrivateKey privateKey, SkeletonKeyToken token) + { + byte[] tokenBytes = null; + try + { + tokenBytes = JsonSerialization.toByteArray(token, false); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + String encodedToken = new JWSBuilder() + .content(tokenBytes) + .rsa256(privateKey); + + return accessTokenResponse(token, encodedToken); + } + + protected AccessTokenResponse accessTokenResponse(SkeletonKeyToken token, String encodedToken) + { + 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; + } + + @Path("{realm}/auth/request") + @GET + public Response requestAccessCode(@PathParam("realm") String realmId, + @QueryParam("response_type") String responseType, + @QueryParam("redirect_uri") String redirect, + @QueryParam("client_id") String clientId, + @QueryParam("scope") String scopeParam, + @QueryParam("state") String state) + { + RealmModel realm = adapter.getRealm(realmId); + if (realm == null) + { + throw new NotFoundException("Realm not found"); + } + if (!realm.isEnabled()) + { + throw new NotAuthorizedException("Realm not enabled"); + } + User client = realm.getIdm().getUser(clientId); + if (client == null) + return Response.ok("

Security Alert

Unknown client trying to get access to your account.

").type("text/html").build(); + + return loginForm(null, redirect, clientId, scopeParam, state, realm, client); + } + + private Response loginForm(String validationError, String redirect, String clientId, String scopeParam, String state, RealmModel realm, User client) + { + StringBuffer html = new StringBuffer(); + if (scopeParam != null) + { + html.append("

Grant Request For ").append(realm.getName()).append(" Realm

"); + if (validationError != null) + { + try + { + Thread.sleep(1000); // put in a delay + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + html.append("

").append(validationError).append("

"); + } + html.append("

A Third Party is requesting access to the following resources

"); + html.append(""); + SkeletonKeyScope scope = tokenManager.decodeScope(scopeParam); + Map resourceMap = realm.getResourceMap(); + + for (String res : scope.keySet()) + { + ResourceModel resource = resourceMap.get(res); + html.append(""); + } + html.append("
Resource: ").append(resource.getName()).append("Roles:"); + Set scopeMapping = resource.getScope(client); + for (String role : scope.get(res)) + { + html.append(" ").append(role); + if (!scopeMapping.contains("*") && !scopeMapping.contains(role)) + { + return Response.ok("

Security Alert

Known client not authorized for the requested scope.

").type("text/html").build(); + } + } + html.append("

To Authorize, please login below

"); + } + else + { + Set scopeMapping = realm.getScope(client); + if (scopeMapping.contains("*")) + { + html.append("

Login For ").append(realm.getName()).append(" Realm

"); + if (validationError != null) + { + try + { + Thread.sleep(1000); // put in a delay + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + html.append("

").append(validationError).append("

"); + } + } + else + { + html.append("

Grant Request For ").append(realm.getName()).append(" Realm

"); + if (validationError != null) + { + try + { + Thread.sleep(1000); // put in a delay + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + html.append("

").append(validationError).append("

"); + } + SkeletonKeyScope scope = new SkeletonKeyScope(); + List resources = realm.getResources(); + boolean found = false; + for (ResourceModel resource : resources) + { + Set resourceScope = resource.getScope(client); + if (resourceScope == null) continue; + if (resourceScope.size() == 0) continue; + if (!found) + { + found = true; + html.append("

A Third Party is requesting access to the following resources

"); + html.append(""); + } + html.append("
Resource: ").append(resource.getName()).append("Roles:"); + // todo add description of role + for (String role : resourceScope) + { + html.append(" ").append(role); + scope.add(resource.getName(), role); + } + } + if (!found) + { + return Response.ok("

Security Alert

Known client not authorized to access this realm.

").type("text/html").build(); + } + html.append("
"); + try + { + String json = JsonSerialization.toString(scope, false); + scopeParam = Base64Url.encode(json.getBytes("UTF-8")); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + + } + } + + UriBuilder formActionUri = uriInfo.getBaseUriBuilder().path(TokenService.class).path(TokenService.class, "login"); + String action = formActionUri.build(realm.getId()).toString(); + html.append("
"); + html.append("Username:
"); + + for (RequiredCredentialModel credential : realm.getRequiredCredentials()) + { + if (!credential.isInput()) continue; + html.append(credential.getType()).append(": "); + if (credential.isSecret()) + { + html.append("
"); + + } else + { + html.append("
"); + } + } + html.append(""); + if (scopeParam != null) + { + html.append(""); + } + if (state != null) html.append(""); + html.append(""); + html.append(""); + html.append("
"); + return Response.ok(html.toString()).type("text/html").build(); + } +} diff --git a/services/src/test/java/org/keycloak/test/AdapterTest.java b/services/src/test/java/org/keycloak/test/AdapterTest.java new file mode 100755 index 0000000000..998bedd018 --- /dev/null +++ b/services/src/test/java/org/keycloak/test/AdapterTest.java @@ -0,0 +1,159 @@ +package org.keycloak.test; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; +import org.keycloak.representations.idm.RequiredCredentialRepresentation; +import org.keycloak.services.model.RealmManager; +import org.keycloak.services.model.RealmModel; +import org.keycloak.services.model.RealmResourceRelationship; +import org.keycloak.services.model.RequiredCredentialModel; +import org.keycloak.services.model.RequiredCredentialRelationship; +import org.keycloak.services.model.ScopeRelationship; +import org.keycloak.services.model.UserCredentialModel; +import org.picketlink.idm.IdentityManager; +import org.picketlink.idm.config.IdentityConfigurationBuilder; +import org.picketlink.idm.credential.Credentials; +import org.picketlink.idm.credential.Password; +import org.picketlink.idm.credential.UsernamePasswordCredentials; +import org.picketlink.idm.file.internal.FileUtils; +import org.picketlink.idm.internal.IdentityManagerFactory; +import org.picketlink.idm.model.Realm; +import org.picketlink.idm.model.Role; +import org.picketlink.idm.model.SimpleRole; +import org.picketlink.idm.model.SimpleUser; +import org.picketlink.idm.model.User; + +import java.io.File; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class AdapterTest +{ + private static IdentityManagerFactory factory; + public static final String WORKING_DIRECTORY = "/tmp/keycloak"; + public RealmManager adapter; + public RealmModel realmModel; + @Before + public void before() throws Exception + { + after(); + factory = createFactory(); + adapter = new RealmManager(factory); + } + + private static IdentityManagerFactory createFactory() { + IdentityConfigurationBuilder builder = new IdentityConfigurationBuilder(); + + builder + .stores() + .file() + .addRealm(Realm.DEFAULT_REALM) + .workingDirectory(WORKING_DIRECTORY) + .preserveState(true) + .supportAllFeatures() + .supportRelationshipType(RealmResourceRelationship.class, RequiredCredentialRelationship.class, ScopeRelationship.class); + + return new IdentityManagerFactory(builder.build()); + } + + @After + public void after() throws Exception + { + File file = new File(WORKING_DIRECTORY); + FileUtils.delete(file); + Thread.sleep(10); // my windows machine seems to have delays on deleting files sometimes + } + + @Test + public void test1CreateRealm() throws Exception + { + realmModel = adapter.create("JUGGLER"); + realmModel.setAccessCodeLifespan(100); + realmModel.setCookieLoginAllowed(true); + realmModel.setEnabled(true); + realmModel.setName("JUGGLER"); + realmModel.setPrivateKeyPem("0234234"); + realmModel.setPublicKeyPem("0234234"); + realmModel.setTokenLifespan(1000); + realmModel.updateRealm(); + + System.out.println(realmModel.getId()); + realmModel = adapter.getRealm(realmModel.getId()); + Assert.assertNotNull(realmModel); + Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100); + Assert.assertEquals(realmModel.getTokenLifespan(), 1000); + Assert.assertEquals(realmModel.isEnabled(), true); + Assert.assertEquals(realmModel.getName(), "JUGGLER"); + Assert.assertEquals(realmModel.getPrivateKeyPem(), "0234234"); + Assert.assertEquals(realmModel.getPublicKeyPem(), "0234234"); + } + + @Test + public void test2RequiredCredential() throws Exception + { + test1CreateRealm(); + RequiredCredentialModel creds = new RequiredCredentialModel(); + creds.setSecret(true); + creds.setType(RequiredCredentialRepresentation.PASSWORD); + creds.setInput(true); + realmModel.addRequiredCredential(creds); + creds = new RequiredCredentialModel(); + creds.setSecret(true); + creds.setType(RequiredCredentialRepresentation.TOTP); + creds.setInput(true); + realmModel.addRequiredCredential(creds); + List storedCreds = realmModel.getRequiredCredentials(); + Assert.assertEquals(2, storedCreds.size()); + boolean totp = false; + boolean password = false; + for (RequiredCredentialModel cred : storedCreds) + { + if (cred.getType().equals(RequiredCredentialRepresentation.PASSWORD)) password = true; + else if (cred.getType().equals(RequiredCredentialRepresentation.TOTP)) totp = true; + } + Assert.assertTrue(totp); + Assert.assertTrue(password); + } + + @Test + public void testCredentialValidation() throws Exception + { + test1CreateRealm(); + User user = new SimpleUser("bburke"); + realmModel.getIdm().add(user); + UserCredentialModel cred = new UserCredentialModel(); + cred.setType(RequiredCredentialRepresentation.PASSWORD); + cred.setValue("geheim"); + realmModel.updateCredential(user, cred); + IdentityManager idm = realmModel.getIdm(); + UsernamePasswordCredentials creds = new UsernamePasswordCredentials(user.getLoginName(), new Password("geheim")); + idm.validateCredentials(creds); + Assert.assertEquals(creds.getStatus(), Credentials.Status.VALID); + } + + @Test + public void testRoles() throws Exception + { + test1CreateRealm(); + IdentityManager idm = realmModel.getIdm(); + idm.add(new SimpleRole("admin")); + idm.add(new SimpleRole("user")); + List roles = realmModel.getRoles(); + Assert.assertEquals(2, roles.size()); + SimpleUser user = new SimpleUser("bburke"); + idm.add(user); + Role role = idm.getRole("user"); + idm.grantRole(user, role); + Assert.assertTrue(idm.hasRole(user, role)); + } + + +} diff --git a/services/src/test/resources/META-INF/persistence.xml b/services/src/test/resources/META-INF/persistence.xml new file mode 100755 index 0000000000..1e5524fe05 --- /dev/null +++ b/services/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,29 @@ + + + org.hibernate.ejb.HibernatePersistence + + org.picketlink.idm.jpa.schema.IdentityObject + org.picketlink.idm.jpa.schema.PartitionObject + org.picketlink.idm.jpa.schema.RelationshipObject + org.picketlink.idm.jpa.schema.RelationshipIdentityObject + org.picketlink.idm.jpa.schema.RelationshipIdentityWeakObject + org.picketlink.idm.jpa.schema.RelationshipObjectAttribute + org.picketlink.idm.jpa.schema.IdentityObjectAttribute + org.picketlink.idm.jpa.schema.CredentialObject + org.picketlink.idm.jpa.schema.CredentialObjectAttribute + + + + + + + + + + + + + diff --git a/services/src/test/resources/testrealm.json b/services/src/test/resources/testrealm.json new file mode 100644 index 0000000000..6002c79a83 --- /dev/null +++ b/services/src/test/resources/testrealm.json @@ -0,0 +1,101 @@ +{ + "realm" : "test-realm", + "enabled" : true, + "tokenLifespan" : 6000, + "accessCodeLifespan" : 30, + "requiredCredentials" : [ + { + "type" : "Password", + "input" : true, + "secret" : true + } + ], + "users" : [ + { + "username" : "wburke", + "enabled" : true, + "attributes" : { + "email" : "bburke@redhat.com" + }, + "credentials" : [ + { "type" : "Password", + "value" : "userpassword" } + ] + }, + { + "username" : "loginclient", + "enabled" : true, + "credentials" : [ + { "type" : "Password", + "value" : "clientpassword" } + ] + }, + { + "username" : "admin", + "enabled" : true, + "credentials" : [ + { "type" : "Password", + "value" : "adminpassword" } + ] + }, + { + "username" : "oauthclient", + "enabled" : true, + "credentials" : [ + { "type" : "Password", + "value" : "clientpassword" } + ] + } + ], + "roleMappings" : [ + { + "username" : "admin", + "roles" : ["admin"] + } + ], + "scopeMappings" : [ + { + "username" : "loginclient", + "roles" : ["login"] + } + ], + "resources" : [ + { + "name" : "Application", + "roles" : ["admin", "user"], + "roleMappings" : [ + { + "username" : "wburke", + "roles" : ["user"] + }, + { + "username" : "admin", + "roles" : ["admin"] + } + ], + "scopeMappings" : [ + { + "username" : "oauthclient", + "roles" : ["user"] + } + ] + }, + { + "name" : "OtherApp", + "roles" : ["admin", "user"], + "roleMappings" : [ + { + "username" : "wburke", + "roles" : ["user"] + }, + { + "username" : "admin", + "roles" : ["admin"] + } + ] + } + + ] + + +} \ No newline at end of file