This commit is contained in:
Bill Burke 2015-03-06 18:42:20 -05:00
commit 1de285b724
50 changed files with 1268 additions and 108 deletions

View file

@ -1,4 +1,4 @@
package org.keycloak.models;
package org.keycloak.constants;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -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";
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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);
}
}
}

View file

@ -217,6 +217,12 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-wildfly-extensions</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View file

@ -107,6 +107,12 @@
<exclude name="**/*.iml"/>
</fileset>
</copy>
<copy todir="target/examples/kerberos" overwrite="true">
<fileset dir="../../examples/kerberos">
<exclude name="**/target/**"/>
<exclude name="**/*.iml"/>
</fileset>
</copy>
<copy file="../../examples/README.md" tofile="target/examples/README.md"/>
<move file="target/examples/unconfigured-demo/README.md.unconfigured" tofile="target/examples/unconfigured-demo/README.md"/>
<move file="target/examples/unconfigured-demo/customer-app/src/main/webapp/WEB-INF/web.xml.unconfigured" tofile="target/examples/unconfigured-demo/customer-app/src/main/webapp/WEB-INF/web.xml"/>

View file

@ -66,6 +66,10 @@
<maven-resource group="org.keycloak" artifact="keycloak-services"/>
</module-def>
<module-def name="org.keycloak.keycloak-wildfly-extensions">
<maven-resource group="org.keycloak" artifact="keycloak-wildfly-extensions"/>
</module-def>
<module-def name="com.google.zxing.core">
<maven-resource group="com.google.zxing" artifact="core"/>
</module-def>
@ -232,6 +236,8 @@
<maven-resource group="org.keycloak" artifact="keycloak-kerberos-federation"/>
</module-def>
<module-def name="sun.jdk.jgss"></module-def>
<module-def name="org.keycloak.keycloak-ldap-federation">
<maven-resource group="org.keycloak" artifact="keycloak-ldap-federation"/>
</module-def>

View file

@ -14,6 +14,8 @@
<module name="net.iharder.base64"/>
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="sun.jdk" optional="true" />
<module name="sun.jdk.jgss" optional="true" />
</dependencies>
</module>

View file

@ -47,6 +47,7 @@
<module name="org.keycloak.keycloak-model-sessions-mem" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-mongo" services="import"/>
<module name="org.keycloak.keycloak-picketlink-api" services="import"/>
<module name="org.keycloak.keycloak-wildfly-extensions" services="import"/>
<module name="org.keycloak.keycloak-picketlink-ldap" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-wildfly-extensions">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-services"/>
<module name="org.jboss.modules"/>
</dependencies>
</module>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="sun.jdk.jgss">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<system export="true">
<paths>
<path name="sun/security/jgss" />
<path name="sun/security/jgss/spi" />
<path name="sun/security/jgss/krb5" />
</paths>
</system>
</dependencies>
</module>

View file

@ -204,6 +204,40 @@ ktadd -k /tmp/http.keytab HTTP/www.mydomain.org@MYDOMAIN.ORG
</para>
</section>
</section>
<section>
<title>Credential delegation</title>
<para>
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).
</para>
<para>
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 <literal>gss delegation credential</literal> 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.
</para>
<para>
GSSContext will need to
be created with this credential passed to the method <literal>GSSManager.createContext</literal> for example like this:
<programlisting><![CDATA[
GSSContext context = gssManager.createContext(serviceName, krb5Oid,
deserializedGssCredFromKeycloakAccessToken, GSSContext.DEFAULT_LIFETIME);
]]></programlisting>
</para>
<para>
Note that you also need to configure <literal>forwardable</literal> kerberos tickets in <literal>krb5.conf</literal> file
and add support for delegated credentials to your browser. See the kerberos example from Keycloak example set for details.
</para>
<warning>
<para>
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
<ulink url="http://www.microhowto.info/howto/configure_firefox_to_authenticate_using_spnego_and_kerberos.html#idp18752">this article</ulink>
for details.
</para>
</warning>
</section>
<section>
<title>Troubleshooting</title>
<para>

