A complete implementation guide for creating a custom Keycloak SPI that automatically registers users when they don't exist in the system after successful external provider authentication.
Prerequisites
- Java Development Kit (JDK) 11 or higher
- Apache Maven for dependency management
- Keycloak Server 18.x or higher
- IDE (IntelliJ IDEA or Eclipse)
Maven Dependencies
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
Project Structure
src/
└── main/
├── java/
│ └── com/
│ └── company/
│ └── keycloak/
│ └── provider/
│ ├── CustomIdentityProvider.java
│ ├── CustomIdentityProviderConfig.java
│ ├── CustomIdentityProviderFactory.java
│ └── model/
│ └── UserInfo.java
└── resources/
└── META-INF/
└── services/
└── org.keycloak.broker.provider.IdentityProviderFactory
Implementation
1. User Information Model
Create a model class to hold user information from the external provider:
package com.company.keycloak.provider.model;
public class UserInfo {
private String username;
private String firstName;
private String lastName;
private String email;
private String phoneNumber;
private String externalUserId;
private String nationalId;
private String dateOfBirth;
private String gender;
// Constructors
public UserInfo() {}
public UserInfo(String username, String firstName, String lastName, String email) {
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhoneNumber() { return phoneNumber; }
public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
public String getExternalUserId() { return externalUserId; }
public void setExternalUserId(String externalUserId) { this.externalUserId = externalUserId; }
public String getNationalId() { return nationalId; }
public void setNationalId(String nationalId) { this.nationalId = nationalId; }
public String getDateOfBirth() { return dateOfBirth; }
public void setDateOfBirth(String dateOfBirth) { this.dateOfBirth = dateOfBirth; }
public String getGender() { return gender; }
public void setGender(String gender) { this.gender = gender; }
}
2. Identity Provider Configuration
package com.company.keycloak.provider;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.models.IdentityProviderModel;
public class CustomIdentityProviderConfig extends OAuth2IdentityProviderConfig {
public CustomIdentityProviderConfig(IdentityProviderModel model) {
super(model);
}
public String getApiUrl() {
return getConfig().get("apiUrl");
}
public void setApiUrl(String apiUrl) {
getConfig().put("apiUrl", apiUrl);
}
public String getApiKey() {
return getConfig().get("apiKey");
}
public void setApiKey(String apiKey) {
getConfig().put("apiKey", apiKey);
}
public boolean isAutoCreateUsers() {
return Boolean.parseBoolean(getConfig().getOrDefault("autoCreateUsers", "true"));
}
public void setAutoCreateUsers(boolean autoCreateUsers) {
getConfig().put("autoCreateUsers", String.valueOf(autoCreateUsers));
}
public String getDefaultUserRole() {
return getConfig().getOrDefault("defaultUserRole", "user");
}
public void setDefaultUserRole(String defaultUserRole) {
getConfig().put("defaultUserRole", defaultUserRole);
}
}
3. Core Identity Provider Implementation
package com.company.keycloak.provider;
import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.models.*;
import org.keycloak.sessions.AuthenticationSessionModel;
import com.company.keycloak.provider.model.UserInfo;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class CustomIdentityProvider extends AbstractIdentityProvider<CustomIdentityProviderConfig> {
private static final String PROVIDER_ID = "custom-external";
public CustomIdentityProvider(KeycloakSession session, CustomIdentityProviderConfig config) {
super(session, config);
}
@Override
public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {
return null;
}
@Override
public Response performLogin(AuthenticationRequest request) {
try {
AuthenticationSessionModel authSession = request.getAuthenticationSession();
// Extract parameters from the request
String userId = request.getHttpRequest().getUri().getQueryParameters().getFirst("user_id");
String transactionId = request.getHttpRequest().getUri().getQueryParameters().getFirst("transaction_id");
// Validate parameters
if (!validateParameters(userId, transactionId)) {
throw new IdentityBrokerException("Invalid parameters provided");
}
// Store parameters in authentication session
authSession.setAuthNote("external_user_id", userId);
authSession.setAuthNote("transaction_id", transactionId);
// Call external API to get user information
UserInfo userInfo = getExternalUserInfo(userId, transactionId);
// Create brokered identity context
BrokeredIdentityContext context = createIdentityContext(userInfo);
// Return callback response
return callback.authenticated(context);
} catch (Exception e) {
throw new IdentityBrokerException("Authentication failed", e);
}
}
@Override
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
// This method is called when a new user needs to be created
// Set basic user properties
user.setEnabled(true);
user.setEmailVerified(true);
// Map attributes from external provider context
mapUserAttributes(user, context);
// Add default roles
addDefaultRoles(realm, user);
// Set required actions for new users
user.addRequiredAction("UPDATE_PROFILE");
// Log user creation
logger.infof("New user created from external provider: %s", user.getUsername());
}
@Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
// Update existing user attributes
mapUserAttributes(user, context);
logger.infof("Updated existing user from external provider: %s", user.getUsername());
}
private boolean validateParameters(String userId, String transactionId) {
if (userId == null || userId.trim().isEmpty() || userId.length() > 50) {
return false;
}
if (transactionId == null || transactionId.trim().isEmpty() || transactionId.length() > 100) {
return false;
}
Pattern userIdPattern = Pattern.compile("^[a-zA-Z0-9_-]+$");
Pattern transactionIdPattern = Pattern.compile("^[a-zA-Z0-9_-]+$");
return userIdPattern.matcher(userId).matches() &&
transactionIdPattern.matcher(transactionId).matches();
}
private UserInfo getExternalUserInfo(String userId, String transactionId) {
// Simulate external API call
// In real implementation, make HTTP call to external service
UserInfo userInfo = new UserInfo();
userInfo.setUsername("user_" + userId);
userInfo.setFirstName("John");
userInfo.setLastName("Doe");
userInfo.setEmail("john.doe@example.com");
userInfo.setPhoneNumber("+1234567890");
userInfo.setExternalUserId(userId);
userInfo.setNationalId("123456789");
userInfo.setDateOfBirth("1990-01-01");
userInfo.setGender("M");
return userInfo;
}
private BrokeredIdentityContext createIdentityContext(UserInfo userInfo) {
BrokeredIdentityContext context = new BrokeredIdentityContext(userInfo.getExternalUserId());
context.setUsername(userInfo.getUsername());
context.setFirstName(userInfo.getFirstName());
context.setLastName(userInfo.getLastName());
context.setEmail(userInfo.getEmail());
context.setIdpConfig(getConfig());
context.setIdp(this);
// Add custom attributes to context
context.getContextData().put("phone_number", userInfo.getPhoneNumber());
context.getContextData().put("national_id", userInfo.getNationalId());
context.getContextData().put("date_of_birth", userInfo.getDateOfBirth());
context.getContextData().put("gender", userInfo.getGender());
context.getContextData().put("external_user_id", userInfo.getExternalUserId());
return context;
}
private void mapUserAttributes(UserModel user, BrokeredIdentityContext context) {
// Map standard attributes
if (context.getFirstName() != null) {
user.setFirstName(context.getFirstName());
}
if (context.getLastName() != null) {
user.setLastName(context.getLastName());
}
if (context.getEmail() != null) {
user.setEmail(context.getEmail());
}
// Map custom attributes from context
Map<String, Object> contextAttributes = context.getContextData();
if (contextAttributes != null) {
mapStringAttribute(user, contextAttributes, "external_user_id");
mapStringAttribute(user, contextAttributes, "national_id");
mapStringAttribute(user, contextAttributes, "phone_number");
mapStringAttribute(user, contextAttributes, "date_of_birth");
mapStringAttribute(user, contextAttributes, "gender");
}
// Set authentication source and timestamp
user.setSingleAttribute("auth_source", "external_provider");
user.setSingleAttribute("registration_timestamp", String.valueOf(System.currentTimeMillis()));
}
private void mapStringAttribute(UserModel user, Map<String, Object> contextAttributes, String attributeName) {
Object value = contextAttributes.get(attributeName);
if (value != null) {
user.setSingleAttribute(attributeName, value.toString());
}
}
private void addDefaultRoles(RealmModel realm, UserModel user) {
// Add default realm roles
RoleModel defaultRole = realm.getRole("default-roles-" + realm.getName());
if (defaultRole != null) {
user.grantRole(defaultRole);
}
// Add custom role for external provider users
String customRoleName = getConfig().getDefaultUserRole();
RoleModel customRole = realm.getRole(customRoleName);
if (customRole != null) {
user.grantRole(customRole);
}
}
@Override
public BrokeredIdentityContext getFederatedIdentity(Response response) {
// This method processes the response from external provider
// Implementation depends on the specific external provider's response format
return null;
}
}
4. Identity Provider Factory
package com.company.keycloak.provider;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
public class CustomIdentityProviderFactory extends AbstractIdentityProviderFactory<CustomIdentityProvider> {
public static final String PROVIDER_ID = "custom-external";
@Override
public String getName() {
return "Custom External Provider";
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public CustomIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new CustomIdentityProvider(session, new CustomIdentityProviderConfig(model));
}
@Override
public CustomIdentityProviderConfig createConfig() {
return new CustomIdentityProviderConfig(new IdentityProviderModel());
}
}
5. Service Provider Registration
Create the file META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory
:
com.company.keycloak.provider.CustomIdentityProviderFactory
Building and Deployment
1. Build the Project
mvn clean package
2. Deploy to Keycloak
- Copy the generated JAR file to the
providers/
directory of your Keycloak installation - Restart Keycloak server
- Check server logs for any errors during startup
3. Configuration in Keycloak Admin Console
-
Add Identity Provider:
- Navigate to your realm in Keycloak Admin Console
- Go to Identity Providers
- Click "Add provider" and select "Custom External Provider"
- Configure the API URL, API Key, and other settings
-
Configure Authentication Flow:
- Go to Authentication → Flows
- Create a new flow or modify existing "First Broker Login" flow
- Ensure the flow handles user creation properly
-
Set Default Roles:
- Go to Realm Settings → Roles
- Create necessary roles (e.g., "external-user")
- Configure default role mappings
Testing the Implementation
1. Test URL Format
https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth?
client_id=myclient&
response_type=code&
redirect_uri=https://app.example.com/callback&
user_id=12345&
transaction_id=tx_abc123
2. Verify User Creation
- Access the test URL with valid parameters
- Check that the user is created in Keycloak
- Verify that custom attributes are properly mapped
- Confirm that default roles are assigned
Security Considerations
- Input Validation: Always validate and sanitize input parameters
- API Security: Use HTTPS for all external API calls
- Error Handling: Implement proper error handling and logging
- Duplicate Prevention: Check for existing users before creation
- Audit Logging: Log all authentication attempts and user creations
This implementation provides a complete solution for automatically registering users in Keycloak after successful external provider authentication, with proper validation, error handling, and security measures.
Top comments (0)