Skip to content

Commit

Permalink
redirect to first log in page and user lookup by user record identifier
Browse files Browse the repository at this point in the history
  • Loading branch information
ErykKul committed Oct 5, 2024
1 parent 61703e8 commit 004613f
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 67 deletions.
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" : "[email protected]",
"email" : "dataverse-admin@mailinator.com",
"credentials" : [ {
"id" : "28f1ece7-26fb-40f1-9174-5ffce7b85c0a",
"type" : "password",
Expand Down
23 changes: 9 additions & 14 deletions src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.DataverseRole;
import edu.harvard.iq.dataverse.authorization.RoleAssignee;
import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier;
import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.OIDCLoginBackingBean;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
Expand Down Expand Up @@ -37,10 +38,8 @@
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 @@ -237,9 +236,6 @@ String getWrappedMessageWhenJson() {
@EJB
GuestbookResponseServiceBean gbRespSvc;

@Inject
OpenIdContext openIdContext;

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

Expand Down Expand Up @@ -331,15 +327,14 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon
if (requestUser.isAuthenticated()) {
return (AuthenticatedUser) requestUser;
} else {
try {
final String email = oidcLoginBackingBean.getVerifiedEmail();
final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email);
if (authUser != null) {
return authUser;
} else {
throw new WrappedResponse(authenticatedUserRequired());
}
} catch (final Exception ignore) {
final UserRecordIdentifier userRecordIdentifier = oidcLoginBackingBean.getUserRecordIdentifier();
if (userRecordIdentifier == null) {
throw new WrappedResponse(authenticatedUserRequired());
}
final AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier);
if (authUser != null) {
return authUser;
} else {
throw new WrappedResponse(authenticatedUserRequired());
}
}
Expand Down
26 changes: 17 additions & 9 deletions src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.net.URI;

import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.OIDCLoginBackingBean;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.util.SystemConfig;
Expand Down Expand Up @@ -67,18 +68,25 @@ public Response token(@Context ContainerRequestContext crc) {
@Path("session")
@GET
public Response session(@Context ContainerRequestContext crc) {
final String email = oidcLoginBackingBean.getVerifiedEmail();
final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email);
final UserRecordIdentifier userRecordIdentifier = oidcLoginBackingBean.getUserRecordIdentifier();
if (userRecordIdentifier == null) {
return notFound("user record identifier not found");
}
final AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier);
if (authUser != null) {
oidcLoginBackingBean.storeBearerToken();
return ok(
jsonObjectBuilder()
.add("user", authUser.toJson())
.add("session", crc.getCookies().get("JSESSIONID").getValue())
.add("accessToken", openIdContext.getAccessToken().getToken())
.add("identityToken", openIdContext.getIdentityToken().getToken()));
try {
return ok(
jsonObjectBuilder()
.add("user", authUser.toJson())
.add("session", crc.getCookies().get("JSESSIONID").getValue())
.add("accessToken", openIdContext.getAccessToken().getToken())
.add("identityToken", openIdContext.getIdentityToken().getToken()));
} catch (Exception e) {
return badRequest(e.getMessage());
}
} else {
return notFound("user with email " + email + " not found");
return notFound("user with record identifier " + userRecordIdentifier + " not found");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import edu.harvard.iq.dataverse.UserServiceBean;
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.authorization.users.User;
Expand Down Expand Up @@ -38,19 +39,19 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext)
}

// Validate and verify provided Bearer Token, and retrieve email
String verifiedEmail = getVerifiedEmail(bearerToken.get());
UserRecordIdentifier userRecordIdentifier = getUserRecordIdentifier(bearerToken.get());

// retrieve Authenticated User from AuthService
AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(verifiedEmail);
AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier);
if (authUser != null) {
// track the API usage
authUser = userSvc.updateLastApiUseTime(authUser);
return authUser;
} else {
// a valid Token was presented, but we have no associated user account.
logger.log(Level.WARNING,
"Bearer token detected, OIDC provider found verified email {0} but no linked UserAccount",
verifiedEmail);
"Bearer token detected, OIDC provider found user record identifier {0} but no linked UserAccount",
userRecordIdentifier);
// TODO: Instead of returning null, we should throw a meaningful error to the
// client. Probably this will be a wrapped auth error response with an error
// code and a string describing the problem.
Expand All @@ -67,7 +68,7 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext)
* @param token The string containing the encoded JWT
* @return
*/
private String getVerifiedEmail(String token) throws WrappedAuthErrorResponse {
private UserRecordIdentifier getUserRecordIdentifier(String token) throws WrappedAuthErrorResponse {
// Get list of all authentication providers using Open ID Connect
// @TASK: Limited to OIDCAuthProviders, could be widened to OAuth2Providers.
List<OIDCAuthProvider> providers = authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream()
Expand All @@ -82,11 +83,11 @@ private String getVerifiedEmail(String token) throws WrappedAuthErrorResponse {
// Iterate over all OIDC providers if multiple. Sadly needed as do not know
// which provided the Token.
for (OIDCAuthProvider provider : providers) {
final String email = provider.getVerifiedEmail(token);
if (email != null) {
final UserRecordIdentifier userRecordIdentifier = provider.getUserRecordIdentifier(token);
if (userRecordIdentifier != null) {
logger.log(Level.FINE, "Bearer token detected, provider {0} confirmed validity and provided identifier",
provider.getId());
return email;
return userRecordIdentifier;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.github.scribejava.core.model.OAuth2AccessToken;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import fish.payara.security.openid.api.OpenIdContext;

import java.io.Serializable;
import java.sql.Timestamp;
import jakarta.persistence.Column;
Expand Down Expand Up @@ -83,6 +85,19 @@ public static OAuth2TokenData from( OAuth2AccessToken accessTokenResponse ) {

return retVal;
}

public static OAuth2TokenData from(OpenIdContext openIdContext) {
OAuth2TokenData retVal = new OAuth2TokenData();
retVal.setAccessToken(openIdContext.getAccessToken().getToken());
//retVal.setRefreshToken(openIdContext.getRefreshToken().isPresent() ? openIdContext.getRefreshToken().get().getToken() : null);
retVal.setRefreshToken("too long > 64 chars");
retVal.setTokenType(openIdContext.getTokenType());
if (openIdContext.getExpiresIn().isPresent()) {
retVal.setExpiryDate( new Timestamp(System.currentTimeMillis() + openIdContext.getExpiresIn().get()));
}
retVal.setRawResponse("Not Applicable");
return retVal;
}

public Long getId() {
return id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
import edu.harvard.iq.dataverse.DataverseSession;
import edu.harvard.iq.dataverse.UserServiceBean;
import edu.harvard.iq.dataverse.api.OpenIDConfigBean;
import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo;
import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider;
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.settings.FeatureFlags;
import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier;
import edu.harvard.iq.dataverse.util.SystemConfig;
import fish.payara.security.openid.api.JwtClaims;
import fish.payara.security.openid.api.OpenIdConstant;
import fish.payara.security.openid.api.OpenIdContext;
import jakarta.ejb.EJB;
Expand Down Expand Up @@ -62,8 +65,8 @@ public class OIDCLoginBackingBean implements Serializable {
*/
public String getLogInLink(final OIDCAuthProvider oidcAuthProvider) {
oidcAuthProvider.setConfig(openIdConfigBean);
final String email = getVerifiedEmail();
if (email != null) {
final UserRecordIdentifier userRecordIdentifier = getUserRecordIdentifier();
if (userRecordIdentifier != null) {
setUser();
return SystemConfig.getDataverseSiteUrlStatic();
}
Expand All @@ -77,32 +80,43 @@ public String getLogInLink(final OIDCAuthProvider oidcAuthProvider) {
* @throws IOException
*/
public void setUser() {
final String email = getVerifiedEmail();
AuthenticatedUser dvUser = authenticationSvc.getAuthenticatedUserByEmail(email);
if (dvUser == null) {
logger.log(Level.INFO, "user not found: " + email);
if (!systemConfig.isSignupDisabledForRemoteAuthProvider("oidc-mpconfig")) {
logger.log(Level.INFO, "redirect to first login: " + email);
/*
* final OAuth2UserRecord userRecord = OAuth2UserRecord("oidc",
* parsed.userIdInProvider,
* openIdContext.getSubject(),
* OAuth2TokenData.from(openIdContext.getAccessToken()),
* parsed.displayInfo,
* parsed.emails);
* newAccountPage.setNewUser(userRecord);
*/
Faces.redirect("/oauth2/firstLogin.xhtml");
try {
final String subject = openIdContext.getSubject();
final OIDCAuthProvider provider = getProvider();
final UserRecordIdentifier userRecordIdentifier = new UserRecordIdentifier(provider.getId(), subject);
AuthenticatedUser dvUser = authenticationSvc.lookupUser(userRecordIdentifier);
if (dvUser == null) {
if (!systemConfig.isSignupDisabledForRemoteAuthProvider(provider.getId())) {
final JwtClaims claims = openIdContext.getIdentityToken().getJwtClaims();
final String firstName = claims.getStringClaim(OpenIdConstant.GIVEN_NAME).orElse("");
final String lastName = claims.getStringClaim(OpenIdConstant.FAMILY_NAME).orElse("");
final String verifiedEmailAddress = getVerifiedEmail();
final String emailAddress = verifiedEmailAddress == null ? "" : verifiedEmailAddress;
final String affiliation = claims.getStringClaim("affiliation").orElse("");
final String position = claims.getStringClaim("position").orElse("");
final OAuth2UserRecord userRecord = new OAuth2UserRecord(
provider.getId(),
subject,
claims.getStringClaim(OpenIdConstant.PREFERRED_USERNAME).orElse(subject),
OAuth2TokenData.from(openIdContext),
new AuthenticatedUserDisplayInfo(firstName, lastName, emailAddress, affiliation, position),
List.of(emailAddress));
logger.log(Level.INFO, "redirect to first login: " + userRecordIdentifier);
newAccountPage.setNewUser(userRecord);
Faces.redirect("/oauth2/firstLogin.xhtml");
}
} else {
dvUser = userService.updateLastLogin(dvUser);
session.setUser(dvUser);
storeBearerToken();
Faces.redirect("/");
}
} else {
dvUser = userService.updateLastLogin(dvUser);
session.setUser(dvUser);
storeBearerToken();
Faces.redirect("/");
} catch (Exception e) {
logger.log(Level.SEVERE, "Setting user failed: " + e.getMessage());
}
}

public String getVerifiedEmail() {
private String getVerifiedEmail() {
try {
if (openIdContext.getAccessToken().isExpired()) {
return null;
Expand Down Expand Up @@ -132,28 +146,46 @@ public void storeBearerToken() {
if (!FeatureFlags.API_BEARER_AUTH.enabled()) {
return;
}
final String email = getVerifiedEmail();
if (email == null) {
logger.log(Level.WARNING, "Could not store bearer token, verified email not found");
try {
final OIDCAuthProvider provider = getProvider();
final String subject = openIdContext.getSubject();
final UserRecordIdentifier userRecordIdentifier = new UserRecordIdentifier(provider.getId(), subject);
final String token = openIdContext.getAccessToken().getToken();
provider.storeBearerToken(token, userRecordIdentifier);
} catch (Exception e) {
logger.log(Level.SEVERE, "Storing token failed: " + e.getMessage());
}
}

private OIDCAuthProvider getProvider() {
final String issuerEndpointURL = openIdContext.getIdentityToken().getJwtClaims()
.getStringClaim(OpenIdConstant.ISSUER_IDENTIFIER)
.orElse(null);
if (issuerEndpointURL == null) {
logger.log(Level.SEVERE,
"Issuer URL (iss) not found in " + openIdContext.getIdentityToken().getJwtClaims().toString());
return;
return null;
}
List<OIDCAuthProvider> providers = authenticationSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)
.stream()
.map(providerId -> (OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(providerId))
.filter(provider -> issuerEndpointURL.equals(provider.getIssuerEndpointURL()))
.collect(Collectors.toUnmodifiableList());
if (providers.isEmpty()) {
logger.log(Level.WARNING, "OIDC provider not found for URL: " + issuerEndpointURL);
logger.log(Level.SEVERE, "OIDC provider not found for URL: " + issuerEndpointURL);
return null;
} else {
final String token = openIdContext.getAccessToken().getToken();
providers.get(0).storeBearerToken(token, email);
return providers.get(0);
}
}

public UserRecordIdentifier getUserRecordIdentifier() {
try {
final String subject = openIdContext.getSubject();
final OIDCAuthProvider provider = getProvider();
return new UserRecordIdentifier(provider.getId(), subject);
} catch (final Exception ignore) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.github.scribejava.core.builder.api.DefaultApi20;

import edu.harvard.iq.dataverse.api.OpenIDConfigBean;
import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider;
import edu.harvard.iq.dataverse.settings.JvmSettings;

Expand All @@ -27,7 +28,7 @@ public class OIDCAuthProvider extends AbstractOAuth2AuthenticationProvider {
* To be absolutely sure this may not be abused to DDoS us and not let unused
* verifiers rot, use an evicting cache implementation and not a standard map.
*/
private final Cache<String, String> verifierCache = Caffeine.newBuilder()
private final Cache<String, UserRecordIdentifier> verifierCache = Caffeine.newBuilder()
.maximumSize(JvmSettings.OIDC_BEARER_CACHE_MAXSIZE.lookup(Integer.class))
.expireAfterWrite(
Duration.of(JvmSettings.OIDC_BEARER_CACHE_MAXAGE.lookup(Integer.class), ChronoUnit.SECONDS))
Expand Down Expand Up @@ -92,7 +93,7 @@ protected ParsedUserResponse parseUserResponse(String responseBody) {
* @param accessToken The access token
* @return Returns an email if found
*/
public String getVerifiedEmail(String accessToken) {
public UserRecordIdentifier getUserRecordIdentifier(String accessToken) {
return this.verifierCache.getIfPresent(accessToken);
}

Expand All @@ -102,7 +103,7 @@ public String getVerifiedEmail(String accessToken) {
* @param accessToken The access token
* @param email The email
*/
public void storeBearerToken(String accessToken, String email) {
this.verifierCache.put(accessToken, email);
public void storeBearerToken(String accessToken, UserRecordIdentifier userRecordIdentifier) {
this.verifierCache.put(accessToken, userRecordIdentifier);
}
}

0 comments on commit 004613f

Please sign in to comment.