KEYCLOAK-1066 Kerberos credential delegation support
This commit is contained in:
parent
8cf31bd3f1
commit
db07d79009
42 changed files with 1189 additions and 89 deletions
|
@ -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";
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"/>
|
||||
|
|
|
@ -236,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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
66
examples/kerberos/README.md
Normal file
66
examples/kerberos/README.md
Normal 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).
|
BIN
examples/kerberos/http.keytab
Normal file
BIN
examples/kerberos/http.keytab
Normal file
Binary file not shown.
94
examples/kerberos/kerberosrealm.json
Normal file
94
examples/kerberos/kerberosrealm.json
Normal 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
83
examples/kerberos/pom.xml
Normal 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>
|
|
@ -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, krb5Oid, deserializedGssCredFromKeycloakAccessToken, GSSContext.DEFAULT_LIFETIME);
|
||||
*
|
||||
* In this example we will authenticate with GSSCredential against LDAP server, which calls GSS API under the hood
|
||||
*
|
||||
* @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) 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, 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(gssCredential, 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;
|
||||
}
|
||||
}
|
||||
}
|
11
examples/kerberos/src/main/webapp/WEB-INF/keycloak.json
Normal file
11
examples/kerberos/src/main/webapp/WEB-INF/keycloak.json
Normal 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"
|
||||
}
|
||||
}
|
37
examples/kerberos/src/main/webapp/WEB-INF/web.xml
Normal file
37
examples/kerberos/src/main/webapp/WEB-INF/web.xml
Normal 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>
|
37
examples/kerberos/src/main/webapp/index.jsp
Normal file
37
examples/kerberos/src/main/webapp/index.jsp
Normal 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>List of users 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 invoking LDAP. Check server.log for more details</b>");
|
||||
}
|
||||
%>
|
||||
</body>
|
||||
</html>
|
|
@ -33,5 +33,6 @@
|
|||
<module>multi-tenant</module>
|
||||
<module>basic-auth</module>
|
||||
<module>fuse</module>
|
||||
<module>kerberos</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
} else {
|
||||
Map<String, Object> state = new HashMap<String, Object>();
|
||||
state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken());
|
||||
return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -2,5 +2,6 @@ org.keycloak.protocol.oidc.mappers.OIDCUserAttributeMapper
|
|||
org.keycloak.protocol.oidc.mappers.OIDCFullNameMapper
|
||||
org.keycloak.protocol.oidc.mappers.OIDCUserModelMapper
|
||||
org.keycloak.protocol.oidc.mappers.OIDCAddressMapper
|
||||
org.keycloak.protocol.oidc.mappers.OIDCUserSessionNoteMapper
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Binary file not shown.
|
@ -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 = {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue