Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#6155: OAuth 2.0 - Microsoft #6192

Merged
merged 22 commits into from
Oct 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
79ccd4c
6155 - OAuth 2.0 - Microsoft
Sep 19, 2019
481dc81
adding documentation
djbrooke Sep 19, 2019
246d2b8
updating documentation
djbrooke Sep 19, 2019
efee662
Merge branch '6155-OAuth-2.0-Microsoft' of https://github.com/CIMMYT/…
djbrooke Sep 19, 2019
1bf38c2
adding microsoft to page title
djbrooke Sep 19, 2019
ff84855
adding documentation
alejandratenorio Sep 20, 2019
1f9630a
adding documentation
alejandratenorio Sep 20, 2019
9ef52c5
Add fix for ORCID XML problem when an user is authenticated with Micr…
Sep 20, 2019
982bd88
Merge branch '6155-OAuth-2.0-Microsoft' of https://github.com/CIMMYT/…
Sep 20, 2019
1638ca5
Merge branch 'develop' into 6155-OAuth-2.0-Microsoft
Oct 9, 2019
a4d8114
Merge branch 'develop' into 6155-OAuth-2.0-Microsoft
Oct 21, 2019
4828bf7
Update MicrosoftOauth2AP for ScribeJava 6.6.3
Oct 22, 2019
3f2420d
Update MicrosoftOauth2AP for ScribeJava 6.6.3
Oct 22, 2019
ed8950f
Remove comments from AbstractOAuth2AuthenticationProvider.java
Oct 23, 2019
3ab5e25
switching back to ol' Google
djbrooke Oct 24, 2019
97a7549
changing from ms to ms azure ad
djbrooke Oct 24, 2019
a44bb2b
one more instance of ms to ms azure ad
djbrooke Oct 24, 2019
31df5a2
a few more cases where we want to be specific about azure ad
djbrooke Oct 24, 2019
4931d6a
correct number of =, scalable title
djbrooke Oct 24, 2019
44c37b4
Update clases and .gitignore
Oct 25, 2019
38d88e2
Merge branch '6155-OAuth-2.0-Microsoft' of https://github.com/CIMMYT/…
Oct 25, 2019
ab8714e
make error message non-ORCID specific
djbrooke Oct 25, 2019
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
Binary file modified doc/Architecture/update-user-account-info.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id":"microsoft",
"factoryAlias":"oauth2",
"title":"Microsoft",
"subtitle":"",
"factoryData":"type: microsoft | userEndpoint: NONE | clientId: FIXME | clientSecret: FIXME",
"enabled":true
}
14 changes: 8 additions & 6 deletions doc/sphinx-guides/source/installation/oauth2.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
OAuth Login: ORCID, GitHub, Google
==================================
OAuth Login Options
===================

.. contents:: |toctitle|
:local:
Expand All @@ -11,7 +11,7 @@ As explained under "Auth Modes" in the :doc:`config` section, OAuth2 is one of t

`OAuth2 <https://oauth.net/2/>`_ is an authentication protocol that allows systems to share user data, while letting the users control what data is being shared. When you see buttons stating "login with Google" or "login through Facebook", OAuth2 is probably involved. For the purposes of this section, we will shorten "OAuth2" to just "OAuth." OAuth can be compared and contrasted with :doc:`shibboleth`.

