commit
6b82f0868c
13 changed files with 1281 additions and 1265 deletions
11
SUMMARY.adoc
11
SUMMARY.adoc
|
@ -11,3 +11,14 @@
|
|||
. link:topics/events.adoc[Event Listener SPI]
|
||||
{% endif %}
|
||||
. link:topics/user-storage.adoc[User Storage SPI]
|
||||
.. link:topics/user-storage/provider-interfaces.adoc[Provider Interfaces]
|
||||
.. link:topics/user-storage/provider-capability-interfaces.adoc[Provider Capability Interfaces]
|
||||
.. link:topics/user-storage/model-interfaces.adoc[Model Interfaces]
|
||||
.. link:topics/user-storage/packaging.adoc[Packaging and Deployment]
|
||||
.. link:topics/user-storage/simple-example.adoc[Simple Read Only, Lookup Example]
|
||||
.. link:topics/user-storage/configuration.adoc[Configuration Techniques]
|
||||
.. link:topics/user-storage/registration-query.adoc[Add/Remove User and Query Capability interfaces]
|
||||
.. link:topics/user-storage/augmenting.adoc[Augmenting External Storage]
|
||||
.. link:topics/user-storage/import.adoc[Import Implementation Strategy]
|
||||
.. link:topics/user-storage/cache.adoc[User Caches]
|
||||
.. link:topics/user-storage/javaee.adoc[Leveraging Java EE ]
|
||||
|
|
File diff suppressed because it is too large
Load diff
69
topics/user-storage/augmenting.adoc
Normal file
69
topics/user-storage/augmenting.adoc
Normal file
|
@ -0,0 +1,69 @@
|
|||
|
||||
=== Augmenting External Storage
|
||||
|
||||
The `PropertyProfileUserStorageProvider` example is really limited. While we will be able to login with users stored
|
||||
in a property file, we won't be able to do much else. If users loaded by this provider need special role or group
|
||||
mappings to fully access particular applications there is no way for us to add additional role mappings to these users.
|
||||
You also can't modify or add additional important attributes like email, first and last name.
|
||||
|
||||
For these types of situations, {{book.project.name}} allows you to augment your external store by storing extra information
|
||||
in {{book.project.name}}'s database. This is called federated user storage and is encapsulated within the
|
||||
`org.keycloak.storage.federated.UserFederatedStorageProvider` class.
|
||||
|
||||
.UserFederatedStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
package org.keycloak.storage.federated;
|
||||
|
||||
public interface UserFederatedStorageProvider extends Provider {
|
||||
|
||||
Set<GroupModel> getGroups(RealmModel realm, String userId);
|
||||
void joinGroup(RealmModel realm, String userId, GroupModel group);
|
||||
void leaveGroup(RealmModel realm, String userId, GroupModel group);
|
||||
List<String> getMembership(RealmModel realm, GroupModel group, int firstResult, int max);
|
||||
|
||||
...
|
||||
|
||||
----
|
||||
|
||||
The `UserFederatedStorageProvider` instance is available on the `KeycloakSession.userFederatedStorage()` method.
|
||||
It has all different kinds of methods for storing attributes, group and role mappings, different credential types,
|
||||
and required actions. If your external store's datamodel cannot support the full {{book.project.name}} feature
|
||||
set, then this service can fill in the gaps.
|
||||
|
||||
{{book.project.name}} comes with a helper class `org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage`
|
||||
that will delegate every single `UserModel` method except get/set of username to user federated storage. Override
|
||||
the methods you need to override to delegate to your external storage representations. It is strongly
|
||||
suggested you read the javadoc of this class as it has smaller protected methods you may want to override. Specifically
|
||||
surrounding group membership and role mappings.
|
||||
|
||||
==== Augmentation Example
|
||||
|
||||
In our `PropertyFileUserStorageProvider` example, we just need a simple change to our provider to use the
|
||||
`AbstractUserAdapterFederatedStorage`.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
protected UserModel createAdapter(RealmModel realm, String username) {
|
||||
return new AbstractUserAdapterFederatedStorage(session, realm, model) {
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsername(String username) {
|
||||
String pw = (String)properties.remove(username);
|
||||
if (pw != null) {
|
||||
properties.put(username, pw);
|
||||
save();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
----
|
||||
|
||||
We instead define an anonymous class implementation of `AbstractUserAdapterFederatedStorage`. The `setUsername()`
|
||||
method makes changes to the properties file and saves it.
|
||||
|
107
topics/user-storage/cache.adoc
Normal file
107
topics/user-storage/cache.adoc
Normal file
|
@ -0,0 +1,107 @@
|
|||
|
||||
=== User Caches
|
||||
|
||||
When a user is loaded by id, username, or email queries it will be cached. When a user is cached, it iterates through
|
||||
the entire `UserModel` interface and pulls this information to a local in-memory only cache. In a cluster, this cache
|
||||
is still local, but it becomes an invalidation cache. When a user is modified, it is evicted. This eviction event
|
||||
is propagated to the entire cluster so that other nodes' user cache is also invalidated.
|
||||
|
||||
==== Managing the user cache
|
||||
|
||||
You can get access to the user cache by calling `KeycloakSession.userCache()`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
/**
|
||||
* All these methods effect an entire cluster of Keycloak instances.
|
||||
*
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface UserCache extends UserProvider {
|
||||
/**
|
||||
* Evict user from cache.
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
void evict(RealmModel realm, UserModel user);
|
||||
|
||||
/**
|
||||
* Evict users of a specific realm
|
||||
*
|
||||
* @param realm
|
||||
*/
|
||||
void evict(RealmModel realm);
|
||||
|
||||
/**
|
||||
* Clear cache entirely.
|
||||
*
|
||||
*/
|
||||
void clear();
|
||||
}
|
||||
----
|
||||
|
||||
There are methods for evicting a specific users, users contained in a specific realm, or the entire cache.
|
||||
|
||||
==== OnUserCache Callback Interface
|
||||
|
||||
You may want to cache additional information that is specific to your provider implementation. The User Storage SPI
|
||||
has a callback whenever a user is cached: `org.keycloak.models.cache.OnUserCache`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public interface OnUserCache {
|
||||
void onCache(RealmModel realm, CachedUserModel user, UserModel delegate);
|
||||
}
|
||||
----
|
||||
|
||||
Your provider class should implement this interface if it wants this callback. The `UserModel` delegate parameter
|
||||
is the `UserModel` instance returned by your provider. The `CachedUserModel` is an expanded `UserModel` interface.
|
||||
This is the instance that is cached locally in local storage.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public interface CachedUserModel extends UserModel {
|
||||
|
||||
/**
|
||||
* Invalidates the cache for this user and returns a delegate that represents the actual data provider
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
UserModel getDelegateForUpdate();
|
||||
|
||||
boolean isMarkedForEviction();
|
||||
|
||||
/**
|
||||
* Invalidate the cache for this model
|
||||
*
|
||||
*/
|
||||
void invalidate();
|
||||
|
||||
/**
|
||||
* When was the model was loaded from database.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
long getCacheTimestamp();
|
||||
|
||||
/**
|
||||
* Returns a map that contains custom things that are cached along with this model. You can write to this map.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
ConcurrentHashMap getCachedWith();
|
||||
}
|
||||
----
|
||||
|
||||
This `CachedUserModel` interface allows you to evict the user from cache and get the provider `UserModel` instance.
|
||||
The most interesting method is `getCachedWith()`. This returns a map that allows you to cache additional information
|
||||
pertaining to the user. For example, credentials are not part of the `UserModel` interface. If you wanted to cache
|
||||
credentials in memory, you would implement `OnUserCache` and cache your user's credentials using the `getCachedWith()`
|
||||
method.
|
||||
|
||||
==== Cache Policies
|
||||
|
||||
Each configured user storage provider can specify unique cache policies. Go to the admin console management page
|
||||
for your provider to see how to do this.
|
||||
|
131
topics/user-storage/configuration.adoc
Normal file
131
topics/user-storage/configuration.adoc
Normal file
|
@ -0,0 +1,131 @@
|
|||
|
||||
=== Configuration Techniques
|
||||
|
||||
Our `PropertyFileUserStorageProvider` example is bit contrived. It is hardcoded to a property file that is embedded
|
||||
in the jar of the provider. Not very useful at all. We may want to make the location of this file configurable per
|
||||
instance of the provider. In other words, we may want to re-use this provider mulitple times in multiple different realms
|
||||
and point to completely different user property files. We'll also want to do this configuration within the admin
|
||||
console UI.
|
||||
|
||||
The `UserStorageProviderFactory` has additional methods you can implement that deal with provider configuration.
|
||||
You describe the variables you want to configure per provider and the admin console will automatically render
|
||||
a generic input page to gather this configuration. There's also callback methods to validate configuration
|
||||
before it is saved, when a provider is created for the first time, and when it is updated. `UserStorageProviderFactory`
|
||||
inherits these methods from the `org.keycloak.component.ComponentFactory` interface.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
List<ProviderConfigProperty> getConfigProperties();
|
||||
|
||||
default
|
||||
void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
|
||||
throws ComponentValidationException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
default
|
||||
void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
|
||||
|
||||
}
|
||||
|
||||
default
|
||||
void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel model) {
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
The `ComponentFactory.getConfigProperties()` method returns a list of `org.keycloak.provider.ProviderConfigProperty`
|
||||
instances. These instances declare metadata that is needed to render and store each configuration variable of the
|
||||
provider.
|
||||
|
||||
==== Configuration Example
|
||||
|
||||
Let's expand our `PropertyFileUserStorageProviderFactory` example to allow you to to point a provider instance to a specific
|
||||
file on disk.
|
||||
|
||||
.PropertyFileUserStorageProviderFactory
|
||||
[source,java]
|
||||
----
|
||||
public class PropertyFileUserStorageProviderFactory
|
||||
implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {
|
||||
|
||||
protected static final List<ProviderConfigProperty> configMetadata;
|
||||
|
||||
static {
|
||||
configMetadata = ProviderConfigurationBuilder.create()
|
||||
.property().name("path")
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.label("Path")
|
||||
.defaultValue("${jboss.server.config.dir}/example-users.properties")
|
||||
.helpText("File path to properties file")
|
||||
.default
|
||||
.add().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configMetadata;
|
||||
}
|
||||
----
|
||||
|
||||
The `ProviderConfigurationBuilder` class is a great helper class to create a list of configuration properties. Here
|
||||
we specify a variable named `path` that is a string type. In the admin console config page for this provider,
|
||||
this config variable will be labed as `Path` and have a default value of `${jboss.server.config.dir}/example-users.properties`.
|
||||
When you hover over the tooltip of this config option, it will display the help text `File path to properties file`.
|
||||
|
||||
The next thing we want to do is to verify that this file exists on disk. We don't want to enable an instance of this
|
||||
provider in the realm unless it points to a valid user property file. To do this we implement the `validateConfiguration()`
|
||||
method.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config)
|
||||
throws ComponentValidationException {
|
||||
String fp = config.getConfig().getFirst("path");
|
||||
if (fp == null) throw new ComponentValidationException("user property file does not exist");
|
||||
fp = EnvUtil.replace(fp);
|
||||
File file = new File(fp);
|
||||
if (!file.exists()) {
|
||||
throw new ComponentValidationException("user property file does not exist");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In the `validateConfiguration()` method we get the config variable from the `ComponentModel` and we check to see
|
||||
if that file exists on disk. Notice that we use the `org.keycloak.common.util.EnvUtil.replace()`method. With this method
|
||||
any string that has `${}` within it will replace that with a system property value. The `${jboss.server.config.dir}`
|
||||
string corresponds to the `configuration/` directory of our server and is really useful for this example.
|
||||
|
||||
Next thing we have to do is remove the old `init()` method. We do this because user property files are going to be
|
||||
unique per provider instance. We move this logic to the `create()` method.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
|
||||
String path = model.getConfig().getFirst("path");
|
||||
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
InputStream is = new FileInputStream(path);
|
||||
props.load(is);
|
||||
is.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return new PropertyFileUserStorageProvider(session, model, props);
|
||||
}
|
||||
----
|
||||
|
||||
This logic, is of course, is really inefficient as every different transaction will read the entire user property file from disk,
|
||||
but hopefully this illustrates, in a simple way, how to hook in configuration variables.
|
||||
|
||||
==== Configure in Admin Console
|
||||
|
||||
Now that the configuration is enabled, you can set the `path` variable when you configure the provider in the admin console.
|
||||
|
||||
.Configured Provider
|
||||
image:../../{{book.images}}/storage-provider-with-config.png[]
|
119
topics/user-storage/import.adoc
Normal file
119
topics/user-storage/import.adoc
Normal file
|
@ -0,0 +1,119 @@
|
|||
|
||||
=== Import Implementation Strategy
|
||||
|
||||
When implementing a user storage provider, there's another strategy you can take. Instead of using user federated storage,
|
||||
you can create a user locally in the {{book.project.name}} built in user database and copy attributes from your external
|
||||
store into this local copy. There are a bunch of advantages to this approach.
|
||||
|
||||
* {{book.project.name}} basically becomes a persistence user cache for your external store. Once the user is imported
|
||||
you'll no longer hit the external store thus taking load off of it.
|
||||
* If you are moving to {{book.project.name}} as your official user store and deprecating the old external store, you
|
||||
can slowly migrate applications to use {{book.project.name}}. When all applications have been migrated, unlink the
|
||||
imported user, and retire the old legacy external store.
|
||||
|
||||
There are some obvious disadvantages though to using an import strategy:
|
||||
|
||||
* Looking up a user for the first time will require multiple updates to {{book.project.name}} database. This can
|
||||
be a big performance loss under load and put a lot of strain on the {{book.project.name}} database. The user federated
|
||||
storage approach will only store extra data as needed and may never be used depending on the capabilities of your external store.
|
||||
* With the import approach, you have to keep local keycloak storage and external storage in sync. The User Storage SPI
|
||||
has capability interfaces that you can implement to support synchronization, but this can quickly become painful and messy.
|
||||
|
||||
To implement the import strategy you simply check to see first if the user has been imported locally. If so return the
|
||||
local user, if not create the user locally and import data from the external store. You can also proxy the local user
|
||||
so that most changes are automatically synchronized.
|
||||
|
||||
This will be a bit contrived, but we can extend our `PropertyFileUserStorageProvider` to take this approach. We
|
||||
begin first by modifying the `createAdapter()` method.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source.java]
|
||||
----
|
||||
protected UserModel createAdapter(RealmModel realm, String username) {
|
||||
UserModel local = session.userLocalStorage().getUserByUsername(username, realm);
|
||||
if (local == null) {
|
||||
local = session.userLocalStorage().addUser(realm, username);
|
||||
local.setFederationLink(model.getId());
|
||||
}
|
||||
return new UserModelDelegate(local) {
|
||||
@Override
|
||||
public void setUsername(String username) {
|
||||
String pw = (String)properties.remove(username);
|
||||
if (pw != null) {
|
||||
properties.put(username, pw);
|
||||
save();
|
||||
}
|
||||
super.setUsername(username);
|
||||
}
|
||||
};
|
||||
}
|
||||
----
|
||||
|
||||
In this method we call the `KeycloakSession.userLocalStorage()` method to obtain a reference to local {{book.project.name}}
|
||||
user storage. We see if the user is stored locally, if not, we add it locally. Also note that we call
|
||||
`UserModel.setFederationLink()` and pass in the id of the `ComponentModel` of our provider. This sets a link between
|
||||
the provider and the imported user.
|
||||
|
||||
NOTE: When a user storage provider is removed, any user imported by it will also be removed. This is one of the
|
||||
purposes of calling `UserModel.setFederationLink()`.
|
||||
|
||||
Another thing to note is that if a local user is linked, your storage provider will still be delegated to for methods
|
||||
that it implements from the `CredentialInputValidator` and `CredentialInputUpdater` interfaces. Returning `false`
|
||||
from a validation or update will just result in {{book.project.name}} seeing if it can validate or update using
|
||||
local storage.
|
||||
|
||||
Also notice that we are proxying the local user using the `org.keycloak.models.utils.UserModelDelegate' class.
|
||||
This class is an implementation of `UserModel`. Every method just delegates to the `UserModel` it was instantiated with.
|
||||
We override the `setUsername()` method of this delegate class to synchronize automatically with the property file.
|
||||
For your providers, you can use this to _intercept_ other methods on the local `UserModel` to perform synchronization
|
||||
with your extern store. For example, get methods could make sure that the local store is in sync. Set methods
|
||||
keep external store in sync with local one.
|
||||
|
||||
==== ImportedUserValidation Interface
|
||||
|
||||
If you remember earlier in this chapter, we discussed how querying for a user worked. Local storage is queried first,
|
||||
if the user is found there, then the query ends. This is a problem for our above implementation as we want
|
||||
to proxy the local `UserModel` so that we can keep usernames in sync. The User Storage SPI has a callback for whenever
|
||||
a linked local user is loaded from the local database.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
package org.keycloak.storage.user;
|
||||
public interface ImportedUserValidation {
|
||||
/**
|
||||
* If this method returns null, then the user in local storage will be removed
|
||||
*
|
||||
* @param realm
|
||||
* @param user
|
||||
* @return null if user no longer valid
|
||||
*/
|
||||
UserModel validate(RealmModel realm, UserModel user);
|
||||
}
|
||||
----
|
||||
|
||||
Whenever a linked local user is loaded, if the user storage provider class implements this interface, then the
|
||||
`validate()` method is called. Here you can proxy the local user passed in as a parameter and return it. That
|
||||
new `UserModel` will be used. You can also optionally do a check to see if the user exists still in the external store.
|
||||
if `validate()` returns `null`, then the local user will be removed from the database.
|
||||
|
||||
==== ImportSynchronization Interface
|
||||
|
||||
With the import strategy you can see that it would be possible for the local user copy could get out of sync with
|
||||
external storage. For example, maybe a user has been removed from the external store. The User Storage SPI has
|
||||
an additional interface you can implement to deal with this. `org.keycloak.storage.user.ImportSynchronization`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
package org.keycloak.storage.user;
|
||||
|
||||
public interface ImportSynchronization {
|
||||
SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
|
||||
SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
|
||||
}
|
||||
----
|
||||
|
||||
This interface is implemented by the provider factory. Once this interface is implemented by the provider factory,
|
||||
the admin console management page for the provider will show additional options. There is a button that will allow
|
||||
you to manually force a synchronization. This invokes the `ImportSynchronization.sync()` method. Also, some additional
|
||||
configuration options will show up that allow you to automatically schedule a synchronization. Automatic syncs invoke
|
||||
the `syncSince()` method.
|
78
topics/user-storage/javaee.adoc
Normal file
78
topics/user-storage/javaee.adoc
Normal file
|
@ -0,0 +1,78 @@
|
|||
|
||||
=== Leveraging Java EE
|
||||
|
||||
The user storage providers can be packaged within any Java EE component so long as you set up the `META-INF/services`
|
||||
file correctly to point to your providers. For example, if your provider needs to use third party libraries, you
|
||||
can package up your provider within an ear and store these third pary libraries in the ear's `lib/` directory.
|
||||
Also note that provider jars can make use of the `jboss-deployment-structure.xml` file that EJBs, WARS, and EARs
|
||||
can use in a JBoss/Wildfly environment. See the JBoss/Wildfly documentation for more details on this file. It
|
||||
allows you to pull in external dependencies among other fine grain actions.
|
||||
|
||||
Implementations of `UserStorageProviderFactory` are required to be plain java objects. But, we also currently support
|
||||
implementing `UserStorageProvider` classes as Stateful EJBs. This is especially useful if you want to use JPA
|
||||
to connect to a relational store. This is how you would do it:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Stateful
|
||||
@Local(EjbExampleUserStorageProvider.class)
|
||||
public class EjbExampleUserStorageProvider implements UserStorageProvider,
|
||||
UserLookupProvider,
|
||||
UserRegistrationProvider,
|
||||
UserQueryProvider,
|
||||
CredentialInputUpdater,
|
||||
CredentialInputValidator,
|
||||
OnUserCache
|
||||
{
|
||||
@PersistenceContext
|
||||
protected EntityManager em;
|
||||
|
||||
protected ComponentModel model;
|
||||
protected KeycloakSession session;
|
||||
|
||||
public void setModel(ComponentModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public void setSession(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
|
||||
@Remove
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
You have to define the `@Local` annotation and specify your provider class there. If you don't do this, EJB will
|
||||
not proxy the user correctly and your provider won't work.
|
||||
|
||||
You must put the `@Remove` annotation on the `close()` method of your provider. If you don't, the stateful bean
|
||||
will never be cleaned up and you may eventually see error messages.
|
||||
|
||||
Implementations of `UserStorageProviderFactory` are required to be plain java objects. Your factory class would
|
||||
perform a JNDI lookup of the Stateful EJB in its create() method.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class EjbExampleUserStorageProviderFactory
|
||||
implements UserStorageProviderFactory<EjbExampleUserStorageProvider> {
|
||||
|
||||
@Override
|
||||
public EjbExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) {
|
||||
try {
|
||||
InitialContext ctx = new InitialContext();
|
||||
EjbExampleUserStorageProvider provider = (EjbExampleUserStorageProvider)ctx.lookup(
|
||||
"java:global/user-storage-jpa-example/" + EjbExampleUserStorageProvider.class.getSimpleName());
|
||||
provider.setModel(model);
|
||||
provider.setSession(session);
|
||||
return provider;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
59
topics/user-storage/model-interfaces.adoc
Normal file
59
topics/user-storage/model-interfaces.adoc
Normal file
|
@ -0,0 +1,59 @@
|
|||
|
||||
=== Model Interfaces
|
||||
|
||||
Most of the methods defined in the _capability_ _interfaces_ either return or are passed in representations of a user. These representations are defined
|
||||
by the `org.keycloak.models.UserModel` interface. App developers are required to implement this interface. It provides
|
||||
a mapping between the external user store and the user metamodel that {{book.project.name}} uses.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
package org.keycloak.models;
|
||||
|
||||
public interface UserModel extends RoleMapperModel {
|
||||
String getId();
|
||||
|
||||
String getUsername();
|
||||
void setUsername(String username);
|
||||
|
||||
String getFirstName();
|
||||
void setFirstName(String firstName);
|
||||
|
||||
String getLastName();
|
||||
void setLastName(String lastName);
|
||||
|
||||
String getEmail();
|
||||
void setEmail(String email);
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
`UserModel` implementations provide access to read and update metadata about the user including things like username, name, email,
|
||||
role and group mappings, as well as other arbitrary attributes.
|
||||
|
||||
There are other model classes within the `org.keycloak.models` package the represent other parts of the {{book.project.name}}
|
||||
metamodel: `RealmModel`, `RoleModel`, `GroupModel`, and `ClientModel`.
|
||||
|
||||
==== Storage Ids
|
||||
|
||||
One really import method of `UserModel` is the `getId()` method. When implementing `UserModel` developers must be aware
|
||||
of the user id format. The format must be
|
||||
|
||||
----
|
||||
"f:" + component id + ":" + external id
|
||||
----
|
||||
|
||||
The {{book.project.name}} runtime often has to lookup users by their user id. The user id contains enough information
|
||||
so that the runtime does not have to query every single `UserStorageProvider` in the system to find the user.
|
||||
|
||||
The component id is the id returned from `ComponentModel.getId()`. The `ComponentModel` is passed in as a parameter
|
||||
when creating the provider class so you can get it from there. The external id is information your provider class
|
||||
needs to find the user in the external store. This is often a username or a uid. For example, it might look something
|
||||
like this:
|
||||
|
||||
----
|
||||
f:332a234e31234:wburke
|
||||
----
|
||||
|
||||
When the runtime does a lookup by id, the id is parsed to obtain the component id. The component id is used to
|
||||
locate the `UserStorageProvider` that was originally used to load the user. That provider is then passed the id.
|
||||
The provider again parses the id to obtain the external id it will use to locate the user in external user storage.
|
17
topics/user-storage/packaging.adoc
Normal file
17
topics/user-storage/packaging.adoc
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
=== Packaging and Deployment
|
||||
|
||||
User Storage providers are packaged in a jar and deployed or undeployed to the {{book.project.name}} runtime in the same exact
|
||||
way as you would deploy something in the JBoss/Wildfly application server. You can either copy the jar directly to
|
||||
the `deploy/` directory if the server, or use the JBoss CLI to execute the deployment. In order for {{book.project.name}}
|
||||
to recognize the provider, there's one special file you need to add to the jar: `META-INF/services/org.keycloak.storage.UserStorageProviderFactory`.
|
||||
This file must contain a line separated list of fully qualified classnames of use `UserStorageProviderFactory` implementation.
|
||||
|
||||
----
|
||||
org.keycloak.examples.federation.properties.ClasspathPropertiesStorageFactory
|
||||
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
|
||||
----
|
||||
|
||||
{{book.project.name}} supports hot deployment of these provider jars. You'll also see later in this chapter that you can
|
||||
package within and as Java EE components.
|
||||
|
21
topics/user-storage/provider-capability-interfaces.adoc
Normal file
21
topics/user-storage/provider-capability-interfaces.adoc
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
=== Provider Capability Interfaces
|
||||
|
||||
If you've examined the `UserStorageProvider` interface closely you may be scratching your head a bit because it does
|
||||
not define any methods for locating or managing users. These methods are actually defined in other _capability_
|
||||
_interfaces_ depending on what scope of capabilities your external user store can provide and execute on. For example,
|
||||
some external stores are read only and can only do simple queries and credential validation. You will only be required to implement the
|
||||
_capability_ _interfaces_ for the features you are able to. Here's a list of interfaces that you can implement:
|
||||
|
||||
|
||||
|===
|
||||
|SPI|Description
|
||||
|
||||
|`org.keycloak.storage.user.UserLookupProvider`|This interface is required if you want to be able to login with users from this external store. Most (all?) providers implement this interface.
|
||||
|`org.keycloak.storage.user.UserQueryProvider`|Defines complex queries that are used to locate one or more users. You must implement this interface if you want to view and manager users from the administration console.
|
||||
|`org.keycloak.storage.user.UserRegistrationProvider`|Implement this interface if your provider supports adding and removing users.
|
||||
|`org.keycloak.storage.user.UserBulkUpdateProvider`|Implement this interface if your provider supports bulk update of a set of users.
|
||||
|`org.keycloak.credential.CredentialInputValidator`|Implement this interface if your provider can validate one or more different credential types. (i.e. can validate a password)
|
||||
|`org.keycloak.credential.CredentialInputUpdater`|Implement this interface if your provider supports updating one more different credential types.
|
||||
|===
|
||||
|
117
topics/user-storage/provider-interfaces.adoc
Normal file
117
topics/user-storage/provider-interfaces.adoc
Normal file
|
@ -0,0 +1,117 @@
|
|||
|
||||
=== Provider Interfaces
|
||||
|
||||
When building an implementation of the User Storage SPI you have to define a provider class and a provider factory.
|
||||
Provider class instances are created per transaction by provider factories.
|
||||
Provider classes do all the heavy lifting of user lookup and other user operations. They must implement the
|
||||
`org.keycloak.storage.UserStorageProvider` interface.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
package org.keycloak.storage;
|
||||
|
||||
public interface UserStorageProvider extends Provider {
|
||||
|
||||
|
||||
/**
|
||||
* Callback when a realm is removed. Implement this if, for example, you want to do some
|
||||
* cleanup in your user storage when a realm is removed
|
||||
*
|
||||
* @param realm
|
||||
*/
|
||||
default
|
||||
void preRemove(RealmModel realm) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when a group is removed. Allows you to do things like remove a user
|
||||
* group mapping in your external store if appropriate
|
||||
*
|
||||
* @param realm
|
||||
* @param group
|
||||
*/
|
||||
default
|
||||
void preRemove(RealmModel realm, GroupModel group) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when a role is removed. Allows you to do things like remove a user
|
||||
* role mapping in your external store if appropriate
|
||||
|
||||
* @param realm
|
||||
* @param role
|
||||
*/
|
||||
default
|
||||
void preRemove(RealmModel realm, RoleModel role) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
You may be thinking that the `UserStorageProvider` interface is pretty sparse? You'll see later in this chapter that
|
||||
there are other mix-in interfaces your provider class may implement to support the meat of user integration.
|
||||
|
||||
`UserStorageProvider` instances are created once per transaction. When the transaction is complete, the
|
||||
`UserStorageProvider.close()` method is invoked and the instance is then garbage collections. Instances are created
|
||||
by provider factories. Provider factories implement the `org.keycloak.storage.UserStorageProviderFactory` interface.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
package org.keycloak.storage;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface UserStorageProviderFactory<T extends UserStorageProvider> extends ComponentFactory<T, UserStorageProvider> {
|
||||
|
||||
/**
|
||||
* This is the name of the provider and will be showed in the admin console as an option.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* called per Keycloak transaction.
|
||||
*
|
||||
* @param session
|
||||
* @param model
|
||||
* @return
|
||||
*/
|
||||
T create(KeycloakSession session, ComponentModel model);
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
Provider factory classses must specify the concrete provider class as a template parameter when implementing the
|
||||
`UserStorageProviderFactory`. This is a must as the runtime will introspect this class to scan for its capabilities
|
||||
(the other interfaces it implements). So for example, if your provider class is named `FileProvider`, then the
|
||||
factory class should look like this:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class FileProviderFactory implements UserStorageProviderFactory<FileProvider> {
|
||||
|
||||
public String getId() { return "file-provider"; }
|
||||
|
||||
public FileProvider create(KeycloakSession session, ComponentModel model) {
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
The `getId()` method returns the name of the User Storage provider. This id will be displayed in the admin console's
|
||||
`UserFederation` page when you want to enable the provider for a specific realm.
|
||||
|
||||
The `create()` method is responsible for allocating an instance of the provider class. It takes a `org.keycloak.models.KeycloakSession`
|
||||
parameter. This object can be used to lookup other information and metadata as well as provide access to various other
|
||||
components within the runtime. The `ComponentModel` parameter represents how the provider was enabled and configured within
|
||||
a specific realm. It contains the instance id of the enabled provider as well as any configuration you may have specified
|
||||
for it when you enabled through the admin console.
|
||||
|
||||
The `UserStorageProviderFactory` has other capabilities as well which we will go over later in this chapter.
|
||||
|
226
topics/user-storage/registration-query.adoc
Normal file
226
topics/user-storage/registration-query.adoc
Normal file
|
@ -0,0 +1,226 @@
|
|||
|
||||
=== Add/Remove User and Query Capability interfaces
|
||||
|
||||
One thing we have not done with our example is allow it to add and remove users or change passwords. Users defined in our example are
|
||||
also not queryable or viewable in the admin console. To add these enhancements, our example provider must implement
|
||||
the `UserQueryProvider` and `UserRegistrationProvider` interfaces.
|
||||
|
||||
==== Implementing UserRegistrationProvider
|
||||
|
||||
To implement adding and removing users from this particular store, we first have to be able to save our properties
|
||||
file to disk.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
public void save() {
|
||||
String path = model.getConfig().getFirst("path");
|
||||
path = EnvUtil.replace(path);
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(path);
|
||||
properties.store(fos, "");
|
||||
fos.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Then, the implementation of the `addUser()` and `removeUser()` methods becomes pretty simple.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
public static final String UNSET_PASSWORD="#$!-UNSET-PASSWORD";
|
||||
|
||||
@Override
|
||||
public UserModel addUser(RealmModel realm, String username) {
|
||||
synchronized (properties) {
|
||||
properties.setProperty(username, UNSET_PASSWORD);
|
||||
save();
|
||||
}
|
||||
return createAdapter(realm, username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeUser(RealmModel realm, UserModel user) {
|
||||
synchronized (properties) {
|
||||
if (properties.remove(user.getUsername()) == null) return false;
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Notice that when adding a user we set the password value of the property map to be `UNSET_PASSWORD`. We do this as
|
||||
we can't have null values for a property in the property value. We also have to modify the `CredentialInputValidator`
|
||||
methods to reflect this.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
||||
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
|
||||
|
||||
UserCredentialModel cred = (UserCredentialModel)input;
|
||||
String password = properties.getProperty(user.getUsername());
|
||||
if (password == null || UNSET_PASSWORD.equals(password)) return false;
|
||||
return password.equals(cred.getValue());
|
||||
}
|
||||
----
|
||||
|
||||
Since we can now save our property file, probably also makes sense to allow password updates.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
|
||||
if (!(input instanceof UserCredentialModel)) return false;
|
||||
if (!input.getType().equals(CredentialModel.PASSWORD)) return false;
|
||||
UserCredentialModel cred = (UserCredentialModel)input;
|
||||
synchronized (properties) {
|
||||
properties.setProperty(user.getUsername(), cred.getValue());
|
||||
save();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
----
|
||||
|
||||
We can now also implement disabling a password too.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
|
||||
if (!credentialType.equals(CredentialModel.PASSWORD)) return;
|
||||
synchronized (properties) {
|
||||
properties.setProperty(user.getUsername(), UNSET_PASSWORD);
|
||||
save();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final Set<String> disableableTypes = new HashSet<>();
|
||||
|
||||
static {
|
||||
disableableTypes.add(CredentialModel.PASSWORD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
|
||||
|
||||
return disableableTypes;
|
||||
}
|
||||
----
|
||||
|
||||
With these methods implemented, you'll now be able to change and disable the password for the user in the admin console.
|
||||
|
||||
==== Implementing UserQueryProvider
|
||||
|
||||
Without implementing `UserQueryProvider` the admin console would not be able to view and manage users that were loaded
|
||||
by our example provider. Let's look at implementing this interface.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public int getUsersCount(RealmModel realm) {
|
||||
return properties.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> getUsers(RealmModel realm) {
|
||||
return getUsers(realm, 0, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
|
||||
List<UserModel> users = new LinkedList<>();
|
||||
int i = 0;
|
||||
for (Object obj : properties.keySet()) {
|
||||
if (i++ < firstResult) continue;
|
||||
String username = (String)obj;
|
||||
UserModel user = getUserByUsername(username, realm);
|
||||
users.add(user);
|
||||
if (users.size() >= maxResults) break;
|
||||
}
|
||||
return users;
|
||||
}
|
||||
----
|
||||
|
||||
The `getUser()` method simple iterates the key set of the property file delegating to `getuserByUsername` to load a user.
|
||||
Notice that we are indexing this call based on the `firstResult` and `maxResults` parameter. If your external store
|
||||
doesn't support pagination, you'll have to do similar logic.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public List<UserModel> searchForUser(String search, RealmModel realm) {
|
||||
return searchForUser(search, realm, 0, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
|
||||
List<UserModel> users = new LinkedList<>();
|
||||
int i = 0;
|
||||
for (Object obj : properties.keySet()) {
|
||||
String username = (String)obj;
|
||||
if (!username.contains(search)) continue;
|
||||
if (i++ < firstResult) continue;
|
||||
UserModel user = getUserByUsername(username, realm);
|
||||
users.add(user);
|
||||
if (users.size() >= maxResults) break;
|
||||
}
|
||||
return users;
|
||||
}
|
||||
----
|
||||
|
||||
The first declaration of `searchForUser()` takes a string paraeter. This is supposed to be a string that you use to
|
||||
search username and email attributes to find the user. This string can be a substring which is why we use the `String.contains()`
|
||||
method when doing our search.
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
|
||||
return searchForUser(params, realm, 0, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
|
||||
// only support searching by username
|
||||
String usernameSearchString = params.get("username");
|
||||
if (usernameSearchString == null) return Collections.EMPTY_LIST;
|
||||
return searchForUser(usernameSearchString, realm, firstResult, maxResults);
|
||||
}
|
||||
----
|
||||
|
||||
The `searchForUser()` method that takes a `Map` parameter can search for a user based on first, last name, username, and email.
|
||||
We only store usernames, so we only search based on usernames. We delegate to `searchForUser()` for this.
|
||||
|
||||
|
||||
.PropertyFileUserStorageProvider
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
----
|
||||
|
||||
We don't store and groups or attributes, so the other methods just return an empty list.
|
326
topics/user-storage/simple-example.adoc
Normal file
326
topics/user-storage/simple-example.adoc
Normal file
|
@ -0,0 +1,326 @@
|
|||
=== Simple Read Only, Lookup Example
|
||||
|
||||
To illustrate the basics of implementing the User Storage SPI let's walk through a simple example. In this chapter
|
||||
you'll see the implementation of a simple `UserStorageProvider` that looks up users in a simple property file. The
|
||||
property file contains username and password definitions and is hardcoded to a specific location on the classpath.
|
||||
The provider will be able to lookup the user by id, and username and also be able to validate passwords. Users that
|
||||
originate from this provider will be read only.
|
||||
|
||||
==== Provider Class
|
||||
|
||||
The first thing we will walk through is the `UserStorageProvider` class.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class PropertyFileUserStorageProvider implements
|
||||
UserStorageProvider,
|
||||
UserLookupProvider,
|
||||
CredentialInputValidator,
|
||||
CredentialInputUpdater
|
||||
{
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
Our provider class, `PropertyFileUserStorageProvider`, implements a bunch of interfaces. It implements the
|
||||
`UserStorageProvider` as that is a base requirement of the SPI. It implements the `UserLookupProvider` interface
|
||||
because we want to be able to login with users stored by this provider. It implements the `CredentialInputValidator`
|
||||
interface because we want to be able to validate passwords entered in via the login screen. Our property file
|
||||
is going to be read only. We implement the `CredentialInputUpdater` because was want to post an error condition
|
||||
when the user's password is attempted to be updated.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
protected KeycloakSession session;
|
||||
protected Properties properties;
|
||||
protected ComponentModel model;
|
||||
// map of loaded users in this transaction
|
||||
protected Map<String, UserModel> loadedUsers = new HashMap<>();
|
||||
|
||||
public PropertyFileUserStorageProvider(KeycloakSession session, ComponentModel model, Properties properties) {
|
||||
this.session = session;
|
||||
this.model = model;
|
||||
this.properties = properties;
|
||||
}
|
||||
----
|
||||
|
||||
The constructor for this provider class is going to store the reference to the `KeycloakSession`, `ComponentModel`, and
|
||||
property file. We'll use all of these later. Also notice that there is a map of loaded users. Whenever we find a user
|
||||
we will store it in this map so that we avoid recreating it again within the same transaction. This is a good practice
|
||||
to do as many providers will need to do this (i.e. one that integrates with JPA). Remember also that provider class
|
||||
instances are created once per transaction and are closed after the transaction completes.
|
||||
|
||||
===== UserLookupProvider implementation
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public UserModel getUserByUsername(String username, RealmModel realm) {
|
||||
UserModel adapter = loadedUsers.get(username);
|
||||
if (adapter == null) {
|
||||
String password = properties.getProperty(username);
|
||||
if (password != null) {
|
||||
adapter = createAdapter(realm, username);
|
||||
loadedUsers.put(username, adapter);
|
||||
}
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
|
||||
protected UserModel createAdapter(RealmModel realm, String username) {
|
||||
return new AbstractUserAdapter(session, realm, model) {
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getUserById(String id, RealmModel realm) {
|
||||
StorageId storageId = new StorageId(id);
|
||||
String username = storageId.getExternalId();
|
||||
return getUserByUsername(username, realm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getUserByEmail(String email, RealmModel realm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
----
|
||||
|
||||
The `getUserByUsername()` method is invoked by the {{book.project.name}} login page when a user logs in. In our
|
||||
implementation we first check the `loadedUsers` map to see if the user has already been loaded within this transaction.
|
||||
If it hasn't been loaded we look in the property file for the username. If it exists we create an implementation
|
||||
of `UserModel`, store it in `loadedUsers` for future reference and return this instance.
|
||||
|
||||
The `createAdapter()` method uses the helper class `org.keycloak.storage.adapter.AbstractUserAdapter`. This provides
|
||||
a base implementation for `UserModel`. It automatically generates a user id based on the required storage id format
|
||||
using the username of the user as the external id.
|
||||
|
||||
----
|
||||
"f:" + component id + ":" + username
|
||||
----
|
||||
|
||||
Every get method of `AbstractUserAdapter` either returns null or empty collections. However, methods that return
|
||||
role and group mappings will return the default roles and groups configured for the realm for every user. Every set
|
||||
method of `AbstractUserAdapter` will throw a `org.keycloak.storage.ReadOnlyException`. So if you attempt
|
||||
to modify the user in the admin console you will get an error.
|
||||
|
||||
The `getUserById()` method parses the `id` parameter using the `org.keycloak.storage.StorageId' helper class. The
|
||||
`StorageId.getExternalId()` method is invoked to obtain the username embeded in the `id` parameter. The method
|
||||
then delegates to `getUserByUsername()`.
|
||||
|
||||
Emails are not stored at all, so the `getUserByEmail() method
|
||||
|
||||
===== CredentialInputValidator implementation
|
||||
|
||||
Next let's look at the method implementations for `CredentialInputValidator`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
|
||||
String password = properties.getProperty(user.getUsername());
|
||||
return credentialType.equals(CredentialModel.PASSWORD) && password != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCredentialType(String credentialType) {
|
||||
return credentialType.equals(CredentialModel.PASSWORD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
|
||||
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
|
||||
|
||||
UserCredentialModel cred = (UserCredentialModel)input;
|
||||
String password = properties.getProperty(user.getUsername());
|
||||
if (password == null) return false;
|
||||
return password.equals(cred.getValue());
|
||||
}
|
||||
----
|
||||
|
||||
The `isConfiguredFor()` method is called by the runtime to determine if a specific credential type is configured for
|
||||
the user. This method checks to see that the password is set for the user.
|
||||
|
||||
The `suportsCredentialType()` method returns whether validation is supported for a specific credential type. We check
|
||||
to see if the credential type is `password`.
|
||||
|
||||
The `isValid()` method is responsible for validating passwords. The `CredentialInput` parameter is really just an abstract
|
||||
interface for all credential types. We make sure that we support the credential type and also that it is an instance
|
||||
of `UserCredentialModel`. When a user logs in through the login page, the plain text of the password input is put into
|
||||
an instance of `UserCredentialModel`. The `isValid()` method checks this value against the plain text password stored
|
||||
in the properties file. A return value of `true` means the password is valid.
|
||||
|
||||
===== CredentialInputUpdater implementation
|
||||
|
||||
As noted before, the only reason we implement the `CredentialInputUpdater` interface in this example is to forbid modifications of
|
||||
user passwords. The reason we have to do this is because otherwise the runtime would allow the password to be overriden
|
||||
in {{book.project.name}} local storage. We'll talk more about this later in this chapter
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
|
||||
if (input.getType().equals(CredentialModel.PASSWORD)) throw new ReadOnlyException("user is read only for this update");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getDisableableCredentialTypes(RealmModel realm, UserModel user) {
|
||||
return Collections.EMPTY_SET;
|
||||
}
|
||||
----
|
||||
|
||||
The `updateCredential()` method just checks to see if the credential type is password. If it is, a `ReadOnlyException`
|
||||
is thrown.
|
||||
|
||||
==== Provider Factory implementation
|
||||
|
||||
Now that the provider class is complete, we now turn our attention to the provider factory class.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public class PropertyFileUserStorageProviderFactory
|
||||
implements UserStorageProviderFactory<PropertyFileUserStorageProvider> {
|
||||
|
||||
public static final String PROVIDER_NAME = "readonly-property-file";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_NAME;
|
||||
}
|
||||
----
|
||||
|
||||
First thing to notice is that when implementing the `UserStorageProviderFactory` class, you must pass in the concrete
|
||||
provider class implementation as a template parameter. Here we specify the provider class we defined before: `PropertyFileUserStorageProvider`.
|
||||
|
||||
WARNING: If you do not specify the template parameter, your provider will not function. The runtime does class introspection
|
||||
to determine the _capability interfaces_ that the provider implements.
|
||||
|
||||
The `getId()` method identifies the factory in the runtime and will also be the string shown in the admin console when you want
|
||||
to enable a user storage provider for the realm.
|
||||
|
||||
===== Initialization
|
||||
|
||||
[source,java]
|
||||
----
|
||||
private static final Logger logger = Logger.getLogger(PropertyFileUserStorageProviderFactory.class);
|
||||
protected Properties properties = new Properties();
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
InputStream is = getClass().getClassLoader().getResourceAsStream("/users.properties");
|
||||
|
||||
if (is == null) {
|
||||
logger.warn("Could not find users.properties in classpath");
|
||||
} else {
|
||||
try {
|
||||
properties.load(is);
|
||||
} catch (IOException ex) {
|
||||
logger.error("Failed to load users.properties file", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new PropertyFileUserStorageProvider(session, model, properties);
|
||||
}
|
||||
----
|
||||
|
||||
The `UserStorageProviderFactory` interface has an optional `init()` method you can implement. When {{book.project.name}}
|
||||
boots up, one and only one instance of each different provider factory. Also at boot time, the `init()` method will
|
||||
be called on each one of these factory instances. There's also a `postInit()` method you can implement as well. After
|
||||
each factory's `init()` method is invoked, their `postInit()` methods will be called.
|
||||
|
||||
In our `init()` method implementation, we find the property file containing our user declarations from the classpath.
|
||||
We then load the `properties` field with the username and password combinations stored there.
|
||||
|
||||
The `Config.Scope` parameter is factory configuration that can be set up
|
||||
within `standalone.xml`, `standalone-ha.xml`, or `domain.xml`.
|
||||
See the link:{{book.installguide.link}}[{{book.installguide.name}}] for more details on
|
||||
where the `standalone.xml`, `standalone-ha.xml`, or `domain.xml` file lives.
|
||||
|
||||
For example by adding the following to `standalone.xml`:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<spi name="storage">
|
||||
<provider name="readonly-property-file" enabled="true">
|
||||
<properties>
|
||||
<property name="path" value="/other-users.properties"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
----
|
||||
|
||||
We can specify the classpath of the user property file instead of hard coded it.
|
||||
Then you can retrieve the config in the `PropertyFileUserStorageProviderFactory.init()` :
|
||||
|
||||
[source,java]
|
||||
----
|
||||
public void init(Config.Scope config) {
|
||||
String path = config.get("path");
|
||||
InputStream is = getClass().getClassLoader().getResourceAsStream(path);
|
||||
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
===== Create method
|
||||
|
||||
Our last step in creating the provider factory is the `create()` method.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Override
|
||||
public PropertyFileUserStorageProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new PropertyFileUserStorageProvider(session, model, properties);
|
||||
}
|
||||
----
|
||||
|
||||
We simply allocate the `PropertyFileUserStorageProvider` class. This create method will be called once per transaction.
|
||||
|
||||
==== Packaging and Deployment
|
||||
|
||||
The class files for our provider implementation should be placed in a jar. You also have to declare the provider
|
||||
factory class within the `META-INF/services/org.keycloak.storage.UserStorageProviderFactory` file.
|
||||
|
||||
----
|
||||
org.keycloak.examples.federation.properties.FilePropertiesStorageFactory
|
||||
----
|
||||
|
||||
Once you create the jar you can deploy it using regular JBoss/Wildfly means: copy the jar into the `deploy/` directory
|
||||
or using the JBoss CLI.
|
||||
|
||||
==== Enabling the Provider in Admin Console
|
||||
|
||||
You enable user storage providers per realm within the `User Federation` page in the admin console.
|
||||
|
||||
.User Federation
|
||||
image:../../{{book.images}}/empty-user-federation-page.png[]
|
||||
|
||||
Select the provider we just created from the list: `readonly-property-file`. It brings you to the configuration
|
||||
page for our provider. We don't have anything to configure, so just click the `Save` button.
|
||||
|
||||
.Configured Provider
|
||||
image:../../{{book.images}}/storage-provider-created.png[]
|
||||
|
||||
When you go back to the main `User Federation` page, you'll now see your provider listed.
|
||||
|
||||
.User Federation
|
||||
image:../../{{book.images}}/user-federation-page.png[]
|
||||
|
||||
You will now be able to login with a user declared in the `users.properties` file. Of course, this user will have
|
||||
zero permissions to do anything and will be read only. You can though view the user on its account page after you
|
||||
login.
|
Loading…
Reference in a new issue