Skip to content

Commit e3bc7cf

Browse files
authored
Merge pull request #11193 from IQSS/11157-builtin-users-oidc-auth
Keycloak SPI for Builtin Users Authentication
2 parents 6e08455 + e52b5af commit e3bc7cf

33 files changed

+1467
-45
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,6 @@ src/main/webapp/resources/images/cc0.png.thumb140
6161
src/main/webapp/resources/images/dataverseproject.png.thumb140
6262

6363
# Docker development volumes
64+
/conf/keycloak/docker-dev-volumes
6465
/docker-dev-volumes
6566
/.vs

conf/keycloak/.env

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
APP_IMAGE=gdcc/dataverse:unstable
2+
POSTGRES_VERSION=17
3+
DATAVERSE_DB_USER=dataverse
4+
SOLR_VERSION=9.8.0
5+
SKIP_DEPLOY=0

conf/keycloak/Dockerfile

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ------------------------------------------
2+
# Stage 1: Build SPI with Maven
3+
# ------------------------------------------
4+
FROM maven:3.9.5-eclipse-temurin-17 AS builder
5+
6+
WORKDIR /app
7+
8+
# Copy SPI source code
9+
COPY ./builtin-users-spi /app
10+
11+
# Build the SPI JAR
12+
RUN mvn clean package
13+
14+
# ------------------------------------------
15+
# Stage 2: Build Keycloak Image
16+
# ------------------------------------------
17+
FROM quay.io/keycloak/keycloak:26.1.4
18+
19+
# Add the Oracle JDBC jars
20+
ARG ORACLE_JDBC_VERSION=23.7.0.25.01
21+
ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/jdbc/ojdbc11/${ORACLE_JDBC_VERSION}/ojdbc11-${ORACLE_JDBC_VERSION}.jar /opt/keycloak/providers/ojdbc11.jar
22+
ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/com/oracle/database/nls/orai18n/${ORACLE_JDBC_VERSION}/orai18n-${ORACLE_JDBC_VERSION}.jar /opt/keycloak/providers/orai18n.jar
23+
24+
# Health build parameter
25+
ENV KC_HEALTH_ENABLED=true
26+
27+
# Copy SPI JAR from builder stage
28+
COPY --from=builder /app/target/keycloak-dv-builtin-users-authenticator-1.0-SNAPSHOT.jar /opt/keycloak/providers/
29+
30+
# Copy additional configurations
31+
COPY ./builtin-users-spi/conf/quarkus.properties /opt/keycloak/conf/
32+
COPY ./test-realm.json /opt/keycloak/data/import/
33+
34+
# Set the Keycloak command
35+
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
36+
CMD ["start-dev", "--import-realm", "--http-port=8090"]
37+
38+
# Expose port 8090
39+
EXPOSE 8090
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
quarkus.datasource.user-store.db-kind=postgresql
2+
quarkus.datasource.user-store.jdbc.url=jdbc:postgresql://${DATAVERSE_DB_HOST}:${DATAVERSE_DB_PORT}/dataverse
3+
quarkus.datasource.user-store.username=${DATAVERSE_DB_USER}
4+
quarkus.datasource.user-store.password=${DATAVERSE_DB_PASSWORD}
5+
6+
quarkus.datasource.user-store.jdbc.driver=org.postgresql.Driver
7+
quarkus.datasource.user-store.jdbc.transactions=disabled
8+
quarkus.transaction-manager.unsafe-multiple-last-resources=allow
9+
10+
quarkus.datasource.user-store.jdbc.recovery.username=${DATAVERSE_DB_USER}
11+
quarkus.datasource.user-store.jdbc.recovery.password=${DATAVERSE_DB_PASSWORD}
12+
13+
quarkus.datasource.user-store.jdbc.xa-properties.serverName=${DATAVERSE_DB_HOST}
14+
quarkus.datasource.user-store.jdbc.xa-properties.portNumber=${DATAVERSE_DB_PORT}
15+
quarkus.datasource.user-store.jdbc.xa-properties.databaseName=dataverse
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<groupId>edu.harvard.iq.keycloak</groupId>
5+
<artifactId>keycloak-dv-builtin-users-authenticator</artifactId>
6+
<version>1.0-SNAPSHOT</version>
7+
<packaging>jar</packaging>
8+
9+
<dependencies>
10+
<!-- Keycloak Server SPI -->
11+
<dependency>
12+
<groupId>org.keycloak</groupId>
13+
<artifactId>keycloak-server-spi</artifactId>
14+
<version>${keycloak.version}</version>
15+
<scope>provided</scope>
16+
</dependency>
17+
18+
<!-- Keycloak Server SPI Private -->
19+
<dependency>
20+
<groupId>org.keycloak</groupId>
21+
<artifactId>keycloak-server-spi-private</artifactId>
22+
<version>${keycloak.version}</version>
23+
<scope>provided</scope>
24+
</dependency>
25+
26+
<!-- Keycloak Services -->
27+
<dependency>
28+
<groupId>org.keycloak</groupId>
29+
<artifactId>keycloak-services</artifactId>
30+
<version>${keycloak.version}</version>
31+
<scope>provided</scope>
32+
</dependency>
33+
34+
<!-- Keycloak Model JPA -->
35+
<dependency>
36+
<groupId>org.keycloak</groupId>
37+
<artifactId>keycloak-model-jpa</artifactId>
38+
<version>${keycloak.version}</version>
39+
<scope>provided</scope>
40+
</dependency>
41+
42+
<!-- Jakarta Persistence API -->
43+
<dependency>
44+
<groupId>jakarta.persistence</groupId>
45+
<artifactId>jakarta.persistence-api</artifactId>
46+
<version>${jakarta.persistence.version}</version>
47+
</dependency>
48+
49+
<!-- jBCrypt -->
50+
<dependency>
51+
<groupId>org.mindrot</groupId>
52+
<artifactId>jbcrypt</artifactId>
53+
<version>${mindrot.jbcrypt.version}</version>
54+
<scope>compile</scope>
55+
</dependency>
56+
57+
<!-- TEST DEPENDENCIES -->
58+
59+
<!-- JUnit -->
60+
<dependency>
61+
<groupId>org.junit.jupiter</groupId>
62+
<artifactId>junit-jupiter-api</artifactId>
63+
<version>${junit.jupiter.version}</version>
64+
<scope>test</scope>
65+
</dependency>
66+
67+
<!-- Mockito -->
68+
<dependency>
69+
<groupId>org.mockito</groupId>
70+
<artifactId>mockito-core</artifactId>
71+
<version>${mockito.version}</version>
72+
<scope>test</scope>
73+
</dependency>
74+
</dependencies>
75+
<build>
76+
<plugins>
77+
<plugin>
78+
<groupId>org.apache.maven.plugins</groupId>
79+
<artifactId>maven-shade-plugin</artifactId>
80+
<version>3.2.4</version>
81+
<executions>
82+
<execution>
83+
<phase>package</phase>
84+
<goals>
85+
<goal>shade</goal>
86+
</goals>
87+
</execution>
88+
</executions>
89+
</plugin>
90+
91+
<plugin>
92+
<groupId>org.apache.maven.plugins</groupId>
93+
<artifactId>maven-compiler-plugin</artifactId>
94+
<configuration>
95+
<source>17</source>
96+
<target>17</target>
97+
</configuration>
98+
</plugin>
99+
</plugins>
100+
</build>
101+
102+
<properties>
103+
<keycloak.version>26.1.4</keycloak.version>
104+
<java.version>17</java.version>
105+
<jakarta.persistence.version>3.2.0</jakarta.persistence.version>
106+
<mindrot.jbcrypt.version>0.4</mindrot.jbcrypt.version>
107+
<mockito.version>5.15.2</mockito.version>
108+
<junit.jupiter.version>5.11.4</junit.jupiter.version>
109+
</properties>
110+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package edu.harvard.iq.keycloak.auth.spi.adapters;
2+
3+
import edu.harvard.iq.keycloak.auth.spi.models.DataverseUser;
4+
import org.keycloak.component.ComponentModel;
5+
import org.keycloak.models.GroupModel;
6+
import org.keycloak.models.KeycloakSession;
7+
import org.keycloak.models.RealmModel;
8+
import org.keycloak.storage.StorageId;
9+
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;
10+
11+
import java.util.stream.Stream;
12+
13+
public class DataverseUserAdapter extends AbstractUserAdapterFederatedStorage {
14+
15+
protected DataverseUser dataverseUser;
16+
protected String keycloakId;
17+
18+
public DataverseUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, DataverseUser dataverseUser) {
19+
super(session, realm, model);
20+
this.dataverseUser = dataverseUser;
21+
keycloakId = StorageId.keycloakId(model, dataverseUser.getBuiltinUser().getId().toString());
22+
}
23+
24+
@Override
25+
public void setUsername(String s) {
26+
}
27+
28+
@Override
29+
public String getUsername() {
30+
return dataverseUser.getBuiltinUser().getUsername();
31+
}
32+
33+
@Override
34+
public String getEmail() {
35+
return dataverseUser.getAuthenticatedUser().getEmail();
36+
}
37+
38+
@Override
39+
public String getFirstName() {
40+
return dataverseUser.getAuthenticatedUser().getFirstName();
41+
}
42+
43+
@Override
44+
public String getLastName() {
45+
return dataverseUser.getAuthenticatedUser().getLastName();
46+
}
47+
48+
@Override
49+
public Stream<GroupModel> getGroupsStream(String search, Integer first, Integer max) {
50+
return super.getGroupsStream(search, first, max);
51+
}
52+
53+
@Override
54+
public long getGroupsCount() {
55+
return super.getGroupsCount();
56+
}
57+
58+
@Override
59+
public long getGroupsCountByNameContaining(String search) {
60+
return super.getGroupsCountByNameContaining(search);
61+
}
62+
63+
@Override
64+
public String getId() {
65+
return keycloakId;
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package edu.harvard.iq.keycloak.auth.spi.models;
2+
3+
import jakarta.persistence.*;
4+
5+
@NamedQueries({
6+
@NamedQuery(name = "DataverseAuthenticatedUser.findByEmail",
7+
query = "select au from DataverseAuthenticatedUser au WHERE LOWER(au.email)=LOWER(:email)"),
8+
@NamedQuery(name = "DataverseAuthenticatedUser.findByIdentifier",
9+
query = "select au from DataverseAuthenticatedUser au WHERE LOWER(au.userIdentifier)=LOWER(:identifier)"),
10+
})
11+
@Entity
12+
@Table(name = "authenticateduser")
13+
public class DataverseAuthenticatedUser {
14+
@Id
15+
private Integer id;
16+
private String email;
17+
private String lastName;
18+
private String firstName;
19+
private String userIdentifier;
20+
21+
public void setId(Integer id) {
22+
this.id = id;
23+
}
24+
25+
public void setEmail(String email) {
26+
this.email = email;
27+
}
28+
29+
public void setUserIdentifier(String userIdentifier) {
30+
this.userIdentifier = userIdentifier;
31+
}
32+
33+
public String getEmail() {
34+
return email;
35+
}
36+
37+
public String getLastName() {
38+
return lastName;
39+
}
40+
41+
public String getFirstName() {
42+
return firstName;
43+
}
44+
45+
public String getUserIdentifier() {
46+
return userIdentifier;
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package edu.harvard.iq.keycloak.auth.spi.models;
2+
3+
import jakarta.persistence.*;
4+
5+
@NamedQueries({
6+
@NamedQuery(name = "DataverseBuiltinUser.findByUsername",
7+
query = "SELECT u FROM DataverseBuiltinUser u WHERE LOWER(u.username)=LOWER(:username)")
8+
})
9+
@Entity
10+
@Table(name = "builtinuser")
11+
public class DataverseBuiltinUser {
12+
@Id
13+
private Integer id;
14+
15+
private String username;
16+
17+
private String encryptedPassword;
18+
19+
private Integer passwordEncryptionVersion;
20+
21+
public void setId(Integer id) {
22+
this.id = id;
23+
}
24+
25+
public void setUsername(String username) {
26+
this.username = username;
27+
}
28+
29+
public Integer getId() {
30+
return id;
31+
}
32+
33+
public String getUsername() {
34+
return username;
35+
}
36+
37+
public Integer getPasswordEncryptionVersion() {
38+
return passwordEncryptionVersion;
39+
}
40+
41+
public void setEncryptedPassword(String encryptedPassword) {
42+
this.encryptedPassword = encryptedPassword;
43+
}
44+
45+
public String getEncryptedPassword() {
46+
return encryptedPassword;
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package edu.harvard.iq.keycloak.auth.spi.models;
2+
3+
public class DataverseUser {
4+
5+
private final DataverseAuthenticatedUser authenticatedUser;
6+
private final DataverseBuiltinUser builtinUser;
7+
8+
public DataverseUser(DataverseAuthenticatedUser authenticatedUser, DataverseBuiltinUser builtinUser) {
9+
this.authenticatedUser = authenticatedUser;
10+
this.builtinUser = builtinUser;
11+
}
12+
13+
public DataverseAuthenticatedUser getAuthenticatedUser() {
14+
return authenticatedUser;
15+
}
16+
17+
public DataverseBuiltinUser getBuiltinUser() {
18+
return builtinUser;
19+
}
20+
}

0 commit comments

Comments
 (0)