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

API OIDC authentication mechanism #10905

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
2 changes: 1 addition & 1 deletion conf/keycloak/test-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@
"emailVerified" : true,
"firstName" : "Dataverse",
"lastName" : "Admin",
"email" : "dataverse-admin@mailinator.com",
"email" : "[email protected]",
Copy link
Member

Choose a reason for hiding this comment

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

Can you please remind me why this change is necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We talked about matching users from OIDC to Dataverse users multiple times at LIBIS. We feel like matching users by verified emails would be the best approach. The user with "[email protected]" does not exist in the dev env, so it did not work. Matching by email would mean that you can log in with google, facebook, whatever, as long as you use the same email.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I reverted it and implemented the regular UserRecordIdentifier lookup, however, user is not known:
image

I have also implemented the redirect to the first login page if user is not known:
image

After that, I can log in:
image

Is it as it supposed to be? I have also noticed that oauth tokens are stored in the DB. The side effect of creating a new user after OIDC authentication is that it inserts one token there...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I can just pass null as token data to prevent storing it in DB. I am not sure why we store them, I assume it is like the cache for bearer tokens? Something to investigate a little bit more.

"credentials" : [ {
"id" : "28f1ece7-26fb-40f1-9174-5ffce7b85c0a",
"type" : "password",
Expand Down
4 changes: 4 additions & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ services:
DATAVERSE_AUTH_OIDC_CLIENT_ID: test
DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8
DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test
DATAVERSE_AUTH_API_OIDC_CLIENT_ID: test
DATAVERSE_AUTH_API_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8
DATAVERSE_AUTH_API_OIDC_PROVIDER_URI: http://keycloak.mydomain.com:8090/realms/test
DATAVERSE_AUTH_API_OIDC_REDIRECT_URI: http://localhost:8080/api/v1/callback/token
DATAVERSE_SPI_EXPORTERS_DIRECTORY: "/dv/exporters"
# These two oai settings are here to get HarvestingServerIT to pass
dataverse_oai_server_maxidentifiers: "2"
Expand Down
17 changes: 16 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
import edu.harvard.iq.dataverse.util.json.JsonUtil;
import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder;
import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean;
import fish.payara.security.openid.api.OpenIdContext;
import jakarta.ejb.EJB;
import jakarta.ejb.EJBException;
import jakarta.inject.Inject;
import jakarta.json.*;
import jakarta.json.JsonValue.ValueType;
import jakarta.persistence.EntityManager;
Expand Down Expand Up @@ -234,6 +236,9 @@ String getWrappedMessageWhenJson() {
@EJB
GuestbookResponseServiceBean gbRespSvc;

@Inject
OpenIdContext openIdContext;

@PersistenceContext(unitName = "VDCNet-ejbPU")
protected EntityManager em;

Expand Down Expand Up @@ -322,7 +327,17 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon
if (requestUser.isAuthenticated()) {
return (AuthenticatedUser) requestUser;
} else {
throw new WrappedResponse(authenticatedUserRequired());
try {
final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null);
final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email);
if (authUser != null) {
return authUser;
} else {
throw new WrappedResponse(authenticatedUserRequired());
}
} catch (final Exception ignore) {
throw new WrappedResponse(authenticatedUserRequired());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package edu.harvard.iq.dataverse.api;

import java.io.IOException;

import fish.payara.security.annotations.OpenIdAuthenticationDefinition;
import jakarta.annotation.security.DeclareRoles;
import jakarta.ejb.EJB;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.HttpConstraint;
import jakarta.servlet.annotation.ServletSecurity;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/oidc/login")
@OpenIdAuthenticationDefinition(
providerURI="#{openIdConfigBean.providerURI}",
clientId="#{openIdConfigBean.clientId}",
clientSecret="#{openIdConfigBean.clientSecret}",
redirectURI="#{openIdConfigBean.redirectURI}",
scope="email"
)
@DeclareRoles("all")
@ServletSecurity(@HttpConstraint(rolesAllowed = "all"))
public class OpenIDAuthentication extends HttpServlet {
@EJB
OpenIDConfigBean openIdConfigBean;

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().println("...");
ErykKul marked this conversation as resolved.
Show resolved Hide resolved
}
}
49 changes: 49 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package edu.harvard.iq.dataverse.api;

import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder;
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import fish.payara.security.openid.api.OpenIdContext;
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.json.JsonObjectBuilder;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;

@Stateless
@Path("callback")
public class OpenIDCallback extends AbstractApiBean {
@Inject
OpenIdContext openIdContext;

@Inject
protected AuthenticationServiceBean authSvc;

@Path("token")
Copy link
Member

Choose a reason for hiding this comment

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

Docs please! (API Guide.)

@GET
public Response token(@Context ContainerRequestContext crc) {
return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("callback/session")).build();
}

@Path("session")
Copy link
Member

Choose a reason for hiding this comment

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

Docs here too, please.

@GET
public Response session(@Context ContainerRequestContext crc) {
try {
final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null);
final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email);
if (authUser != null) {
return ok(
jsonObjectBuilder()
.add("user", authUser.toJson())
.add("session", crc.getCookies().get("JSESSIONID").getValue()));
} else {
return notFound("user with email " + email + " not found");
}
} catch (final Exception ignore) {
return authenticatedUserRequired();
}
}
}
25 changes: 25 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package edu.harvard.iq.dataverse.api;

import edu.harvard.iq.dataverse.settings.JvmSettings;
import jakarta.ejb.Stateless;
import jakarta.inject.Named;

@Stateless
@Named("openIdConfigBean")
public class OpenIDConfigBean implements java.io.Serializable {
public String getProviderURI() {
return JvmSettings.API_OIDC_PROVIDER_URI.lookup();
}

public String getClientId() {
return JvmSettings.API_OIDC_CLIENT_ID.lookup();
}

public String getClientSecret() {
return JvmSettings.API_OIDC_CLIENT_SECRET.lookup();
}

public String getRedirectURI() {
return JvmSettings.API_OIDC_REDIRECT_URI.lookup();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,13 @@ public enum JvmSettings {
OIDC_PKCE_METHOD(SCOPE_OIDC_PKCE, "method"),
OIDC_PKCE_CACHE_MAXSIZE(SCOPE_OIDC_PKCE, "max-cache-size"),
OIDC_PKCE_CACHE_MAXAGE(SCOPE_OIDC_PKCE, "max-cache-age"),
// AUTH: OPEN_ID SETTINGS
SCOPE_AUTH_API(SCOPE_AUTH, "api"),
SCOPE_OPEN_ID(SCOPE_AUTH_API, "oidc"),
API_OIDC_PROVIDER_URI(SCOPE_OPEN_ID, "provider-uri"),
API_OIDC_CLIENT_ID(SCOPE_OPEN_ID, "client-id"),
API_OIDC_CLIENT_SECRET(SCOPE_OPEN_ID, "client-secret"),
API_OIDC_REDIRECT_URI(SCOPE_OPEN_ID, "redirect-uri"),
ErykKul marked this conversation as resolved.
Show resolved Hide resolved

// UI SETTINGS
SCOPE_UI(PREFIX, "ui"),
Expand Down
Loading