Service Provider Interface (SPI) contracts and abstractions for the Keycloak identity and access management server enabling extensibility through custom providers
—
The credential management framework provides extensible support for various credential types including passwords, OTP tokens, WebAuthn, and custom credential types. It enables validation, storage, and lifecycle management of user credentials.
Base interface for credential providers that manage specific credential types.
public interface CredentialProvider extends Provider {
/**
* Gets the credential type handled by this provider.
*
* @return credential type string
*/
String getType();
/**
* Creates a credential for a user.
*
* @param realm the realm
* @param user the user
* @param credentialModel the credential to create
* @return created credential model
*/
CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel);
/**
* Deletes a credential by ID.
*
* @param realm the realm
* @param user the user
* @param credentialId the credential ID
* @return true if deleted successfully
*/
boolean deleteCredential(RealmModel realm, UserModel user, String credentialId);
/**
* Gets a credential from the stored model.
*
* @param model the stored credential model
* @return credential model
*/
CredentialModel getCredentialFromModel(CredentialModel model);
/**
* Gets the default credential for a user.
*
* @param realm the realm
* @param user the user
* @return default credential or null
*/
default CredentialModel getDefaultCredential(RealmModel realm, UserModel user) {
return null;
}
/**
* Checks if the credential type is configured for a user.
*
* @param realm the realm
* @param user the user
* @return true if configured
*/
default boolean isConfiguredFor(RealmModel realm, UserModel user) {
return false;
}
/**
* Gets metadata for credential types.
*
* @param user the user
* @return credential type metadata
*/
default CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) {
return CredentialTypeMetadata.builder()
.type(getType())
.category(CredentialTypeMetadata.Category.BASIC_AUTHENTICATION)
.helpText("credential." + getType() + ".helpText")
.build(session);
}
}Validates credential input for authentication.
public interface CredentialInputValidator {
/**
* Checks if this validator supports the credential type.
*
* @param credentialType the credential type
* @return true if supported
*/
boolean supportsCredentialType(String credentialType);
/**
* Checks if the credential type is configured for a user.
*
* @param realm the realm
* @param user the user
* @param credentialType the credential type
* @return true if configured
*/
boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType);
/**
* Validates credential input.
*
* @param realm the realm
* @param user the user
* @param input the credential input
* @return true if valid
*/
boolean isValid(RealmModel realm, UserModel user, CredentialInput input);
}Updates user credentials.
public interface CredentialInputUpdater {
/**
* Checks if this updater supports the credential type.
*
* @param credentialType the credential type
* @return true if supported
*/
boolean supportsCredentialType(String credentialType);
/**
* Updates a user's credential.
*
* @param realm the realm
* @param user the user
* @param input the credential input
* @return true if updated successfully
*/
boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input);
/**
* Disables a credential type for a user.
*
* @param realm the realm
* @param user the user
* @param credentialType the credential type to disable
*/
void disableCredentialType(RealmModel realm, UserModel user, String credentialType);
/**
* Gets disabled credential types for a user.
*
* @param realm the realm
* @param user the user
* @return stream of disabled credential types
*/
default Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
return Stream.empty();
}
}Represents a stored credential.
public class CredentialModel {
public static final String PASSWORD = "password";
public static final String PASSWORD_HISTORY = "password-history";
public static final String TOTP = "otp";
public static final String HOTP = "hotp";
public static final String CLIENT_CERT = "cert";
public static final String KERBEROS = "kerberos";
public static final String SECRET = "secret";
public static final String WEBAUTHN = "webauthn";
public static final String WEBAUTHN_PASSWORDLESS = "webauthn-passwordless";
public static final String RECOVERY_AUTHN_CODES = "recovery-authn-codes";
private String id;
private String type;
private String userLabel;
private Long createdDate;
private String secretData;
private String credentialData;
// Constructors
public CredentialModel() {}
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getUserLabel() { return userLabel; }
public void setUserLabel(String userLabel) { this.userLabel = userLabel; }
public Long getCreatedDate() { return createdDate; }
public void setCreatedDate(Long createdDate) { this.createdDate = createdDate; }
public String getSecretData() { return secretData; }
public void setSecretData(String secretData) { this.secretData = secretData; }
public String getCredentialData() { return credentialData; }
public void setCredentialData(String credentialData) { this.credentialData = credentialData; }
// Utility methods
public boolean challengeResponse(String response) {
// Default implementation
return false;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
CredentialModel that = (CredentialModel) obj;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}Interface for credential input during authentication.
public interface CredentialInput {
/**
* Gets the credential type.
*
* @return credential type
*/
String getType();
/**
* Gets the challenge response.
*
* @return challenge response
*/
String getChallengeResponse();
/**
* Gets the credential ID.
*
* @return credential ID
*/
default String getCredentialId() {
return null;
}
}Model for password credentials.
public class PasswordCredentialModel extends CredentialModel {
public static final String TYPE = "password";
public static final String PASSWORD_HISTORY = "password-history";
private PasswordCredentialData credentialData;
private PasswordSecretData secretData;
private PasswordCredentialModel(String credentialId, PasswordCredentialData credentialData, PasswordSecretData secretData) {
setId(credentialId);
setType(TYPE);
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
setSecretData(JsonSerialization.writeValueAsString(secretData));
setCreatedDate(Time.currentTimeMillis());
this.credentialData = credentialData;
this.secretData = secretData;
}
public static PasswordCredentialModel createFromValues(String algorithm, byte[] salt, int hashIterations, String encodedPassword) {
PasswordCredentialData credentialData = new PasswordCredentialData(hashIterations, algorithm);
PasswordSecretData secretData = new PasswordSecretData(encodedPassword, salt);
return new PasswordCredentialModel(generateId(), credentialData, secretData);
}
public static PasswordCredentialModel createFromCredentialModel(CredentialModel credential) {
try {
PasswordCredentialData credentialData = JsonSerialization.readValue(credential.getCredentialData(), PasswordCredentialData.class);
PasswordSecretData secretData = JsonSerialization.readValue(credential.getSecretData(), PasswordSecretData.class);
PasswordCredentialModel passwordCredentialModel = new PasswordCredentialModel(credential.getId(), credentialData, secretData);
passwordCredentialModel.setUserLabel(credential.getUserLabel());
passwordCredentialModel.setCreatedDate(credential.getCreatedDate());
return passwordCredentialModel;
} catch (IOException e) {
throw new ModelException(e);
}
}
public PasswordCredentialData getPasswordCredentialData() {
return credentialData;
}
public PasswordSecretData getPasswordSecretData() {
return secretData;
}
private static String generateId() {
return KeycloakModelUtils.generateId();
}
}Model for OTP (One-Time Password) credentials.
public class OTPCredentialModel extends CredentialModel {
public static final String TYPE = "otp";
public static final String TOTP = "totp";
public static final String HOTP = "hotp";
private OTPCredentialData credentialData;
private OTPSecretData secretData;
private OTPCredentialModel(String credentialId, OTPCredentialData credentialData, OTPSecretData secretData) {
setId(credentialId);
setType(TYPE);
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
setSecretData(JsonSerialization.writeValueAsString(secretData));
setCreatedDate(Time.currentTimeMillis());
this.credentialData = credentialData;
this.secretData = secretData;
}
public static OTPCredentialModel createFromPolicy(RealmModel realm, String secretEncodedBase32) {
return createFromPolicy(realm, secretEncodedBase32, null);
}
public static OTPCredentialModel createFromPolicy(RealmModel realm, String secretEncodedBase32, String userLabel) {
OTPPolicy policy = realm.getOTPPolicy();
OTPCredentialData credentialData = new OTPCredentialData(policy.getType(), policy.getDigits(),
policy.getPeriod(), policy.getInitialCounter(),
policy.getAlgorithm());
OTPSecretData secretData = new OTPSecretData(secretEncodedBase32);
OTPCredentialModel credential = new OTPCredentialModel(generateId(), credentialData, secretData);
if (userLabel != null) {
credential.setUserLabel(userLabel);
}
return credential;
}
public static OTPCredentialModel createFromCredentialModel(CredentialModel credential) {
try {
OTPCredentialData credentialData = JsonSerialization.readValue(credential.getCredentialData(), OTPCredentialData.class);
OTPSecretData secretData = JsonSerialization.readValue(credential.getSecretData(), OTPSecretData.class);
OTPCredentialModel otpCredentialModel = new OTPCredentialModel(credential.getId(), credentialData, secretData);
otpCredentialModel.setUserLabel(credential.getUserLabel());
otpCredentialModel.setCreatedDate(credential.getCreatedDate());
return otpCredentialModel;
} catch (IOException e) {
throw new ModelException(e);
}
}
public OTPCredentialData getOTPCredentialData() {
return credentialData;
}
public OTPSecretData getOTPSecretData() {
return secretData;
}
private static String generateId() {
return KeycloakModelUtils.generateId();
}
}Model for WebAuthn credentials.
public class WebAuthnCredentialModel extends CredentialModel {
public static final String TYPE_TWOFACTOR = "webauthn";
public static final String TYPE_PASSWORDLESS = "webauthn-passwordless";
private WebAuthnCredentialData credentialData;
private WebAuthnSecretData secretData;
private WebAuthnCredentialModel(String credentialId, WebAuthnCredentialData credentialData, WebAuthnSecretData secretData) {
setId(credentialId);
setType(TYPE_TWOFACTOR);
setCredentialData(JsonSerialization.writeValueAsString(credentialData));
setSecretData(JsonSerialization.writeValueAsString(secretData));
setCreatedDate(Time.currentTimeMillis());
this.credentialData = credentialData;
this.secretData = secretData;
}
public static WebAuthnCredentialModel create(String type, String userLabel, String aaguid,
String credentialId, String attestationStatementFormat,
String attestationStatement, String credentialPublicKey,
long counter) {
WebAuthnCredentialData credentialData = new WebAuthnCredentialData(aaguid, credentialId,
attestationStatementFormat,
attestationStatement);
WebAuthnSecretData secretData = new WebAuthnSecretData(credentialPublicKey, counter);
WebAuthnCredentialModel credential = new WebAuthnCredentialModel(generateId(), credentialData, secretData);
credential.setType(type);
credential.setUserLabel(userLabel);
return credential;
}
public static WebAuthnCredentialModel createFromCredentialModel(CredentialModel credential) {
try {
WebAuthnCredentialData credentialData = JsonSerialization.readValue(credential.getCredentialData(), WebAuthnCredentialData.class);
WebAuthnSecretData secretData = JsonSerialization.readValue(credential.getSecretData(), WebAuthnSecretData.class);
WebAuthnCredentialModel webAuthnCredentialModel = new WebAuthnCredentialModel(credential.getId(), credentialData, secretData);
webAuthnCredentialModel.setType(credential.getType());
webAuthnCredentialModel.setUserLabel(credential.getUserLabel());
webAuthnCredentialModel.setCreatedDate(credential.getCreatedDate());
return webAuthnCredentialModel;
} catch (IOException e) {
throw new ModelException(e);
}
}
public WebAuthnCredentialData getWebAuthnCredentialData() {
return credentialData;
}
public WebAuthnSecretData getWebAuthnSecretData() {
return secretData;
}
private static String generateId() {
return KeycloakModelUtils.generateId();
}
}Interface for credential storage operations.
public interface UserCredentialStore {
/**
* Updates a credential for a user.
*
* @param realm the realm
* @param user the user
* @param cred the credential to update
*/
void updateCredential(RealmModel realm, UserModel user, CredentialModel cred);
/**
* Creates a new credential for a user.
*
* @param realm the realm
* @param user the user
* @param cred the credential to create
* @return created credential
*/
CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred);
/**
* Removes a credential for a user.
*
* @param realm the realm
* @param user the user
* @param id the credential ID
* @return true if removed
*/
boolean removeCredential(RealmModel realm, UserModel user, String id);
/**
* Gets a specific credential for a user.
*
* @param realm the realm
* @param user the user
* @param id the credential ID
* @return credential or null
*/
CredentialModel getUserCredentialById(RealmModel realm, UserModel user, String id);
/**
* Gets all credentials for a user.
*
* @param realm the realm
* @param user the user
* @return list of credentials
*/
List<CredentialModel> getStoredCredentials(RealmModel realm, UserModel user);
/**
* Gets credentials of a specific type for a user.
*
* @param realm the realm
* @param user the user
* @param type the credential type
* @return list of credentials
*/
List<CredentialModel> getStoredCredentialsByType(RealmModel realm, UserModel user, String type);
/**
* Gets the most recent credential of a type for a user.
*
* @param realm the realm
* @param user the user
* @param type the credential type
* @return credential or null
*/
CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type);
/**
* Moves a credential to a new position in the list.
*
* @param realm the realm
* @param user the user
* @param id the credential ID
* @param newPosition the new position
*/
void moveCredentialTo(RealmModel realm, UserModel user, String id, String newPosition);
}Manages credentials for a specific subject (user).
public interface SubjectCredentialManager {
/**
* Checks if a credential type is configured.
*
* @param type the credential type
* @return true if configured
*/
boolean isConfiguredFor(String type);
/**
* Updates a credential.
*
* @param input the credential input
* @return true if updated
*/
boolean updateCredential(CredentialInput input);
/**
* Updates a stored credential.
*
* @param cred the credential model
*/
void updateStoredCredential(CredentialModel cred);
/**
* Creates a stored credential.
*
* @param cred the credential model
* @return created credential
*/
CredentialModel createStoredCredential(CredentialModel cred);
/**
* Removes a stored credential.
*
* @param id the credential ID
* @return true if removed
*/
boolean removeStoredCredential(String id);
/**
* Gets a stored credential by ID.
*
* @param id the credential ID
* @return credential or null
*/
CredentialModel getStoredCredentialById(String id);
/**
* Gets stored credentials by type.
*
* @param type the credential type
* @return stream of credentials
*/
Stream<CredentialModel> getStoredCredentialsStream(String type);
/**
* Gets all stored credentials.
*
* @return stream of credentials
*/
Stream<CredentialModel> getStoredCredentialsStream();
/**
* Disables a credential type.
*
* @param credentialType the credential type
*/
void disableCredentialType(String credentialType);
/**
* Gets disableable credential types.
*
* @return stream of credential types
*/
Stream<String> getDisableableCredentialTypesStream();
/**
* Checks if a credential input is valid.
*
* @param input the credential input
* @return true if valid
*/
boolean isValid(CredentialInput input);
/**
* Gets the credential authentication object.
*
* @return credential authentication
*/
CredentialAuthentication authenticate(CredentialInput input);
/**
* Moves a credential to a new position.
*
* @param credentialId the credential ID
* @param newPreviousCredentialId the ID of credential that should be before this one
*/
void moveStoredCredentialTo(String credentialId, String newPreviousCredentialId);
/**
* Creates a credential from user input.
*
* @param input the credential input
* @return created credential
*/
boolean createCredentialThroughProvider(CredentialInput input);
}public class CustomTokenCredentialProvider implements CredentialProvider, CredentialInputValidator, CredentialInputUpdater {
public static final String TYPE = "custom-token";
private final KeycloakSession session;
public CustomTokenCredentialProvider(KeycloakSession session) {
this.session = session;
}
@Override
public String getType() {
return TYPE;
}
@Override
public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel) {
if (!TYPE.equals(credentialModel.getType())) {
return null;
}
// Validate and create the credential
CredentialModel credential = new CredentialModel();
credential.setId(KeycloakModelUtils.generateId());
credential.setType(TYPE);
credential.setCreatedDate(Time.currentTimeMillis());
credential.setSecretData(credentialModel.getSecretData());
credential.setCredentialData(credentialModel.getCredentialData());
credential.setUserLabel(credentialModel.getUserLabel());
return session.userCredentialManager().createStoredCredential(realm, user, credential);
}
@Override
public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) {
return session.userCredentialManager().removeStoredCredential(realm, user, credentialId);
}
@Override
public CredentialModel getCredentialFromModel(CredentialModel model) {
return model;
}
@Override
public boolean supportsCredentialType(String credentialType) {
return TYPE.equals(credentialType);
}
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
if (!supportsCredentialType(credentialType)) {
return false;
}
return !session.userCredentialManager()
.getStoredCredentialsByTypeStream(realm, user, TYPE)
.findFirst().isEmpty();
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType())) {
return false;
}
String token = input.getChallengeResponse();
if (token == null) {
return false;
}
// Validate against stored credential
List<CredentialModel> credentials = session.userCredentialManager()
.getStoredCredentialsByTypeStream(realm, user, TYPE)
.collect(Collectors.toList());
for (CredentialModel credential : credentials) {
if (validateToken(token, credential)) {
return true;
}
}
return false;
}
@Override
public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType())) {
return false;
}
// Remove existing credentials of this type
session.userCredentialManager()
.getStoredCredentialsByTypeStream(realm, user, TYPE)
.forEach(cred -> session.userCredentialManager().removeStoredCredential(realm, user, cred.getId()));
// Create new credential
CredentialModel credential = new CredentialModel();
credential.setType(TYPE);
credential.setSecretData(input.getChallengeResponse());
credential.setUserLabel("Custom Token");
return createCredential(realm, user, credential) != null;
}
@Override
public void disableCredentialType(RealmModel realm, UserModel user, String credentialType) {
if (!supportsCredentialType(credentialType)) {
return;
}
session.userCredentialManager()
.getStoredCredentialsByTypeStream(realm, user, TYPE)
.forEach(cred -> session.userCredentialManager().removeStoredCredential(realm, user, cred.getId()));
}
@Override
public Stream<String> getDisableableCredentialTypesStream(RealmModel realm, UserModel user) {
if (isConfiguredFor(realm, user, TYPE)) {
return Stream.of(TYPE);
}
return Stream.empty();
}
@Override
public void close() {
// Cleanup resources
}
private boolean validateToken(String inputToken, CredentialModel credential) {
String storedToken = credential.getSecretData();
return inputToken.equals(storedToken);
}
}// Working with user credentials
try (KeycloakSession session = sessionFactory.create()) {
RealmModel realm = session.realms().getRealmByName("myrealm");
UserModel user = session.users().getUserByUsername(realm, "john");
// Get user's credential manager
SubjectCredentialManager credManager = user.credentialManager();
// Check if password is configured
boolean hasPassword = credManager.isConfiguredFor(PasswordCredentialModel.TYPE);
// Validate password
PasswordUserCredentialModel passwordInput = new PasswordUserCredentialModel("password123");
boolean isValid = credManager.isValid(passwordInput);
// Update password
boolean updated = credManager.updateCredential(passwordInput);
// Get all stored credentials
List<CredentialModel> allCredentials = credManager.getStoredCredentialsStream()
.collect(Collectors.toList());
// Get OTP credentials
List<CredentialModel> otpCredentials = credManager
.getStoredCredentialsStream(OTPCredentialModel.TYPE)
.collect(Collectors.toList());
// Remove a specific credential
if (!otpCredentials.isEmpty()) {
credManager.removeStoredCredential(otpCredentials.get(0).getId());
}
}// Setup OTP for a user
try (KeycloakSession session = sessionFactory.create()) {
RealmModel realm = session.realms().getRealmByName("myrealm");
UserModel user = session.users().getUserByUsername(realm, "alice");
// Generate secret
String secret = Base32.random();
// Create OTP credential from realm policy
OTPCredentialModel otpCredential = OTPCredentialModel.createFromPolicy(realm, secret, "My Authenticator");
// Store the credential
user.credentialManager().createStoredCredential(otpCredential);
// Generate QR code URL for user setup
String issuer = realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName();
String qrUrl = String.format("otpauth://totp/%s:%s?secret=%s&issuer=%s",
issuer, user.getUsername(), secret, issuer);
}// Set user password
try (KeycloakSession session = sessionFactory.create()) {
RealmModel realm = session.realms().getRealmByName("myrealm");
UserModel user = session.users().getUserByUsername(realm, "bob");
// Use the password credential input to set password
PasswordUserCredentialModel passwordInput = new PasswordUserCredentialModel("newPassword123");
boolean success = user.credentialManager().updateCredential(passwordInput);
if (success) {
System.out.println("Password updated successfully");
}
}// Create password credential with custom hashing
try (KeycloakSession session = sessionFactory.create()) {
RealmModel realm = session.realms().getRealmByName("myrealm");
UserModel user = session.users().getUserByUsername(realm, "charlie");
// Hash password with custom parameters
String algorithm = "pbkdf2-sha256";
int iterations = 100000;
byte[] salt = SecretGenerator.getInstance().randomBytes(16);
String hashedPassword = Pbkdf2PasswordEncoder.encode("password123", salt, iterations);
// Create password credential
PasswordCredentialModel passwordCredential = PasswordCredentialModel.createFromValues(
algorithm, salt, iterations, hashedPassword);
// Store the credential
user.credentialManager().createStoredCredential(passwordCredential);
}Install with Tessl CLI
npx tessl i tessl/maven-org-keycloak--keycloak-server-spi@26.2.1