UserStorageProvider SPI is not available keycloak 20.0.2

2,184 views
Skip to first unread message

Sneha Manohar

unread,
Jan 17, 2023, 2:51:55 AM1/17/23
to keyclo...@googlegroups.com
Hi Dear Keycloak users,

There is need for me to access users from external data source(Oracle DB ).
I have created CustomUserStorageProvider implementing UserStorageProvider
i.e org.keycloak.storage.UserStorageProvider;

Code does not compile when we upgrade to KeyCloak 20.0.2 , is USerStorage Provider deprecated ? Please suggest alternative

image.png

Thanks in advance
Sneha

Bruno Ribeiro

unread,
Jan 17, 2023, 8:18:29 AM1/17/23
to Keycloak User
Hi Sneha,

The org.keycloak.storage.UserStorageProvider interface was moved to the org.keycloak:keycloak-model-legacy dependency so you might need to review your dependency tree and update it.

For further info: https://www.keycloak.org/2022/07/keycloak-1900-released.html#_changes_in_keycloak_storage

Best,

Bruno Ribeiro

Sneha Manohar

unread,
Jan 20, 2023, 8:00:46 AM1/20/23
to Keycloak User
Hey Bruno , thanks a ton .. It works !!!

SearchUser works well but getUserByUsername("test") fails  with following error  , Kindly guide what's that I am doing wrong 

2023-01-20 18:26:13,648 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (executor-thread-21) Uncaught server error: java.lang.NullPointerException
        at org.keycloak.models.cache.infinispan.entities.CachedUser.lambda$new$4(CachedUser.java:75)

Do I need to implement  below ?

@Override
public SubjectCredentialManager credentialManager() {
return null;
}

Below is my test code 
 -------------------------------CustomUser-------------------------------------------------

package com.cgi.auth.provider.user;


import java.util.Date;
import java.util.List;
import java.util.Map;

import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SubjectCredentialManager;
import org.keycloak.models.UserModel;
import org.keycloak.storage.adapter.AbstractUserAdapter;


