federation docs

This commit is contained in:
Bill Burke 2014-08-04 12:25:11 -04:00
parent 95e2da9c9e
commit 04ec750157
15 changed files with 293 additions and 91 deletions

View file

@ -27,8 +27,7 @@
<!ENTITY Timeouts SYSTEM "modules/timeouts.xml">
<!ENTITY Audit SYSTEM "modules/audit.xml">
<!ENTITY AdminApi SYSTEM "modules/admin-rest-api.xml">
<!ENTITY Authentication SYSTEM "modules/authentication-spi.xml">
<!ENTITY Ldap SYSTEM "modules/ldap.xml">
<!ENTITY UserFederation SYSTEM "modules/user-federation.xml">
<!ENTITY ExportImport SYSTEM "modules/export-import.xml">
<!ENTITY ServerCache SYSTEM "modules/cache.xml">
]>
@ -115,8 +114,7 @@ This one is short
&Timeouts;
&AdminApi;
&Audit;
&Authentication;
&Ldap;
&UserFederation;
&ExportImport;
&ServerCache;
&Migration;

View file

@ -1,66 +0,0 @@
<chapter id="authentication-spi">
<title>Authentication SPI</title>
<para>
Keycloak provides Authentication SPI, which allows to choose the <literal>AuthenticationProvider</literal> for authenticating users.
AuthenticationProvider is the interface, which states how will be your usernames/passwords validated. You can choose from
the set of available AuthenticationProviders or you can even implement and plug your own AuthenticationProvider, which
will allow to provide your own way how will Keycloak validates users and their passwords.
</para>
<section id="authentication-available-providers">
<title>Available Authentication Providers</title>
<para>
<itemizedlist>
<listitem><literal>Model</literal> - This provider validates users and their passwords based on the Keycloak model. So it just delegates
to model implementation provided either by RDBMS or Mongo at this moment. This is default AuthenticationProvider,
which is configured for <literal>keycloak-admin</literal> realm by default and it's also automatically configured for newly created realms.
</listitem>
<listitem><literal>External-model</literal> - This provider also uses Keycloak model, but it uses different realm to validate your users against.
For example if you want to create new realm "foo" and you want all users of already existing realm "bar" that they are automatically
able to login into realm "foo" with their usernames and passwords, you can choose this provider.
</listitem>
<listitem><literal>Picketlink</literal> - This provider delegates Authentication to <ulink url="http://docs.jboss.org/picketlink/2/latest/reference/html-single/#chap-Identity_Management_-_Overview">Picketlink IDM</ulink>
framework. Right now, Picketlink IDM in Keycloak is configured to always use LDAP based Identity store, which means that picketlink provider
allows you to authenticate your users against LDAP server. Note that you will first need to configure LDAP server as described
<link linkend="ldap">here</link> . <literal>PicketlinkAuthenticationProvider</literal> configured for the realm will automatically use LDAP configuration for this realm.
</listitem>
</itemizedlist>
</para>
</section>
<section id="authentication-features">
<title>Features and configuration</title>
<para>
<itemizedlist>
<listitem>
You can configure AuthenticationProviders separately for each realm. So for example you can choose that just realm
"foo" will use <literal>PicketlinkAuthenticationProvider</literal> and authenticate users against LDAP but realm "keycloak-admin" will still use default <literal>ModelAuthenticationProvider</literal>.
</listitem>
<listitem>
There is also possibility to choose more authentication providers for the realm, which actually means that Keycloak
will use first available AuthenticationProvider and just in case that user doesn't exist here,
it will fallback to second AuthenticationProvider in chain. So this may allow for example scenario, in which
you authenticate user against Keycloak database (model) and just if he doesn't exist in database, it will fallback to LDAP (picketlink).
</listitem>
<listitem>
You can configure for each AuthenticationProvider if you want to update passwords - option <literal>passwordUpdateSupported</literal>.
This means that when user update password or his profile through Keycloak UI, this change will be propagated into AuthenticationProvider.
So for example password in LDAP will be updated if it's <literal>true</literal>, but for read-only LDAP, you will likely switch it to <literal>false</literal>.
It also means that newly registered users will be propagated to particular AuthenticationProvider too,
but note that each user is always bind just to one AuthenticationProvider.
</listitem>
<listitem>
You can add/edit/remove AuthenticationProviders in the <literal>Authentication</literal> tab in admin console, which is under URL
<ulink url="http://localhost:8080/auth/admin/keycloak-admin/console/#/realms/YOUR_REALM/auth-settings">http://localhost:8080/auth/admin/keycloak-admin/console/#/realms/YOUR_REALM/auth-settings</ulink>
</listitem>
</itemizedlist>
</para>
</section>
<section id="authentication-new-provider">
<title>Creating your own Authentication Provider</title>
<para>
You need to implement interface AuthenticationProvider and add the name of your AuthenticationProviderFactory class into
<literal>META-INF/services/org.keycloak.authentication.AuthenticationProviderFactory</literal> file inside your JAR with AuthenticationProvider. You also need to copy this JAR into
<literal>standalone/deployments/auth-server.war/WEB-INF/lib</literal> . The best is to look at <ulink url="https://github.com/keycloak/keycloak/tree/master/examples/providers/authentication-properties">example</ulink> and try it out.
</para>
</section>
</chapter>

