diff --git a/conf/keycloak/test-realm.json b/conf/keycloak/test-realm.json index 5dc0bd6d6ee..efe71cc5d29 100644 --- a/conf/keycloak/test-realm.json +++ b/conf/keycloak/test-realm.json @@ -398,7 +398,7 @@ "emailVerified" : true, "firstName" : "Dataverse", "lastName" : "Admin", - "email" : "dataverse@mailinator.com", + "email" : "dataverse-admin@mailinator.com", "credentials" : [ { "id" : "28f1ece7-26fb-40f1-9174-5ffce7b85c0a", "type" : "password", diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 4b1bdbc02e9..e5cc5f4fa15 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -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; @@ -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; @@ -237,9 +236,6 @@ String getWrappedMessageWhenJson() { @EJB GuestbookResponseServiceBean gbRespSvc; - @Inject - OpenIdContext openIdContext; - @PersistenceContext(unitName = "VDCNet-ejbPU") protected EntityManager em; @@ -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()); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index 22ccf859361..c0c7d7a8286 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -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; @@ -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"); } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java index 044526a1354..4016fbacb6a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java @@ -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; @@ -38,10 +39,10 @@ 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); @@ -49,8 +50,8 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) } 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. @@ -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 providers = authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream() @@ -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; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java index 59f659ff297..8f41f80053a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java @@ -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; @@ -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; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java index 42eb5d33695..c1851134145 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java @@ -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; @@ -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(); } @@ -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; @@ -132,17 +146,25 @@ 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 providers = authenticationSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class) .stream() @@ -150,10 +172,20 @@ public void storeBearerToken() { .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; } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java index 29f86d67ff5..b79683869f6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java @@ -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; @@ -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 verifierCache = Caffeine.newBuilder() + private final Cache 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)) @@ -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); } @@ -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); } }