View file

@ -87,33 +87,69 @@ public class MyEventListenerProvider implements EventListenerProvider {
<section>
<title>Registering provider implementations</title>
<para>
Keycloak loads provider implementations from the file-system. By default all JARs inside
<literal>standalone/configuration/providers</literal> 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.
</para>
<para>
To register your provider simply copy the JAR including the ProviderFactory and Provider classes and the
provider configuration file to <literal>standalone/configuration/providers</literal>.
</para>
<para>
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:
<section>
<title>Register a provider using Modules</title>
<para>
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 <literal>module.xml</literal>. For example to add the event listener
sysout example provider create the folder <literal>KEYCLOAK_HOME/modules/org/keycloak/examples/event-sysout/main</literal>.
Copy <literal>event-listener-sysout-example.jar</literal> to this folder and create <literal>module.xml</literal>
with the following content:
<programlisting><![CDATA[{
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.examples.event-sysout">
<resources>
<resource-root path="event-listener-sysout-example.jar"/>
</resources>
<dependencies>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-events-api"/>
</dependencies>
</module>
}]]></programlisting>
Next you need to register this module with Keycloak. This is done by editing keycloak-server.json and adding
it to the providers:
<programlisting><![CDATA[{
"providers": [
...
"module:org.keycloak.examples.event-sysout"
]
}]]></programlisting>
</para>
</section>
<section>
<title>Register a provider using file-system</title>
<para>
To register your provider simply copy the JAR including the ProviderFactory and Provider classes and the
provider configuration file to <literal>standalone/configuration/providers</literal>.
</para>
<para>
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:
<programlisting><![CDATA[{
"providers": [
"classpath:provider1.jar;lib-v1.jar",
"classpath:provider2.jar;lib-v2.jar"
]
}]]></programlisting>
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:
<programlisting><![CDATA[{
"providers": [
"classpath:/home/user/providers/*"
]
}]]></programlisting>
</para>
</para>
</section>
</section>
<section>

View file

@ -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).

Binary file not shown.

View file

@ -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"
}
}
]
}

83
examples/kerberos/pom.xml Normal file
View file

@ -0,0 +1,83 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.2.0.Beta1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<name>Keycloak Examples - Kerberos Credential Delegation</name>
<artifactId>examples-kerberos</artifactId>
<packaging>war</packaging>
<description>
Kerberos Credential Delegation Example
</description>
<repositories>
<repository>
<id>jboss</id>
<name>jboss repo</name>
<url>http://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>kerberos-portal</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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;
}
}
}

View file

@ -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"
}
}

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>kerberos-portal</module-name>
<security-constraint>
<web-resource-collection>
<web-resource-name>KerberosApp</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<!--
<security-constraint>
<web-resource-collection>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint> -->
<login-config>
<auth-method>KEYCLOAK</auth-method>
<realm-name>does-not-matter</realm-name>
</login-config>
<security-role>
<role-name>user</role-name>
</security-role>
</web-app>

View file

@ -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" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Kerberos Credentials Delegation Example</title>
</head>
<body bgcolor="#ffffff">
<h1>Kerberos Credentials Delegation Example</h1>
<hr />
<%
String logoutUri = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
.queryParam("redirect_uri", "/kerberos-portal").build("kerberos-demo").toString();
%>
<b>Details about user from LDAP</b> | <a href="<%=logoutUri%>">Logout</a><br />
<hr />
<%
try {
GSSCredentialsClient.LDAPUser ldapUser = GSSCredentialsClient.getUserFromLDAP(request);
out.println("<p>uid: <b>" + ldapUser.getUid() + "</b></p>");
out.println("<p>cn: <b>" + ldapUser.getCn() + "</b></p>");
out.println("<p>sn: <b>" + ldapUser.getSn() + "</b></p>");
} catch (Exception e) {
e.printStackTrace();
out.println("<b>There was a failure in retrieve GSS credential or invoking LDAP. Check server.log for more details</b>");
}
%>
</body>
</html>

View file

