From 9bc29edf7a2e3beb0458465ece966591b91cc326 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 14 Nov 2019 00:10:43 +0100 Subject: [PATCH 1/8] Annotate OAuth2AuthenticationProviderFactory.parseFactoryData() with TODO about using real JSON --- .../providers/oauth2/OAuth2AuthenticationProviderFactory.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProviderFactory.java index 594ee6f718d..bb8f4dcbf0d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProviderFactory.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProviderFactory.java @@ -64,6 +64,7 @@ public String getInfo() { /** * Expected map format.: {@code name: value|name: value|...} + * TODO: this should be refactored to use proper JSON objects ("dicts") instead of custom string format. * * @param factoryData * @return A map of the factory data. From 4f2d0b3462ad575152979c2f9e075beaf995ee5f Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Thu, 14 Nov 2019 00:11:55 +0100 Subject: [PATCH 2/8] Make OAuth2AuthenticationProviderFactory.parseFactoryData() public + static for reuse in OIDC factory --- .../providers/oauth2/OAuth2AuthenticationProviderFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProviderFactory.java index bb8f4dcbf0d..360e2c53e92 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProviderFactory.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2AuthenticationProviderFactory.java @@ -65,11 +65,12 @@ public String getInfo() { /** * Expected map format.: {@code name: value|name: value|...} * TODO: this should be refactored to use proper JSON objects ("dicts") instead of custom string format. + * TODO: this should be included in some base class when refactoring the package to be about token flow based auth * * @param factoryData * @return A map of the factory data. */ - protected Map parseFactoryData(String factoryData) { + public static Map parseFactoryData(String factoryData) { return Arrays.asList(factoryData.split("\\|")).stream() .map(s -> s.split(":", 2)) .filter(p -> p.length == 2) From 96d70552ac7108a9f611e5e63eef83b2bea808dc Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 15 Nov 2019 11:26:45 +0100 Subject: [PATCH 3/8] Annotate AbstractOAuth2AuthenticationProvider with TODOs for refactoring --- .../oauth2/AbstractOAuth2AuthenticationProvider.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java index 9671621ac73..382259cf734 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java @@ -22,6 +22,8 @@ /** * Base class for OAuth2 identity providers, such as GitHub and ORCiD. + * + * TODO: this really should become an interface (contract with {@link OAuth2LoginBackingBean}) when refactoring package * * @author michael */ @@ -96,6 +98,9 @@ public String toString() { */ protected List scope = Arrays.asList(""); + /** + * TODO: when refactoring the package to be about token flow auth, this hard dependency should be removed. + */ public abstract DefaultApi20 getApiInstance(); protected abstract ParsedUserResponse parseUserResponse( String responseBody ); From 3fd326c23e6a2ee6bcc850448178d6381fa4873f Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Fri, 15 Nov 2019 12:15:13 +0100 Subject: [PATCH 4/8] Refactor LoginPage.listAuthenticationProviders() to represent actual provider configuration. Before this refactoring, a static list of providers has been used, no matter if they where enabled or not. This is not acceptable for the new OIDC provider, as we might have multiple of 'em and it retrieves metadata on creation. So the idea was to not create some static list of IDs (which had been used to look up the real provider from the "registry"), but instead get the registred providers, sort them and retrieve the display info. Sorting is done by a numerical value first, then by id attribute (which is set by factories), so we have a deterministic, unchanging order for the user. This lead to the creation of a new method AuthenticationProvider.getOrder(), defaulting to 1 and overridden by implementations. This opens up the possibility to change ordering by configuration (as suggested in an old TODO comment of the refactored code) --- .../edu/harvard/iq/dataverse/LoginPage.java | 26 +++++++++---------- .../authorization/AuthenticationProvider.java | 1 + .../AuthenticationServiceBean.java | 25 ------------------ .../AbstractOAuth2AuthenticationProvider.java | 10 +++++++ .../shib/ShibAuthenticationProvider.java | 5 ++++ ...tractOAuth2AuthenticationProviderTest.java | 26 +++++++++++++++++++ 6 files changed, 55 insertions(+), 38 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProviderTest.java diff --git a/src/main/java/edu/harvard/iq/dataverse/LoginPage.java b/src/main/java/edu/harvard/iq/dataverse/LoginPage.java index dc2ccb552e2..8067e1f150f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/LoginPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/LoginPage.java @@ -17,11 +17,7 @@ import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Random; +import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.EJB; @@ -129,16 +125,20 @@ public List listCredentialsAuthenticationProv return infos; } + /** + * Retrieve information about all enabled identity providers in a sorted order to be displayed to the user. + * @return list of display information for each provider + */ public List listAuthenticationProviders() { List infos = new LinkedList<>(); - for (String id : authSvc.getAuthenticationProviderIdsSorted()) { - AuthenticationProvider authenticationProvider = authSvc.getAuthenticationProvider(id); - if (authenticationProvider != null) { - if (ShibAuthenticationProvider.PROVIDER_ID.equals(authenticationProvider.getId())) { - infos.add(authenticationProvider.getInfo()); - } else { - infos.add(authenticationProvider.getInfo()); - } + List idps = new ArrayList<>(authSvc.getAuthenticationProviders()); + + // sort by order first. in case of same order values, be deterministic in UI and sort by id, too. + Collections.sort(idps, Comparator.comparing(AuthenticationProvider::getOrder).thenComparing(AuthenticationProvider::getId)); + + for (AuthenticationProvider idp : idps) { + if (idp != null) { + infos.add(idp.getInfo()); } } return infos; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationProvider.java index ec989317758..2b137ca0dec 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationProvider.java @@ -28,6 +28,7 @@ public interface AuthenticationProvider { AuthenticationProviderDisplayInfo getInfo(); + default int getOrder() { return 1; } default boolean isPasswordUpdateAllowed() { return false; }; default boolean isUserInfoUpdateAllowed() { return false; }; default boolean isUserDeletionAllowed() { return false; }; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 44903624c1a..f3373858381 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -17,10 +17,6 @@ import edu.harvard.iq.dataverse.authorization.providers.builtin.PasswordEncryption; import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2AuthenticationProviderFactory; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GitHubOAuth2AP; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.GoogleOAuth2AP; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.OrcidOAuth2AP; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.impl.MicrosoftOAuth2AP; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProviderFactory; import edu.harvard.iq.dataverse.authorization.users.ApiToken; @@ -29,11 +25,9 @@ import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.passwordreset.PasswordResetData; import edu.harvard.iq.dataverse.passwordreset.PasswordResetServiceBean; -import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; import edu.harvard.iq.dataverse.workflows.WorkflowComment; import java.sql.Timestamp; -import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -873,25 +867,6 @@ public AuthenticatedUser canLogInAsBuiltinUser(String username, String password) return null; } } - - /** - * @todo Consider making the sort order configurable by making it a colum on - * AuthenticationProviderRow - */ - public List getAuthenticationProviderIdsSorted() { - GitHubOAuth2AP github = new GitHubOAuth2AP(null, null); - GoogleOAuth2AP google = new GoogleOAuth2AP(null, null); - MicrosoftOAuth2AP microsoft = new MicrosoftOAuth2AP(null, null); - return Arrays.asList( - BuiltinAuthenticationProvider.PROVIDER_ID, - ShibAuthenticationProvider.PROVIDER_ID, - OrcidOAuth2AP.PROVIDER_ID_PRODUCTION, - OrcidOAuth2AP.PROVIDER_ID_SANDBOX, - github.getId(), - google.getId(), - microsoft.getId() - ); - } public List getWorkflowCommentsByAuthenticatedUser(AuthenticatedUser user){ Query query = em.createQuery("SELECT wc FROM WorkflowComment wc WHERE wc.authenticatedUser.id = :auid"); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java index 382259cf734..01139cd2e27 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProvider.java @@ -8,6 +8,7 @@ import com.github.scribejava.core.model.Verb; import com.github.scribejava.core.oauth.AuthorizationUrlBuilder; import com.github.scribejava.core.oauth.OAuth20Service; +import edu.harvard.iq.dataverse.LoginPage; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.AuthenticationProviderDisplayInfo; @@ -92,6 +93,7 @@ public String toString() { protected String clientSecret; protected String baseUserEndpoint; protected String redirectUrl; + /** * List of scopes to be requested for authorization at identity provider. * Defaults to empty so no scope will be requested (use case: public info from GitHub) @@ -212,6 +214,14 @@ public AuthenticationProviderDisplayInfo getInfo() { return new AuthenticationProviderDisplayInfo(getId(), getTitle(), getSubTitle()); } + /** + * Used in {@link LoginPage#listAuthenticationProviders()} for sorting the providers in the UI + * TODO: this might be extended to use a value set by the admin when configuring the provider via JSON. + * @return an integer value (sort ascending) + */ + @Override + public int getOrder() { return 100; } + @Override public String getId() { return id; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibAuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibAuthenticationProvider.java index 85c076ebea7..f7c00a1635d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibAuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibAuthenticationProvider.java @@ -7,6 +7,11 @@ public class ShibAuthenticationProvider implements AuthenticationProvider { public static final String PROVIDER_ID = "shib"; + + @Override + public int getOrder() { + return 20; + } @Override public String getId() { diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProviderTest.java b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProviderTest.java new file mode 100644 index 00000000000..a75cc2568b1 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/AbstractOAuth2AuthenticationProviderTest.java @@ -0,0 +1,26 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.*; + +class AbstractOAuth2AuthenticationProviderTest { + + AbstractOAuth2AuthenticationProvider idp; + + @BeforeEach + void setUp() { + this.idp = Mockito.mock(AbstractOAuth2AuthenticationProvider.class, Mockito.CALLS_REAL_METHODS); + } + + /** + * Ensure this is working as expected. + */ + @Test + void getOrderDefaultValue() { + assertEquals(100, this.idp.getOrder()); + } + +} \ No newline at end of file From 908e15f4eaceda60fe0e2824787d8159422fdd36 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 25 Nov 2019 16:42:55 +0100 Subject: [PATCH 5/8] Make OAuth2 login controller beans capable to deal with empty access token data --- .../providers/oauth2/OAuth2FirstLoginPage.java | 8 +++++--- .../providers/oauth2/OAuth2LoginBackingBean.java | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2FirstLoginPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2FirstLoginPage.java index 213feeba8f6..a3ce3c5bdf7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2FirstLoginPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2FirstLoginPage.java @@ -192,9 +192,11 @@ public String createNewAccount() { UserNotification.Type.CREATEACC, null); final OAuth2TokenData tokenData = newUser.getTokenData(); - tokenData.setUser(user); - tokenData.setOauthProviderId(newUser.getServiceId()); - oauth2Tokens.store(tokenData); + if (tokenData != null) { + tokenData.setUser(user); + tokenData.setOauthProviderId(newUser.getServiceId()); + oauth2Tokens.store(tokenData); + } return "/dataverse.xhtml?faces-redirect=true"; } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 618b8e5ca1d..e42f82d48d8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -109,9 +109,11 @@ public void exchangeCodeForToken() throws IOException { session.setUser(dvUser); session.configureSessionTimeout(); final OAuth2TokenData tokenData = oauthUser.getTokenData(); - tokenData.setUser(dvUser); - tokenData.setOauthProviderId(idp.getId()); - oauth2Tokens.store(tokenData); + if (tokenData != null) { + tokenData.setUser(dvUser); + tokenData.setOauthProviderId(idp.getId()); + oauth2Tokens.store(tokenData); + } Faces.redirect(redirectPage.orElse("/")); } From bd817fb81c9b9352d374fce1540e97b00a8435e1 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Mon, 25 Nov 2019 16:43:55 +0100 Subject: [PATCH 6/8] Introduce first version of a working Open ID Connect authentication provider. #5974 --- pom.xml | 6 + .../AuthenticationServiceBean.java | 2 + .../oauth2/oidc/OIDCAuthProvider.java | 263 ++++++++++++++++++ .../OIDCAuthenticationProviderFactory.java | 47 ++++ src/main/java/propertyFiles/Bundle.properties | 3 + 5 files changed, 321 insertions(+) create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java diff --git a/pom.xml b/pom.xml index 97cc58fbecf..c6803205988 100644 --- a/pom.xml +++ b/pom.xml @@ -490,6 +490,12 @@ scribejava-apis 6.9.0 + + + com.nimbusds + oauth2-oidc-sdk + 6.18 + diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index f3373858381..f7ded8c11c4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -2,6 +2,7 @@ import edu.harvard.iq.dataverse.UserNotificationServiceBean; import edu.harvard.iq.dataverse.UserServiceBean; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthenticationProviderFactory; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord; import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; @@ -113,6 +114,7 @@ public void startup() { registerProviderFactory( new BuiltinAuthenticationProviderFactory(builtinUserServiceBean, passwordValidatorService, this) ); registerProviderFactory( new ShibAuthenticationProviderFactory() ); registerProviderFactory( new OAuth2AuthenticationProviderFactory() ); + registerProviderFactory( new OIDCAuthenticationProviderFactory() ); } catch (AuthorizationSetupException ex) { logger.log(Level.SEVERE, "Exception setting up the authentication provider factories: " + ex.getMessage(), ex); 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 new file mode 100644 index 00000000000..29cc4b9685d --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java @@ -0,0 +1,263 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; + +import com.github.scribejava.core.builder.api.DefaultApi20; +import com.nimbusds.oauth2.sdk.*; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.http.HTTPRequest; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import com.nimbusds.openid.connect.sdk.*; +import com.nimbusds.openid.connect.sdk.claims.UserInfo; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderConfigurationRequest; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; +import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationSetupException; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2Exception; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2TokenData; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; +import edu.harvard.iq.dataverse.util.BundleUtil; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; + +/** + * TODO: this should not EXTEND, but IMPLEMENT the contract to be used in {@link edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean} + */ +public class OIDCAuthProvider extends AbstractOAuth2AuthenticationProvider { + + private static final Logger logger = Logger.getLogger(OIDCAuthProvider.class.getName()); + + protected String id = "oidc"; + protected String title = "Open ID Connect"; + protected List scope = Arrays.asList("openid", "email", "profile"); + + Issuer issuer; + ClientAuthentication clientAuth; + OIDCProviderMetadata idpMetadata; + + public OIDCAuthProvider(String aClientId, String aClientSecret, String issuerEndpointURL) throws AuthorizationSetupException { + this.clientSecret = aClientSecret; // nedded for state creation + this.clientAuth = new ClientSecretBasic(new ClientID(aClientId), new Secret(aClientSecret)); + this.issuer = new Issuer(issuerEndpointURL); + getMetadata(); + } + + /** + * Although this is defined in {@link edu.harvard.iq.dataverse.authorization.AuthenticationProvider}, + * this needs to be present due to bugs in ELResolver (has been modified for Spring). + * TODO: for the future it might be interesting to make this configurable via the provider JSON (it's used for ORCID!) + * @see JBoss Issue 159 + * @see Jakarta EE Bug 43 + * @return false + */ + @Override + public boolean isDisplayIdentifier() { return false; } + + /** + * Setup metadata from OIDC provider during creation of the provider representation + * @return The OIDC provider metadata, if successfull + * @throws IOException when sth. goes wrong with the retrieval + * @throws ParseException when the metadata is not parsable + */ + void getMetadata() throws AuthorizationSetupException { + try { + this.idpMetadata = getMetadata(this.issuer); + } catch (IOException ex) { + logger.severe("OIDC provider metadata at \"+issuerEndpointURL+\" not retrievable: "+ex.getMessage()); + throw new AuthorizationSetupException("OIDC provider metadata at "+this.issuer.getValue()+" not retrievable."); + } catch (ParseException ex) { + logger.severe("OIDC provider metadata at \"+issuerEndpointURL+\" not parsable: "+ex.getMessage()); + throw new AuthorizationSetupException("OIDC provider metadata at "+this.issuer.getValue()+" not parsable."); + } + + // Assert that the provider supports the code flow + if (! this.idpMetadata.getResponseTypes().stream().filter(idp -> idp.impliesCodeFlow()).findAny().isPresent()) { + throw new AuthorizationSetupException("OIDC provider at "+this.issuer.getValue()+" does not support code flow, disabling."); + } + } + + /** + * Retrieve metadata from OIDC provider (moved here to be mock-/spyable) + * @param issuer The OIDC provider (basically a wrapped URL to endpoint) + * @return The OIDC provider metadata, if successfull + * @throws IOException when sth. goes wrong with the retrieval + * @throws ParseException when the metadata is not parsable + */ + OIDCProviderMetadata getMetadata(Issuer issuer) throws IOException, ParseException { + // Will resolve the OpenID provider metadata automatically + OIDCProviderConfigurationRequest request = new OIDCProviderConfigurationRequest(issuer); + + // Make HTTP request + HTTPRequest httpRequest = request.toHTTPRequest(); + HTTPResponse httpResponse = httpRequest.send(); + + // Parse OpenID provider metadata + return OIDCProviderMetadata.parse(httpResponse.getContentAsJSONObject()); + } + + /** + * TODO: remove when refactoring package and {@link AbstractOAuth2AuthenticationProvider} + */ + @Override + public DefaultApi20 getApiInstance() { + throw new UnsupportedOperationException("OIDC provider cannot provide a ScribeJava API instance object"); + } + + /** + * TODO: remove when refactoring package and {@link AbstractOAuth2AuthenticationProvider} + */ + @Override + protected ParsedUserResponse parseUserResponse(String responseBody) { + throw new UnsupportedOperationException("OIDC provider cannot provide a ScribeJava API instance object"); + } + + /** + * Create the authz URL for the OIDC provider + * @param state A randomized state, necessary to secure the authorization flow. @see OAuth2LoginBackingBean.createState() + * @param callbackUrl URL where the provider should send the browser after authn in code flow + * @return + */ + @Override + public String buildAuthzUrl(String state, String callbackUrl) { + State stateObject = new State(state); + URI callback = URI.create(callbackUrl); + Nonce nonce = new Nonce(); + + AuthenticationRequest req = new AuthenticationRequest.Builder(new ResponseType("code"), + Scope.parse(this.scope), + this.clientAuth.getClientID(), + callback) + .endpointURI(idpMetadata.getAuthorizationEndpointURI()) + .state(stateObject) + .nonce(nonce) + .build(); + + return req.toURI().toString(); + } + + /** + * Receive user data from OIDC provider after authn/z has been successfull. (Callback view uses this) + * Request a token and access the resource, parse output and return user details. + * @param code The authz code sent from the provider + * @param redirectUrl The redirect URL (some providers require this when fetching the access token, e. g. Google) + * @return A user record containing all user details accessible for us + * @throws IOException Thrown when communication with the provider fails + * @throws OAuth2Exception Thrown when we cannot access the user details for some reason + * @throws InterruptedException Thrown when the requests thread is failing + * @throws ExecutionException Thrown when the requests thread is failing + */ + @Override + public OAuth2UserRecord getUserRecord(String code, String redirectUrl) + throws IOException, OAuth2Exception, InterruptedException, ExecutionException { + // Create grant object + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(new AuthorizationCode(code), URI.create(redirectUrl)); + + // Get Access Token first + Optional accessToken = getAccessToken(codeGrant); + + // Now retrieve User Info + if (accessToken.isPresent()) { + Optional userInfo = getUserInfo(accessToken.get()); + + // Construct our internal user representation + if (userInfo.isPresent()) { + return getUserRecord(userInfo.get()); + } + } + + // this should never happen, as we are throwing exceptions like champs before. + throw new OAuth2Exception(-1, "", "auth.providers.token.failGetUser"); + } + + /** + * Create the OAuth2UserRecord from the OIDC UserInfo. + * TODO: extend to retrieve and insert claims about affiliation and position. + * @param userInfo + * @return the usable user record for processing ing {@link edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean} + */ + OAuth2UserRecord getUserRecord(UserInfo userInfo) { + return new OAuth2UserRecord( + this.getId(), + userInfo.getSubject().getValue(), + userInfo.getPreferredUsername(), + null, + new AuthenticatedUserDisplayInfo(userInfo.getGivenName(), userInfo.getFamilyName(), userInfo.getEmailAddress(), "", ""), + null + ); + } + + /** + * Retrieve the Access Token from provider. Encapsulate for testing. + * @param grant + * @return The bearer access token used in code (grant) flow. May be empty if SDK could not cast internally. + */ + Optional getAccessToken(AuthorizationGrant grant) throws IOException, OAuth2Exception { + // Request token + HTTPResponse response = new TokenRequest(this.idpMetadata.getTokenEndpointURI(), + this.clientAuth, + grant, + Scope.parse(this.scope)) + .toHTTPRequest() + .send(); + + // Parse response + try { + TokenResponse tokenRespone = OIDCTokenResponseParser.parse(response); + + // If error --> oauth2 ex + if (! tokenRespone.indicatesSuccess() ) { + ErrorObject error = tokenRespone.toErrorResponse().getErrorObject(); + throw new OAuth2Exception(error.getHTTPStatusCode(), error.getDescription(), "auth.providers.token.failRetrieveToken"); + } + + // Success --> return token + OIDCTokenResponse successResponse = (OIDCTokenResponse)tokenRespone.toSuccessResponse(); + + return Optional.of(successResponse.getOIDCTokens().getBearerAccessToken()); + + } catch (ParseException ex) { + throw new OAuth2Exception(-1, ex.getMessage(), "auth.providers.token.failParseToken"); + } + } + + /** + * Retrieve User Info from provider. Encapsulate for testing. + * @param accessToken The access token to enable reading data from userinfo endpoint + */ + Optional getUserInfo(BearerAccessToken accessToken) throws IOException, OAuth2Exception { + // Retrieve data + HTTPResponse response = new UserInfoRequest(this.idpMetadata.getUserInfoEndpointURI(), accessToken) + .toHTTPRequest() + .send(); + + // Parse/Extract + try { + UserInfoResponse infoResponse = UserInfoResponse.parse(response); + + // If error --> oauth2 ex + if (! infoResponse.indicatesSuccess() ) { + ErrorObject error = infoResponse.toErrorResponse().getErrorObject(); + throw new OAuth2Exception(error.getHTTPStatusCode(), + error.getDescription(), + BundleUtil.getStringFromBundle("auth.providers.exception.userinfo", Arrays.asList(this.getTitle()))); + } + + // Success --> return info + return Optional.of(infoResponse.toSuccessResponse().getUserInfo()); + + } catch (ParseException ex) { + throw new OAuth2Exception(-1, ex.getMessage(), BundleUtil.getStringFromBundle("auth.providers.exception.userinfo", Arrays.asList(this.getTitle()))); + } + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java new file mode 100644 index 00000000000..c6d1a28e19d --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java @@ -0,0 +1,47 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; + +import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; +import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationSetupException; +import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderFactory; +import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderRow; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2AuthenticationProviderFactory; + +import java.util.Map; + +public class OIDCAuthenticationProviderFactory implements AuthenticationProviderFactory { + + /** + * The alias of a factory. Has to be unique in the system. + * @return The alias of the factory. + */ + @Override + public String getAlias() { + return "oidc"; + } + + /** + * @return A human readable display string describing this factory. + */ + @Override + public String getInfo() { + return "Factory for Open ID Connect providers"; + } + + /** + * Instantiates an {@link AuthenticationProvider} based on the row passed. + * @param aRow The row on which the created provider is based. + * @return The provider + * @throws AuthorizationSetupException If {@code aRow} contains malformed data. + */ + @Override + public AuthenticationProvider buildProvider( AuthenticationProviderRow aRow ) throws AuthorizationSetupException { + Map factoryData = OAuth2AuthenticationProviderFactory.parseFactoryData(aRow.getFactoryData()); + + OIDCAuthProvider oidc = new OIDCAuthProvider(factoryData.get("clientId"), factoryData.get("clientSecret"), factoryData.get("issuer")); + oidc.setId(aRow.getId()); + oidc.setTitle(aRow.getTitle()); + oidc.setSubTitle(aRow.getSubtitle()); + + return oidc; + } +} diff --git a/src/main/java/propertyFiles/Bundle.properties b/src/main/java/propertyFiles/Bundle.properties index 3f97c897747..07ddff2e7cd 100755 --- a/src/main/java/propertyFiles/Bundle.properties +++ b/src/main/java/propertyFiles/Bundle.properties @@ -318,6 +318,9 @@ auth.providers.persistentUserIdTooltip.orcid=ORCID provides a persistent digital auth.providers.persistentUserIdTooltip.github=GitHub assigns a unique number to every user. auth.providers.insufficientScope=Dataverse was not granted the permission to read user data from {0}. auth.providers.exception.userinfo=Error getting the user info record from {0}. +auth.providers.token.failRetrieveToken=Dataverse could not retrieve an access token. +auth.providers.token.failParseToken=Dataverse could not parse the access token. +auth.providers.token.failGetUser=Dataverse could not get your user record. Please consult your administrator. auth.providers.orcid.helpmessage1=ORCID is an open, non-profit, community-based effort to provide a registry of unique researcher identifiers and a transparent method of linking research activities and outputs to these identifiers. ORCID is unique in its ability to reach across disciplines, research sectors, and national boundaries and its cooperation with other identifier systems. Find out more at orcid.org/about. auth.providers.orcid.helpmessage2=This repository uses your ORCID for authentication (so you don't need another username/password combination). Having your ORCID associated with your datasets also makes it easier for people to find the datasets you have published. From 105e62fb13368c496789002b3974ec630188b92c Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 26 Nov 2019 08:47:48 +0100 Subject: [PATCH 7/8] Fix wrong error message in OIDCAuthProvider. --- .../authorization/providers/oauth2/oidc/OIDCAuthProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 29cc4b9685d..d9a1baa9e3e 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 @@ -119,7 +119,7 @@ public DefaultApi20 getApiInstance() { */ @Override protected ParsedUserResponse parseUserResponse(String responseBody) { - throw new UnsupportedOperationException("OIDC provider cannot provide a ScribeJava API instance object"); + throw new UnsupportedOperationException("OIDC provider uses the SDK to parse the response."); } /** From e96799dfd6f2866ab77533507b21c8f6d754fdc4 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 4 Dec 2019 13:38:53 +0100 Subject: [PATCH 8/8] Create a first, simple doc for the OIDC feature in #5974 --- .../source/installation/index.rst | 1 + .../source/installation/oauth2.rst | 2 + .../source/installation/oidc.rst | 91 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 doc/sphinx-guides/source/installation/oidc.rst diff --git a/doc/sphinx-guides/source/installation/index.rst b/doc/sphinx-guides/source/installation/index.rst index ab94d5b4949..8ed0571b466 100755 --- a/doc/sphinx-guides/source/installation/index.rst +++ b/doc/sphinx-guides/source/installation/index.rst @@ -20,5 +20,6 @@ Installation Guide geoconnect shibboleth oauth2 + oidc external-tools advanced diff --git a/doc/sphinx-guides/source/installation/oauth2.rst b/doc/sphinx-guides/source/installation/oauth2.rst index 14483d655c3..300df5e20e0 100644 --- a/doc/sphinx-guides/source/installation/oauth2.rst +++ b/doc/sphinx-guides/source/installation/oauth2.rst @@ -13,6 +13,8 @@ As explained under "Auth Modes" in the :doc:`config` section, OAuth2 is one of t Dataverse supports four OAuth providers: `ORCID `_, `Microsoft Azure Active Directory (AD) `_, `GitHub `_, and `Google `_. +In addition :doc:`oidc` are supported, using a standard based on OAuth2. + Setup ----- diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst new file mode 100644 index 00000000000..6d206d2576b --- /dev/null +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -0,0 +1,91 @@ +OpenID Connect Login Options +============================ + +.. contents:: |toctitle| + :local: + +Introduction +------------ + +The `OpenID Connect `_ (or OIDC) standard support is closely related to our :doc:`oauth2`, +as it has been based on the `OAuth 2.0 `_ standard. Quick summary: OIDC is using OAuth 2.0, but +adds a standardized way how authentication is done, while this is up to providers when using OAuth 2.0 for authentication. + +Being a standard, you can easily enable the use of any OpenID connect compliant provider out there for login into your +Dataverse installation. + +Some prominent provider examples: + +- `Google `_ +- `Microsoft Azure AD `_ +- `Yahoo `_ +- ORCID `announced support `_ + +You can also either host an OpenID Connect identity management on your own or use a customizable hosted service: + +- `Okta `_ is a hosted solution +- `Keycloak `_ is an open source solution for an IDM/IAM +- `Unity IDM `_ is another open source IDM/IAM solution + +Other use cases and combinations +-------------------------------- + +- Using your custom identity management solution might be a workaround when you seek for LDAP support, but + don't want to go for services like Microsoft Azure AD et al. +- You want to enable users to login in multiple different ways but appear as one account to Dataverse. This is + currently not possible within Dataverse itself, but hosting an IDM and attaching Dataverse solves it. +- You want to use the `eduGain Federation `_ or other well known SAML federations, but don't want + to deploy Shibboleth as your service provider. Using an IDM solution in front easily allows you to use them + without hassle. +- There's also a `Shibboleth IdP (not SP!) extension `_, + so if you already have a Shibboleth identity provider at your institution, you can reuse it more easily with Dataverse. +- In the future, OpenID Connect might become a successor to the large scale R&E SAML federations we have nowadays. + See also `OpenID Connect Federation Standard `_ (in development) + +How to use +---------- + +Just like with :doc:`oauth2` you need to obtain a *Client ID* and a *Client Secret* from your provider(s). + +.. note:: + Dataverse does not support `OpenID Connect Dynamic Registration `_. + You need to apply for credentials out-of-band. + +Dataverse will discover all necessary metadata for a given provider on its own (this is `part of the standard +`_). + +To enable this, you need to specify an *Issuer URL* when creating the configuration for your provider (see below). + +Finding the issuer URL is best done by searching for terms like "discovery" in the documentation of your provider. +The discovery document is always located at ``/.well-known/openid-configuration`` (standardized). +To be sure, you can always lookup the ``issuer`` value inside the live JSON-based discovery document. + +Please create a file like this, replacing every ``<...>`` with your values: + +.. code-block:: json + :caption: my-oidc-provider.json + + { + "id":"", + "factoryAlias":"oidc", + "title":"", + "subtitle":"", + "factoryData":"type: oidc | issuer: | clientId: | clientSecret: ", + "enabled":true + } + +Now load the configuration into Dataverse using the same API as with :doc:`oauth2`: + +``curl -X POST -H 'Content-type: application/json' --upload-file my-oidc-provider.json http://localhost:8080/api/admin/authenticationProviders`` + +Dataverse will automatically try to load the provider and retrieve the metadata. Watch the Glassfish log for errors. +You should see the new provider under "Other options" on the Log In page, as described in the :doc:`/user/account` +section of the User Guide. + +By default, the Log In page will show the "builtin" provider, but you can adjust this via the ``:DefaultAuthProvider`` +configuration option. For details, see :doc:`config`. + +.. hint:: + In contrast to our :doc:`oauth2`, you can use multiple providers by creating distinct configurations enabled by + the same technology and without modifying the Dataverse code base (standards for the win!). +