Dataverse supports three OAuth providers: `ORCID <http://orcid.org>`_, `GitHub <https://github.com>`_, and `Google <https://console.developers.google.com>`_.
djbrooke marked this conversation as resolved.
Show resolved Hide resolved
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>`_.

Setup
-----
Expand All @@ -24,18 +24,19 @@ Identity Provider Side
Obtain Client ID and Client Secret
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Before OAuth providers will release information about their users (first name, last name, etc.) to your Dataverse installation, you must request a "Client ID" and "Client Secret" from them. In the case of GitHub and Google, this is as simple as clicking a few buttons and there is no cost associated with using their authentication service. ORCID, on the other hand, does not have an automated system for requesting these credentials, and it is not free to use the ORCID authentication service.
Before OAuth providers will release information about their users (first name, last name, etc.) to your Dataverse installation, you must request a "Client ID" and "Client Secret" from them. In the case of GitHub and Google, this is as simple as clicking a few buttons and there is no cost associated with using their authentication service. ORCID and Microsoft, on the other hand, do not have an automated system for requesting these credentials, and it is not free to use these authentication services.

URLs to help you request a Client ID and Client Secret from the providers supported by Dataverse are provided below. For all of these providers, it's a good idea to request the Client ID and Client secret using a generic account, perhaps the one that's associated with the ``:SystemEmail`` you've configured for Dataverse, rather than your own personal ORCID, GitHub, or Google account:
URLs to help you request a Client ID and Client Secret from the providers supported by Dataverse are provided below. For all of these providers, it's a good idea to request the Client ID and Client secret using a generic account, perhaps the one that's associated with the ``:SystemEmail`` you've configured for Dataverse, rather than your own personal Microsoft Azure AD, ORCID, GitHub, or Google account:

- ORCID: https://orcid.org/content/register-client-application-production-trusted-party
- Microsoft: https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code
- GitHub: https://github.com/settings/applications/new via https://developer.github.com/v3/oauth/
- Google: https://console.developers.google.com/projectselector/apis/credentials via https://developers.google.com/identity/protocols/OAuth2WebServer (pick "OAuth client ID")

Each of these providers will require the following information from you:

- Basic information about your Dataverse installation such as a name, description, URL, logo, privacy policy, etc.
- OAuth2 Redirect URI (ORCID) or Authorization Callback URL (GitHub) or Authorized Redirect URIs (Google): This is the URL on the Dataverse side to which the user will be sent after successfully authenticating with the identity provider. This should be the advertised URL of your Dataverse installation (the protocol, fully qualified domain name, and optional port configured via the ``dataverse.siteUrl`` JVM option mentioned in the :doc:`config` section) appended with ``/oauth2/callback.xhtml`` such as ``https://dataverse.example.edu/oauth2/callback.xhtml``.
- OAuth2 Redirect URI (ORCID) or Redirect URI (Microsoft Azure AD) or Authorization Callback URL (GitHub) or Authorized Redirect URIs (Google): This is the URL on the Dataverse side to which the user will be sent after successfully authenticating with the identity provider. This should be the advertised URL of your Dataverse installation (the protocol, fully qualified domain name, and optional port configured via the ``dataverse.siteUrl`` JVM option mentioned in the :doc:`config` section) appended with ``/oauth2/callback.xhtml`` such as ``https://dataverse.example.edu/oauth2/callback.xhtml``.

When you are finished you should have a Client ID and Client Secret from the provider. Keep them safe and secret.

Expand All @@ -51,6 +52,7 @@ We will ``POST`` a JSON file containing the Client ID and Client Secret to this
- :download:`orcid.json <../_static/installation/files/root/auth-providers/orcid.json>`
- :download:`github.json <../_static/installation/files/root/auth-providers/github.json>`
- :download:`google.json <../_static/installation/files/root/auth-providers/google.json>`
- :download:`microsoft.json <../_static/installation/files/root/auth-providers/microsoft.json>`

Here's how the JSON template for GitHub looks, for example:

Expand Down
9 changes: 5 additions & 4 deletions doc/sphinx-guides/source/user/account.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ Dataverse has been configured for one or more of the following log in options:
- Username/Email and Password
- Institutional Log In
- ORCID
- Microsoft Azure AD
- GitHub
- Google

Please note that once you create your Dataverse account, it will be associated with only one of the log in options above.

The Institutional Log In, ORCID, GitHub, and Google options are described in more detail below under "Remote Authentication."
The Institutional Log In, ORCID, Microsoft, GitHub, and Google options are described in more detail below under "Remote Authentication."

Create Account
~~~~~~~~~~~~~~
Expand Down Expand Up @@ -134,10 +135,10 @@ Convert your Dataverse account away from ORCID for log in

If you don't want to log in to Dataverse using ORCID any more, you will want to convert your Dataverse account to the Dataverse Username/Email log in option. To do this, you will need to contact support for the Dataverse installation you are using. On your account page, there is a link that will open a popup form to contact support for assistance.

GitHub and Google Log In
~~~~~~~~~~~~~~~~~~~~~~~~~
Microsoft Azure AD, GitHub, and Google Log In
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can also convert your Dataverse account to use authentication provided by GitHub or Google. These options may be found in the "Other options" section of the log in page, and function similarly to how ORCID is outlined above. If you would like to convert your account away from using one of these services for log in, then you can follow the same steps as listed above for converting away from the ORCID log in.
You can also convert your Dataverse account to use authentication provided by GitHub, Microsoft, or Google. These options may be found in the "Other options" section of the log in page, and function similarly to how ORCID is outlined above. If you would like to convert your account away from using one of these services for log in, then you can follow the same steps as listed above for converting away from the ORCID log in.
djbrooke marked this conversation as resolved.
Show resolved Hide resolved

My Data
-------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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 Down Expand Up @@ -880,13 +881,15 @@ public AuthenticatedUser canLogInAsBuiltinUser(String username, String password)
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()
google.getId(),
microsoft.getId()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,20 @@ public OAuth2UserRecord getUserRecord(String code, @NotNull OAuth20Service servi
throws IOException, OAuth2Exception, InterruptedException, ExecutionException {

OAuth2AccessToken accessToken = service.getAccessToken(code);

//final String userEndpoint = getUserEndpoint(accessToken);
// We need to check if scope is null first: GitHub is used without scope, so the responses scope is null.
// Checking scopes via Stream to be independent from order.
if ( ( accessToken.getScope() != null && ! getScope().stream().allMatch(accessToken.getScope()::contains) ) ||
( accessToken.getScope() == null && ! getSpacedScope().isEmpty() ) ) {
// We did not get the permissions on the scope(s) we need. Abort and inform the user.
throw new OAuth2Exception(200, BundleUtil.getStringFromBundle("auth.providers.insufficientScope", Arrays.asList(this.getTitle())), "");
}

OAuthRequest request = new OAuthRequest(Verb.GET, getUserEndpoint(accessToken));
request.setCharset("UTF-8");
if (id.equals("microsoft")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I still didn't get why this is needed. Could you please elaborate and add a comment next to the code so someone looking at the codebase later has a direct reminder why this is here? Thank you!

request.addHeader("Accept", "application/json");
}
service.signRequest(accessToken, request);

Response response = service.execute(request);
int responseCode = response.getCode();
String body = response.getBody();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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 java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -31,6 +32,7 @@ public OAuth2AuthenticationProviderFactory() {
builders.put("github", (row, data) -> readRow(row, new GitHubOAuth2AP(data.get("clientId"), data.get("clientSecret"))));
builders.put("google", (row, data) -> readRow(row, new GoogleOAuth2AP(data.get("clientId"), data.get("clientSecret"))));
builders.put("orcid", (row, data) -> readRow(row, new OrcidOAuth2AP(data.get("clientId"), data.get("clientSecret"), data.get("userEndpoint"))));
builders.put("microsoft", (row, data) -> readRow(row, new MicrosoftOAuth2AP(data.get("clientId"), data.get("clientSecret"))));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package edu.harvard.iq.dataverse.authorization.providers.oauth2.impl;

import com.github.scribejava.apis.MicrosoftAzureActiveDirectory20Api;
import com.github.scribejava.core.builder.api.DefaultApi20;
import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider;

import java.util.Arrays;
import java.util.Collections;
import java.util.logging.Logger;
import java.io.StringReader;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo;

/**
*
* @author
*/
public class MicrosoftOAuth2AP extends AbstractOAuth2AuthenticationProvider{
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO this class should be renamed to MicrosoftAzureOAuth2AP. One might add other MS based OAuth2 flows later (like MS Live, ...).


private static final Logger logger = Logger.getLogger(MicrosoftOAuth2AP.class.getCanonicalName());

public MicrosoftOAuth2AP(String aClientId, String aClientSecret){
this.id = "microsoft";
Copy link
Contributor

Choose a reason for hiding this comment

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

The ID should correspond to the class name, so if that is going to be changed, this should change as well.

this.title = "Microsoft";
this.clientId = aClientId;
this.clientSecret = aClientSecret;
this.scope = Arrays.asList("User.Read");
this.baseUserEndpoint = "https://graph.microsoft.com/v1.0/me";
}

@Override
public DefaultApi20 getApiInstance(){
return MicrosoftAzureActiveDirectory20Api.instance();
}

@Override
protected ParsedUserResponse parseUserResponse(final String responseBody) {
try ( StringReader rdr = new StringReader(responseBody);
JsonReader jrdr = Json.createReader(rdr) ) {
JsonObject response = jrdr.readObject();
AuthenticatedUserDisplayInfo displayInfo = new AuthenticatedUserDisplayInfo(
response.getString("givenName", ""),
response.getString("surname", ""),
response.getString("userPrincipalName", ""),
Copy link
Contributor

@poikilotherm poikilotherm Oct 23, 2019

Choose a reason for hiding this comment

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

IMHO it's not a good idea to prefill the users email attribute with the userPrincipalName claim.
According to https://docs.microsoft.com/en-us/azure/active-directory/hybrid/plan-connect-userprincipalname, it is not guaranteed that you will have an email-adress in there. Your AD admin can decide otherwise.

Shouldn't we be using the mail claim instead?

Related docs:

As Dataverse is able to deal with an empty mail attribute, it should be ok if we receive an empty value.

Copy link
Member

Choose a reason for hiding this comment

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

I didn't click any of the links above, but yes, if an email address is not provided, the user will be asked to fill it in before creating their Dataverse account.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you considered the mail claim is the best option for this problem, We can change it.
An advantage for Microsoft Login is the possibility of get a user email directly from API compared with others services such as Orcid or GitHub.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hi!

We test with mail claim but is valid only for institutional accounts, unfortunaly the jobTitle claim is similar.
If we considered all Microsoft accounts for login in Dataverse, We need use userPrincipalName.

Copy link
Member

Choose a reason for hiding this comment

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

@Gerafp if you're happy with the code, I'm happy. I'm moving this to QA.

"", "");
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

We view the options for receive these values and can implement this in the future. It's really interesting for us. :D

Copy link
Contributor

@poikilotherm poikilotherm Oct 23, 2019

Choose a reason for hiding this comment

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

Could the position field be filled with jobTitle claim already accessible with User.Read? Does that value make sense here? 🤔

String persistentUserId = response.getString("id");
String username = response.getString("userPrincipalName");
return new ParsedUserResponse(displayInfo, persistentUserId, username,
(displayInfo.getEmailAddress().length() > 0 ? Collections.singletonList(displayInfo.getEmailAddress()) : Collections.emptyList() )
Copy link
Contributor

@poikilotherm poikilotherm Oct 23, 2019

Choose a reason for hiding this comment

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

I know that the GitHub implementation is using this, too. I am really keen to see if this actually works, as the email response is retrieved as a string, not being converted to a list anywhere as far as I can see.

I don't know if this has ever been tested with the GitHub provider. It's a good idea to test this anyway, as I don't know what happens when you receive a JSON array of mails in response.getString().

Could you please test this for us?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, We test this. :D

);
}
}

public boolean isDisplayIdentifier()
{
return false;
}
}
2 changes: 1 addition & 1 deletion src/main/java/propertyFiles/Bundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ oauth2.convertAccount.success=Your Dataverse account is now associated with your

# oauth2/callback.xhtml
oauth2.callback.page.title=OAuth Callback
oauth2.callback.message=<strong>Authentication Error</strong> - Dataverse could not authenticate your ORCID login. Please make sure you authorize your ORCID account to connect with Dataverse. For more details about the information being requested, see the <a href="{0}/{1}/user/account.html#orcid-log-in" title="ORCID Log In - Dataverse User Guide" target="_blank">User Guide</a>.
oauth2.callback.message=<strong>Authentication Error</strong> - Dataverse could not authenticate your login with the provider that you selected. Please make sure you authorize your account to connect with Dataverse. For more details about the information being requested, see the <a href="{0}/{1}/user/account.html#remote-authentication" title="Remote Authentication - Dataverse User Guide" target="_blank">User Guide</a>.

# tab on dataverseuser.xhtml
apitoken.title=API Token
Expand Down