public class CustomUser extends AbstractUserAdapter {

private final String username;
private final String email;
private final String firstName;
private final String lastName;
private final Date birthDate;

private CustomUser(KeycloakSession session, RealmModel realm,
ComponentModel storageProviderModel,
String username,
String email,
String firstName,
String lastName,
Date birthDate ) {
super(session, realm, storageProviderModel);
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
this.birthDate = birthDate;

}

@Override
public String getUsername() {
return username;
}

@Override
public String getFirstName() {
return firstName;
}

@Override
public String getLastName() {
return lastName;
}

@Override
public String getEmail() {
return email;
}

@Override
public SubjectCredentialManager credentialManager() {
return null;
}

public Date getBirthDate() {
return birthDate;
}

@Override
public Map<String, List<String>> getAttributes() {
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
attributes.add(UserModel.USERNAME, getUsername());
attributes.add(UserModel.EMAIL,getEmail());
attributes.add(UserModel.FIRST_NAME,getFirstName());
attributes.add(UserModel.LAST_NAME,getLastName());
attributes.add("birthDate",getBirthDate().toString());
return attributes;
}

static class Builder {
private final KeycloakSession session;
private final RealmModel realm;
private final ComponentModel storageProviderModel;
private String username;
private String email;
private String firstName;
private String lastName;
private Date birthDate;

Builder(KeycloakSession session, RealmModel realm, ComponentModel storageProviderModel,String username) {
this.session = session;
this.realm = realm;
this.storageProviderModel = storageProviderModel;
this.username = username;
}

CustomUser.Builder email(String email) {
this.email = email;
return this;
}

CustomUser.Builder firstName(String firstName) {
this.firstName = firstName;
return this;
}

CustomUser.Builder lastName(String lastName) {
this.lastName = lastName;
return this;
}

CustomUser.Builder birthDate(Date birthDate) {
this.birthDate = birthDate;
return this;
}

CustomUser build() {
return new CustomUser(
session,
realm,
storageProviderModel,
username,
email,
firstName,
lastName,
birthDate);

}
}
}
_____________________ Here CustomUserStorageProvider_____________________

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.models.*;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.adapter.AbstractUserAdapter;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserQueryProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CustomUserStorageProvider implements UserStorageProvider,
UserLookupProvider,
CredentialInputValidator,
UserQueryProvider {

private static final Logger log = LoggerFactory.getLogger(CustomUserStorageProvider.class);
private KeycloakSession ksession;
private ComponentModel model;

public CustomUserStorageProvider(KeycloakSession ksession, ComponentModel model) {
this.ksession = ksession;
this.model = model;
}

@Override
public void close() {
log.info("[I30] close()");
}

@Override
public UserModel getUserById(RealmModel realm,String id) {
log.info("[I35] getUserById({})",id);
StorageId sid = new StorageId(id);
return getUserByUsername(realm,sid.getExternalId());
}

@Override
public UserModel getUserByUsername( RealmModel realm,String username) {
log.info("[I41] getUserByUsername({})",username);
try ( Connection c = DbUtil.getConnection(this.model)) {
log.info(" got connection");
PreparedStatement st = c.prepareStatement("select username, firstName,lastName, email, birthDate from users where username = 'test'");
log.info("created");
// st.setString(1, username);
log.info("set string skipped");
st.execute();
log.info("Done ");
ResultSet rs = st.getResultSet();
log.info("ResultSet is set ");
if ( rs.next()) {
log.info("Inside result set");
return mapUser(realm,rs);
}
else {
return null;
}
}
catch(SQLException ex) {
throw new RuntimeException("Database error:" + ex.getMessage(),ex);
}
}

@Override
public UserModel getUserByEmail( RealmModel realm,String email) {
log.info("[I48] getUserByEmail({})",email);
try ( Connection c = DbUtil.getConnection(this.model)) {
PreparedStatement st = c.prepareStatement("select username, firstName,lastName, email, birthDate from users where email = ?");
st.setString(1, email);
st.execute();
ResultSet rs = st.getResultSet();
if ( rs.next()) {
return mapUser(realm,rs);
}
else {
return null;
}
}
catch(SQLException ex) {
throw new RuntimeException("Database error:" + ex.getMessage(),ex);
}
}

@Override
public boolean supportsCredentialType(String credentialType) {
log.info("[I57] supportsCredentialType({})",credentialType);
return PasswordCredentialModel.TYPE.endsWith(credentialType);
}

@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
log.info("[I57] isConfiguredFor(realm={},user={},credentialType={})",realm.getName(), user.getUsername(), credentialType);
// In our case, password is the only type of credential, so we allways return 'true' if
// this is the credentialType
return supportsCredentialType(credentialType);
}

@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) {
log.info("[I57] isValid(realm={},user={},credentialInput.type={})",realm.getName(), user.getUsername(), credentialInput.getType());
if( !this.supportsCredentialType(credentialInput.getType())) {
return false;
}
StorageId sid = new StorageId(user.getId());
String username = sid.getExternalId();

try ( Connection c = DbUtil.getConnection(this.model)) {
PreparedStatement st = c.prepareStatement("select password from users where username = ?");
st.setString(1, username);
st.execute();
ResultSet rs = st.getResultSet();
if ( rs.next()) {
String pwd = rs.getString(1);
return pwd.equals(credentialInput.getChallengeResponse());
}
else {
return false;
}
}
catch(SQLException ex) {
throw new RuntimeException("Database error:" + ex.getMessage(),ex);
}
}

// UserQueryProvider implementation

