Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keycloak SPI for Builtin Users Authentication #11193

Open
wants to merge 43 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2f7e1b3
Stash: building builtin users Keycloak SPI boilerplate (WIP)
GPortas Jan 27, 2025
fa72703
Added: Keycloak SPI base logic to support searching dv builtin users …
GPortas Jan 28, 2025
68fbbb0
Merge branch 'develop' of github.com:IQSS/dataverse into 11157-builti…
GPortas Jan 28, 2025
db34307
Added: DataverseAuthenticatedUser for populating extra information in…
GPortas Jan 29, 2025
bc97f9a
Merge branch 'develop' of github.com:IQSS/dataverse into 11157-builti…
GPortas Jan 29, 2025
72acd63
Added: achieving authentication from Keycloak builtin users SPI throu…
GPortas Feb 2, 2025
d05ea89
Changed: removed UserQueryMethodsProvider impl and minor tweaks and f…
GPortas Feb 2, 2025
59f19c2
Added: email/password builtin users auth
GPortas Feb 2, 2025
1f5b2b7
Refactor: DataverseUserStorageProvider
GPortas Feb 3, 2025
0ed0b79
Refactor: DataverseAPIService
GPortas Feb 3, 2025
c8d8af0
Refactor: DataverseUserStorageProvider
GPortas Feb 3, 2025
b54aeb8
Changed: temporal warning log levels
GPortas Feb 4, 2025
2206e2a
Added: temporal warning log in AuthenticationServiceBean
GPortas Feb 4, 2025
ec76ce3
Changed: querying authenticated users before lookup in OIDC flow
GPortas Feb 4, 2025
a1ea5e0
Changed: temporal disable unit test for PoC
GPortas Feb 4, 2025
87ab989
Changed: reverted log level back to warning in AuthenticationServiceBean
GPortas Feb 4, 2025
6b49842
Changed: log level in AuthenticationServiceBean
GPortas Feb 4, 2025
e60f4a3
Merge branch 'develop' of github.com:IQSS/dataverse into 11157-builti…
GPortas Feb 6, 2025
04f7b39
Added: UserEventListenerProvider logging users login
GPortas Feb 10, 2025
8b0defc
Merge branch 'develop' of github.com:IQSS/dataverse into 11157-builti…
GPortas Feb 11, 2025
267c6e7
Removed: event listeners for a simpler first iteration
GPortas Feb 11, 2025
c49b35d
Changed: parametrized quarkus properties in builtin-users-spi
GPortas Feb 11, 2025
fbfa9c4
Changed: spi folders structure and parametrized DATAVERSE_BASE_URL
GPortas Feb 11, 2025
7abce92
Added: testing dependencies
GPortas Feb 11, 2025
26b5f57
Added: DataverseUserStorageProviderTest
GPortas Feb 11, 2025
9c25eeb
Added: DataverseAPIServiceTest
GPortas Feb 11, 2025
ee6e9f0
Removed: logs from AuthenticationServiceBean
GPortas Feb 11, 2025
6695b79
Changed: AuthenticationServiceBeanTest re-enabled with updates
GPortas Feb 11, 2025
92aaa91
Refactor: AuthenticationServiceBeanTest.setupAuthenticatedUserByIdent…
GPortas Feb 11, 2025
169aa75
Changed: updated oauth2-oidc-sdk and excluded builtin-users-spi from …
GPortas Feb 12, 2025
f137e62
Merge branch 'develop' of github.com:IQSS/dataverse into 11157-builti…
GPortas Feb 14, 2025
7fd32c9
Added: API_BEARER_AUTH_USE_BUILTIN_USER_ON_ID_MATCH feature flag
GPortas Feb 14, 2025
67552e1
Added: docs for api-bearer-auth-use-builtin-user-on-id-match
GPortas Feb 14, 2025
22fc4da
Changed: null check added to AuthenticationServiceBean and updated te…
GPortas Feb 14, 2025
779dfd6
Fixed: docs rst format issue
GPortas Feb 14, 2025
db83ddc
Added: refined implementation and tests for canLoginWithGivenCredenti…
GPortas Feb 14, 2025
321b8f2
Added: custom Keycloak Dockerfile for spi building and realm setup
GPortas Feb 14, 2025
57b3a3b
Added: dev_keycloak_initializer for SPI setup in realm
GPortas Feb 16, 2025
9ddb86b
Added: release notes for #11197
GPortas Feb 17, 2025
2dc196b
Added: API docs for builtin-users/canLoginWithGivenCredentials
GPortas Feb 17, 2025
fdda614
Merge branch 'develop' of github.com:IQSS/dataverse into 11157-builti…
GPortas Feb 17, 2025
2e5dde2
Fixed: maven docker plugin build by setting an image for dev_keycloak
GPortas Feb 17, 2025
3ca4940
Changed: dev keycloak image name
GPortas Feb 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions conf/keycloak/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ------------------------------------------
# Stage 1: Build SPI with Maven
# ------------------------------------------
FROM maven:3.9.5-eclipse-temurin-17 AS builder