View file

@ -1,12 +0,0 @@
<chapter id="ldap">
<title>LDAP Integration</title>
<para>
Right now, LDAP server is configured separately for each Realm. Configuration is in admin console in tab <literal>Ldap</literal>
under realm settings. It's under URL like <ulink url="http://localhost:8080/auth/admin/keycloak-admin/console/index.html#/realms/YOUR_REALM/ldap-settings">http://localhost:8080/auth/admin/keycloak-admin/console/index.html#/realms/YOUR_REALM/ldap-settings</ulink> .
There is nothing like "shared" LDAP server for more realms in Keycloak, but it's planned for the future.
</para>
<para>
LDAP is currently used just for authentication of users done through <literal>PicketlinkAuthenticationProvider</literal> as described <link linkend="authentication-available-providers">here</link> .
In the future, we have plan to have full Sync SPI, which will allow one-way or two-way synchronization between LDAP server and Keycloak database including users and roles.
</para>
</chapter>

View file

@ -0,0 +1,140 @@
<chapter id="user_federation">
<title>User Federation SPI and LDAP/AD Integration</title>
<para>
Keycloak can federate external user databases. Out of the box we have support for LDAP and Active Directory.
Before you dive into this, you should understand how Keycloak does federation.
</para>
<para>
Keycloak performs federation a bit differently than other products/projects. The vision of Keycloak is that it
is an out of the box solution that should provide a core set of feature irregardless of the backend user storage you
want to use. Because of this requirement/vision, Keycloak has a set data model that all of its services use.
Most of the time when you want to federate an external user store, much of the metadata that would be needed to
provide this complete feature set does not exist in that external store. For example your LDAP server may only
provide password validation, but not support TOTP or user role mappings. The Keycloak User Federation SPI was
written to support these completely variable locations
</para>
<para>
The way user federation works is that Keycloak will import your federated users on demand to its local storage. How
much metadata that is imported depends on the underlying federation plugin and how that plugin is configured. Some
federation plugins may only import the username into Keycloak storage, others might import everything from name,
address, and phone number, to user role mappings. Some plugins might want to import credentials directly into
Keycloak storage and let Keycloak handle credential validation. Others might want to handle credential validation
themselves. Thegoal of the Federation SPI is to support all of these scenarios.
</para>
<section>
<title>LDAP and Active Directory Plugin</title>
<para>
Keycloak comes with a built-in LDAP/AD plugin. Currently it is set up only to import username, email, first and last name.
It supports password validation via LDAP/AD protocols and different user metadata synchronization modes. To configure
a federated LDAP store go to the admin console. Click on the <literal>Users</literal> menu option to get you
to the user management page. Then click on the <literal>Federation</literal> submenu option. When
you get to this page there is an "Add Provider" select box. You should see "ldap" within this list. Selecting
"ldap" will bring you to the ldap configuration page.
</para>
<section>
<title>Edit Mode</title>
<para>
Edit mode defines various synchronization options with your LDAP store depending on what privileges
you have.
<variablelist>
<varlistentry>
<term>READONLY</term>
<listitem>
<para>
Username, email, first and last name will be unchangable. Keycloak will show an error
anytime anybody tries to update these fields. Also, password updates will not be supported.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>WRITABLE</term>
<listitem>
<para>
Username, email, first and last name, and passwords can all be updated and will
be synchronized automatically with your LDAP store.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>UNSYNCED</term>
<listitem>
<para>
Any changes to username, email, first and last name, and passwords will be stored
in Keycloak local storage. It is up to you to figure out how to synchronize back to
LDAP.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
<section>
<title>Other config options</title>
<para>
<variablelist>
<varlistentry>
<term>Display Name</term>
<listitem>
<para>
Name used when this provider is referenced in the admin consle
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Priority</term>
<listitem>
<para>
The priority of this provider when looking up users or for adding registrations.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Sync Registrations</term>
<listitem>
<para>
If a new user is added through a registration page or admin console, should the user
be eligible to be synchronized to this provider.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Other options</term>
<listitem>
<para>
The rest of the configuration options should be self explanatory.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
</section>
<section>
<title>Writing your own User Federation Provider</title>
<para>
The keycloak examples directory contains an example of a simple User Federation Provider backed by
a simple properties file. See <literal>examples/providers/federation-provider</literal>. Most of how
to create a federation provider is explain directly within the example code, but some information is here too.
</para>
<para>
Writing a User Federation Provider starts by implementing the <literal>UserFederationProvider</literal>
and <literal>UserFederationProviderFactory</literal> interfaces. Please see the Javadoc and example
for complete details on on how to do this. Some important methods of note:
getUserByUsername() and getUserByEmail() require that you query your federated storage and if the user exists
create and import the user into Keycloak storage. How much metadata you import is fully up to you. This
import is done by invoking methods on the object returned <literal>KeycloakSession.userStorage()</literal>
to add and import user information. The proxy() method will be called whenever Keycloak has found an imported
UserModel. This allows the federation provider to proxy the UserModel which is useful if you want to support
external storage updates on demand.
</para>
<para>
After your code is written you must package up all your classes within a JAR file. This jar file must
contain a file called <literal>org.keycloak.models.UserFederationProviderFactory</literal>
within the <literal>META-INF/services</literal> directory of the JAR. This file is a list
of fully qualified classnames of all implementations of <literal>UserFederationProviderFactory</literal>.
This is how Keycloak discovers which providers have been deployment. Place the JAR in the
keycloak WAR deployment in the <literal>WEB-INF/lib</literal> directory.
</para>
</section>
</chapter>