@Override
public int getUsersCount(RealmModel realm) {
int i =1 ;
log.info("[I93] getUsersCount: realm={}", realm.getName() );
// try ( Connection c = DbUtil.getConnection(this.model)) {
// Statement st = c.createStatement();
// st.execute("select count(*) from users");
// ResultSet rs = st.getResultSet();
// rs.next();
// String str= rs.getString(1);
// log.info("Users count "+str);
// log.info("In val of users count "+Integer.valueOf(str));
// return i;
// }
// catch(SQLException ex) {
// throw new RuntimeException("Database error:" + ex.getMessage(),ex);
// }
return i;
}
//looks like this is called for * search
@Override
public Stream<UserModel> searchForUserStream(RealmModel realmModel, String s, Integer firstResult, Integer maxResult) {
log.info("searchForUserStream(RealmModel realmModel, String s, Integer firstResult, Integer maxResult):: with params String s ::" +s
+" firstResult:: "+firstResult+" maxResult :: "+maxResult);
return convertListToStream(getUsers( realmModel, firstResult,maxResult ));
}

//This is called for search by name , last name etc
@Override
public Stream<UserModel> searchForUserStream(RealmModel realmModel, Map<String, String> map, Integer firstResult, Integer maxResult) {
log.info("searchForUserStream(RealmModel realmModel, Map<String, String> map, Integer firstResult, Integer maxResult) :: with map ::" +map.size()
+ " firstResult:: "+firstResult+" maxResult :: "+maxResult);
return convertListToStream(searchForUser( map, realmModel));
}

@Override
public Stream<UserModel> getGroupMembersStream(RealmModel realmModel, GroupModel groupModel, Integer integer, Integer integer1) {
return null;
}

@Override
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realmModel, String s, String s1) {
return null;
}

// @Override
public List<UserModel> getUsers(RealmModel realm) {
return getUsers(realm,0, 5000); // Keep a reasonable maxResults
}

// @Override
public List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults) {
log.info("[I113] getUsers: realm={}", realm.getName());

try ( Connection c = DbUtil.getConnection(this.model)) {
PreparedStatement st = c.prepareStatement("select username, firstName,lastName, email, birthDate from users order by username ");
// st.setInt(1, maxResults);
// st.setInt(2, firstResult);
st.execute();
ResultSet rs = st.getResultSet();
List<UserModel> users = new ArrayList<>();
while(rs.next()) {
users.add(mapUser(realm,rs));
}
return users;
}
catch(SQLException ex) {
throw new RuntimeException("Database error:" + ex.getMessage(),ex);
}
}

//@Override
public List<UserModel> searchForUser(String search, RealmModel realm) {
return searchForUser(search,realm,0,5000);
}

//@Override
public List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults) {
log.info("[I139] searchForUser: realm={}", realm.getName());

try ( Connection c = DbUtil.getConnection(this.model)) {
PreparedStatement st = c.prepareStatement("select username, firstName,lastName, email, birthDate from users where username like ? order by username limit ? offset ?");
st.setString(1, search);
st.setInt(2, maxResults);
st.setInt(3, firstResult);
st.execute();
ResultSet rs = st.getResultSet();
List<UserModel> users = new ArrayList<>();
while(rs.next()) {
users.add(mapUser(realm,rs));
}
return users;
}
catch(SQLException ex) {
throw new RuntimeException("Database error:" + ex.getMessage(),ex);
}
}

// @Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
return searchForUser(params,realm,0,5000);
}

//@Override
public List<UserModel> searchForUser(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
return getUsers(realm, firstResult, maxResults);
}

// @Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
return Collections.emptyList();
}

//@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
return Collections.emptyList();
}

//@Override
public List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {
return Collections.emptyList();
}