WORKDIR /app

# Copy SPI source code
COPY ./builtin-users-spi /app

# Build the SPI JAR
RUN mvn clean package

# ------------------------------------------
# Stage 2: Build Keycloak Image
# ------------------------------------------
FROM quay.io/keycloak/keycloak:22.0

# Copy SPI JAR from builder stage
COPY --from=builder /app/target/keycloak-dv-builtin-users-authenticator-1.0-SNAPSHOT.jar /opt/keycloak/providers/

# Copy additional configurations
COPY ./builtin-users-spi/conf/quarkus.properties /opt/keycloak/conf/
COPY ./test-realm.json /opt/keycloak/data/import/

# Set the Keycloak command
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
CMD ["start-dev", "--import-realm", "--http-port=8090"]

# Expose port 8090
EXPOSE 8090
14 changes: 14 additions & 0 deletions conf/keycloak/builtin-users-spi/conf/quarkus.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
quarkus.datasource.user-store.db-kind=postgresql
quarkus.datasource.user-store.jdbc.url=jdbc:postgresql://${DATAVERSE_DB_HOST}:${DATAVERSE_DB_PORT}/dataverse
quarkus.datasource.user-store.username=${DATAVERSE_DB_USER}
quarkus.datasource.user-store.password=${DATAVERSE_DB_PASSWORD}

quarkus.datasource.user-store.jdbc.driver=org.postgresql.Driver
quarkus.datasource.user-store.jdbc.transactions=disabled

quarkus.datasource.user-store.jdbc.recovery.username=${DATAVERSE_DB_USER}
quarkus.datasource.user-store.jdbc.recovery.password=${DATAVERSE_DB_PASSWORD}

quarkus.datasource.user-store.jdbc.xa-properties.serverName=${DATAVERSE_DB_HOST}
quarkus.datasource.user-store.jdbc.xa-properties.portNumber=${DATAVERSE_DB_PORT}
quarkus.datasource.user-store.jdbc.xa-properties.databaseName=dataverse
87 changes: 87 additions & 0 deletions conf/keycloak/builtin-users-spi/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<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>edu.harvard.iq.keycloak</groupId>
<artifactId>keycloak-dv-builtin-users-authenticator</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<dependencies>
<!-- Keycloak Server SPI -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>

<!-- Keycloak Server SPI Private -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>

<!-- Keycloak Services -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>

<!-- Keycloak Model JPA -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-jpa</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>

<!-- Jakarta Persistence API -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>${jakarta.persistence.version}</version>
</dependency>

<!-- TEST DEPENDENCIES -->

<!-- JUnit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>

<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>16</source>
<target>16</target>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 for these?

</configuration>
</plugin>
</plugins>
</build>