@ -33,5 +33,6 @@
<module>multi-tenant</module>
<module>basic-auth</module>
<module>fuse</module>
<module>kerberos</module>
</modules>
</project>

View file

@ -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

View file

@ -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;

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -176,19 +176,21 @@ public class KerberosFederationProvider implements UserFederationProvider {
spnegoAuthenticator.authenticate();
Map<String, String> state = new HashMap<String, String>();
if (spnegoAuthenticator.isAuthenticated()) {
Map<String, Object> state = new HashMap<String, Object>();
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<String, Object> state = new HashMap<String, Object>();
state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken());
return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state);
}

View file

@ -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);
}
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -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);

View file

@ -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<String, String> state = new HashMap<String, String>();
if (spnegoAuthenticator.isAuthenticated()) {
Map<String, Object> state = new HashMap<String, Object>();
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<String, Object> state = new HashMap<String, Object>();
state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken());
return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state);
}

View file

@ -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;
/**

View file

@ -23,6 +23,7 @@
<module>jetty</module>
<module>undertow</module>
<module>wildfly-adapter</module>
<module>wildfly-extensions</module>
<module>keycloak-subsystem</module>
<module>keycloak-as7-subsystem</module>
<module>js</module>

View file

@ -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);

View file

@ -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<String, Object> state; // Additional state related to authentication. It can contain data to be sent back to client or data about used credentials.
private final Map<String, String> 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<String, Object> state) {
public CredentialValidationOutput(UserModel authenticatedUser, Status authStatus, Map<String, String> state) {
this.authenticatedUser = authenticatedUser;
this.authStatus = authStatus;
this.state = state;
}
public static CredentialValidationOutput failed() {
return new CredentialValidationOutput(null, CredentialValidationOutput.Status.FAILED, new HashMap<String, Object>());
return new CredentialValidationOutput(null, CredentialValidationOutput.Status.FAILED, new HashMap<String, String>());
}
public UserModel getAuthenticatedUser() {
@ -32,7 +32,7 @@ public class CredentialValidationOutput {
return authStatus;
}
public Map<String, Object> getState() {
public Map<String, String> getState() {
return state;
}

View file

@ -302,6 +302,7 @@ public abstract class ClientAdapter<T extends MongoIdentifiableEntity> 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<String, String> config = new HashMap<String, String>();
@ -309,6 +310,7 @@ public abstract class ClientAdapter<T extends MongoIdentifiableEntity> extends A
config.putAll(entity.getConfig());
}
mapping.setConfig(config);
result.add(mapping);
}
return result;
}

View file

@ -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) {

View file

@ -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

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCUserSessionNoteMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper {
private static final List<ConfigProperty> configProperties = new ArrayList<ConfigProperty>();
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<ConfigProperty> 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<String, String> config = new HashMap<String, String>();
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;
}
}

View file

@ -20,7 +20,7 @@ public class ProviderManager {
public ProviderManager(ClassLoader baseClassLoader, String... resources) {
List<ProviderLoaderFactory> factories = new LinkedList<ProviderLoaderFactory>();
for (ProviderLoaderFactory f : ServiceLoader.load(ProviderLoaderFactory.class)) {
for (ProviderLoaderFactory f : ServiceLoader.load(ProviderLoaderFactory.class, getClass().getClassLoader())) {
factories.add(f);
}

View file

@ -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<String, String> 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<String, String> entry : authState.entrySet()) {
userSession.setNote(entry.getKey(), entry.getValue());
}
TokenManager.attachClientSession(userSession, clientSession);
event.user(user)
.session(userSession)

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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);

View file

@ -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 = {

View file

@ -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();
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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("<html><head><title>%s</title></head><body>", "Kerberos Test");
pw.printf("Kerberos servlet secured content<br>");
if (ldapData != null) {
pw.printf("LDAP Data: " + ldapData + "<br>");
}
pw.print("</body></html>");
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();
}
}
}

View file

@ -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<String,String> 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");

View file

@ -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<String,String> 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<String,String> 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"));
}
};

View file

@ -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");

View file

@ -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"
}
}

View file

@ -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"
}
]
}
}