Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions doc/sphinx-guides/source/installation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ Installation Guide
geoconnect
shibboleth
oauth2
oidc
external-tools
advanced
2 changes: 2 additions & 0 deletions doc/sphinx-guides/source/installation/oauth2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://orcid.org>`_, `Microsoft Azure Active Directory (AD) <https://docs.microsoft.com/azure/active-directory/>`_, `GitHub <https://github.com>`_, and `Google <https://console.developers.google.com>`_.

In addition :doc:`oidc` are supported, using a standard based on OAuth2.

Setup
-----

Expand Down
91 changes: 91 additions & 0 deletions doc/sphinx-guides/source/installation/oidc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
OpenID Connect Login Options
============================

.. contents:: |toctitle|
:local:

Introduction
------------

The `OpenID Connect <https://openid.net/connect/>`_ (or OIDC) standard support is closely related to our :doc:`oauth2`,
as it has been based on the `OAuth 2.0 <https://oauth.net/2/>`_ 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 <https://developers.google.com/identity/protocols/OpenIDConnect>`_
- `Microsoft Azure AD <https://docs.microsoft.com/de-de/azure/active-directory/develop/v2-protocols-oidc>`_
- `Yahoo <https://developer.yahoo.com/oauth2/guide/openid_connect>`_
- ORCID `announced support <https://orcid.org/blog/2019/04/17/orcid-openid-connect-and-implicit-authentication>`_

You can also either host an OpenID Connect identity management on your own or use a customizable hosted service:

- `Okta <https://developer.okta.com/docs/reference/api/oidc/>`_ is a hosted solution
- `Keycloak <https://www.keycloak.org>`_ is an open source solution for an IDM/IAM
- `Unity IDM <https://www.unity-idm.eu>`_ 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 <https://edugain.org>`_ 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 <https://github.com/CSCfi/shibboleth-idp-oidc-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 <https://openid.net/specs/openid-connect-federation-1_0.html>`_ (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 <https://openid.net/specs/openid-connect-registration-1_0.html>`_.
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
<http://openid.net/specs/openid-connect-discovery-1_0.html>`_).

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 ``<issuer url>/.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":"<a unique id>",
"factoryAlias":"oidc",
"title":"<a title - shown in UI>",
"subtitle":"<a subtitle - currently unused in UI>",
"factoryData":"type: oidc | issuer: <issuer url> | clientId: <client id> | clientSecret: <client secret>",
"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!).

6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,12 @@
<artifactId>scribejava-apis</artifactId>
<version>6.9.0</version>
</dependency>
<!-- OpenID Connect authentication -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>oauth2-oidc-sdk</artifactId>
<version>6.18</version>
</dependency>
<!-- EXPERIMENTAL: -->
<!-- lyncode xoai OAI-PMH implementation: -->
<!-- unfortunately, their 4.10 version -->
Expand Down
26 changes: 13 additions & 13 deletions src/main/java/edu/harvard/iq/dataverse/LoginPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -129,16 +125,20 @@ public List<AuthenticationProviderDisplayInfo> 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<AuthenticationProviderDisplayInfo> listAuthenticationProviders() {
List<AuthenticationProviderDisplayInfo> 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<AuthenticationProvider> 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());
Copy link
Member

Choose a reason for hiding this comment

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

Is the change above necessary for OIDC to work? I don't think so. It does look like a nice cleanup though. I like how it's deterministic. Perhaps this change should be documented.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually yes it is. From this function, authSvc.getAuthenticationProviderIdsSorted() was called, which is deleted below. This was done in 3fd326c and here's what I wrote in the commit log:

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)

}
}
return infos;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,10 +18,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;
Expand All @@ -29,11 +26,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;
Expand Down Expand Up @@ -119,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);
Expand Down Expand Up @@ -905,25 +901,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<String> 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 <WorkflowComment> getWorkflowCommentsByAuthenticatedUser(AuthenticatedUser user){
Query query = em.createQuery("SELECT wc FROM WorkflowComment wc WHERE wc.authenticatedUser.id = :auid");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,6 +23,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
*/
Expand Down Expand Up @@ -90,12 +93,16 @@ 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)
*/
protected List<String> 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 );
Expand Down Expand Up @@ -207,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; }
Copy link
Member

Choose a reason for hiding this comment

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

What if there are more than 100 authentication providers? Some have the same number? Is it ok to have two 42s?

Copy link
Contributor Author

@poikilotherm poikilotherm Dec 5, 2019

Choose a reason for hiding this comment

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

Actually it doesn't matter if some have the same number, as they are sorted by number first, then by name.

This is mostly about sorting Builtin first (order = 1), Shib second (order=20) and OAuth last (all order = 100), as it has been sorted before my refactoring.

Once we have a configurable value for this, you could also realize sth. like provider A first, then all the others.
Please see also my comment above, containing the commit log message explaining details.


@Override
public String getId() {
return id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ 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<String, String> parseFactoryData(String factoryData) {
public static Map<String, String> parseFactoryData(String factoryData) {
return Arrays.asList(factoryData.split("\\|")).stream()
.map(s -> s.split(":", 2))
.filter(p -> p.length == 2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("/"));
}
Expand Down
Loading