<properties>
<keycloak.version>22.0.0</keycloak.version>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW: This is pretty old now. 26.1.0 is current and I think versions earlier than 24.x have sec. issues. Perhaps we need an issue to update the Keycloak in use?

<java.version>17</java.version>
<jakarta.persistence.version>3.2.0</jakarta.persistence.version>
<mockito.version>5.15.2</mockito.version>
<junit.jupiter.version>5.11.4</junit.jupiter.version>
</properties>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package edu.harvard.iq.keycloak.auth.spi.adapters;

import edu.harvard.iq.keycloak.auth.spi.models.DataverseAuthenticatedUser;
import edu.harvard.iq.keycloak.auth.spi.models.DataverseBuiltinUser;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage;

import java.util.stream.Stream;

public class DataverseUserAdapter extends AbstractUserAdapterFederatedStorage {

protected DataverseBuiltinUser builtinUser;
protected DataverseAuthenticatedUser authenticatedUser;
protected String keycloakId;

public DataverseUserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, DataverseBuiltinUser builtinUser, DataverseAuthenticatedUser authenticatedUser) {
super(session, realm, model);
this.builtinUser = builtinUser;
this.authenticatedUser = authenticatedUser;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I could see an alternate design in which there's just one user class that joins from the two tables. As coded now, that logic is in the Java instead, so more code, but more flexibility if we ever did need to map other user types in the future? Probably not worth changing at this point.

keycloakId = StorageId.keycloakId(model, builtinUser.getId().toString());
}

@Override
public void setUsername(String s) {
}

@Override
public String getUsername() {
return builtinUser.getUsername();
}

@Override
public String getEmail() {
return authenticatedUser.getEmail();
}

@Override
public String getFirstName() {
return authenticatedUser.getFirstName();
}

@Override
public String getLastName() {
return authenticatedUser.getLastName();
}

@Override
public Stream<GroupModel> getGroupsStream(String search, Integer first, Integer max) {
return super.getGroupsStream(search, first, max);
}

@Override
public long getGroupsCount() {
return super.getGroupsCount();
}

@Override
public long getGroupsCountByNameContaining(String search) {
return super.getGroupsCountByNameContaining(search);
}

@Override
public String getId() {
return keycloakId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package edu.harvard.iq.keycloak.auth.spi.models;

import jakarta.persistence.*;

@NamedQueries({
@NamedQuery(name = "DataverseAuthenticatedUser.findByEmail",
query = "select au from DataverseAuthenticatedUser au WHERE LOWER(au.email)=LOWER(:email)"),
@NamedQuery(name = "DataverseAuthenticatedUser.findByIdentifier",
query = "select au from DataverseAuthenticatedUser au WHERE LOWER(au.userIdentifier)=LOWER(:identifier)"),
})
@Entity
@Table(name = "authenticateduser")
public class DataverseAuthenticatedUser {
@Id
private Integer id;
private String email;
private String lastName;
private String firstName;
private String userIdentifier;

public void setId(Integer id) {
this.id = id;
}

public void setEmail(String email) {
this.email = email;
}

public void setUserIdentifier(String userIdentifier) {
this.userIdentifier = userIdentifier;
}

public String getEmail() {
return email;
}

public String getLastName() {
return lastName;
}

public String getFirstName() {
return firstName;
}

public String getUserIdentifier() {
return userIdentifier;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package edu.harvard.iq.keycloak.auth.spi.models;

import jakarta.persistence.*;

@NamedQueries({
@NamedQuery(name = "DataverseBuiltinUser.findByUsername",
query = "SELECT u FROM DataverseBuiltinUser u WHERE LOWER(u.username)=LOWER(:username)")
})
@Entity
@Table(name = "builtinuser")
public class DataverseBuiltinUser {
@Id
private Integer id;

private String username;

public void setId(Integer id) {
this.id = id;
}

public void setUsername(String username) {
this.username = username;
}

public Integer getId() {
return id;
}

public String getUsername() {
return username;
}
}
Loading
Loading