diff --git a/cypress/component/UserProfile/user_profile.spec.jsx b/cypress/component/UserProfile/user_profile.spec.jsx index 4046bcea4..ddcd85e34 100644 --- a/cypress/component/UserProfile/user_profile.spec.jsx +++ b/cypress/component/UserProfile/user_profile.spec.jsx @@ -3,6 +3,7 @@ import {mount} from 'cypress/react'; import React from 'react'; import {Storage} from '../../../src/libs/storage'; +import {AuthenticateNIH} from '../../../src/libs/ajax/AuthenticateNIH'; import {User} from '../../../src/libs/ajax/User'; import {Institution} from '../../../src/libs/ajax/Institution'; import UserProfile from '../../../src/pages/user_profile/UserProfile'; @@ -32,6 +33,7 @@ describe('User Profile', () => { cy.stub(User, 'getMe').returns(duosUser); cy.stub(User, 'getApprovedDatasets').returns([]); cy.stub(User, 'getAcknowledgements').returns({}); + cy.stub(AuthenticateNIH, 'getECMAccountStatus').returns(undefined); cy.intercept( {method: 'PUT', url: '**/user'}, {statusCode: 200, body: duosUser} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index c256a0e7a..802ecd235 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -50,6 +50,7 @@ Cypress.Commands.add('initApplicationConfig', () => { 'ontologyApiUrl': '', 'terraUrl': '', 'tdrApiUrl': '', + 'ecmApiUrl': '', 'errorApiKey': '', 'profileUrl': '', 'nihUrl': '', diff --git a/docs/eRA_Commons.md b/docs/eRA_Commons.md new file mode 100644 index 000000000..2269ff5ed --- /dev/null +++ b/docs/eRA_Commons.md @@ -0,0 +1,35 @@ +# RAS/eRA Commons Integration + +DUOS uses ECM as an intermediary to allow users to authenticate +with NIH. ECM provides a redirect url that we point the user to. +Once authenticated, the user is redirected back to ECM which saves +the authentication information and then redirects the user back to +the originating URL. DUOS, historically, also saved this information +locally in Consent. This allows Data Access Committees the ability to +see if a researcher is an NIH user. + +```mermaid +%%{init: { 'theme': 'forest' } }%% +sequenceDiagram + User ->> DUOS: clicks the eRA Commons button + DUOS ->> ECM: Get authorization url + Note over DUOS, ECM: POST /api/oauth/v1/{provider}/authorization-url + Note over DUOS, ECM: include a redirectUri query parameter + Note over DUOS, ECM: include a { "redirectTo": "url" } request body + ECM ->> DUOS: return auth url + DUOS ->> User: send user new url to follow + User ->> NIH: User is forwarded to NIH + NIH ->> NIH: User Auths + NIH ->> DUOS: Return with user state + Note over DUOS, NIH: Gets the oauth code from NIH + DUOS ->> ECM: Post oauthcode to ECM + Note over DUOS, ECM: POST /api/oauth/v1/{provider}/oauthcode + Note over DUOS, ECM: include state, oauthcode + ECM ->> DUOS: return LinkInfo + Note over ECM, DUOS: response includes externalUserId redirectTo + DUOS ->> DUOS: Decode/validate ECM response + DUOS ->> Consent: Save eRA Commons state to Consent for local purposes + DUOS ->> User: Redirect user to original redirectTo + User ->> DUOS: Original page is refreshed + DUOS ->> User: Updates user display +``` diff --git a/public/config-example.json b/public/config-example.json index 96ee406df..b4636db4f 100644 --- a/public/config-example.json +++ b/public/config-example.json @@ -6,6 +6,7 @@ "ontologyApiUrl": "https://ontologyURL.org/", "terraUrl": "https://terraURL.org/", "tdrApiUrl": "https://tdrApiUrl.org/", + "ecmApiUrl": "https://ecmApiUrl.org", "errorApiKey": "example", "gaId": "", "profileUrl": "https://profile-dot-broad-shibboleth-prod.appspot.com/dev", diff --git a/src/components/ERACommons.jsx b/src/components/ERACommons.jsx index 9da78b9d9..682760d90 100644 --- a/src/components/ERACommons.jsx +++ b/src/components/ERACommons.jsx @@ -62,15 +62,32 @@ export default function ERACommons(props) { setExpirationCount(eraAuthState.expirationCount); setEraCommonsId(eraAuthState.eraCommonsId); onNihStatusUpdate(eraAuthState.nihValid); + // TODO Testing code to replace old functionality with: + try { + const ecmResponse = AuthenticateNIH.getECMAccountStatus(); + console.log('ecmResponse', ecmResponse); + } catch (err) { + console.log(err); + } }; initResearcherProfile(); }, [researcherProfile, onNihStatusUpdate]); + // eslint-disable-next-line no-unused-vars const redirectToNihLogin = async () => { const returnUrl = window.location.origin + '/' + destination + '?nih-username-token='; window.location.href = `${ await Config.getNihUrl() }?${queryString.stringify({ 'return-url': returnUrl })}`; }; + // eslint-disable-next-line no-unused-vars + const redirectToECMAuthUrl = async () => { + let origin = window.location.origin; + const redirectTo = origin + '/' + destination; + const authUrl = await AuthenticateNIH.getECMProviderAuthUrl(origin, redirectTo); + console.log('authUrl', authUrl); + window.location.href = authUrl; + }; + const deleteNihAccount = async () => { const deleteResponse = await AuthenticateNIH.deleteAccountLinkage(); if (deleteResponse) { @@ -78,7 +95,7 @@ export default function ERACommons(props) { const eraAuthState = extractEraAuthenticationState(response.researcherProperties); setAuthorized(eraAuthState.isAuthorized); setExpirationCount(eraAuthState.expirationCount); - setEraCommonsId(researcherProfile.eraCommonsId); + setEraCommonsId(undefined); onNihStatusUpdate(eraAuthState.nihValid); setSearch(''); } else { @@ -101,7 +118,7 @@ export default function ERACommons(props) {
Authenticate your account diff --git a/src/libs/ajax.js b/src/libs/ajax.js index bb123494a..be9bb2173 100644 --- a/src/libs/ajax.js +++ b/src/libs/ajax.js @@ -42,6 +42,10 @@ export const getOntologyUrl = async() => { return await Config.getOntologyApiUrl(); }; +export const getECMUrl = async() => { + return await Config.getECMUrl(); +}; + export const fetchOk = async (...args) => { //TODO: Remove spinnerService calls spinnerService.showAll(); diff --git a/src/libs/ajax/AuthenticateNIH.js b/src/libs/ajax/AuthenticateNIH.js index ff454dfdf..a06aed727 100644 --- a/src/libs/ajax/AuthenticateNIH.js +++ b/src/libs/ajax/AuthenticateNIH.js @@ -1,18 +1,55 @@ -import * as fp from 'lodash/fp'; -import { Config } from '../config'; -import { getApiUrl, fetchOk } from '../ajax'; +import {Config} from '../config'; +import {getApiUrl, getECMUrl, reportError} from '../ajax'; +import axios from 'axios'; +import {get, isNil, merge} from 'lodash'; +/** + * ECM has several different providers such as `era-commons`, `ras`, `github`, `fence`, and others. + * @type {string} + */ +const provider = 'ras'; + +axios.interceptors.response.use(function (response) { + return response; +}, function (error) { + // Default to a 502 when we can't get a real response object. + const status = get(error, 'response.status', 502); + const reportUrl = get(error, 'response.config.url', null); + if (!isNil(reportUrl) && status >= 500) { + reportError(reportUrl, status); + } + return Promise.reject(error); +}); export const AuthenticateNIH = { saveNihUsr: async (decodedData) => { const url = `${await getApiUrl()}/api/nih`; - const res = await fetchOk(url, fp.mergeAll([Config.authOpts(), Config.jsonBody(decodedData), { method: 'POST' }])); - return await res.json(); + const res = await axios.post(url, JSON.stringify(decodedData), merge(Config.authOpts(), {headers: {'Content-Type': 'application/json'}})); + return await res.data; }, deleteAccountLinkage: async () => { const url = `${await getApiUrl()}/api/nih`; - const res = await fetchOk(url, fp.mergeAll([Config.authOpts(), { method: 'DELETE' }])); - return await res; + return await axios.delete(url, Config.authOpts()); + }, + + getECMAccountStatus: async () => { + const url = `${await getECMUrl()}/api/oauth/v1/${provider}`; + const res = await axios.get(url, Config.authOpts()); + if (res.status === 200) { + return res.data; + } + return undefined; }, + + getECMProviderAuthUrl: async (redirectUri, redirectTo) => { + const url = `${await getECMUrl()}/api/oauth/v1/${provider}/authorization-url?redirectUri=${redirectUri}`; + console.log('url', url); + const res = await axios.post(url, {redirectTo: redirectTo}, Config.authOpts()); + if (res.status === 200) { + return res.data; + } + return undefined; + }, + }; diff --git a/src/libs/config.js b/src/libs/config.js index 9916046a8..8900d14b7 100644 --- a/src/libs/config.js +++ b/src/libs/config.js @@ -11,6 +11,8 @@ export const Config = { getOntologyApiUrl: async () => (await getConfig()).ontologyApiUrl, + getECMUrl: async () => (await getConfig()).ecmApiUrl, + getTdrApiUrl: async () => (await getConfig()).tdrApiUrl, getTerraUrl: async () => (await getConfig()).terraUrl,