View file

@ -0,0 +1,16 @@
Example User Federation Provider
===================================================
This is an example of user federation backed by a simple properties file. This properties file only contains username/password
key pairs. To deploy, build this directory then take the jar and copy it to the WEB-INF/lib of the keycloak server's
WAR file.
The ClasspathPropertiesFederationProvider is an example of a readonly provider. If you go to the Users/Federation
page of the admin console you will see this provider listed under "classpath-properties. To configure this provider you
specify a classpath to a properties file in the "path" field of the admin page for this plugin. This example includes
a "test-users.properties" within the JAR that you can use as the variable.
The FilePropertiesFederationProvider is an exxample of a writable provider. It synchronizes changes made to
username and password with the properties file. If you go to the Users/Federation page of the admin console you will
see this provider listed under "file-properties". To configure this provider you specify a fully qualified file path to
a properties file in the "path" field of the admin page for this plugin.

View file

@ -27,10 +27,12 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
@Override
public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) {
// first get the path to our properties file from the stored configuration of this provider instance.
String path = model.getConfig().get("path");
if (path == null) {
throw new IllegalStateException("Path attribute not configured for provider");
}
// see if we already loaded the config file
Properties props = files.get(path);
if (props != null) return createProvider(session, model, props);
@ -43,6 +45,7 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
} catch (IOException e) {
throw new RuntimeException(e);
}
// remember the properties file for next time
files.put(path, props);
return createProvider(session, model, props);
}
@ -51,7 +54,12 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
protected abstract BasePropertiesFederationProvider createProvider(KeycloakSession session, UserFederationProviderModel model, Properties props);
/**
* List the configuration options to render and display in the admin console's generic management page for this
* plugin
*
* @return
*/
@Override
public Set<String> getConfigurationOptions() {
return configOptions;
@ -62,6 +70,11 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
return null;
}
/**
* You can import additional plugin configuration from keycloak-server.json here.
*
* @param config
*/
@Override
public void init(Config.Scope config) {

View file

@ -32,10 +32,6 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
this.properties = properties;
}
public static Set<String> getSupportedCredentialTypes() {
return supportedCredentialTypes;
}
static
{
supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
@ -71,12 +67,22 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
return null;
}
/**
* We only search for Usernames as that is all that is stored in the properties file. Not that if the user
* does exist in the properties file, we only import it if the user hasn't been imported already.
*
* @param attributes
* @param realm
* @param maxResults
* @return
*/
@Override
public List<UserModel> searchByAttributes(Map<String, String> attributes, RealmModel realm, int maxResults) {
String username = attributes.get(USERNAME);
if (username != null) {
// make sure user isn't already in storage
if (session.userStorage().getUserByUsername(username, realm) == null) {
// user is not already imported, so let's import it until local storage.
UserModel user = getUserByUsername(realm, username);
if (user != null) {
List<UserModel> list = new ArrayList<UserModel>(1);
@ -90,19 +96,32 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
@Override
public void preRemove(RealmModel realm) {
// complete We don't care about the realm being removed
}
@Override
public void preRemove(RealmModel realm, RoleModel role) {
// complete we dont'care if a role is removed
}
/**
* See if the user is still in the properties file
*
* @param local
* @return
*/
@Override
public boolean isValid(UserModel local) {
return properties.containsKey(local.getUsername());
}
/**
* hardcoded to only return PASSWORD
*
* @param user
* @return
*/
@Override
public Set<String> getSupportedCredentialTypes(UserModel user) {
return supportedCredentialTypes;

View file

@ -26,21 +26,43 @@ public class ClasspathPropertiesFederationProvider extends BasePropertiesFederat
super(session, model, properties);
}
/**
* Keycloak will call this method if it finds an imported UserModel. Here we proxy the UserModel with
* a Readonly proxy which will barf if password is updated.
*
* @param local
* @return
*/
@Override
public UserModel proxy(UserModel local) {
return new ReadonlyUserModelProxy(local);
}
/**
* The properties file is readonly so don't suppport registration.
*
* @return
*/
@Override
public boolean synchronizeRegistrations() {
return false;
}
/**
* The properties file is readonly so don't suppport registration.
*
* @return
*/
@Override
public UserModel register(RealmModel realm, UserModel user) {
throw new IllegalStateException("Registration not supported");
}
/**
* The properties file is readonly so don't removing a user
*
* @return
*/
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
throw new IllegalStateException("Remove not supported");

View file

@ -27,8 +27,12 @@ public class FilePropertiesFederationFactory extends BasePropertiesFederationFac
}
/**
* Name of the provider. This will show up under the "Add Provider" select box on the Federation page in the
* admin console
*
* @return
*/
@Override
public String getId() {
return "file-properties";

View file

@ -28,11 +28,23 @@ public class FilePropertiesFederationProvider extends BasePropertiesFederationPr
super(session, model, properties);
}
/**
* Keycloak will call this method if it finds an imported UserModel. Here we proxy the UserModel with
* a Writable proxy which will synchronize updates to username and password back to the properties file
*
* @param local
* @return
*/
@Override
public UserModel proxy(UserModel local) {
return new WritableUserModelProxy(local, this);
}
/**
* Adding new users is supported
*
* @return
*/
@Override
public boolean synchronizeRegistrations() {
return true;
@ -49,6 +61,13 @@ public class FilePropertiesFederationProvider extends BasePropertiesFederationPr
}
}
/**
* Update the properties file with the new user.
*
* @param realm
* @param user
* @return
*/
@Override
public UserModel register(RealmModel realm, UserModel user) {
synchronized (properties) {

View file

@ -6,6 +6,8 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
/**
* Readonly proxy for a UserModel that prevents passwords from being updated.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/

View file

@ -10,6 +10,8 @@ import java.io.IOException;
import java.util.Properties;
/**
* Proxy that will synchronize password updates to the properties file.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@ -21,6 +23,13 @@ public class WritableUserModelProxy extends UserModelDelegate {
this.provider = provider;
}
/**
* Updates the properties file if the username changes. If you have a more complex user storage, you can
* override other methods on UserModel to synchronize updates back to your external storage.
*
* @param username
*/
@Override
public void setUsername(String username) {
if (delegate.getUsername().equals(username)) return;

View file

@ -60,6 +60,14 @@ public interface UserFederationProvider extends Provider {
* @return
*/
boolean synchronizeRegistrations();
/**
* Called if this federation provider has priority and supports synchronized registrations.
*
* @param realm
* @param user
* @return
*/
UserModel register(RealmModel realm, UserModel user);
boolean removeUser(RealmModel realm, UserModel user);
@ -91,7 +99,19 @@ public interface UserFederationProvider extends Provider {
*/
List<UserModel> searchByAttributes(Map<String, String> attributes, RealmModel realm, int maxResults);
/**
* called whenever a Realm is removed
*
* @param realm
*/
void preRemove(RealmModel realm);
/**
* called before a role is removed.
*
* @param realm
* @param role
*/
void preRemove(RealmModel realm, RoleModel role);
/**
@ -111,7 +131,8 @@ public interface UserFederationProvider extends Provider {
Set<String> getSupportedCredentialTypes(UserModel user);
/**
* Validate credentials for this user.
* Validate credentials for this user. This method will only be called with credential parameters supported
* by this provider
*
* @param realm
* @param user

View file

@ -10,6 +10,13 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public interface UserFederationProviderFactory extends ProviderFactory<UserFederationProvider> {
/**
* called per Keycloak transaction.
*
* @param session
* @param model
* @return
*/
UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model);
/**
@ -18,4 +25,12 @@ public interface UserFederationProviderFactory extends ProviderFactory<UserFeder
* @return
*/
Set<String> getConfigurationOptions();
/**
* This is the name of the provider and will be showed in the admin console as an option.
*
* @return
*/
@Override
String getId();
}

View file

@ -4,6 +4,8 @@ import java.util.HashMap;
import java.util.Map;
/**
* Stored configuration of a User Federation provider instance.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/