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

Draft
wants to merge 18 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 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
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
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://postgres:5432/dataverse
quarkus.datasource.user-store.username=${DATAVERSE_DB_USER}
quarkus.datasource.user-store.password=secret

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=secret

quarkus.datasource.user-store.jdbc.xa-properties.serverName=postgres
quarkus.datasource.user-store.jdbc.xa-properties.portNumber=5432
quarkus.datasource.user-store.jdbc.xa-properties.databaseName=dataverse
67 changes: 67 additions & 0 deletions conf/keycloak/builtin-users-spi/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>16</source>
<target>16</target>
</configuration>
</plugin>
</plugins>
</build>

<properties>
<keycloak.version>22.0.0</keycloak.version>
<java.version>17</java.version>
<jakarta.persistence.version>3.2.0</jakarta.persistence.version>
</properties>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package edu.harvard.iq.keycloak.auth.spi;

import org.jboss.logging.Logger;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
* Provides API interaction methods for Dataverse authentication.
*/
public class DataverseAPIService {

private static final Logger logger = Logger.getLogger(DataverseAPIService.class);
private static final String DATAVERSE_API_URL = "http://dataverse:8080/api/builtin-users/%s/canLoginWithGivenCredentials?password=%s";

/**
* Validates if a Dataverse built-in user can log in with the given credentials.
*
* @param username The username of the Dataverse built-in user.
* @param password The password to be validated.
* @return {@code true} if the user can log in, {@code false} otherwise.
*/
public static boolean canLogInAsBuiltinUser(String username, String password) {
HttpURLConnection connection = null;

try {
String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8);
String encodedPassword = URLEncoder.encode(password, StandardCharsets.UTF_8);
String requestUrl = String.format(DATAVERSE_API_URL, encodedUsername, encodedPassword);

URL url = new URL(requestUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/json");

int responseCode = connection.getResponseCode();
logger.infof("Dataverse API response code for user '%s': %d", username, responseCode);

return responseCode == HttpURLConnection.HTTP_OK;
} catch (IOException e) {
logger.errorf(e, "Error occurred while validating login for user '%s'", username);
return false;
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package edu.harvard.iq.keycloak.auth.spi;

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 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,24 @@
package edu.harvard.iq.keycloak.auth.spi;

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 Integer getId() {
return id;
}

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

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;
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;
}
}
Loading