Skip to content

Commit

Permalink
[BAEL-4757] Initial code
Browse files Browse the repository at this point in the history
  • Loading branch information
psevestre committed Jan 13, 2021
1 parent 0ce81d0 commit c421418
Show file tree
Hide file tree
Showing 11 changed files with 645 additions and 1 deletion.
92 changes: 92 additions & 0 deletions oauth-rest/keycloack-custom-providers/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.baeldung</groupId>
<artifactId>keycloak-custom-providers</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath />
</parent>

<dependencies>

<!-- Embedded Keycloak sample -->
<dependency>
<groupId>com.baeldung</groupId>
<artifactId>oauth-authorization-server</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>

<!-- config properties processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<!-- test -->

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>


<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/*LiveTest.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

<developers>
<developer>
<email>[email protected]</email>
<name>Eugen Paraschiv</name>
<url>https://github.com/eugenp</url>
<id>eugenp</id>
</developer>
</developers>

<properties>
<!-- non-dependencies -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>13</java.version>

<keycloak.version>11.0.2</keycloak.version>

<!-- these should be updated together with Keycloak -->
<!-- check keycloak-dependencies-server-all effective pom -->
<infinispan.version>10.1.8.Final</infinispan.version>
<resteasy.version>3.12.1.Final</resteasy.version>
</properties>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/**
*
*/
package com.baeldung.auth.provider.user;

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

import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
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;

/**
* @author Philippe
*
*/
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(String id, RealmModel realm) {
log.info("[I35] getUserById({})",id);
String[] parts = id.split(":");
if ( parts != null && parts.length == 3) {
return getUserByUsername(parts[2],realm);
}
return null;
}

@Override
public UserModel getUserByUsername(String username, RealmModel realm) {
log.info("[I41] getUserByUsername({})",username);
try ( Connection c = DbUtil.getConnection(this.model)) {
PreparedStatement st = c.prepareStatement("select username, firstName,lastName, email, birthDate from users where username = ?");
st.setString(1, username);
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 UserModel getUserByEmail(String email, RealmModel realm) {
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;
}
String username = user.getUsername();
if ( username == null ) {
// Fallback to id hack
String[] idParts = user.getId().split(":");
if ( idParts != null && idParts.length == 3 ) {
username = idParts[2];
}
else {
return false;
}
}
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) {
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();
return rs.getInt(1);
}
catch(SQLException ex) {
throw new RuntimeException("Database error:" + ex.getMessage(),ex);
}
}

@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 limit ? offset ?");
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();
}



private UserModel mapUser(RealmModel realm, ResultSet rs) throws SQLException {

DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");
CustomUser user = new CustomUser(ksession, realm, model, rs.getString("username"));
// user.setEmail(rs.getString("email"));
// user.setFirstName(rs.getString("firstName"));
// user.setLastName(rs.getString("lastName"));
// user.setAttribute("birthDate", Arrays.asList(fmt.format(rs.getDate("birthDate"))));

return user;
}

private static class CustomUser extends AbstractUserAdapter {

private String username;

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

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

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.baeldung.auth.provider.user;

public final class CustomUserStorageProviderConstants {
public static final String CONFIG_KEY_JDBC_DRIVER = "jdbcDriver";
public static final String CONFIG_KEY_JDBC_URL = "jdbcUrl";
public static final String CONFIG_KEY_DB_USERNAME = "username";
public static final String CONFIG_KEY_DB_PASSWORD = "password";
public static final String CONFIG_KEY_VALIDATION_QUERY = "validationQuery";
}
Loading

0 comments on commit c421418

Please sign in to comment.