//------------------- Implementation
private UserModel mapUser(RealmModel realm, ResultSet rs) throws SQLException {
log.info(" mapUser(RealmModel realm, ResultSet rs) :: username ::"+rs.getString("username")
+"firstName ::"+rs.getString("firstName")+" lastName :: "+rs.getString("lastName")+" birthDate:: "+(rs.getDate("birthDate")));
DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
CustomUser user = new CustomUser.Builder(ksession, realm, model, rs.getString("username"))
.email(rs.getString("email"))
.firstName(rs.getString("firstName"))
.lastName(rs.getString("lastName"))
.birthDate(rs.getDate("birthDate"))
.build();

return user;
}
// Generic function to convert a list to stream
private static <T> Stream<T> convertListToStream(List<T> list)
{
return list.stream();
}

private UserModel createAdapter(RealmModel realm, String username) {
System.out.println("Called createAdapter");
return new AbstractUserAdapter(ksession, realm, model) {

@Override

public String getUsername() {

return username;

}

@Override
public SubjectCredentialManager credentialManager() {
return null;
}

};

}
}
_______________________Provider factory_____________
package com.cgi.auth.provider.user;

import java.sql.Connection;
import java.util.List;

import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.storage.UserStorageProviderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.cgi.auth.provider.user.CustomUserStorageProviderConstants.*;

public class CustomUserStorageProviderFactory implements UserStorageProviderFactory<CustomUserStorageProvider> {
private static final Logger log = LoggerFactory.getLogger(CustomUserStorageProviderFactory.class);
protected final List<ProviderConfigProperty> configMetadata;

public CustomUserStorageProviderFactory() {
log.info("[I24] CustomUserStorageProviderFactory created");


// Create config metadata
configMetadata = ProviderConfigurationBuilder.create()
.property()
.name(CONFIG_KEY_JDBC_DRIVER)
.label("JDBC Driver Class")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue("oracle.jdbc.driver.OracleDriver")
.helpText("Fully qualified class name of the JDBC driver")
.add()
.property()
.name(CONFIG_KEY_JDBC_URL)
.label("JDBC URL")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue("jdbc:oracle:thin:@10.126.82.82:1532/DEVTPS19")
.helpText("JDBC URL used to connect to the user database")
.add()
.property()
.name(CONFIG_KEY_DB_USERNAME)
.label("Database User")
.type(ProviderConfigProperty.STRING_TYPE)
.helpText("Username used to connect to the database")
.add()
.property()
.name(CONFIG_KEY_DB_PASSWORD)
.label("Database Password")
.type(ProviderConfigProperty.STRING_TYPE)
.helpText("Password used to connect to the database")
.secret(true)
.add()
.property()
.name(CONFIG_KEY_VALIDATION_QUERY)
.label("SQL Validation Query")
.type(ProviderConfigProperty.STRING_TYPE)
.helpText("SQL query used to validate a connection")
.defaultValue("select * from users")
.add()
.build();

}

@Override
public CustomUserStorageProvider create(KeycloakSession ksession, ComponentModel model) {
log.info("[I63] creating new CustomUserStorageProvider");
return new CustomUserStorageProvider(ksession,model);
}

@Override
public String getId() {
log.info("[I69] getId()");
return "custom-user-provider";
}


// Configuration support methods
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configMetadata;
}

@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {

try (Connection c = DbUtil.getConnection(config)) {
log.info("[I84] Testing connection..." );
c.createStatement().execute(config.get(CONFIG_KEY_VALIDATION_QUERY));
log.info("[I92] Connection OK !" );
}
catch(Exception ex) {
log.warn("[W94] Unable to validate connection: ex={}", ex.getMessage());
throw new ComponentValidationException("Unable to validate database connection",ex);
}
}

@Override
public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) {
log.info("[I94] onUpdate()" );
}

@Override
public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) {
log.info("[I99] onCreate()" );
}
}

Nicola Beghin

unread,
Jan 21, 2023, 3:20:22 AM1/21/23
to Keycloak User
I had to go through the same upgrade for my custom user storage provider. If useful, take a look at the latest commits in this git repo https://github.com/nicolabeghin/keycloak-multiple-ds-user-storage
Reply all
Reply to author
Forward
0 new messages