diff --git a/model/api/src/main/java/org/keycloak/models/KerberosConstants.java b/core/src/main/java/org/keycloak/constants/KerberosConstants.java similarity index 70% rename from model/api/src/main/java/org/keycloak/models/KerberosConstants.java rename to core/src/main/java/org/keycloak/constants/KerberosConstants.java index b2a676e8b3..3b7aa60934 100644 --- a/model/api/src/main/java/org/keycloak/models/KerberosConstants.java +++ b/core/src/main/java/org/keycloak/constants/KerberosConstants.java @@ -1,4 +1,4 @@ -package org.keycloak.models; +package org.keycloak.constants; /** * @author Marek Posolda @@ -23,6 +23,12 @@ public class KerberosConstants { public static final String KRB5_OID = "1.2.840.113554.1.2.2"; + /** + * OID of Kerberos v5 name. See http://www.oid-info.com/get/1.2.840.113554.1.2.2.1 + */ + public static final String KRB5_NAME_OID = "1.2.840.113554.1.2.2.1"; + + /** * Configuration federation provider model attributes. */ @@ -43,8 +49,13 @@ public class KerberosConstants { /** - * Internal attribute used in "state" map . Contains credential from SPNEGO/Kerberos successful authentication + * Internal attribute used in "userSession.note" map and in accessToken claims . Contains credential from SPNEGO/Kerberos successful authentication */ - public static final String GSS_DELEGATION_CREDENTIAL = "GssDelegationCredential"; + public static final String GSS_DELEGATION_CREDENTIAL = "gss_delegation_credential"; + + /** + * Display name for the above in admin console and consent screens + */ + public static final String GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME = "gss delegation credential"; } diff --git a/core/src/main/java/org/keycloak/util/KerberosSerializationUtils.java b/core/src/main/java/org/keycloak/util/KerberosSerializationUtils.java new file mode 100644 index 0000000000..296362386a --- /dev/null +++ b/core/src/main/java/org/keycloak/util/KerberosSerializationUtils.java @@ -0,0 +1,196 @@ +package org.keycloak.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Method; + +import javax.security.auth.kerberos.KerberosTicket; + +import net.iharder.Base64; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.Oid; +import org.keycloak.constants.KerberosConstants; +import org.keycloak.util.reflections.Reflections; +import sun.security.jgss.GSSCredentialImpl; +import sun.security.jgss.GSSManagerImpl; +import sun.security.jgss.krb5.Krb5InitCredential; +import sun.security.jgss.krb5.Krb5NameElement; +import sun.security.jgss.spi.GSSCredentialSpi; +import sun.security.krb5.Credentials; + +/** + * Provides serialization/deserialization of kerberos {@link org.ietf.jgss.GSSCredential}, so it can be transmitted from auth-server to the application + * and used for further calls to kerberos-secured services + * + * @author Marek Posolda + */ +public class KerberosSerializationUtils { + + public static final Oid KRB5_OID; + public static final Oid KRB5_NAME_OID; + public static final String JAVA_INFO; + + static { + try { + KRB5_OID = new Oid(KerberosConstants.KRB5_OID); + KRB5_NAME_OID = new Oid(KerberosConstants.KRB5_NAME_OID); + } catch (GSSException e) { + throw new RuntimeException(e); + } + + String javaVersion = System.getProperty("java.version"); + String javaRuntimeVersion = System.getProperty("java.runtime.version"); + String javaVendor = System.getProperty("java.vendor"); + String os = System.getProperty("os.version"); + JAVA_INFO = "Java version: " + javaVersion + ", runtime version: " + javaRuntimeVersion + ", vendor: " + javaVendor + ", os: " + os; + } + + private KerberosSerializationUtils() { + } + + public static String serializeCredential(GSSCredential gssCredential) throws KerberosSerializationException { + try { + if (gssCredential == null) { + throw new KerberosSerializationException("Null credential given as input"); + } + + if (!(gssCredential instanceof GSSCredentialImpl)) { + throw new KerberosSerializationException("Unknown credential type: " + gssCredential.getClass()); + } + + GSSCredentialImpl gssCredImpl = (GSSCredentialImpl) gssCredential; + Oid[] mechs = gssCredImpl.getMechs(); + + for (Oid oid : mechs) { + if (oid.equals(KRB5_OID)) { + int usage = gssCredImpl.getUsage(oid); + boolean initiate = (usage == GSSCredential.INITIATE_ONLY || usage == GSSCredential.INITIATE_AND_ACCEPT); + + GSSCredentialSpi credentialSpi = gssCredImpl.getElement(oid, initiate); + if (credentialSpi instanceof Krb5InitCredential) { + Krb5InitCredential credential = (Krb5InitCredential) credentialSpi; + KerberosTicket kerberosTicket = new KerberosTicket(credential.getEncoded(), + credential.getClient(), + credential.getServer(), + credential.getSessionKey().getEncoded(), + credential.getSessionKeyType(), + credential.getFlags(), + credential.getAuthTime(), + credential.getStartTime(), + credential.getEndTime(), + credential.getRenewTill(), + credential.getClientAddresses()); + return serialize(kerberosTicket); + } else { + throw new KerberosSerializationException("Unsupported type of credentialSpi: " + credentialSpi.getClass()); + } + } + } + + throw new KerberosSerializationException("Kerberos credential not found. Available mechanisms: " + mechs); + } catch (IOException e) { + throw new KerberosSerializationException("Exception occured", e); + } catch (GSSException e) { + throw new KerberosSerializationException("Exception occured", e); + } + } + + + public static GSSCredential deserializeCredential(String serializedCred) throws KerberosSerializationException { + if (serializedCred == null) { + throw new KerberosSerializationException("Null credential given as input. Did you enable kerberos credential delegation for your web browser and mapping of gss credential to access token?"); + } + + try { + Object deserializedCred = deserialize(serializedCred); + if (!(deserializedCred instanceof KerberosTicket)) { + throw new KerberosSerializationException("Deserialized object is not KerberosTicket! Type is: " + deserializedCred); + } + + KerberosTicket ticket = (KerberosTicket) deserializedCred; + String fullName = ticket.getClient().getName(); + + Method getInstance = Reflections.findDeclaredMethod(Krb5NameElement.class, "getInstance", String.class, Oid.class); + Krb5NameElement krb5Name = Reflections.invokeMethod(true, getInstance, Krb5NameElement.class, null, fullName, KRB5_NAME_OID); + + Credentials krb5CredsInternal = new Credentials( + ticket.getEncoded(), + ticket.getClient().getName(), + ticket.getServer().getName(), + ticket.getSessionKey().getEncoded(), + ticket.getSessionKeyType(), + ticket.getFlags(), + ticket.getAuthTime(), + ticket.getStartTime(), + ticket.getEndTime(), + ticket.getRenewTill(), + ticket.getClientAddresses() + ); + + Method getInstance2 = Reflections.findDeclaredMethod(Krb5InitCredential.class, "getInstance", Krb5NameElement.class, Credentials.class); + Krb5InitCredential initCredential = Reflections.invokeMethod(true, getInstance2, Krb5InitCredential.class, null, krb5Name, krb5CredsInternal); + + GSSManagerImpl manager = (GSSManagerImpl) GSSManager.getInstance(); + return new GSSCredentialImpl(manager, initCredential); + } catch (Exception ioe) { + throw new KerberosSerializationException("Exception occured", ioe); + } + } + + + private static String serialize(Serializable obj) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutput out = null; + try { + out = new ObjectOutputStream(bos); + out.writeObject(obj); + byte[] objBytes = bos.toByteArray(); + return Base64.encodeBytes(objBytes); + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + + private static Object deserialize(String serialized) throws ClassNotFoundException, IOException { + byte[] bytes = Base64.decode(serialized); + ByteArrayInputStream bis = new ByteArrayInputStream(bytes); + ObjectInput in = null; + try { + in = new ObjectInputStream(bis); + return in.readObject(); + } finally { + try { + if (in != null) { + in.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + + public static class KerberosSerializationException extends RuntimeException { + + public KerberosSerializationException(String message, Throwable cause) { + super(message + ", " + JAVA_INFO, cause); + } + + public KerberosSerializationException(String message) { + super(message + ", " + JAVA_INFO); + } + } +} diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index 339afad74c..43907f6efa 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -217,6 +217,12 @@ + + + org.keycloak + keycloak-wildfly-extensions + ${project.version} + \ No newline at end of file diff --git a/distribution/examples-docs-zip/build.xml b/distribution/examples-docs-zip/build.xml index 74d503206d..c2790db6ad 100755 --- a/distribution/examples-docs-zip/build.xml +++ b/distribution/examples-docs-zip/build.xml @@ -107,6 +107,12 @@ + + + + + + diff --git a/distribution/modules/build.xml b/distribution/modules/build.xml index 0cd950a8a4..efb3ba4be3 100755 --- a/distribution/modules/build.xml +++ b/distribution/modules/build.xml @@ -66,6 +66,10 @@ + + + + @@ -232,6 +236,8 @@ + + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml index c10c776c26..5a35b132aa 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-core/main/module.xml @@ -14,6 +14,8 @@ + + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml index 4cc0d1336f..74e67a7320 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml @@ -47,6 +47,7 @@ + diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-wildfly-extensions/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-wildfly-extensions/main/module.xml new file mode 100755 index 0000000000..9ccc11e01a --- /dev/null +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-wildfly-extensions/main/module.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/distribution/modules/src/main/resources/modules/sun/jdk/jgss/main/module.xml b/distribution/modules/src/main/resources/modules/sun/jdk/jgss/main/module.xml new file mode 100644 index 0000000000..6df03ff4ba --- /dev/null +++ b/distribution/modules/src/main/resources/modules/sun/jdk/jgss/main/module.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docbook/reference/en/en-US/modules/kerberos.xml b/docbook/reference/en/en-US/modules/kerberos.xml index 24b5d9baee..31c8d10bd7 100644 --- a/docbook/reference/en/en-US/modules/kerberos.xml +++ b/docbook/reference/en/en-US/modules/kerberos.xml @@ -204,6 +204,40 @@ ktadd -k /tmp/http.keytab HTTP/www.mydomain.org@MYDOMAIN.ORG + +
+ Credential delegation + + One scenario supported by Kerberos 5 is credential delegation. In this case when user receives forwardable TGT and authenticates to the web server, + then web server might be able to reuse the ticket and forward it to another service secured by Kerberos (for example LDAP server or IMAP server). + + + The scenario is supported by Keycloak, but there is tricky thing that SPNEGO authentication is done by Keycloak server but + GSS credential will need to be used by your application. So you need to enable built-in gss delegation credential protocol mapper + in admin console for your application. This will cause that Keycloak will deserialize GSS credential and transmit it to the application + in access token. Application will need to deserialize it and use it for further GSS calls against other services. + + + GSSContext will need to + be created with this credential passed to the method GSSManager.createContext for example like this: + + + + Note that you also need to configure forwardable kerberos tickets in krb5.conf file + and add support for delegated credentials to your browser. See the kerberos example from Keycloak example set for details. + + + + Credential delegation has some security implications. So enable the protocol claim and support in browser just if you really need it. + It's highly recommended to use it together with HTTPS. See for example + this article + for details. + + +
Troubleshooting diff --git a/docbook/reference/en/en-US/modules/providers.xml b/docbook/reference/en/en-US/modules/providers.xml index 4e2239dcbd..e88bc34e2a 100755 --- a/docbook/reference/en/en-US/modules/providers.xml +++ b/docbook/reference/en/en-US/modules/providers.xml @@ -87,33 +87,69 @@ public class MyEventListenerProvider implements EventListenerProvider {
Registering provider implementations - Keycloak loads provider implementations from the file-system. By default all JARs inside - standalone/configuration/providers are loaded. This is simple, but requires all providers - to share the same library. All provides also inherit all classes from the Keycloak class-loader. In the future - we'll add support to load providers from modules, which allows better control of class isolation. + Keycloak can load provider implementations from JBoss Modules or directly from the file-system. Using Modules + is recommended as you can control exactly what classes are available to your provider. Any providers loaded + from the file-system uses a classloader with the Keycloak classloader as its parent. - - To register your provider simply copy the JAR including the ProviderFactory and Provider classes and the - provider configuration file to standalone/configuration/providers. - - - You can also define multiple provider class-path if you want to create isolated class-loaders. To do this - edit keycloak-server.json and add more classpath entries to the providers array. For example: + +
+ Register a provider using Modules + + To register a provider using Modules first create a module. To do this you have to create a folder inside + KEYCLOAK_HOME/modules and add your jar and a module.xml. For example to add the event listener + sysout example provider create the folder KEYCLOAK_HOME/modules/org/keycloak/examples/event-sysout/main. + Copy event-listener-sysout-example.jar to this folder and create module.xml + with the following content: + + + + + + + + + + + +}]]> + Next you need to register this module with Keycloak. This is done by editing keycloak-server.json and adding + it to the providers: + + +
+ + +
+ Register a provider using file-system + + To register your provider simply copy the JAR including the ProviderFactory and Provider classes and the + provider configuration file to standalone/configuration/providers. + + + You can also define multiple provider class-path if you want to create isolated class-loaders. To do this + edit keycloak-server.json and add more classpath entries to the providers array. For example: - The above example will create two separate class-loaders for providers. The classpath entries follow the - same syntax as Java classpath, with ';' separating multiple-entries. Wildcard is also supported allowing - loading all jars (files with .jar or .JAR extension) in a folder, for example: + The above example will create two separate class-loaders for providers. The classpath entries follow the + same syntax as Java classpath, with ';' separating multiple-entries. Wildcard is also supported allowing + loading all jars (files with .jar or .JAR extension) in a folder, for example: - + +
diff --git a/examples/kerberos/README.md b/examples/kerberos/README.md new file mode 100644 index 0000000000..5acdb624ba --- /dev/null +++ b/examples/kerberos/README.md @@ -0,0 +1,66 @@ +Keycloak Example - Kerberos Credential Delegation +================================================= + +This example requires that Keycloak is configured with Kerberos/SPNEGO authentication. It's showing how the forwardable TGT is sent from +the Keycloak auth-server to the application, which deserializes it and authenticates with it to further Kerberized service, which in the example is LDAP server. + +Example is using built-in ApacheDS Kerberos server from the keycloak testsuite and the realm with preconfigured federation provider and `gss delegation credential` protocol mapper. +It also needs to enable forwardable ticket support in Kerberos configuration and your browser. + +Detailed steps: + +**1)** Build and deploy this sample's WAR file. For this example, deploy on the same server that is running the Keycloak Server, although this is not required for real world scenarios. + + +**2)** Copy `http.keytab` file from the root directory of example to `/tmp` directory (On Linux): + +``` +cp http.keytab /tmp/http.keytab +``` + +Alternative is to configure different location for `keyTab` property in `kerberosrealm.json` configuration file (On Windows this will be needed). +Note that in production, keytab file should be in secured location accessible just to the user under which is Keycloak server running. + + +**3)** Run Keycloak server and import `kerberosrealm.json` into it through admin console. This will import realm with sample application +and configured LDAP federation provider with Kerberos/SPNEGO authentication support enabled and with `gss delegation credential` protocol mapper +added to the application. + +**WARNING:** It's recommended to use JDK8 to run Keycloak server. For JDK7 you may be faced with the bug described [here](http://darranl.blogspot.cz/2014/09/kerberos-encrypteddata-null-key-keytype.html) . +Alternatively you can use OpenJDK7 but in this case you will need to use aes256-cts-hmac-sha1-96 for both KDC and Kerberos client configuration. For server, +you can add system property to the maven command when running ApacheDS Kerberos server `-Dkerberos.encTypes=aes256-cts-hmac-sha1-96` (see below) and for +client add encryption types to configuration file like `/etc/krb5.conf` (but they should be already available. See below). + + +**4)** Run ApacheDS based Kerberos server embedded in Keycloak. Easiest is to checkout keycloak sources, build and then run KerberosEmbeddedServer +as shown here: + +``` +git clone https://github.com/keycloak/keycloak.git +mvn clean install -DskipTests=true +cd testsuite/integration +mvn exec:java -Pkerberos +``` + +More details about embedded Kerberos server in [testsuite README](https://github.com/keycloak/keycloak/blob/master/testsuite/integration/README.md#kerberos-server). + + +**5)** Configure Kerberos client (On linux it's in file `/etc/krb5.conf` ). You need to configure `KEYCLOAK.ORG` realm and enable `forwardable` flag, which is needed +for credential delegation example, as application needs to forward Kerberos ticket and authenticate with it against LDAP server. +See [this file](https://github.com/keycloak/keycloak/blob/master/testsuite/integration/src/main/resources/kerberos/test-krb5.conf) for inspiration. + + +**6)** Configure browser (Firefox, Chrome or other) and enable SPNEGO authentication and credential delegation for `localhost` . +In Firefox it can be done by adding `localhost` to both `network.negotiate-auth.trusted-uris` and `network.negotiate-auth.delegation-uris` . +More info in [testsuite README](https://github.com/keycloak/keycloak/blob/master/testsuite/integration/README.md#kerberos-server). + + +**7)** Test the example. Obtain kerberos ticket by running command from CMD (on linux): +``` +kinit hnelson@KEYCLOAK.ORG +``` +with password `secret` . + +Then in your web browser open `http://localhost:8080/kerberos-portal` . You should be logged-in automatically through SPNEGO without displaying Keycloak login screen. +Keycloak will also transmit the delegated GSS credential to the application inside access token and application will be able to login with this credential +to the LDAP server and retrieve some data from it (Actually it just retrieve few simple data about authenticated user himself). \ No newline at end of file diff --git a/examples/kerberos/http.keytab b/examples/kerberos/http.keytab new file mode 100644 index 0000000000..0e7fd96fa7 Binary files /dev/null and b/examples/kerberos/http.keytab differ diff --git a/examples/kerberos/kerberosrealm.json b/examples/kerberos/kerberosrealm.json new file mode 100644 index 0000000000..0f044f6ae9 --- /dev/null +++ b/examples/kerberos/kerberosrealm.json @@ -0,0 +1,94 @@ +{ + "id": "kerberos-demo", + "realm": "kerberos-demo", + "enabled": true, + "sslRequired": "external", + "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "requiredCredentials": [ "password", "kerberos" ], + "defaultRoles": [ "user" ], + "scopeMappings": [ + { + "client": "kerberos-app", + "roles": [ "user" ] + } + ], + "applications": [ + { + "name": "kerberos-app", + "enabled": true, + "baseUrl": "/kerberos-portal", + "redirectUris": [ + "/kerberos-portal/*" + ], + "adminUrl": "/kerberos-portal", + "secret": "password", + "protocolMappers": [ + { + "protocolMapper" : "oidc-usermodel-property-mapper", + "protocol" : "openid-connect", + "name" : "username", + "consentText" : "username", + "consentRequired" : true, + "config" : { + "Claim JSON Type" : "String", + "user.attribute" : "username", + "Token Claim Name" : "preferred_username", + "id.token.claim" : "true", + "access.token.claim" : "true" + } + }, + { + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "protocol" : "openid-connect", + "name" : "gss delegation credential", + "consentText" : "gss delegation credential", + "consentRequired" : true, + "config" : { + "user.session.note" : "gss_delegation_credential", + "Token Claim Name" : "gss_delegation_credential", + "id.token.claim" : "false", + "access.token.claim" : "true" + } + } + ] + } + ], + "roles" : { + "realm" : [ + { + "name": "user", + "description": "Have User privileges" + } + ] + }, + "userFederationProviders": [ + { + "displayName": "kerberos-ldap-provider", + "providerName": "ldap", + "priority": 1, + "fullSyncPeriod": -1, + "changedSyncPeriod": -1, + "config": { + "syncRegistrations" : "false", + "userAccountControlsAfterPasswordUpdate" : "true", + "connectionPooling" : "true", + "pagination" : "true", + "allowKerberosAuthentication" : "true", + "debug" : "true", + "editMode" : "WRITABLE", + "vendor" : "other", + "usernameLDAPAttribute" : "uid", + "userObjectClasses" : "inetOrgPerson, organizationalPerson", + "connectionUrl" : "ldap://localhost:10389", + "baseDn" : "dc=keycloak,dc=org", + "userDnSuffix" : "ou=People,dc=keycloak,dc=org", + "bindDn" : "uid=admin,ou=system", + "bindCredential" : "secret", + "kerberosRealm" : "KEYCLOAK.ORG", + "serverPrincipal" : "HTTP/localhost@KEYCLOAK.ORG", + "keyTab" : "/tmp/http.keytab" + } + } + ] +} \ No newline at end of file diff --git a/examples/kerberos/pom.xml b/examples/kerberos/pom.xml new file mode 100644 index 0000000000..431876e35e --- /dev/null +++ b/examples/kerberos/pom.xml @@ -0,0 +1,83 @@ + + 4.0.0 + + + keycloak-parent + org.keycloak + 1.2.0.Beta1-SNAPSHOT + ../../pom.xml + + + Keycloak Examples - Kerberos Credential Delegation + examples-kerberos + war + + + Kerberos Credential Delegation Example + + + + + jboss + jboss repo + http://repository.jboss.org/nexus/content/groups/public/ + + + + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-adapter-core + ${project.version} + provided + + + + + kerberos-portal + + + org.jboss.as.plugins + jboss-as-maven-plugin + + false + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + diff --git a/examples/kerberos/src/main/java/org/keycloak/example/kerberos/GSSCredentialsClient.java b/examples/kerberos/src/main/java/org/keycloak/example/kerberos/GSSCredentialsClient.java new file mode 100644 index 0000000000..3017b1e7a3 --- /dev/null +++ b/examples/kerberos/src/main/java/org/keycloak/example/kerberos/GSSCredentialsClient.java @@ -0,0 +1,101 @@ +package org.keycloak.example.kerberos; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.security.sasl.Sasl; +import javax.servlet.http.HttpServletRequest; + +import org.ietf.jgss.GSSCredential; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.constants.KerberosConstants; +import org.keycloak.representations.AccessToken; +import org.keycloak.util.KerberosSerializationUtils; + +/** + * Sample client able to authenticate against ApacheDS LDAP server with Krb5 GSS Credential. + * + * Credential was previously retrieved from SPNEGO authentication against Keycloak auth-server and transmitted from + * Keycloak to the application in OIDC access token + * + * We can use GSSCredential to further GSS API calls . Note that if you will use GSS API directly, you can + * attach GSSCredential when creating GSSContext like this: + * GSSContext context = gssManager.createContext(serviceName, KerberosSerializationUtils.KRB5_OID, deserializedGssCredential, GSSContext.DEFAULT_LIFETIME); + * + * In this example we authenticate against LDAP server, which calls GSS API under the hood when credential is attached to env under Sasl.CREDENTIALS key + * + * @author Marek Posolda + */ +public class GSSCredentialsClient { + + public static LDAPUser getUserFromLDAP(HttpServletRequest req) throws Exception { + KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) req.getUserPrincipal(); + AccessToken accessToken = keycloakPrincipal.getKeycloakSecurityContext().getToken(); + String username = accessToken.getPreferredUsername(); + + // Retrieve kerberos credential from accessToken and deserialize it + String serializedGssCredential = (String) accessToken.getOtherClaims().get(KerberosConstants.GSS_DELEGATION_CREDENTIAL); + GSSCredential deserializedGssCredential = KerberosSerializationUtils.deserializeCredential(serializedGssCredential); + + // First try to invoke without gssCredential. It should fail. This is here just for illustration purposes + try { + invokeLdap(null, username); + throw new RuntimeException("Not expected to authenticate to LDAP without credential"); + } catch (NamingException nse) { + System.out.println("GSSCredentialsClient: Expected exception: " + nse.getMessage()); + } + + return invokeLdap(deserializedGssCredential, username); + } + + private static LDAPUser invokeLdap(GSSCredential gssCredential, String username) throws NamingException { + Hashtable env = new Hashtable(11); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, "ldap://localhost:10389"); + + if (gssCredential != null) { + env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + env.put(Sasl.CREDENTIALS, gssCredential); + } + + DirContext ctx = new InitialDirContext(env); + try { + Attributes attrs = ctx.getAttributes("uid=" + username + ",ou=People,dc=keycloak,dc=org"); + String uid = username; + String cn = (String) attrs.get("cn").get(); + String sn = (String) attrs.get("sn").get(); + return new LDAPUser(uid, cn, sn); + } finally { + ctx.close(); + } + } + + public static class LDAPUser { + + private final String uid; + private final String cn; + private final String sn; + + public LDAPUser(String uid, String cn, String sn) { + this.uid = uid; + this.cn = cn; + this.sn = sn; + } + + public String getUid() { + return uid; + } + + public String getCn() { + return cn; + } + + public String getSn() { + return sn; + } + } +} diff --git a/examples/kerberos/src/main/webapp/WEB-INF/keycloak.json b/examples/kerberos/src/main/webapp/WEB-INF/keycloak.json new file mode 100644 index 0000000000..dfb8577471 --- /dev/null +++ b/examples/kerberos/src/main/webapp/WEB-INF/keycloak.json @@ -0,0 +1,11 @@ +{ + "realm" : "kerberos-demo", + "resource" : "kerberos-app", + "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "/auth", + "ssl-required" : "external", + "enable-basic-auth" : "true", + "credentials": { + "secret": "password" + } +} \ No newline at end of file diff --git a/examples/kerberos/src/main/webapp/WEB-INF/web.xml b/examples/kerberos/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..aa2116608a --- /dev/null +++ b/examples/kerberos/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,37 @@ + + + + kerberos-portal + + + + KerberosApp + /* + + + user + + + + + + + KEYCLOAK + does-not-matter + + + + user + + \ No newline at end of file diff --git a/examples/kerberos/src/main/webapp/index.jsp b/examples/kerberos/src/main/webapp/index.jsp new file mode 100644 index 0000000000..c1df8f0878 --- /dev/null +++ b/examples/kerberos/src/main/webapp/index.jsp @@ -0,0 +1,37 @@ +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1" %> +<%@ page import="org.keycloak.constants.ServiceUrlConstants" %> +<%@ page import="org.keycloak.util.KeycloakUriBuilder" %> +<%@ page import="org.keycloak.example.kerberos.GSSCredentialsClient" %> +<%@ page import="org.keycloak.example.kerberos.GSSCredentialsClient.LDAPUser" %> +<%@ page session="false" %> + + + + + Kerberos Credentials Delegation Example + + +

Kerberos Credentials Delegation Example

+
+ +<% + String logoutUri = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH) + .queryParam("redirect_uri", "/kerberos-portal").build("kerberos-demo").toString(); +%> + Details about user from LDAP | Logout
+
+<% + try { + GSSCredentialsClient.LDAPUser ldapUser = GSSCredentialsClient.getUserFromLDAP(request); + out.println("

uid: " + ldapUser.getUid() + "

"); + out.println("

cn: " + ldapUser.getCn() + "

"); + out.println("

sn: " + ldapUser.getSn() + "

"); + } catch (Exception e) { + e.printStackTrace(); + out.println("There was a failure in retrieve GSS credential or invoking LDAP. Check server.log for more details"); + } +%> + + \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index 9a8a5853ba..eb68e33864 100755 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -33,5 +33,6 @@ multi-tenant basic-auth fuse + kerberos diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java index 04b498e48f..f98364ad6b 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/CommonKerberosConfig.java @@ -3,7 +3,7 @@ package org.keycloak.federation.kerberos; import java.util.Map; import org.keycloak.models.UserFederationProviderModel; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; /** * Common configuration useful for all providers diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosConfig.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosConfig.java index c1f59b76ec..32349b7829 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosConfig.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosConfig.java @@ -1,6 +1,6 @@ package org.keycloak.federation.kerberos; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; import org.keycloak.models.LDAPConstants; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java index 603f3bd950..40094ef5aa 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java @@ -20,7 +20,7 @@ import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; /** * @author Marek Posolda @@ -176,19 +176,21 @@ public class KerberosFederationProvider implements UserFederationProvider { spnegoAuthenticator.authenticate(); + Map state = new HashMap(); if (spnegoAuthenticator.isAuthenticated()) { - Map state = new HashMap(); - state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, spnegoAuthenticator.getDelegationCredential()); - String username = spnegoAuthenticator.getAuthenticatedUsername(); UserModel user = findOrCreateAuthenticatedUser(realm, username); if (user == null) { return CredentialValidationOutput.failed(); } else { + String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential(); + if (delegationCredential != null) { + state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, delegationCredential); + } + return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state); } } else { - Map state = new HashMap(); state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken()); return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state); } diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java index ddebe667bc..fa136220c6 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java @@ -105,7 +105,7 @@ public class KerberosUsernamePasswordAuthenticator { try { loginContext.logout(); } catch (LoginException le) { - logger.error("Failed to logout kerberos server subject: " + config.getServerPrincipal(), le); + logger.error("Failed to logout kerberos subject", le); } } } diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java index 1132351b17..6dd2576448 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java @@ -1,7 +1,6 @@ package org.keycloak.federation.kerberos.impl; import java.io.IOException; -import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import javax.security.auth.Subject; @@ -13,6 +12,7 @@ import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; import org.jboss.logging.Logger; import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.util.KerberosSerializationUtils; /** * @author Marek Posolda @@ -21,8 +21,6 @@ public class SPNEGOAuthenticator { private static final Logger log = Logger.getLogger(SPNEGOAuthenticator.class); - private static final GSSManager GSS_MANAGER = GSSManager.getInstance(); - private final KerberosServerSubjectAuthenticator kerberosSubjectAuthenticator; private final String spnegoToken; private final CommonKerberosConfig kerberosConfig; @@ -61,8 +59,24 @@ public class SPNEGOAuthenticator { return responseToken; } - public GSSCredential getDelegationCredential() { - return delegationCredential; + public String getSerializedDelegationCredential() { + if (delegationCredential == null) { + if (log.isTraceEnabled()) { + log.trace("No delegation credential available."); + } + + return null; + } + + try { + if (log.isTraceEnabled()) { + log.trace("Serializing credential " + delegationCredential); + } + return KerberosSerializationUtils.serializeCredential(delegationCredential); + } catch (KerberosSerializationUtils.KerberosSerializationException kse) { + log.warn("Couldn't serialize credential: " + delegationCredential, kse); + return null; + } } /** @@ -114,7 +128,8 @@ public class SPNEGOAuthenticator { protected GSSContext establishContext() throws GSSException, IOException { - GSSContext gssContext = GSS_MANAGER.createContext((GSSCredential) null); + GSSManager manager = GSSManager.getInstance(); + GSSContext gssContext = manager.createContext((GSSCredential) null); byte[] inputToken = Base64.decode(spnegoToken); byte[] respToken = gssContext.acceptSecContext(inputToken, 0, inputToken.length); diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index 7aabe26811..f48880cfc9 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -15,7 +15,7 @@ import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; import org.picketlink.idm.IdentityManagementException; import org.picketlink.idm.IdentityManager; import org.picketlink.idm.PartitionManager; @@ -344,9 +344,8 @@ public class LDAPFederationProvider implements UserFederationProvider { spnegoAuthenticator.authenticate(); + Map state = new HashMap(); if (spnegoAuthenticator.isAuthenticated()) { - Map state = new HashMap(); - state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, spnegoAuthenticator.getDelegationCredential()); // TODO: This assumes that LDAP "uid" is equal to kerberos principal name. Like uid "hnelson" and kerberos principal "hnelson@KEYCLOAK.ORG". // Check if it's correct or if LDAP attribute for mapping kerberos principal should be available (For ApacheDS it seems to be attribute "krb5PrincipalName" but on MSAD it's likely different) @@ -356,11 +355,15 @@ public class LDAPFederationProvider implements UserFederationProvider { if (user == null) { logger.warn("Kerberos/SPNEGO authentication succeeded with username [" + username + "], but couldn't find or create user with federation provider [" + model.getDisplayName() + "]"); return CredentialValidationOutput.failed(); - } + } else { + String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential(); + if (delegationCredential != null) { + state.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, delegationCredential); + } - return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state); + return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state); + } } else { - Map state = new HashMap(); state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken()); return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/kerberos/LDAPProviderKerberosConfig.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/kerberos/LDAPProviderKerberosConfig.java index 4363ce927c..d4d5c97775 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/kerberos/LDAPProviderKerberosConfig.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/kerberos/LDAPProviderKerberosConfig.java @@ -1,7 +1,7 @@ package org.keycloak.federation.ldap.kerberos; import org.keycloak.federation.kerberos.CommonKerberosConfig; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; import org.keycloak.models.UserFederationProviderModel; /** diff --git a/integration/pom.xml b/integration/pom.xml index 2ccda8e59e..7549c55b90 100755 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -23,6 +23,7 @@ jetty undertow wildfly-adapter + wildfly-extensions keycloak-subsystem keycloak-as7-subsystem js diff --git a/integration/wildfly-extensions/src/main/java/org/keycloak/provider/wildfly/ModuleProviderLoaderFactory.java b/integration/wildfly-extensions/src/main/java/org/keycloak/provider/wildfly/ModuleProviderLoaderFactory.java index cb4a6f19e6..217c203ad9 100644 --- a/integration/wildfly-extensions/src/main/java/org/keycloak/provider/wildfly/ModuleProviderLoaderFactory.java +++ b/integration/wildfly-extensions/src/main/java/org/keycloak/provider/wildfly/ModuleProviderLoaderFactory.java @@ -21,7 +21,6 @@ public class ModuleProviderLoaderFactory implements ProviderLoaderFactory { @Override public ProviderLoader create(ClassLoader baseClassLoader, String resource) { try { - System.out.println("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx"); Module module = Module.getContextModuleLoader().loadModule(ModuleIdentifier.fromString(resource)); ModuleClassLoader classLoader = module.getClassLoader(); return new DefaultProviderLoader(classLoader); diff --git a/model/api/src/main/java/org/keycloak/models/CredentialValidationOutput.java b/model/api/src/main/java/org/keycloak/models/CredentialValidationOutput.java index 70e4093ab5..a056ef9ec6 100644 --- a/model/api/src/main/java/org/keycloak/models/CredentialValidationOutput.java +++ b/model/api/src/main/java/org/keycloak/models/CredentialValidationOutput.java @@ -12,16 +12,16 @@ public class CredentialValidationOutput { private final UserModel authenticatedUser; // authenticated user. private final Status authStatus; // status whether user is authenticated or more steps needed - private final Map state; // Additional state related to authentication. It can contain data to be sent back to client or data about used credentials. + private final Map state; // Additional state related to authentication. It can contain data to be sent back to client or data about used credentials. - public CredentialValidationOutput(UserModel authenticatedUser, Status authStatus, Map state) { + public CredentialValidationOutput(UserModel authenticatedUser, Status authStatus, Map state) { this.authenticatedUser = authenticatedUser; this.authStatus = authStatus; this.state = state; } public static CredentialValidationOutput failed() { - return new CredentialValidationOutput(null, CredentialValidationOutput.Status.FAILED, new HashMap()); + return new CredentialValidationOutput(null, CredentialValidationOutput.Status.FAILED, new HashMap()); } public UserModel getAuthenticatedUser() { @@ -32,7 +32,7 @@ public class CredentialValidationOutput { return authStatus; } - public Map getState() { + public Map getState() { return state; } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java index 92465e1620..f52e9ef39a 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java @@ -302,6 +302,7 @@ public abstract class ClientAdapter extends A mapping.setId(entity.getId()); mapping.setName(entity.getName()); mapping.setProtocol(entity.getProtocol()); + mapping.setProtocolMapper(entity.getProtocolMapper()); mapping.setConsentRequired(entity.isConsentRequired()); mapping.setConsentText(entity.getConsentText()); Map config = new HashMap(); @@ -309,6 +310,7 @@ public abstract class ClientAdapter extends A config.putAll(entity.getConfig()); } mapping.setConfig(config); + result.add(mapping); } return result; } diff --git a/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java index abb915ee5e..a00932380d 100755 --- a/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java +++ b/services/src/main/java/org/keycloak/protocol/ProtocolMapperUtils.java @@ -10,10 +10,13 @@ import java.lang.reflect.Method; */ public class ProtocolMapperUtils { public static final String USER_ATTRIBUTE = "user.attribute"; + public static final String USER_SESSION_NOTE = "user.session.note"; public static final String USER_MODEL_PROPERTY_LABEL = "User Property"; public static final String USER_MODEL_PROPERTY_HELP_TEXT = "Name of the property method in the UserModel interface. For example, a value of 'email' would reference the UserModel.getEmail() method."; public static final String USER_MODEL_ATTRIBUTE_LABEL = "User Attribute"; public static final String USER_MODEL_ATTRIBUTE_HELP_TEXT = "Name of stored user attribute which is the name of an attribute within the UserModel.attribute map."; + public static final String USER_SESSION_MODEL_NOTE_LABEL = "User Session Note"; + public static final String USER_SESSION_MODEL_NOTE_HELP_TEXT = "Name of stored user session note within the UserSessionModel.note map."; public static String getUserModelValue(UserModel user, String propertyName) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java index 1fc1277fb1..1c27033383 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocolFactory.java @@ -1,5 +1,6 @@ package org.keycloak.protocol.oidc; +import org.keycloak.constants.KerberosConstants; import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; @@ -11,6 +12,7 @@ import org.keycloak.protocol.oidc.mappers.OIDCAddressMapper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.OIDCFullNameMapper; import org.keycloak.protocol.oidc.mappers.OIDCUserModelMapper; +import org.keycloak.protocol.oidc.mappers.OIDCUserSessionNoteMapper; import org.keycloak.services.managers.AuthenticationManager; import java.util.ArrayList; @@ -89,6 +91,13 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory { ProtocolMapperModel address = OIDCAddressMapper.createAddressMapper(); builtins.add(address); + + model = OIDCUserSessionNoteMapper.createClaimMapper(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, + KerberosConstants.GSS_DELEGATION_CREDENTIAL, + KerberosConstants.GSS_DELEGATION_CREDENTIAL, "String", + true, KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, + true, false); + builtins.add(model); } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCUserSessionNoteMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCUserSessionNoteMapper.java new file mode 100644 index 0000000000..effa69af83 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCUserSessionNoteMapper.java @@ -0,0 +1,134 @@ +package org.keycloak.protocol.oidc.mappers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.ProtocolMapperUtils; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.IDToken; + +/** + * Mappings UserSessionModel.note to an ID Token claim. + * + * @author Marek Posolda + */ +public class OIDCUserSessionNoteMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper { + + private static final List configProperties = new ArrayList(); + + static { + ConfigProperty property; + property = new ConfigProperty(); + property.setName(ProtocolMapperUtils.USER_SESSION_NOTE); + property.setLabel(ProtocolMapperUtils.USER_SESSION_MODEL_NOTE_LABEL); + property.setHelpText(ProtocolMapperUtils.USER_SESSION_MODEL_NOTE_HELP_TEXT); + property.setType(ConfigProperty.STRING_TYPE); + configProperties.add(property); + property = new ConfigProperty(); + property.setName(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); + property.setLabel(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); + property.setType(ConfigProperty.STRING_TYPE); + property.setHelpText("Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created."); + configProperties.add(property); + property = new ConfigProperty(); + property.setName(OIDCAttributeMapperHelper.JSON_TYPE); + property.setLabel(OIDCAttributeMapperHelper.JSON_TYPE); + property.setType(ConfigProperty.STRING_TYPE); + property.setDefaultValue(ConfigProperty.STRING_TYPE); + property.setHelpText("JSON type that should be used to populate the json claim in the token. long, int, boolean, and String are valid values."); + configProperties.add(property); + property = new ConfigProperty(); + property.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN); + property.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_LABEL); + property.setType(ConfigProperty.BOOLEAN_TYPE); + property.setDefaultValue("true"); + property.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_HELP_TEXT); + configProperties.add(property); + property = new ConfigProperty(); + property.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN); + property.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_LABEL); + property.setType(ConfigProperty.BOOLEAN_TYPE); + property.setDefaultValue("true"); + property.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_HELP_TEXT); + configProperties.add(property); + + } + + public static final String PROVIDER_ID = "oidc-usersessionmodel-note-mapper"; + + + public List getConfigProperties() { + return configProperties; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return "User Session Note"; + } + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + @Override + public String getHelpText() { + return "Map a custom user session note to a token claim."; + } + + @Override + public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, + UserSessionModel userSession, ClientSessionModel clientSession) { + if (!OIDCAttributeMapperHelper.includeInAccessToken(mappingModel)) return token; + + setClaim(token, mappingModel, userSession); + return token; + } + + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { + String noteName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_SESSION_NOTE); + String noteValue = userSession.getNote(noteName); + if (noteValue == null) return; + OIDCAttributeMapperHelper.mapClaim(token, mappingModel, noteValue); + } + + @Override + public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)) return token; + setClaim(token, mappingModel, userSession); + return token; + } + + public static ProtocolMapperModel createClaimMapper(String name, + String userSessionNote, + String tokenClaimName, String jsonType, + boolean consentRequired, String consentText, + boolean accessToken, boolean idToken) { + ProtocolMapperModel mapper = new ProtocolMapperModel(); + mapper.setName(name); + mapper.setProtocolMapper(PROVIDER_ID); + mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + mapper.setConsentRequired(consentRequired); + mapper.setConsentText(consentText); + Map config = new HashMap(); + config.put(ProtocolMapperUtils.USER_SESSION_NOTE, userSessionNote); + config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, tokenClaimName); + config.put(OIDCAttributeMapperHelper.JSON_TYPE, jsonType); + if (accessToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); + if (idToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); + mapper.setConfig(config); + return mapper; + } +} diff --git a/services/src/main/java/org/keycloak/provider/ProviderManager.java b/services/src/main/java/org/keycloak/provider/ProviderManager.java index 6a301fd36f..6246d83973 100644 --- a/services/src/main/java/org/keycloak/provider/ProviderManager.java +++ b/services/src/main/java/org/keycloak/provider/ProviderManager.java @@ -20,7 +20,7 @@ public class ProviderManager { public ProviderManager(ClassLoader baseClassLoader, String... resources) { List factories = new LinkedList(); - for (ProviderLoaderFactory f : ServiceLoader.load(ProviderLoaderFactory.class)) { + for (ProviderLoaderFactory f : ServiceLoader.load(ProviderLoaderFactory.class, getClass().getClassLoader())) { factories.add(f); } diff --git a/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java index 9791de321d..7dca9c1482 100644 --- a/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/HttpAuthenticationManager.java @@ -1,5 +1,7 @@ package org.keycloak.services.managers; +import java.util.Map; + import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -19,7 +21,7 @@ import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.messages.Messages; @@ -94,7 +96,7 @@ public class HttpAuthenticationManager { CredentialValidationOutput output = session.users().validCredentials(realm, spnegoCredential); if (output.getAuthStatus() == CredentialValidationOutput.Status.AUTHENTICATED) { - return sendResponse(output.getAuthenticatedUser(), "spnego"); + return sendResponse(output.getAuthenticatedUser(), output.getState(), "spnego"); } else { String spnegoResponseToken = (String) output.getState().get(KerberosConstants.RESPONSE_TOKEN); return challengeNegotiation(spnegoResponseToken); @@ -104,7 +106,7 @@ public class HttpAuthenticationManager { // Send response after successful authentication - private HttpAuthOutput sendResponse(UserModel user, String authMethod) { + private HttpAuthOutput sendResponse(UserModel user, Map authState, String authMethod) { if (logger.isTraceEnabled()) { logger.trace("User " + user.getUsername() + " authenticated with " + authMethod); } @@ -115,6 +117,12 @@ public class HttpAuthenticationManager { response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, Messages.ACCOUNT_DISABLED); } else { UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), authMethod, false); + + // Propagate state (like kerberos delegation credentials etc) as attributes of userSession + for (Map.Entry entry : authState.entrySet()) { + userSession.setNote(entry.getKey(), entry.getValue()); + } + TokenManager.attachClientSession(userSession, clientSession); event.user(user) .session(userSession) diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java index e4fff5ff4a..717a1168a3 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java @@ -4,23 +4,11 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; import org.keycloak.models.ClientModel; -import org.keycloak.models.KerberosConstants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RequiredCredentialModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserFederationProvider; -import org.keycloak.models.UserFederationProviderFactory; -import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; -import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.ProtocolMapperRepresentation; -import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation; -import org.keycloak.representations.idm.UserFederationProviderRepresentation; -import org.keycloak.services.managers.UsersSyncManager; -import org.keycloak.timer.TimerProvider; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -30,14 +18,12 @@ import javax.ws.rs.PUT; 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.Response; import javax.ws.rs.core.UriInfo; -import java.util.HashMap; + import java.util.LinkedList; import java.util.List; -import java.util.Map; /** * Base resource for managing users diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java index 04e3c7f9f4..47f1b65faa 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserFederationResource.java @@ -10,7 +10,7 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderFactory; import org.keycloak.models.UserFederationProviderModel; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.UserFederationProviderFactoryRepresentation; diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index d89c1bf2a1..f8cc934068 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -5,5 +5,6 @@ org.keycloak.protocol.oidc.mappers.OIDCAddressMapper org.keycloak.protocol.oidc.mappers.OIDCAddClaimMapper org.keycloak.protocol.oidc.mappers.OIDCAddRoleMapper org.keycloak.protocol.oidc.mappers.OIDCRoleMapper +org.keycloak.protocol.oidc.mappers.OIDCUserSessionNoteMapper diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPConfiguration.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPConfiguration.java index 63445b2a39..94b767233d 100644 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPConfiguration.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPConfiguration.java @@ -8,7 +8,7 @@ import java.util.Map; import java.util.Properties; import org.jboss.logging.Logger; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; import org.keycloak.models.LDAPConstants; import org.keycloak.models.UserFederationProvider; diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPEmbeddedServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPEmbeddedServer.java index 1bf4f80df2..4fde15bd3e 100644 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPEmbeddedServer.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/ldap/LDAPEmbeddedServer.java @@ -121,6 +121,7 @@ public class LDAPEmbeddedServer { LdapServer ldapServer = new LdapServer(); ldapServer.setServiceName("DefaultLdapServer"); + ldapServer.setSearchBaseDn(this.baseDN); // Read the transports Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50); diff --git a/testsuite/integration/src/main/resources/kerberos/http.keytab b/testsuite/integration/src/main/resources/kerberos/http.keytab index c156500fee..0e7fd96fa7 100644 Binary files a/testsuite/integration/src/main/resources/kerberos/http.keytab and b/testsuite/integration/src/main/resources/kerberos/http.keytab differ diff --git a/testsuite/integration/src/main/resources/kerberos/test-krb5.conf b/testsuite/integration/src/main/resources/kerberos/test-krb5.conf index 350c086af7..a775b47c86 100644 --- a/testsuite/integration/src/main/resources/kerberos/test-krb5.conf +++ b/testsuite/integration/src/main/resources/kerberos/test-krb5.conf @@ -1,13 +1,14 @@ [libdefaults] default_realm = KEYCLOAK.ORG - default_tgs_enctypes = des3-cbc-sha1-kd rc4-hmac - default_tkt_enctypes = des3-cbc-sha1-kd rc4-hmac - permitted_enctypes = des3-cbc-sha1-kd rc4-hmac + default_tgs_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac + default_tkt_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac + permitted_enctypes = des3-cbc-sha1-kd aes256-cts-hmac-sha1-96 rc4-hmac kdc_timeout = 30000 dns_lookup_realm = false dns_lookup_kdc = false dns_canonicalize_hostname = false ignore_acceptor_hostname = true + forwardable = true [realms] KEYCLOAK.ORG = { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java index 0bb3cd2587..2cbe7aeed2 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java @@ -16,31 +16,33 @@ import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.FixMethodOrder; -import org.junit.Rule; import org.junit.Test; -import org.junit.runners.MethodSorters; import org.keycloak.OAuth2Constants; import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.events.Details; import org.keycloak.federation.kerberos.CommonKerberosConfig; -import org.keycloak.federation.kerberos.KerberosConfig; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; +import org.keycloak.models.ApplicationModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.LDAPConstants; +import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory; +import org.keycloak.protocol.oidc.mappers.OIDCUserSessionNoteMapper; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.adapter.AdapterTest; +import org.keycloak.testsuite.adapter.AdapterTestStrategy; import org.keycloak.testsuite.pages.AccountPasswordPage; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; -import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; /** @@ -48,6 +50,8 @@ import org.openqa.selenium.WebDriver; */ public abstract class AbstractKerberosTest { + protected String KERBEROS_APP_URL = "http://localhost:8081/kerberos-portal"; + protected KeycloakSPNegoSchemeFactory spnegoSchemeFactory; protected ResteasyClient client; @@ -63,9 +67,6 @@ public abstract class AbstractKerberosTest { @WebResource protected AccountPasswordPage changePasswordPage; - @WebResource - protected AppPage appPage; - protected abstract CommonKerberosConfig getKerberosConfig(); protected abstract KeycloakRule getKeycloakRule(); protected abstract AssertEvents getAssertEvents(); @@ -88,7 +89,11 @@ public abstract class AbstractKerberosTest { @Test public void spnegoNotAvailableTest() throws Exception { initHttpClient(false); - Response response = client.target(oauth.getLoginFormUrl()).request().get(); + + driver.navigate().to(KERBEROS_APP_URL); + String kcLoginPageLocation = driver.getCurrentUrl(); + + Response response = client.target(kcLoginPageLocation).request().get(); Assert.assertEquals(401, response.getStatus()); Assert.assertEquals(KerberosConstants.NEGOTIATE, response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE)); String responseText = response.readEntity(String.class); @@ -105,17 +110,21 @@ public abstract class AbstractKerberosTest { Assert.assertEquals(302, spnegoResponse.getStatus()); events.expectLogin() + .client("kerberos-app") .user(keycloakRule.getUser("test", "hnelson").getId()) + .detail(Details.REDIRECT_URI, KERBEROS_APP_URL) .detail(Details.AUTH_METHOD, "spnego") .detail(Details.USERNAME, "hnelson") .assertEvent(); String location = spnegoResponse.getLocation().toString(); driver.navigate().to(location); - Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); - Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + String pageSource = driver.getPageSource(); + Assert.assertTrue(pageSource.contains("Kerberos Test") && pageSource.contains("Kerberos servlet secured content")); spnegoResponse.close(); + events.clear(); } @@ -157,18 +166,72 @@ public abstract class AbstractKerberosTest { Response spnegoResponse = spnegoLogin("jduke", "theduke"); Assert.assertEquals(302, spnegoResponse.getStatus()); events.expectLogin() + .client("kerberos-app") .user(keycloakRule.getUser("test", "jduke").getId()) + .detail(Details.REDIRECT_URI, KERBEROS_APP_URL) .detail(Details.AUTH_METHOD, "spnego") .detail(Details.USERNAME, "jduke") .assertEvent(); spnegoResponse.close(); } + @Test + public void credentialDelegationTest() throws Exception { + // Add kerberos delegation credential mapper + getKeycloakRule().update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + ProtocolMapperModel protocolMapper = OIDCUserSessionNoteMapper.createClaimMapper(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, + KerberosConstants.GSS_DELEGATION_CREDENTIAL, + KerberosConstants.GSS_DELEGATION_CREDENTIAL, "String", + true, KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME, + true, false); + + ApplicationModel kerberosApp = appRealm.getApplicationByName("kerberos-app"); + kerberosApp.addProtocolMapper(protocolMapper); + } + + }); + + // SPNEGO login + spnegoLoginTestImpl(); + + // Assert servlet authenticated to LDAP with delegated credential + driver.navigate().to(KERBEROS_APP_URL + KerberosCredDelegServlet.CRED_DELEG_TEST_PATH); + String pageSource = driver.getPageSource(); + Assert.assertTrue(pageSource.contains("LDAP Data: Horatio Nelson")); + + // Remove kerberos delegation credential mapper + getKeycloakRule().update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + ApplicationModel kerberosApp = appRealm.getApplicationByName("kerberos-app"); + ProtocolMapperModel toRemove = kerberosApp.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME); + kerberosApp.removeProtocolMapper(toRemove); + } + + }); + + // Clear driver and login again. I can't invoke LDAP now as GSS Credential is not in accessToken + driver.manage().deleteAllCookies(); + spnegoLoginTestImpl(); + driver.navigate().to(KERBEROS_APP_URL + KerberosCredDelegServlet.CRED_DELEG_TEST_PATH); + pageSource = driver.getPageSource(); + Assert.assertFalse(pageSource.contains("LDAP Data: Horatio Nelson")); + Assert.assertTrue(pageSource.contains("LDAP Data: ERROR")); + } + protected Response spnegoLogin(String username, String password) { + driver.navigate().to(KERBEROS_APP_URL); + String kcLoginPageLocation = driver.getCurrentUrl(); + + // Request for SPNEGO login sent with Resteasy client spnegoSchemeFactory.setCredentials(username, password); - return client.target(oauth.getLoginFormUrl()).request().get(); + return client.target(kcLoginPageLocation).request().get(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosCredDelegServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosCredDelegServlet.java new file mode 100644 index 0000000000..22baba480d --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosCredDelegServlet.java @@ -0,0 +1,96 @@ +package org.keycloak.testsuite.federation; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.security.sasl.Sasl; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.ietf.jgss.GSSCredential; +import org.keycloak.KeycloakPrincipal; +import org.keycloak.constants.KerberosConstants; +import org.keycloak.util.KerberosSerializationUtils; + +/** + * @author Marek Posolda + */ +public class KerberosCredDelegServlet extends HttpServlet { + + public static final String CRED_DELEG_TEST_PATH = "/cred-deleg-test"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + String ldapData = null; + + if (req.getRequestURI().endsWith(CRED_DELEG_TEST_PATH)) { + + try { + // Retrieve kerberos credential from accessToken and deserialize it + KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) req.getUserPrincipal(); + String serializedGssCredential = (String) keycloakPrincipal.getKeycloakSecurityContext().getToken().getOtherClaims().get(KerberosConstants.GSS_DELEGATION_CREDENTIAL); + GSSCredential gssCredential = KerberosSerializationUtils.deserializeCredential(serializedGssCredential); + + // First try to invoke without gssCredential. It should fail + try { + invokeLdap(null); + throw new RuntimeException("Not expected to authenticate to LDAP without credential"); + } catch (NamingException nse) { + System.out.println("Expected exception: " + nse.getMessage()); + } + + ldapData = invokeLdap(gssCredential); + } catch (KerberosSerializationUtils.KerberosSerializationException kse) { + System.err.println("KerberosSerializationUtils.KerberosSerializationException: " + kse.getMessage()); + ldapData = "ERROR"; + } catch (Exception e) { + e.printStackTrace(); + resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + resp.setContentType("text/html"); + PrintWriter pw = resp.getWriter(); + pw.printf("%s", "Kerberos Test"); + pw.printf("Kerberos servlet secured content
"); + + if (ldapData != null) { + pw.printf("LDAP Data: " + ldapData + "
"); + } + + pw.print(""); + pw.flush(); + + + } + + private String invokeLdap(GSSCredential gssCredential) throws NamingException { + Hashtable env = new Hashtable(11); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, "ldap://localhost:10389"); + + if (gssCredential != null) { + env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + env.put(Sasl.CREDENTIALS, gssCredential); + } + + DirContext ctx = new InitialDirContext(env); + try { + Attributes attrs = ctx.getAttributes("uid=hnelson,ou=People,dc=keycloak,dc=org"); + String cn = (String) attrs.get("cn").get(); + String sn = (String) attrs.get("sn").get(); + return cn + " " + sn; + } finally { + ctx.close(); + } + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java index 4123508815..07ca87cf9d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java @@ -1,43 +1,29 @@ package org.keycloak.testsuite.federation; +import java.net.URL; import java.util.Map; -import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import org.junit.Assert; import org.junit.ClassRule; -import org.junit.FixMethodOrder; import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; import org.junit.rules.TestRule; -import org.junit.runners.MethodSorters; -import org.keycloak.OAuth2Constants; import org.keycloak.events.Details; import org.keycloak.federation.kerberos.CommonKerberosConfig; -import org.keycloak.federation.kerberos.KerberosConfig; -import org.keycloak.federation.kerberos.KerberosFederationProviderFactory; import org.keycloak.federation.ldap.LDAPFederationProviderFactory; import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig; -import org.keycloak.models.KerberosConstants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; -import org.keycloak.testsuite.OAuthClient; -import org.keycloak.testsuite.pages.AccountPasswordPage; -import org.keycloak.testsuite.pages.AccountUpdateProfilePage; -import org.keycloak.testsuite.pages.AppPage; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.RegisterPage; import org.keycloak.testsuite.rule.KerberosRule; import org.keycloak.testsuite.rule.KeycloakRule; -import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; -import org.openqa.selenium.WebDriver; /** * Test of LDAPFederationProvider (Kerberos backed by LDAP) @@ -46,21 +32,30 @@ import org.openqa.selenium.WebDriver; */ public class KerberosLdapTest extends AbstractKerberosTest { - public static final String CONFIG_LOCATION = "kerberos/kerberos-ldap-connection.properties"; + private static final String PROVIDER_CONFIG_LOCATION = "kerberos/kerberos-ldap-connection.properties"; private static UserFederationProviderModel ldapModel = null; - private static KerberosRule kerberosRule = new KerberosRule(CONFIG_LOCATION); + private static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION); private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + URL url = getClass().getResource("/kerberos-test/kerberos-app-keycloak.json"); + keycloakRule.deployApplication("kerberos-portal", "/kerberos-portal", KerberosCredDelegServlet.class, url.getPath(), "user"); + Map ldapConfig = kerberosRule.getConfig(); ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "kerberos-ldap", -1, -1, 0); - appRealm.addRequiredCredential(UserCredentialModel.KERBEROS); } - }); + }) { + + @Override + protected void importRealm() { + server.importRealm(getClass().getResourceAsStream("/kerberos-test/kerberosrealm.json")); + } + + }; @ClassRule public static TestRule chain = RuleChain @@ -129,12 +124,15 @@ public class KerberosLdapTest extends AbstractKerberosTest { Response spnegoResponse = spnegoLogin("jduke", "newPass"); Assert.assertEquals(302, spnegoResponse.getStatus()); events.expectLogin() + .client("kerberos-app") .user(keycloakRule.getUser("test", "jduke").getId()) + .detail(Details.REDIRECT_URI, KERBEROS_APP_URL) .detail(Details.AUTH_METHOD, "spnego") .detail(Details.USERNAME, "jduke") .assertEvent(); // Change password back + changePasswordPage.open(); loginPage.login("jduke", "newPass"); changePasswordPage.assertCurrent(); changePasswordPage.changePassword("newPass", "theduke", "theduke"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java index 65a753ea3d..c665f44c3c 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java @@ -1,5 +1,6 @@ package org.keycloak.testsuite.federation; +import java.net.URL; import java.util.Map; import javax.ws.rs.core.Response; @@ -13,16 +14,20 @@ import org.junit.rules.TestRule; import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.KerberosConfig; import org.keycloak.federation.kerberos.KerberosFederationProviderFactory; -import org.keycloak.models.KerberosConstants; +import org.keycloak.constants.KerberosConstants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.adapter.CustomerServlet; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testsuite.rule.KerberosRule; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebRule; +import org.keycloak.testutils.KeycloakServer; /** * Test of KerberosFederationProvider (Kerberos not backed by LDAP) @@ -31,22 +36,31 @@ import org.keycloak.testsuite.rule.WebRule; */ public class KerberosStandaloneTest extends AbstractKerberosTest { - public static final String CONFIG_LOCATION = "kerberos/kerberos-standalone-connection.properties"; + private static final String PROVIDER_CONFIG_LOCATION = "kerberos/kerberos-standalone-connection.properties"; private static UserFederationProviderModel kerberosModel; - private static KerberosRule kerberosRule = new KerberosRule(CONFIG_LOCATION); + private static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION); private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - Map kerberosConfig = kerberosRule.getConfig(); + URL url = getClass().getResource("/kerberos-test/kerberos-app-keycloak.json"); + keycloakRule.deployApplication("kerberos-portal", "/kerberos-portal", KerberosCredDelegServlet.class, url.getPath(), "user"); + Map kerberosConfig = kerberosRule.getConfig(); kerberosModel = appRealm.addUserFederationProvider(KerberosFederationProviderFactory.PROVIDER_NAME, kerberosConfig, 0, "kerberos-standalone", -1, -1, 0); - appRealm.addRequiredCredential(UserCredentialModel.KERBEROS); } - }); + + }) { + + @Override + protected void importRealm() { + server.importRealm(getClass().getResourceAsStream("/kerberos-test/kerberosrealm.json")); + } + + }; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java index 957b66be69..915ed5a30a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -137,7 +137,7 @@ public class RefreshTokenTest { Assert.assertEquals("bearer", tokenResponse.getTokenType()); - Assert.assertThat(token.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); + Assert.assertThat(token.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(200), lessThanOrEqualTo(350))); int actual = refreshToken.getExpiration() - Time.currentTime(); Assert.assertThat(actual, allOf(greaterThanOrEqualTo(1799), lessThanOrEqualTo(1800))); @@ -313,7 +313,7 @@ public class RefreshTokenTest { session.close(); // lastSEssionRefresh should be updated because access code lifespan is higher than sso idle timeout - Assert.assertThat(next, allOf(greaterThan(last), lessThan(last + 6))); + Assert.assertThat(next, allOf(greaterThan(last), lessThan(last + 50))); session = keycloakRule.startSession(); realm = session.realms().getRealmByName("test"); diff --git a/testsuite/integration/src/test/resources/kerberos-test/kerberos-app-keycloak.json b/testsuite/integration/src/test/resources/kerberos-test/kerberos-app-keycloak.json new file mode 100644 index 0000000000..609bcdb1aa --- /dev/null +++ b/testsuite/integration/src/test/resources/kerberos-test/kerberos-app-keycloak.json @@ -0,0 +1,10 @@ +{ + "realm": "test", + "resource": "kerberos-app", + "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "http://localhost:8081/auth", + "ssl-required" : "external", + "credentials": { + "secret": "password" + } +} diff --git a/testsuite/integration/src/test/resources/kerberos-test/kerberosrealm.json b/testsuite/integration/src/test/resources/kerberos-test/kerberosrealm.json new file mode 100644 index 0000000000..b0fb903591 --- /dev/null +++ b/testsuite/integration/src/test/resources/kerberos-test/kerberosrealm.json @@ -0,0 +1,54 @@ +{ + "id": "test", + "realm": "test", + "enabled": true, + "sslRequired": "external", + "registrationAllowed": true, + "resetPasswordAllowed": true, + "passwordCredentialGrantAllowed": true, + "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "requiredCredentials": [ "password", "kerberos" ], + "defaultRoles": [ "user" ], + "users" : [ + { + "username" : "test-user@localhost", + "enabled": true, + "email" : "test-user@localhost", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["user"], + "applicationRoles": { + "account": [ "view-profile", "manage-account" ] + } + } + ], + "scopeMappings": [ + { + "client": "kerberos-app", + "roles": ["user"] + } + ], + "applications": [ + { + "name": "kerberos-app", + "enabled": true, + "baseUrl": "http://localhost:8081/kerberos-portal", + "redirectUris": [ + "http://localhost:8081/kerberos-portal/*" + ], + "adminUrl": "http://localhost:8081/kerberos-portal/logout", + "secret": "password" + } + ], + "roles" : { + "realm" : [ + { + "name": "user", + "description": "Have User privileges" + } + ] + } +} \ No newline at end of file