From 3694d8119b6ba977c7aa6459c3d5a78b72fe3171 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 14 Dec 2022 22:28:14 +0100 Subject: [PATCH] feat(api): query all avail OIDC providers for user info #9229 Replace the placeholder of static endpoint with retrieving the UserInfo endpoint URI from all know OIDC authentication providers and iterate over them. The access token might match for any of them. Also making the errors a bit more descriptive and adding logging. --- .../iq/dataverse/api/AbstractApiBean.java | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) 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 9c70ffd4d3c..e9bd06323ca 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -32,10 +32,12 @@ import edu.harvard.iq.dataverse.UserNotificationServiceBean; import edu.harvard.iq.dataverse.UserServiceBean; import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; +import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; 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.groups.GroupServiceBean; +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.GuestUser; import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; @@ -68,11 +70,13 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.ejb.EJB; import javax.ejb.EJBException; import javax.json.Json; @@ -444,6 +448,8 @@ private AuthenticatedUser findAuthenticatedUserOrDie( String key, String wfid ) // TODO: Only usable for OIDC users for now, just look it up via the subject. // This will need to be modified to provide mappings somehow for existing non-OIDC-users. + // TODO: If we keep the current login infrastructure alive, we should introduce a common static + // method in OIDCAuthProvider to create the identifier in both places. AuthenticatedUser authUser = authSvc.getAuthenticatedUser(userInfo.getSubject().getValue()); // TODO: this is code dup par excellence. Needs refactoring. Maybe fine for Proof-of-Concept. @@ -499,7 +505,6 @@ Optional getOidcBearerToken(HttpServletRequest request) { * TODO: This is a proof of concept, providing value for IQSS#9229 and first steps for our SPA move. It ... * - will need more tweaks (see inline comments), * - should be extended to support JWT access tokens to avoid the extra detour to the OIDC provider, - * - will need a feature flag activation either here or in calling code, * - needs to be moved to a distinct place when we head for authentication filters in future iterations. *

* @@ -509,30 +514,50 @@ Optional getOidcBearerToken(HttpServletRequest request) { UserInfo verifyOidcBearerToken(String token) throws WrappedResponse { try { BearerAccessToken accessToken = BearerAccessToken.parse(token); + + // Get list of all authentication providers using Open ID Connect + List providers = authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream() + .map(providerId -> (OIDCAuthProvider) authSvc.getAuthenticationProvider(providerId)) + .collect(Collectors.toUnmodifiableList()); + + // Iterate over all OIDC providers if multiple. + for (OIDCAuthProvider provider : providers) { - // Retrieve data of the user accessing the API from the provider. - // No need to introspect the token here, the userInfoRequest also validates the token, as the provider - // is the source of truth. - // TODO: retrieve the userinfo endpoint from the OIDC provider already present via AuthenticationServiceBean - HTTPResponse response = new UserInfoRequest(URI.create("http://localhost:8090/realms/master/protocol/openid-connect/userinfo"), accessToken) - .toHTTPRequest() - .send(); - - UserInfoResponse infoResponse = UserInfoResponse.parse(response); + // Retrieve data of the user accessing the API from the provider. + // No need to introspect the token here, the userInfoRequest also validates the token, as the provider + // is the source of truth. + try { + HTTPResponse response = new UserInfoRequest(provider.getUserInfoEndpointURI(), accessToken) + .toHTTPRequest() + .send(); - // If error, throw 401 error exception - if (! infoResponse.indicatesSuccess() ) { - ErrorObject error = infoResponse.toErrorResponse().getErrorObject(); - // TODO: make the response more explaining? - throw new WrappedResponse(error(Status.UNAUTHORIZED, "Could not retrieve user info from provider")); - } + UserInfoResponse infoResponse = UserInfoResponse.parse(response); - // Success --> return info - return infoResponse.toSuccessResponse().getUserInfo(); - } catch (ParseException | IOException e) { - // TODO: make the response more explaining - throw new WrappedResponse(error(Status.UNAUTHORIZED, "Could not retrieve user info from provider")); + // If error, throw 401 error exception + if (! infoResponse.indicatesSuccess() ) { + ErrorObject error = infoResponse.toErrorResponse().getErrorObject(); + logger.log(Level.FINE, + "UserInfo could not be retrieved by access token from provider {0}: {1}", + new String[]{provider.getId(), error.getDescription()}); + // Success, simply return the user info + } else { + return infoResponse.toSuccessResponse().getUserInfo(); + } + } catch (ParseException | IOException e) { + logger.log(Level.WARNING, + "Could not retrieve user info for provider " + provider.getId() + ", skipping", e); + } + } + } catch (ParseException e) { + logger.log(Level.FINE, "Could not parse bearer access token", e); + throw new WrappedResponse(error(Status.UNAUTHORIZED, "Could not parse bearer access token")); } + + // No UserInfo returned means we have an invalid access token. (It could also mean we have no OIDC + // provider, but this would also mean this is an invalid request, as there will be no user available...) + // TODO: Should this include more details about the request? + logger.log(Level.FINE, "Unauthorized bearer access token detected"); + throw new WrappedResponse(error(Status.UNAUTHORIZED, "Unauthorized bearer access token")); } protected Dataverse findDataverseOrDie( String dvIdtf ) throws WrappedResponse {