keycloak-scim/topics/user-storage/simple-example.adoc

325 lines
13 KiB
Text

=== 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`.
For more information on where the `standalone.xml`, `standalone-ha.xml`, or `domain.xml` file resides see the link:{{book.project.doc_base_url}}{{book.project.doc_info_version_url}}{{book.installguide.link}}[{{book.installguide.name}}].
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.