From d589b8bb2fc6a02fa698b3833dbc77c926f20b0a Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Tue, 26 Mar 2024 10:21:18 -0400 Subject: [PATCH 01/15] Added oidcBroker and populated configs --- .eslintrc | 12 ++++++++-- package-lock.json | 52 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ src/App.jsx | 7 ++++++ src/index.js | 17 +++++++++++--- src/libs/oidcBroker.ts | 50 ++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 3 ++- 7 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 src/libs/oidcBroker.ts diff --git a/.eslintrc b/.eslintrc index e1c55dae4..219a4fb2f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -51,6 +51,14 @@ "@babel/implicit-arrow-linebreak": "off", "@babel/array-bracket-newline": "off", "@babel/brace-style": "off", - "@babel/semi": "warn" - } + "@babel/semi": "warn", + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "rules": { + "no-undef": "off" + } + } + ] } diff --git a/package-lock.json b/package-lock.json index 35a9e7d75..005f61420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "mixin-deep": "2.0.1", "moment": "2.30.1", "noty": "3.2.0-beta", + "oidc-client-ts": "^3.0.1", "query-string": "9.0.0", "react": "18.2.0", "react-dom": "18.2.0", @@ -38,6 +39,7 @@ "react-markdown": "9.0.1", "react-material-icon-svg": "3.20.0", "react-modal": "3.16.1", + "react-oidc-context": "^3.0.0", "react-paginating": "1.4.0", "react-protected-mailto": "1.0.3", "react-router-dom": "5.3.0", @@ -14615,6 +14617,14 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -16057,6 +16067,17 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/oidc-client-ts": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz", + "integrity": "sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg==", + "dependencies": { + "jwt-decode": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -18361,6 +18382,18 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" } }, + "node_modules/react-oidc-context": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.0.0.tgz", + "integrity": "sha512-VmSnEGWl3pTMO5zT94pGAwoK58njg6VPVFXbrepUGsLhSM0IVEKN0DtzNJvTtDSUOPA4xnJ6+jiq1fgdrWtHSQ==", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "oidc-client-ts": "^3.0.0", + "react": ">=16.8.0" + } + }, "node_modules/react-paginating": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/react-paginating/-/react-paginating-1.4.0.tgz", @@ -33182,6 +33215,11 @@ "safe-buffer": "^5.0.1" } }, + "jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==" + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -34139,6 +34177,14 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "oidc-client-ts": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz", + "integrity": "sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg==", + "requires": { + "jwt-decode": "^4.0.0" + } + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -35662,6 +35708,12 @@ "warning": "^4.0.3" } }, + "react-oidc-context": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.0.0.tgz", + "integrity": "sha512-VmSnEGWl3pTMO5zT94pGAwoK58njg6VPVFXbrepUGsLhSM0IVEKN0DtzNJvTtDSUOPA4xnJ6+jiq1fgdrWtHSQ==", + "requires": {} + }, "react-paginating": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/react-paginating/-/react-paginating-1.4.0.tgz", diff --git a/package.json b/package.json index 6ff673ad5..87fe4d54c 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "mixin-deep": "2.0.1", "moment": "2.30.1", "noty": "3.2.0-beta", + "oidc-client-ts": "^3.0.1", "query-string": "9.0.0", "react": "18.2.0", "react-dom": "18.2.0", @@ -33,6 +34,7 @@ "react-markdown": "9.0.1", "react-material-icon-svg": "3.20.0", "react-modal": "3.16.1", + "react-oidc-context": "^3.0.0", "react-paginating": "1.4.0", "react-protected-mailto": "1.0.3", "react-router-dom": "5.3.0", diff --git a/src/App.jsx b/src/App.jsx index 908ee2737..7c3f1c372 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -58,6 +58,13 @@ function App() { stackdriverStart(); }); + useEffect(() => { + const initAuth = async () => { + + }; + initAuth(); + },[]); + useEffect(() => { const setUserIsLogged = async () => { const isLogged = await Storage.userIsLogged(); diff --git a/src/index.js b/src/index.js index b05691043..e1e9dbc90 100644 --- a/src/index.js +++ b/src/index.js @@ -10,9 +10,20 @@ import './index.css'; import App from './App'; import { unregister } from './registerServiceWorker'; import { BrowserRouter } from 'react-router-dom'; +import { OidcBroker } from './libs/oidcBroker'; +import { AuthProvider } from 'react-oidc-context'; unregister(); -const container = document.getElementById('root'); -const root = createRoot(container); -root.render(); +OidcBroker.initializeAuth().then(() => { + const container = document.getElementById('root'); + const root = createRoot(container); + root.render( + + + + + ); +}); + + diff --git a/src/libs/oidcBroker.ts b/src/libs/oidcBroker.ts new file mode 100644 index 000000000..b6df2612e --- /dev/null +++ b/src/libs/oidcBroker.ts @@ -0,0 +1,50 @@ +import { UserManagerSettings, WebStorageStateStore } from "oidc-client-ts"; +import { Config } from "./config"; +import axios from 'axios'; // TODO: move this to ajax + +interface OAuthConfig { + clientId: string, + authorityEndpoint: string, +} + +let config: OAuthConfig | null = null; +let userManagerSettings: UserManagerSettings | null = null; + +const generateOidcUserManagerSettings = async (config: OAuthConfig): Promise => { + const metadata = { + authorization_endpoint: `${await Config.getApiUrl()}/oauth2/authorize`, + token_endpoint: `${await Config.getApiUrl()}/oauth2/token`, + }; + return { + authority: config.authorityEndpoint, + client_id: config.clientId, + popup_redirect_uri: `${window.origin}/redirect-from-oauth`, + silent_redirect_uri: `${window.origin}/redirect-from-oauth-silent`, + metadata, + prompt: 'consent login', + scope: 'openid email profile', + stateStore: new WebStorageStateStore({ store: window.localStorage }), + userStore: new WebStorageStateStore({ store: window.localStorage }), + automaticSilentRenew: true, + // Time before access token expires when access token expiring event is fired + accessTokenExpiringNotificationTimeInSeconds: 330, + includeIdTokenInSilentRenew: true, + extraQueryParams: { access_type: 'offline' }, + redirect_uri: '', // this field is not being used currently, but is expected from UserManager + }; + }; + +export const OidcBroker = { + initializeAuth: async (): Promise => { + // TODO: Move request to an AJAX call + const configUrl = `${await Config.getApiUrl()}/oauth2/configuration`; + const res: OAuthConfig = (await axios.get(configUrl)).data; + config = res; + console.log(JSON.stringify(config)); + const ums: UserManagerSettings = await generateOidcUserManagerSettings(config); + userManagerSettings = ums; + }, + getOidcUserManagerSettings: (): UserManagerSettings => userManagerSettings!, +} + + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 36359ff65..811b6c394 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "target": "es2022", "useUnknownInCatchVariables": true, "paths": { - "@/*": ["src/*"] + "src/*": ["./src/*"], + "@/*": ["./*"], } }, "include": [ From 1645f0d951f498b3119a097fc3932ac692be99c3 Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Tue, 2 Apr 2024 07:46:46 -0400 Subject: [PATCH 02/15] WIP appLoader --- src/appLoader.tsx | 16 +++++ src/components/SignIn.jsx | 87 ++++++++++++++++++-------- src/components/Spinner.jsx | 2 +- src/index.js | 29 --------- src/index.ts | 22 +++++++ src/libs/auth/RedirectFromOAuth.ts | 23 +++++++ src/libs/auth/oauth-redirect-loader.ts | 6 ++ src/libs/{ => auth}/oidcBroker.ts | 7 ++- 8 files changed, 133 insertions(+), 59 deletions(-) create mode 100644 src/appLoader.tsx delete mode 100644 src/index.js create mode 100644 src/index.ts create mode 100644 src/libs/auth/RedirectFromOAuth.ts create mode 100644 src/libs/auth/oauth-redirect-loader.ts rename src/libs/{ => auth}/oidcBroker.ts (89%) diff --git a/src/appLoader.tsx b/src/appLoader.tsx new file mode 100644 index 000000000..ce739e991 --- /dev/null +++ b/src/appLoader.tsx @@ -0,0 +1,16 @@ +import { AuthProvider } from "react-oidc-context"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; +import { OidcBroker } from "./libs/auth/oidcBroker"; +import { createRoot } from "react-dom/client"; +import React from "react"; + +const container = document.getElementById("root"); +const root = createRoot(container!); +root.render( + + + + + +); diff --git a/src/components/SignIn.jsx b/src/components/SignIn.jsx index 31c846a2e..fa9f2abaf 100644 --- a/src/components/SignIn.jsx +++ b/src/components/SignIn.jsx @@ -13,12 +13,13 @@ import ReactTooltip from 'react-tooltip'; import { GoogleIS } from '../libs/googleIS'; import eventList from '../libs/events'; import { StackdriverReporter } from '../libs/stackdriverReporter'; +import { AuthContextProps, useAuth } from 'react-oidc-context'; export const SignIn = (props) => { const [clientId, setClientId] = useState(''); const [errorDisplay, setErrorDisplay] = useState({}); const { onSignIn, history, customStyle } = props; - + const authInstance = useAuth(); useEffect(() => { // Using `isSubscribed` resolves the // "To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function." warning @@ -86,7 +87,10 @@ export const SignIn = (props) => { const shouldRedirectTo = (page) => page !== '/' && page !== '/home'; - const attemptSignInCheckToSAndRedirect = async (redirectTo, shouldRedirect) => { + const attemptSignInCheckToSAndRedirect = async ( + redirectTo, + shouldRedirect + ) => { await checkToSAndRedirect(shouldRedirect ? redirectTo : null); Metrics.identify(Storage.getAnonymousId()); Metrics.syncProfile(); @@ -108,7 +112,9 @@ export const SignIn = (props) => { Metrics.identify(Storage.getAnonymousId()); Metrics.syncProfile(); Metrics.captureEvent(eventList.userRegister); - history.push(`/tos_acceptance${shouldRedirect ? `?redirectTo=${redirectTo}` : ''}`); + history.push( + `/tos_acceptance${shouldRedirect ? `?redirectTo=${redirectTo}` : ''}` + ); }; const handleErrors = async (error, redirectTo, shouldRedirect) => { @@ -116,13 +122,21 @@ export const SignIn = (props) => { switch (status) { case 400: - setErrorDisplay({ show: true, title: 'Error', msg: JSON.stringify(error) }); + setErrorDisplay({ + show: true, + title: 'Error', + msg: JSON.stringify(error), + }); break; case 409: handleConflictError(redirectTo, shouldRedirect); break; default: - setErrorDisplay({ show: true, title: 'Error', msg: 'Unexpected error, please try again' }); + setErrorDisplay({ + show: true, + title: 'Error', + msg: 'Unexpected error, please try again', + }); break; } }; @@ -152,37 +166,56 @@ export const SignIn = (props) => { } }; + // add sign in popup here const spinnerOrSignInButton = () => { - return clientId === '' - ? Spinner - :
- {isNil(customStyle) - ? GoogleIS.signInButton(clientId, onSuccess, onFailure) - : } - {isNil(customStyle) && + return clientId === '' ? ( + Spinner + ) : ( +
+ { + // eslint-disable-next-line no-constant-condition + false /*isNil(customStyle)*/ ? ( + GoogleIS.signInButton(clientId, onSuccess, onFailure) + ) : ( + + ) + } + {isNil(customStyle) && ( - } - -
; + )} + +
+ ); }; return (
- {isEmpty(errorDisplay) - ?
- {spinnerOrSignInButton()} -
- :
+ {isEmpty(errorDisplay) ? ( +
{spinnerOrSignInButton()}
+ ) : ( +
{ description={errorDisplay.description} />
- } + )}
); }; diff --git a/src/components/Spinner.jsx b/src/components/Spinner.jsx index f2ff470c2..d6a416710 100644 --- a/src/components/Spinner.jsx +++ b/src/components/Spinner.jsx @@ -17,4 +17,4 @@ export const Spinner = ( // SupportRequestModal, ElectionTimeoutModal, // DacDatasetsModal, EraCommons, ApplicationSummaryModa, AddUserModal, AddInstitutionModal, // LibraryCardTable, DarTableCancelButton, SubmitVoteBox -// See SignIn for example usage of this constant. +// See SignIn for example usage of this constant. \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100644 index e1e9dbc90..000000000 --- a/src/index.js +++ /dev/null @@ -1,29 +0,0 @@ -import 'bootstrap/dist/css/bootstrap.min.css'; - -// jquery is needed for bootstrap -import 'jquery/src/jquery'; -import 'bootstrap/dist/js/bootstrap.min'; - -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import './index.css'; -import App from './App'; -import { unregister } from './registerServiceWorker'; -import { BrowserRouter } from 'react-router-dom'; -import { OidcBroker } from './libs/oidcBroker'; -import { AuthProvider } from 'react-oidc-context'; - -unregister(); - -OidcBroker.initializeAuth().then(() => { - const container = document.getElementById('root'); - const root = createRoot(container); - root.render( - - - - - ); -}); - - diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..dc54f805d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,22 @@ +import "bootstrap/dist/css/bootstrap.min.css"; + +// jquery is needed for bootstrap +import "jquery/src/jquery"; +import "bootstrap/dist/js/bootstrap.min"; + +import React from "react"; +import "./index.css"; +import { OidcBroker } from "./libs/auth/oidcBroker"; +import { unregister } from "./registerServiceWorker"; + +const load = async () => { + unregister(); + await OidcBroker.initializeAuth(); + + window.location.pathname.startsWith("/redirect-from-oauth") + ? import("./libs/auth/oauth-redirect-loader") + : import("./appLoader"); +} + +load(); + diff --git a/src/libs/auth/RedirectFromOAuth.ts b/src/libs/auth/RedirectFromOAuth.ts new file mode 100644 index 000000000..1af00261f --- /dev/null +++ b/src/libs/auth/RedirectFromOAuth.ts @@ -0,0 +1,23 @@ +import { UserManager } from 'oidc-client-ts'; +import { useEffect } from 'react'; +import { Spinner } from '../../components/Spinner'; +import { OidcBroker } from './oidcBroker'; +import React from "react"; + +const RedirectFromOAuth = (): JSX.Element => { + const userManager: UserManager = new UserManager(OidcBroker.getOidcUserManagerSettings()); + console.log("inside redirect"); + const url = window.location.href; + const isSilent = window.location.pathname.startsWith('/redirect-from-oauth-silent'); + useEffect(() => { + if (isSilent) { + userManager.signinSilentCallback(url); + } else { + userManager.signinPopupCallback(url); + } + }, []); + const spinner: JSX.Element = Spinner; + return spinner; + }; + + export default RedirectFromOAuth; \ No newline at end of file diff --git a/src/libs/auth/oauth-redirect-loader.ts b/src/libs/auth/oauth-redirect-loader.ts new file mode 100644 index 000000000..c623b866b --- /dev/null +++ b/src/libs/auth/oauth-redirect-loader.ts @@ -0,0 +1,6 @@ +import { createRoot } from "react-dom/client"; +import RedirectFromOAuth from "./RedirectFromOAuth"; + +const rootElement = document.getElementById('root'); +const root = createRoot(rootElement!); +root.render(RedirectFromOAuth()); \ No newline at end of file diff --git a/src/libs/oidcBroker.ts b/src/libs/auth/oidcBroker.ts similarity index 89% rename from src/libs/oidcBroker.ts rename to src/libs/auth/oidcBroker.ts index b6df2612e..cb0baabee 100644 --- a/src/libs/oidcBroker.ts +++ b/src/libs/auth/oidcBroker.ts @@ -1,5 +1,5 @@ import { UserManagerSettings, WebStorageStateStore } from "oidc-client-ts"; -import { Config } from "./config"; +import { Config } from "../config"; import axios from 'axios'; // TODO: move this to ajax interface OAuthConfig { @@ -11,7 +11,10 @@ let config: OAuthConfig | null = null; let userManagerSettings: UserManagerSettings | null = null; const generateOidcUserManagerSettings = async (config: OAuthConfig): Promise => { - const metadata = { + console.log(`${await Config.getApiUrl()}/oauth2/authorize`); + console.log(`${await Config.getApiUrl()}/oauth2/token`); + console.log(`${config.authorityEndpoint}`); + const metadata = { authorization_endpoint: `${await Config.getApiUrl()}/oauth2/authorize`, token_endpoint: `${await Config.getApiUrl()}/oauth2/token`, }; From 39be391aacdd972874e2ba5d7c662c31ec8483e6 Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Tue, 2 Apr 2024 11:57:59 -0400 Subject: [PATCH 03/15] Successful sign in --- .eslintrc | 2 + src/App.jsx | 7 ++- src/appLoader.tsx | 14 ++--- src/components/SignIn.jsx | 5 +- src/index.ts | 27 +++++---- src/libs/auth/RedirectFromOAuth.ts | 42 ++++++++------ src/libs/auth/oauth-redirect-loader.ts | 5 +- src/libs/auth/oidcBroker.ts | 80 +++++++++++++------------- 8 files changed, 96 insertions(+), 86 deletions(-) diff --git a/.eslintrc b/.eslintrc index 219a4fb2f..cf809850a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -52,6 +52,8 @@ "@babel/array-bracket-newline": "off", "@babel/brace-style": "off", "@babel/semi": "warn", + // `any` is useful for incremental type improvements during the transition from JS to TS. + "@typescript-eslint/no-explicit-any": "off", }, "overrides": [ { diff --git a/src/App.jsx b/src/App.jsx index 7c3f1c372..5d7bb34cb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -13,13 +13,14 @@ import {StackdriverReporter} from './libs/stackdriverReporter'; import {Storage} from './libs/storage'; import Routes from './Routes'; import {GoogleIS} from './libs/googleIS'; +import { useAuth } from 'react-oidc-context'; function App() { const [isLoggedIn, setIsLoggedIn] = useState(false); const [env, setEnv] = useState(''); let history = useHistory(); let location = useLocation(); - + const auth = useAuth(); const trackPageView = (location) => { ReactGA.send({ hitType: 'pageview', page: location.pathname+location.search }); }; @@ -59,8 +60,10 @@ function App() { }); useEffect(() => { + const initAuth = async () => { - + + auth.signoutSilent(); }; initAuth(); },[]); diff --git a/src/appLoader.tsx b/src/appLoader.tsx index ce739e991..e9b432999 100644 --- a/src/appLoader.tsx +++ b/src/appLoader.tsx @@ -1,11 +1,11 @@ -import { AuthProvider } from "react-oidc-context"; -import { BrowserRouter } from "react-router-dom"; -import App from "./App"; -import { OidcBroker } from "./libs/auth/oidcBroker"; -import { createRoot } from "react-dom/client"; -import React from "react"; +import { AuthProvider } from 'react-oidc-context'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import { OidcBroker } from './libs/auth/oidcBroker'; +import { createRoot } from 'react-dom/client'; +import React from 'react'; -const container = document.getElementById("root"); +const container = document.getElementById('root'); const root = createRoot(container!); root.render( diff --git a/src/components/SignIn.jsx b/src/components/SignIn.jsx index fa9f2abaf..aab7b2512 100644 --- a/src/components/SignIn.jsx +++ b/src/components/SignIn.jsx @@ -13,7 +13,7 @@ import ReactTooltip from 'react-tooltip'; import { GoogleIS } from '../libs/googleIS'; import eventList from '../libs/events'; import { StackdriverReporter } from '../libs/stackdriverReporter'; -import { AuthContextProps, useAuth } from 'react-oidc-context'; +import { useAuth } from 'react-oidc-context'; export const SignIn = (props) => { const [clientId, setClientId] = useState(''); @@ -181,6 +181,9 @@ export const SignIn = (props) => { className={'btn-primary'} style={customStyle} onClick={() => { + if (authInstance.user) { + console.log(JSON.stringify(authInstance.user)); + } //pop works but it does not go to the correct location authInstance.signinPopup(); //GoogleIS.requestAccessToken(clientId, onSuccess, onFailure); diff --git a/src/index.ts b/src/index.ts index dc54f805d..c38240c88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,21 @@ -import "bootstrap/dist/css/bootstrap.min.css"; +import 'bootstrap/dist/css/bootstrap.min.css'; // jquery is needed for bootstrap -import "jquery/src/jquery"; -import "bootstrap/dist/js/bootstrap.min"; +import 'jquery/src/jquery'; +import 'bootstrap/dist/js/bootstrap.min'; -import React from "react"; -import "./index.css"; -import { OidcBroker } from "./libs/auth/oidcBroker"; -import { unregister } from "./registerServiceWorker"; +import './index.css'; +import { OidcBroker } from './libs/auth/oidcBroker'; +import { unregister } from './registerServiceWorker'; -const load = async () => { - unregister(); - await OidcBroker.initializeAuth(); +const load = async (): Promise => { + unregister(); + await OidcBroker.initializeAuth(); - window.location.pathname.startsWith("/redirect-from-oauth") - ? import("./libs/auth/oauth-redirect-loader") - : import("./appLoader"); -} + window.location.pathname.startsWith('/redirect-from-oauth') + ? import('./libs/auth/oauth-redirect-loader') + : import('./appLoader'); +}; load(); diff --git a/src/libs/auth/RedirectFromOAuth.ts b/src/libs/auth/RedirectFromOAuth.ts index 1af00261f..06097400c 100644 --- a/src/libs/auth/RedirectFromOAuth.ts +++ b/src/libs/auth/RedirectFromOAuth.ts @@ -1,23 +1,29 @@ import { UserManager } from 'oidc-client-ts'; -import { useEffect } from 'react'; import { Spinner } from '../../components/Spinner'; import { OidcBroker } from './oidcBroker'; -import React from "react"; const RedirectFromOAuth = (): JSX.Element => { - const userManager: UserManager = new UserManager(OidcBroker.getOidcUserManagerSettings()); - console.log("inside redirect"); - const url = window.location.href; - const isSilent = window.location.pathname.startsWith('/redirect-from-oauth-silent'); - useEffect(() => { - if (isSilent) { - userManager.signinSilentCallback(url); - } else { - userManager.signinPopupCallback(url); - } - }, []); - const spinner: JSX.Element = Spinner; - return spinner; - }; - - export default RedirectFromOAuth; \ No newline at end of file + const userManager: UserManager = new UserManager( + OidcBroker.getOidcUserManagerSettings() + ); + const url = window.location.href; + const isSilent = window.location.pathname.startsWith( + '/redirect-from-oauth-silent' + ); + + if (isSilent) { + userManager.signinSilentCallback(url); + } else { + userManager + .signinPopupCallback(url).then(function () { + console.log('signin popup callback response success'); + }) + .catch(function (err: unknown) { + console.error(err); + console.log(err); + }); + } + return Spinner; +}; + +export default RedirectFromOAuth; diff --git a/src/libs/auth/oauth-redirect-loader.ts b/src/libs/auth/oauth-redirect-loader.ts index c623b866b..0ef277df6 100644 --- a/src/libs/auth/oauth-redirect-loader.ts +++ b/src/libs/auth/oauth-redirect-loader.ts @@ -1,6 +1,5 @@ -import { createRoot } from "react-dom/client"; -import RedirectFromOAuth from "./RedirectFromOAuth"; - +import { createRoot } from 'react-dom/client'; +import RedirectFromOAuth from './RedirectFromOAuth'; const rootElement = document.getElementById('root'); const root = createRoot(rootElement!); root.render(RedirectFromOAuth()); \ No newline at end of file diff --git a/src/libs/auth/oidcBroker.ts b/src/libs/auth/oidcBroker.ts index cb0baabee..dcf1a2f0e 100644 --- a/src/libs/auth/oidcBroker.ts +++ b/src/libs/auth/oidcBroker.ts @@ -1,53 +1,51 @@ -import { UserManagerSettings, WebStorageStateStore } from "oidc-client-ts"; -import { Config } from "../config"; +import { UserManagerSettings, WebStorageStateStore } from 'oidc-client-ts'; +import { Config } from '../config'; import axios from 'axios'; // TODO: move this to ajax interface OAuthConfig { - clientId: string, - authorityEndpoint: string, + clientId: string; + authorityEndpoint: string; } let config: OAuthConfig | null = null; let userManagerSettings: UserManagerSettings | null = null; -const generateOidcUserManagerSettings = async (config: OAuthConfig): Promise => { - console.log(`${await Config.getApiUrl()}/oauth2/authorize`); - console.log(`${await Config.getApiUrl()}/oauth2/token`); - console.log(`${config.authorityEndpoint}`); +const generateOidcUserManagerSettings = async ( + config: OAuthConfig +): Promise => { const metadata = { - authorization_endpoint: `${await Config.getApiUrl()}/oauth2/authorize`, - token_endpoint: `${await Config.getApiUrl()}/oauth2/token`, - }; - return { - authority: config.authorityEndpoint, - client_id: config.clientId, - popup_redirect_uri: `${window.origin}/redirect-from-oauth`, - silent_redirect_uri: `${window.origin}/redirect-from-oauth-silent`, - metadata, - prompt: 'consent login', - scope: 'openid email profile', - stateStore: new WebStorageStateStore({ store: window.localStorage }), - userStore: new WebStorageStateStore({ store: window.localStorage }), - automaticSilentRenew: true, - // Time before access token expires when access token expiring event is fired - accessTokenExpiringNotificationTimeInSeconds: 330, - includeIdTokenInSilentRenew: true, - extraQueryParams: { access_type: 'offline' }, - redirect_uri: '', // this field is not being used currently, but is expected from UserManager - }; + authorization_endpoint: `${await Config.getApiUrl()}/oauth2/authorize`, + token_endpoint: `${await Config.getApiUrl()}/oauth2/token`, }; + return { + authority: config.authorityEndpoint, + client_id: config.clientId, + popup_redirect_uri: `${window.origin}/redirect-from-oauth`, + silent_redirect_uri: `${window.origin}/redirect-from-oauth-silent`, + metadata, + prompt: 'consent login', + scope: 'openid email profile', + stateStore: new WebStorageStateStore({ store: window.localStorage }), + userStore: new WebStorageStateStore({ store: window.localStorage }), + automaticSilentRenew: true, + // Time before access token expires when access token expiring event is fired + accessTokenExpiringNotificationTimeInSeconds: 330, + includeIdTokenInSilentRenew: true, + extraQueryParams: { access_type: 'offline' }, + redirect_uri: '', // this field is not being used currently, but is expected from UserManager + }; +}; export const OidcBroker = { - initializeAuth: async (): Promise => { - // TODO: Move request to an AJAX call - const configUrl = `${await Config.getApiUrl()}/oauth2/configuration`; - const res: OAuthConfig = (await axios.get(configUrl)).data; - config = res; - console.log(JSON.stringify(config)); - const ums: UserManagerSettings = await generateOidcUserManagerSettings(config); - userManagerSettings = ums; - }, - getOidcUserManagerSettings: (): UserManagerSettings => userManagerSettings!, -} - - \ No newline at end of file + initializeAuth: async (): Promise => { + // TODO: Move request to an AJAX call + const configUrl = `${await Config.getApiUrl()}/oauth2/configuration`; + const res: OAuthConfig = (await axios.get(configUrl)).data; + config = res; + const ums: UserManagerSettings = await generateOidcUserManagerSettings( + config + ); + userManagerSettings = ums; + }, + getOidcUserManagerSettings: (): UserManagerSettings => userManagerSettings!, +}; From 9b8d34287b36e16585e6a898b43810699c2d0d87 Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Tue, 2 Apr 2024 12:30:07 -0400 Subject: [PATCH 04/15] Added linting scripts --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 87fe4d54c..757fbde6b 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "genschemas": "./scripts/compile-jsonschema.sh", "build": "react-scripts build", "eject": "react-scripts eject", + "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern \"/cypress/*\"", + "lint:fix": "npx eslint . --ext .js,.jsx,.ts,.tsx --ignore-pattern \"/cypress/*\" --fix", "cypress:open": "CYPRESS_ADMIN=$(cat cypress/fixtures/duos-automation-admin.json) CYPRESS_CHAIR=$(cat cypress/fixtures/duos-automation-chair.json) CYPRESS_MEMBER=$(cat cypress/fixtures/duos-automation-member.json) CYPRESS_RESEARCHER=$(cat cypress/fixtures/duos-automation-researcher.json) CYPRESS_SIGNING_OFFICIAL=$(cat cypress/fixtures/duos-automation-signing-official.json) cypress open", "cypress:open:component": "cypress open --component", "cypress:run": "CYPRESS_ADMIN=$(cat cypress/fixtures/duos-automation-admin.json) CYPRESS_CHAIR=$(cat cypress/fixtures/duos-automation-chair.json) CYPRESS_MEMBER=$(cat cypress/fixtures/duos-automation-member.json) CYPRESS_RESEARCHER=$(cat cypress/fixtures/duos-automation-researcher.json) CYPRESS_SIGNING_OFFICIAL=$(cat cypress/fixtures/duos-automation-signing-official.json) cypress run", From b9fd0d13b8aa077a593c6cc1ef0c56d590fa8086 Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Fri, 5 Apr 2024 13:15:19 -0400 Subject: [PATCH 05/15] Successful register/log in with b2c, next need to handle persistent sessions --- src/App.jsx | 10 +- src/components/DuosHeader.jsx | 26 ++-- src/components/{SignIn.jsx => SignIn.tsx} | 157 +++++++++------------- src/custom.d.ts | 8 ++ src/libs/ajax/Metrics.js | 9 +- src/libs/auth/auth.ts | 38 ++++++ src/libs/{ => auth}/googleIS.js | 2 +- src/libs/auth/oidcBroker.ts | 17 ++- src/libs/config.js | 18 +-- src/pages/Home.jsx | 4 +- 10 files changed, 158 insertions(+), 131 deletions(-) rename src/components/{SignIn.jsx => SignIn.tsx} (52%) create mode 100644 src/custom.d.ts create mode 100644 src/libs/auth/auth.ts rename src/libs/{ => auth}/googleIS.js (99%) diff --git a/src/App.jsx b/src/App.jsx index 5d7bb34cb..c0b0337be 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,7 +12,7 @@ import {SpinnerComponent as Spinner} from './components/SpinnerComponent'; import {StackdriverReporter} from './libs/stackdriverReporter'; import {Storage} from './libs/storage'; import Routes from './Routes'; -import {GoogleIS} from './libs/googleIS'; +import {GoogleIS} from './libs/auth/googleIS'; import { useAuth } from 'react-oidc-context'; function App() { @@ -60,9 +60,9 @@ function App() { }); useEffect(() => { - + const initAuth = async () => { - + auth.signoutSilent(); }; initAuth(); @@ -76,6 +76,7 @@ function App() { setUserIsLogged(); }); + //TODO: Move these to auth.ts const signOut = async () => { const clientId = await Config.getGoogleClientId(); await GoogleIS.revokeAccessToken(clientId); @@ -84,6 +85,7 @@ function App() { await setIsLoggedIn(false); }; + const signIn = async () => { await Storage.setUserIsLogged(true); await setIsLoggedIn(true); @@ -93,7 +95,7 @@ function App() {
- +
diff --git a/src/components/DuosHeader.jsx b/src/components/DuosHeader.jsx index aa0a7d412..94da12179 100644 --- a/src/components/DuosHeader.jsx +++ b/src/components/DuosHeader.jsx @@ -18,6 +18,7 @@ import Tab from '@mui/material/Tab'; import Box from '@mui/material/Box'; import {checkEnv, envGroups} from '../utils/EnvironmentUtils'; import {isFunction, isNil} from 'lodash/fp'; +import SignIn from './SignIn'; const styles = { drawerPaper: { @@ -141,7 +142,7 @@ const NavigationTabsComponent = (props) => { orientation, makeNotifications, navbarDuosIcon, duosLogoImage, DuosLogo, navbarDuosText, - currentUser, signOut, isLogged, + currentUser, signIn, signOut, isLogged, contactUsButton, showRequestModal, supportrequestModal, tabs, initialTab, initialSubTab, onSubtabChange @@ -225,7 +226,16 @@ const NavigationTabsComponent = (props) => { ) };
- {/* Navbar right side */} + {/* Navbar right side */ + // when logged in, need log in button + } + {!isLogged && ( +
+ +
+ )} {isLogged && (
{ - const { location, classes } = props; + const { location, classes, onSignIn } = props; const [state, setState] = useState({ showSupportRequestModal: false, hover: false, @@ -380,12 +390,8 @@ const DuosHeader = (props) => { toggleDrawer(false); }; - let isLogged = Storage.userIsLogged(); - let currentUser = {}; - - if (isLogged) { - currentUser = Storage.getCurrentUser(); - } + const isLogged = Storage.userIsLogged(); + const currentUser = isLogged ? Storage.getCurrentUser() : {}; const contactUsSource = state.hover ? contactUsHover : contactUsStandard; const contactUsIcon = isLogged ? '' : ; @@ -477,6 +483,7 @@ const DuosHeader = (props) => { navbarDuosText={navbarDuosText} currentUser={currentUser} isLogged={isLogged} + signIn={onSignIn} signOut={signOut} contactUsButton={contactUsButton} supportrequestModal={supportModal} @@ -523,6 +530,7 @@ const DuosHeader = (props) => { navbarDuosText={navbarDuosText} currentUser={currentUser} isLogged={isLogged} + signIn={onSignIn} signOut={signOut} contactUsButton={contactUsButton} supportrequestModal={supportModal} diff --git a/src/components/SignIn.jsx b/src/components/SignIn.tsx similarity index 52% rename from src/components/SignIn.jsx rename to src/components/SignIn.tsx index aab7b2512..791d80759 100644 --- a/src/components/SignIn.jsx +++ b/src/components/SignIn.tsx @@ -1,46 +1,46 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { isEmpty, isNil } from 'lodash/fp'; import { Alert } from './Alert'; import { ToS } from '../libs/ajax/ToS'; import { User } from '../libs/ajax/User'; import { Metrics } from '../libs/ajax/Metrics'; -import { Config } from '../libs/config'; import { Storage } from '../libs/storage'; import { Navigation, setUserRoleStatuses } from '../libs/utils'; import loadingIndicator from '../images/loading-indicator.svg'; -import { Spinner } from './Spinner'; -import ReactTooltip from 'react-tooltip'; -import { GoogleIS } from '../libs/googleIS'; +import Auth from '../libs/auth/auth'; import eventList from '../libs/events'; import { StackdriverReporter } from '../libs/stackdriverReporter'; import { useAuth } from 'react-oidc-context'; - -export const SignIn = (props) => { - const [clientId, setClientId] = useState(''); - const [errorDisplay, setErrorDisplay] = useState({}); - const { onSignIn, history, customStyle } = props; +import CSS from 'csstype'; +import { useHistory } from 'react-router'; +import { OidcUser } from 'src/libs/auth/oidcBroker'; + +interface SignInProps { + history: any; + customStyle: CSS.Properties | undefined; + onSignIn: any; +} + +interface ErrorDisplay { + title?: string, + description?: string, + show?: boolean, + msg?: string, +} + +interface HttpError extends Error { + status?: number +} + +export const SignIn = (props: SignInProps) => { + const [errorDisplay, setErrorDisplay] = useState({}); + const { onSignIn, customStyle } = props; const authInstance = useAuth(); - useEffect(() => { - // Using `isSubscribed` resolves the - // "To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function." warning - let isSubscribed = true; - const init = async () => { - if (isSubscribed) { - const googleClientId = await Config.getGoogleClientId(); - setClientId(googleClientId); - if (window.google !== undefined && GoogleIS.client === null) { - await GoogleIS.initTokenClient(googleClientId, onSuccess, onFailure); - } - } - ReactTooltip.rebuild(); - }; - init(); - return () => (isSubscribed = false); - }); + const history = useHistory(); // Utility function called in the normal success case and in the undocumented 409 case // Check for ToS Acceptance - redirect user if not set. - const checkToSAndRedirect = async (redirectPath) => { + const checkToSAndRedirect = async (redirectPath: string | null) => { // Check if the user has accepted ToS yet or not: const user = await User.getMe(); if (!user.roles) { @@ -66,30 +66,27 @@ export const SignIn = (props) => { } }; - const onSuccess = async (response) => { - Storage.setGoogleData(response); - + const onSuccess = async (response: OidcUser) => { const redirectTo = getRedirectTo(); const shouldRedirect = shouldRedirectTo(redirectTo); Storage.setAnonymousId(); - try { await attemptSignInCheckToSAndRedirect(redirectTo, shouldRedirect); - } catch (error) { + } catch (error: unknown) { await handleRegistration(redirectTo, shouldRedirect); } }; - const getRedirectTo = () => { + const getRedirectTo = (): string => { const queryParams = new URLSearchParams(window.location.search); return queryParams.get('redirectTo') || window.location.pathname; }; - const shouldRedirectTo = (page) => page !== '/' && page !== '/home'; + const shouldRedirectTo = (page: string): boolean => page !== '/' && page !== '/home'; const attemptSignInCheckToSAndRedirect = async ( - redirectTo, - shouldRedirect + redirectTo: string, + shouldRedirect: boolean, ) => { await checkToSAndRedirect(shouldRedirect ? redirectTo : null); Metrics.identify(Storage.getAnonymousId()); @@ -97,17 +94,17 @@ export const SignIn = (props) => { Metrics.captureEvent(eventList.userSignIn); }; - const handleRegistration = async (redirectTo, shouldRedirect) => { + const handleRegistration = async (redirectTo: string, shouldRedirect: boolean) => { try { await registerAndRedirectNewUser(redirectTo, shouldRedirect); - } catch (error) { - await handleErrors(error, redirectTo, shouldRedirect); + } catch (error: unknown) { + await handleErrors(error as HttpError, redirectTo, shouldRedirect); } }; - const registerAndRedirectNewUser = async (redirectTo, shouldRedirect) => { + const registerAndRedirectNewUser = async (redirectTo: string, shouldRedirect: boolean) => { const registeredUser = await User.registerUser(); - setUserRoleStatuses(registeredUser, Storage); + setUserRoleStatuses(registeredUser, Storage) await onSignIn(); Metrics.identify(Storage.getAnonymousId()); Metrics.syncProfile(); @@ -117,7 +114,7 @@ export const SignIn = (props) => { ); }; - const handleErrors = async (error, redirectTo, shouldRedirect) => { + const handleErrors = async (error: HttpError, redirectTo: string, shouldRedirect: boolean) => { const status = error.status; switch (status) { @@ -141,7 +138,7 @@ export const SignIn = (props) => { } }; - const handleConflictError = async (redirectTo, shouldRedirect) => { + const handleConflictError = async (redirectTo:string, shouldRedirect: boolean) => { try { await checkToSAndRedirect(shouldRedirect ? redirectTo : null); } catch (error) { @@ -149,15 +146,14 @@ export const SignIn = (props) => { } }; - const onFailure = (response) => { + const onFailure = (response: any) => { Storage.clearStorage(); if (response.error === 'popup_closed_by_user') { - setErrorDisplay( - + setErrorDisplay( Sign-in cancelled ... - ); + ); setTimeout(() => { setErrorDisplay({}); }, 2000); @@ -166,64 +162,31 @@ export const SignIn = (props) => { } }; - // add sign in popup here - const spinnerOrSignInButton = () => { - return clientId === '' ? ( - Spinner - ) : ( - - ); - }; + + //TODO: Add spinner aftr registering/logging in + const signInButton = (): JSX.Element => { + return () + } return (
{isEmpty(errorDisplay) ? ( -
{spinnerOrSignInButton()}
+
{signInButton()}
) : (
)} diff --git a/src/custom.d.ts b/src/custom.d.ts new file mode 100644 index 000000000..957f456d8 --- /dev/null +++ b/src/custom.d.ts @@ -0,0 +1,8 @@ +declare module '*.svg' { + export const ReactComponent: React.FunctionComponent< + React.SVGAttributes + >; + + const src: string; + export default src; +} diff --git a/src/libs/ajax/Metrics.js b/src/libs/ajax/Metrics.js index 678b09446..3d19f9cfe 100644 --- a/src/libs/ajax/Metrics.js +++ b/src/libs/ajax/Metrics.js @@ -1,6 +1,7 @@ import axios from 'axios'; import { getDefaultProperties } from '@databiosphere/bard-client'; +import Auth from '../auth/auth'; import { Storage } from '../storage'; import { getBardApiUrl } from '../ajax'; @@ -42,11 +43,11 @@ const captureEventFn = async (event, details = {}, signal) => { method: 'POST', url: `${await getBardApiUrl()}/api/event`, data: body, - headers: isRegistered ? { Authorization: `Bearer ${Storage.getGoogleData()?.accessToken}` } : undefined, + headers: isRegistered ? { Authorization: `Bearer ${Auth.getToken()}` } : undefined, signal, }; - return axios(config); + return axios(config).catch(() => { }); }; /** @@ -59,7 +60,7 @@ const syncProfile = async (signal) => { const config = { method: 'POST', url: `${await getBardApiUrl()}/api/syncProfile`, - headers: { Authorization: `Bearer ${Storage.getGoogleData()?.accessToken}` }, + headers: { Authorization: `Bearer ${Auth.getToken()}` }, signal, }; @@ -80,7 +81,7 @@ const identify = async (anonId, signal) => { method: 'POST', url: `${await getBardApiUrl()}/api/identify`, data: body, - headers: { Authorization: `Bearer ${Storage.getGoogleData()?.accessToken}` }, + headers: { Authorization: `Bearer ${Auth.getToken()}` }, signal, }; diff --git a/src/libs/auth/auth.ts b/src/libs/auth/auth.ts new file mode 100644 index 000000000..1c898eb06 --- /dev/null +++ b/src/libs/auth/auth.ts @@ -0,0 +1,38 @@ +/* + This file should abstract out the oidcBroker actions + and implement DUOS specific auth login (signIn, signOut, etc.) +*/ + +import { AuthContextProps } from "react-oidc-context"; +import { OidcBroker, OidcUser } from "./oidcBroker"; +import { User, UserManagerSettings } from "oidc-client-ts"; + +const Auth = { + getToken: () => { + const settings: UserManagerSettings = + OidcBroker.getOidcUserManagerSettings(); + const oidcStorage: string | null = localStorage.getItem( + `oidc.user:${settings.authority}:${settings.client_id}` + ); + return oidcStorage !== null ? User.fromStorageString(oidcStorage).access_token : "token"; + }, + + signIn: async ( + authInstance: AuthContextProps, + popup: boolean, //TODO: Implement signInSilent + onSuccess?: (response: OidcUser) => Promise | void, + onFailure?: (response: any) => Promise | void + ) => { + try { + const user: OidcUser = await authInstance.signinPopup(); + onSuccess?.(user); + } catch (err) { + onFailure?.(err); + } + }, + + //TODO: Implement signOut + signOut: async () => {}, +}; + +export default Auth; diff --git a/src/libs/googleIS.js b/src/libs/auth/googleIS.js similarity index 99% rename from src/libs/googleIS.js rename to src/libs/auth/googleIS.js index 44208759c..70c671fa3 100644 --- a/src/libs/googleIS.js +++ b/src/libs/auth/googleIS.js @@ -1,5 +1,5 @@ import React from 'react'; - +//TODO: Remove this file /** * This utility is a wrapper around Google's Identity Services API * https://developers.google.com/identity/oauth2/web/guides/migration-to-gis#gis-only diff --git a/src/libs/auth/oidcBroker.ts b/src/libs/auth/oidcBroker.ts index dcf1a2f0e..03a7bf3a3 100644 --- a/src/libs/auth/oidcBroker.ts +++ b/src/libs/auth/oidcBroker.ts @@ -1,12 +1,27 @@ -import { UserManagerSettings, WebStorageStateStore } from 'oidc-client-ts'; +import { IdTokenClaims, User, UserManagerSettings, WebStorageStateStore } from 'oidc-client-ts'; import { Config } from '../config'; import axios from 'axios'; // TODO: move this to ajax +// Our config for b2C claims are defined here: https://github.com/broadinstitute/terraform-ap-deployments/tree/master/azure/b2c/policies +// The standard b2C claims are defined here: https://learn.microsoft.com/en-us/azure/active-directory/develop/id-token-claims-reference +export interface B2cIdTokenClaims extends IdTokenClaims { + email_verified?: boolean; + idp?: string; + idp_access_token?: string; + tid?: string; + ver?: string; +} + +export interface OidcUser extends User { + profile: B2cIdTokenClaims; +} + interface OAuthConfig { clientId: string; authorityEndpoint: string; } + let config: OAuthConfig | null = null; let userManagerSettings: UserManagerSettings | null = null; diff --git a/src/libs/config.js b/src/libs/config.js index 995dc1f7c..9d91f0cae 100644 --- a/src/libs/config.js +++ b/src/libs/config.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import {Storage} from './storage'; +import Auth from './auth/auth'; export const Config = { @@ -42,7 +42,7 @@ export const Config = { } }, - authOpts: (token = Token.getToken()) => ({ + authOpts: (token = Auth.getToken()) => ({ headers: { Authorization: `Bearer ${token}`, Accept: 'application/json', @@ -50,7 +50,7 @@ export const Config = { }, }), - multiPartOpts: (token = Token.getToken()) => ({ + multiPartOpts: (token = Auth.getToken()) => ({ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'multipart/form-data', @@ -65,7 +65,7 @@ export const Config = { }, }), - fileOpts: (token = Token.getToken()) => ({ + fileOpts: (token = Auth.getToken()) => ({ headers: { Authorization: `Bearer ${token}`, Accept: 'application/json', @@ -82,7 +82,7 @@ export const Config = { headers: {'Content-Type': 'application/binary'} }), - fileBody: (token = Token.getToken()) => ({ + fileBody: (token = Auth.getToken()) => ({ headers: { Authorization: `Bearer ${token}`, Accept: '*/*', @@ -90,14 +90,6 @@ export const Config = { }), }; -const Token = { - getToken: () => { - return Storage.getGoogleData() !== null ? - Storage.getGoogleData().accessToken : - 'token'; - }, -}; - const loadConfig = _.memoize(async () => { const res = await fetch('/config.json'); return res.json(); diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index cf07f2078..3e6feabee 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -95,9 +95,9 @@ const Home = (props) => {
Home header background - {!isLogged &&
+ {/* {!isLogged &&
-
} +
} */}
DUOS logo

Data Use Oversight System

From 407faf7d25b8e868bf6ae4f3320bd1be77de663d Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Fri, 12 Apr 2024 18:20:38 -0400 Subject: [PATCH 06/15] added oauth2 ajax module --- src/libs/ajax/OAuth2.ts | 17 +++++++++++++++++ src/libs/auth/oidcBroker.ts | 23 ++++++++++------------- 2 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 src/libs/ajax/OAuth2.ts diff --git a/src/libs/ajax/OAuth2.ts b/src/libs/ajax/OAuth2.ts new file mode 100644 index 000000000..7f1f264bb --- /dev/null +++ b/src/libs/ajax/OAuth2.ts @@ -0,0 +1,17 @@ +import axios from "axios"; +import { Config } from "../config"; + +export interface OAuthConfig { + clientId: string; + authorityEndpoint: string; +} + +export const OAuth2 = { + getConfig: async (): Promise => getConfig(), +}; + +const getConfig = async (): Promise => { + const configUrl = `${await Config.getApiUrl()}/oauth2/configuration`; + const res: OAuthConfig = (await axios.get(configUrl)).data; + return res; +}; diff --git a/src/libs/auth/oidcBroker.ts b/src/libs/auth/oidcBroker.ts index 03a7bf3a3..02696c3df 100644 --- a/src/libs/auth/oidcBroker.ts +++ b/src/libs/auth/oidcBroker.ts @@ -1,6 +1,6 @@ -import { IdTokenClaims, User, UserManagerSettings, WebStorageStateStore } from 'oidc-client-ts'; +import { IdTokenClaims, OidcMetadata, User, UserManager, UserManagerSettings, WebStorageStateStore } from 'oidc-client-ts'; import { Config } from '../config'; -import axios from 'axios'; // TODO: move this to ajax +import { OAuth2, OAuthConfig } from '../ajax/OAuth2'; // Our config for b2C claims are defined here: https://github.com/broadinstitute/terraform-ap-deployments/tree/master/azure/b2c/policies // The standard b2C claims are defined here: https://learn.microsoft.com/en-us/azure/active-directory/develop/id-token-claims-reference @@ -16,11 +16,7 @@ export interface OidcUser extends User { profile: B2cIdTokenClaims; } -interface OAuthConfig { - clientId: string; - authorityEndpoint: string; -} - +type OidcUserManager = UserManager; let config: OAuthConfig | null = null; let userManagerSettings: UserManagerSettings | null = null; @@ -28,7 +24,7 @@ let userManagerSettings: UserManagerSettings | null = null; const generateOidcUserManagerSettings = async ( config: OAuthConfig ): Promise => { - const metadata = { + const metadata: Partial = { authorization_endpoint: `${await Config.getApiUrl()}/oauth2/authorize`, token_endpoint: `${await Config.getApiUrl()}/oauth2/token`, }; @@ -52,15 +48,16 @@ const generateOidcUserManagerSettings = async ( }; export const OidcBroker = { - initializeAuth: async (): Promise => { - // TODO: Move request to an AJAX call - const configUrl = `${await Config.getApiUrl()}/oauth2/configuration`; - const res: OAuthConfig = (await axios.get(configUrl)).data; - config = res; + initialize: async (): Promise => { + config = await OAuth2.getConfig(); const ums: UserManagerSettings = await generateOidcUserManagerSettings( config ); userManagerSettings = ums; }, getOidcUserManagerSettings: (): UserManagerSettings => userManagerSettings!, + getOidcUser: async (): Promise => { + const userManager: OidcUserManager = new UserManager(OidcBroker.getOidcUserManagerSettings()); + return await userManager.getUser(); + } }; From 7eca6ffde58d0551275aa442af680f45c9435f0e Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Wed, 17 Apr 2024 08:35:07 -0400 Subject: [PATCH 07/15] added UserManager events, auth initializer, removed oidc context --- package-lock.json | 19 ---- package.json | 1 - src/App.jsx | 42 ++------ src/Routes.jsx | 7 -- src/appLoader.tsx | 6 +- src/components/DuosHeader.jsx | 4 +- src/components/SignIn.tsx | 10 +- src/index.ts | 4 +- src/libs/ajax.js | 2 + src/libs/ajax/Metrics.js | 10 +- src/libs/ajax/{User.js => User.ts} | 36 ++++++- src/libs/auth/RedirectFromOAuth.ts | 11 +- src/libs/auth/auth.ts | 68 ++++++++---- src/libs/auth/oidcBroker.ts | 40 ++++++- src/libs/config.js | 2 +- src/libs/storage.js | 9 -- src/pages/BackgroundSignIn.jsx | 140 ------------------------- src/pages/TermsOfService.jsx | 4 +- src/pages/TermsOfServiceAcceptance.jsx | 7 +- 19 files changed, 153 insertions(+), 269 deletions(-) rename src/libs/ajax/{User.js => User.ts} (83%) delete mode 100644 src/pages/BackgroundSignIn.jsx diff --git a/package-lock.json b/package-lock.json index 005f61420..ffe3a8e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,6 @@ "react-markdown": "9.0.1", "react-material-icon-svg": "3.20.0", "react-modal": "3.16.1", - "react-oidc-context": "^3.0.0", "react-paginating": "1.4.0", "react-protected-mailto": "1.0.3", "react-router-dom": "5.3.0", @@ -18382,18 +18381,6 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" } }, - "node_modules/react-oidc-context": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.0.0.tgz", - "integrity": "sha512-VmSnEGWl3pTMO5zT94pGAwoK58njg6VPVFXbrepUGsLhSM0IVEKN0DtzNJvTtDSUOPA4xnJ6+jiq1fgdrWtHSQ==", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "oidc-client-ts": "^3.0.0", - "react": ">=16.8.0" - } - }, "node_modules/react-paginating": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/react-paginating/-/react-paginating-1.4.0.tgz", @@ -35708,12 +35695,6 @@ "warning": "^4.0.3" } }, - "react-oidc-context": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.0.0.tgz", - "integrity": "sha512-VmSnEGWl3pTMO5zT94pGAwoK58njg6VPVFXbrepUGsLhSM0IVEKN0DtzNJvTtDSUOPA4xnJ6+jiq1fgdrWtHSQ==", - "requires": {} - }, "react-paginating": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/react-paginating/-/react-paginating-1.4.0.tgz", diff --git a/package.json b/package.json index 757fbde6b..145b8acc0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "react-markdown": "9.0.1", "react-material-icon-svg": "3.20.0", "react-modal": "3.16.1", - "react-oidc-context": "^3.0.0", "react-paginating": "1.4.0", "react-protected-mailto": "1.0.3", "react-router-dom": "5.3.0", diff --git a/src/App.jsx b/src/App.jsx index c0b0337be..ac1a92af8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,6 +3,7 @@ import ReactGA from 'react-ga4'; import Modal from 'react-modal'; import './App.css'; import {Config} from './libs/config'; +import { Auth } from './libs/auth/auth'; import DuosFooter from './components/DuosFooter'; import DuosHeader from './components/DuosHeader'; import {useHistory, useLocation} from 'react-router-dom'; @@ -10,17 +11,16 @@ import loadingImage from './images/loading-indicator.svg'; import {SpinnerComponent as Spinner} from './components/SpinnerComponent'; import {StackdriverReporter} from './libs/stackdriverReporter'; -import {Storage} from './libs/storage'; + import Routes from './Routes'; -import {GoogleIS} from './libs/auth/googleIS'; -import { useAuth } from 'react-oidc-context'; +import { Storage } from './libs/storage'; function App() { - const [isLoggedIn, setIsLoggedIn] = useState(false); + const [isLoggedIn, setIsLoggedIn] = useState(Storage.userIsLogged()); const [env, setEnv] = useState(''); - let history = useHistory(); - let location = useLocation(); - const auth = useAuth(); + const history = useHistory(); + const location = useLocation(); + const trackPageView = (location) => { ReactGA.send({ hitType: 'pageview', page: location.pathname+location.search }); }; @@ -59,15 +59,6 @@ function App() { stackdriverStart(); }); - useEffect(() => { - - const initAuth = async () => { - - auth.signoutSilent(); - }; - initAuth(); - },[]); - useEffect(() => { const setUserIsLogged = async () => { const isLogged = await Storage.userIsLogged(); @@ -76,28 +67,13 @@ function App() { setUserIsLogged(); }); - //TODO: Move these to auth.ts - const signOut = async () => { - const clientId = await Config.getGoogleClientId(); - await GoogleIS.revokeAccessToken(clientId); - await Storage.setUserIsLogged(false); - await Storage.clearStorage(); - await setIsLoggedIn(false); - }; - - - const signIn = async () => { - await Storage.setUserIsLogged(true); - await setIsLoggedIn(true); - }; - return (
- + Auth.signOut()} onSignIn={() => Auth.signIn(true)} /> - + Auth.signOut()} onSignIn={() => Auth.signIn(true)} isLogged={isLoggedIn} env={env} />
diff --git a/src/Routes.jsx b/src/Routes.jsx index 7722fa733..c02bd4217 100644 --- a/src/Routes.jsx +++ b/src/Routes.jsx @@ -21,7 +21,6 @@ import SigningOfficialDataSubmitters from './pages/signing_official_console/Sign import Translator from './pages/Translator'; import NIHPilotInfo from './pages/NIHPilotInfo'; import Status from './pages/Status'; -import BackgroundSignIn from './pages/BackgroundSignIn'; import ConsentTextGenerator from './pages/ConsentTextGenerator'; import AdminManageInstitutions from './pages/AdminManageInstitutions'; import AdminManageLC from './pages/AdminManageLC'; @@ -50,12 +49,6 @@ const Routes = (props) => ( } /> } /> - - checkEnv(envGroups.NON_STAGING) - ? - : - } /> diff --git a/src/appLoader.tsx b/src/appLoader.tsx index e9b432999..d8cb346a4 100644 --- a/src/appLoader.tsx +++ b/src/appLoader.tsx @@ -1,7 +1,5 @@ -import { AuthProvider } from 'react-oidc-context'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; -import { OidcBroker } from './libs/auth/oidcBroker'; import { createRoot } from 'react-dom/client'; import React from 'react'; @@ -9,8 +7,6 @@ const container = document.getElementById('root'); const root = createRoot(container!); root.render( - - - + ); diff --git a/src/components/DuosHeader.jsx b/src/components/DuosHeader.jsx index 94da12179..49ee6ff17 100644 --- a/src/components/DuosHeader.jsx +++ b/src/components/DuosHeader.jsx @@ -349,6 +349,7 @@ const DuosHeader = (props) => { const signOut = () => { props.history.push('/home'); toggleDrawer(false); + props.onSignOut(); }; @@ -391,8 +392,9 @@ const DuosHeader = (props) => { }; const isLogged = Storage.userIsLogged(); + //console.log('is logged:', isLogged); const currentUser = isLogged ? Storage.getCurrentUser() : {}; - + //console.log('currentUser', currentUser); const contactUsSource = state.hover ? contactUsHover : contactUsStandard; const contactUsIcon = isLogged ? '' : ; const contactUsText = isLogged ? 'Contact Us' : Contact Us; diff --git a/src/components/SignIn.tsx b/src/components/SignIn.tsx index 791d80759..c9cb36622 100644 --- a/src/components/SignIn.tsx +++ b/src/components/SignIn.tsx @@ -2,15 +2,14 @@ import React, { useState } from 'react'; import { isEmpty, isNil } from 'lodash/fp'; import { Alert } from './Alert'; import { ToS } from '../libs/ajax/ToS'; -import { User } from '../libs/ajax/User'; +import { DuosUser, User } from '../libs/ajax/User'; import { Metrics } from '../libs/ajax/Metrics'; import { Storage } from '../libs/storage'; import { Navigation, setUserRoleStatuses } from '../libs/utils'; import loadingIndicator from '../images/loading-indicator.svg'; -import Auth from '../libs/auth/auth'; +import { Auth } from '../libs/auth/auth'; import eventList from '../libs/events'; import { StackdriverReporter } from '../libs/stackdriverReporter'; -import { useAuth } from 'react-oidc-context'; import CSS from 'csstype'; import { useHistory } from 'react-router'; import { OidcUser } from 'src/libs/auth/oidcBroker'; @@ -35,14 +34,13 @@ interface HttpError extends Error { export const SignIn = (props: SignInProps) => { const [errorDisplay, setErrorDisplay] = useState({}); const { onSignIn, customStyle } = props; - const authInstance = useAuth(); const history = useHistory(); // Utility function called in the normal success case and in the undocumented 409 case // Check for ToS Acceptance - redirect user if not set. const checkToSAndRedirect = async (redirectPath: string | null) => { // Check if the user has accepted ToS yet or not: - const user = await User.getMe(); + const user: DuosUser = await User.getMe(); if (!user.roles) { await StackdriverReporter.report('roles not found for user: ' + user.email); } @@ -169,7 +167,7 @@ export const SignIn = (props: SignInProps) => { className={'btn-secondary'} style={customStyle} onClick={() => { - Auth.signIn(authInstance, true, onSuccess, onFailure); + Auth.signIn(true, onSuccess, onFailure); }} > Sign In diff --git a/src/index.ts b/src/index.ts index c38240c88..18ee71340 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,11 +7,11 @@ import 'bootstrap/dist/js/bootstrap.min'; import './index.css'; import { OidcBroker } from './libs/auth/oidcBroker'; import { unregister } from './registerServiceWorker'; +import { Auth } from './libs/auth/auth'; const load = async (): Promise => { unregister(); - await OidcBroker.initializeAuth(); - + await Auth.initialize(); window.location.pathname.startsWith('/redirect-from-oauth') ? import('./libs/auth/oauth-redirect-loader') : import('./appLoader'); diff --git a/src/libs/ajax.js b/src/libs/ajax.js index ba9ee02d0..000f3f8d7 100644 --- a/src/libs/ajax.js +++ b/src/libs/ajax.js @@ -1,4 +1,5 @@ import {getOr, isNil} from 'lodash/fp'; +import {Auth} from './auth/auth'; import {Config} from './config'; import {spinnerService} from './spinner-service'; import {StackdriverReporter} from './stackdriverReporter'; @@ -10,6 +11,7 @@ import axios from 'axios'; //return responses with statuses in the 200s and reject the rest const redirectOnLogout = () => { Storage.clearStorage(); + Auth.signOut(); window.location.href = `/home?redirectTo=${window.location.pathname}`; }; diff --git a/src/libs/ajax/Metrics.js b/src/libs/ajax/Metrics.js index 3d19f9cfe..bc5b49b4d 100644 --- a/src/libs/ajax/Metrics.js +++ b/src/libs/ajax/Metrics.js @@ -1,7 +1,7 @@ import axios from 'axios'; import { getDefaultProperties } from '@databiosphere/bard-client'; -import Auth from '../auth/auth'; +import { Auth } from '../auth/auth'; import { Storage } from '../storage'; import { getBardApiUrl } from '../ajax'; @@ -47,7 +47,7 @@ const captureEventFn = async (event, details = {}, signal) => { signal, }; - return axios(config).catch(() => { }); + return axios(config); }; /** @@ -64,7 +64,7 @@ const syncProfile = async (signal) => { signal, }; - return axios(config).catch(() => { }); + return axios(config); }; /** @@ -76,7 +76,6 @@ const syncProfile = async (signal) => { */ const identify = async (anonId, signal) => { const body = { anonId }; - const config = { method: 'POST', url: `${await getBardApiUrl()}/api/identify`, @@ -84,8 +83,7 @@ const identify = async (anonId, signal) => { headers: { Authorization: `Bearer ${Auth.getToken()}` }, signal, }; - - return axios(config).catch(() => { }); + return axios(config); }; diff --git a/src/libs/ajax/User.js b/src/libs/ajax/User.ts similarity index 83% rename from src/libs/ajax/User.js rename to src/libs/ajax/User.ts index 9e4960269..cae5438a9 100644 --- a/src/libs/ajax/User.js +++ b/src/libs/ajax/User.ts @@ -4,26 +4,55 @@ import { Config } from '../config'; import axios from 'axios'; import { getApiUrl, fetchOk, fetchAny } from '../ajax'; +export type UserRoleName = + 'Admin' | 'Chairperson' | 'Member' | 'Researcher' | + 'Alumni' | 'SigningOfficial' | 'DataSubmitter' | 'All'; + +export interface UserRole { + roleId: number, + name: UserRoleName, + userId: number, + userRoleId: number, +} + +export interface DuosUser { + createDate: Date, + displayName: string, + email: string, + emailPreference: boolean, + isAdmin: boolean, + isAlumni: boolean, + isChairPerson: boolean, + isDataSubmitter: boolean, + isMember: boolean, + isResearcher: boolean, + isSigningOfficial: boolean, + roles: UserRole[], + userId: number, +} export const User = { - getMe: async () => { + getMe: async (): Promise => { const url = `${await getApiUrl()}/api/user/me`; const res = await axios.get(url, Config.authOpts()); return res.data; }, + // @ts-ignore getById: async (id) => { const url = `${await getApiUrl()}/api/user/${id}`; const res = await axios.get(url, Config.authOpts()); return res.data; }, + // @ts-ignore list: async (roleName) => { const url = `${await getApiUrl()}/api/user/role/${roleName}`; const res = await fetchOk(url, Config.authOpts()); return res.json(); }, + // @ts-ignore create: async (user) => { const url = `${await getApiUrl()}/api/dacuser`; try { @@ -36,6 +65,7 @@ export const User = { } }, + // @ts-ignore updateSelf: async (payload) => { const url = `${await getApiUrl()}/api/user`; // We should not be updating the user's create date, associated institution, or library cards @@ -49,6 +79,7 @@ export const User = { } }, + // @ts-ignore update: async (user, userId) => { const url = `${await getApiUrl()}/api/user/${userId}`; // We should not be updating the user's create date, associated institution, or library cards @@ -86,12 +117,14 @@ export const User = { return res.data; }, + // @ts-ignore addRoleToUser: async (userId, roleId) => { const url = `${await getApiUrl()}/api/user/${userId}/${roleId}`; const res = await fetchAny(url, fp.mergeAll([Config.authOpts(), { method: 'PUT' }])); return res.json(); }, + // @ts-ignore deleteRoleFromUser: async (userId, roleId) => { const url = `${await getApiUrl()}/api/user/${userId}/${roleId}`; const res = await fetchAny(url, fp.mergeAll([Config.authOpts(), { method: 'DELETE' }])); @@ -107,6 +140,7 @@ export const User = { const res = await axios.get(url, Config.authOpts()); return res.data; }, + // @ts-ignore acceptAcknowledgments: async (...keys) => { if (keys.length === 0) { return {}; diff --git a/src/libs/auth/RedirectFromOAuth.ts b/src/libs/auth/RedirectFromOAuth.ts index 06097400c..7e0237775 100644 --- a/src/libs/auth/RedirectFromOAuth.ts +++ b/src/libs/auth/RedirectFromOAuth.ts @@ -4,7 +4,7 @@ import { OidcBroker } from './oidcBroker'; const RedirectFromOAuth = (): JSX.Element => { const userManager: UserManager = new UserManager( - OidcBroker.getOidcUserManagerSettings() + OidcBroker.getUserManagerSettings() ); const url = window.location.href; const isSilent = window.location.pathname.startsWith( @@ -14,14 +14,7 @@ const RedirectFromOAuth = (): JSX.Element => { if (isSilent) { userManager.signinSilentCallback(url); } else { - userManager - .signinPopupCallback(url).then(function () { - console.log('signin popup callback response success'); - }) - .catch(function (err: unknown) { - console.error(err); - console.log(err); - }); + userManager.signinPopupCallback(url); } return Spinner; }; diff --git a/src/libs/auth/auth.ts b/src/libs/auth/auth.ts index 1c898eb06..fcca3e327 100644 --- a/src/libs/auth/auth.ts +++ b/src/libs/auth/auth.ts @@ -3,36 +3,64 @@ and implement DUOS specific auth login (signIn, signOut, etc.) */ -import { AuthContextProps } from "react-oidc-context"; -import { OidcBroker, OidcUser } from "./oidcBroker"; -import { User, UserManagerSettings } from "oidc-client-ts"; -const Auth = { - getToken: () => { - const settings: UserManagerSettings = - OidcBroker.getOidcUserManagerSettings(); - const oidcStorage: string | null = localStorage.getItem( - `oidc.user:${settings.authority}:${settings.client_id}` - ); - return oidcStorage !== null ? User.fromStorageString(oidcStorage).access_token : "token"; +import { OidcBroker, OidcUser } from './oidcBroker'; +import { Storage } from './../storage'; +import { UserManager } from 'oidc-client-ts'; + + +export const Auth = { + getToken: (): string => { + // In a future ticket, it would be better to make get Token an async function and call + // the UserManager.getUser to get the token. Since authOpts depends on getToken being synchonous + // it would be alot of places to change the uses of authOpts. + const oidcUser: OidcUser | null = OidcBroker.getUserSync(); + return oidcUser !== null ? oidcUser.access_token : "token"; + }, + initialize: async (): Promise => { + await OidcBroker.initialize(); + const oidcUser: OidcUser | null = await OidcBroker.getUser(); + const um: UserManager = OidcBroker.getUserManager(); + // UserManager events. + // For details of each event, see https://authts.github.io/oidc-client-ts/classes/UserManagerEvents.html + um.events.addUserLoaded((user: OidcUser) => { + //TODO: Add metrics for user loaded + }); + um.events.addAccessTokenExpiring((): void => { + //TODO: Add an alert that session will expire soon + console.log('accessTokenExpiring'); + }); + um.events.addAccessTokenExpired((): void => { + Auth.signOut(); + //TODO: Add an alert that session has expired + }); + if (oidcUser !== null) { + Storage.setUserIsLogged(true); + } else { + await Auth.signOut(); + } }, signIn: async ( - authInstance: AuthContextProps, - popup: boolean, //TODO: Implement signInSilent + popup: boolean, onSuccess?: (response: OidcUser) => Promise | void, onFailure?: (response: any) => Promise | void - ) => { + ): Promise => { try { - const user: OidcUser = await authInstance.signinPopup(); - onSuccess?.(user); + const user: OidcUser | null = await OidcBroker.signIn(popup); + if (user === null) { + throw new Error('signInSilent called before signInPopup'); + } + await onSuccess?.(user); + Storage.setUserIsLogged(true); } catch (err) { onFailure?.(err); } }, - //TODO: Implement signOut - signOut: async () => {}, + signOut: async () => { + await Storage.setUserIsLogged(false); + await Storage.clearStorage(); + await OidcBroker.signOut(); + }, }; - -export default Auth; diff --git a/src/libs/auth/oidcBroker.ts b/src/libs/auth/oidcBroker.ts index 02696c3df..2885c6127 100644 --- a/src/libs/auth/oidcBroker.ts +++ b/src/libs/auth/oidcBroker.ts @@ -1,4 +1,4 @@ -import { IdTokenClaims, OidcMetadata, User, UserManager, UserManagerSettings, WebStorageStateStore } from 'oidc-client-ts'; +import { IdTokenClaims, OidcMetadata, SigninPopupArgs, SigninSilentArgs, User, UserManager, UserManagerSettings, WebStorageStateStore } from 'oidc-client-ts'; import { Config } from '../config'; import { OAuth2, OAuthConfig } from '../ajax/OAuth2'; @@ -20,6 +20,7 @@ type OidcUserManager = UserManager; let config: OAuthConfig | null = null; let userManagerSettings: UserManagerSettings | null = null; +let userManager: UserManager | null = null; const generateOidcUserManagerSettings = async ( config: OAuthConfig @@ -54,10 +55,39 @@ export const OidcBroker = { config ); userManagerSettings = ums; + userManager = new UserManager(userManagerSettings); }, - getOidcUserManagerSettings: (): UserManagerSettings => userManagerSettings!, - getOidcUser: async (): Promise => { - const userManager: OidcUserManager = new UserManager(OidcBroker.getOidcUserManagerSettings()); + getUserManager: (): UserManager => { + if (userManager === null) { + throw new Error('Cannot retrieve userManager before OidcBroker is initialized'); + } + return userManager; + }, + getUserManagerSettings: (): UserManagerSettings => { + if (userManagerSettings === null) { + throw new Error('Cannot retrieve userManagerSettings before OidcBroker is initialized'); + } + return userManagerSettings; + }, + getUser: async (): Promise => { + const userManager: OidcUserManager = new UserManager(OidcBroker.getUserManagerSettings()); return await userManager.getUser(); - } + }, + getUserSync: (): OidcUser | null => { + const settings: UserManagerSettings = + OidcBroker.getUserManagerSettings(); + const oidcStorage: string | null = localStorage.getItem( + `oidc.user:${settings.authority}:${settings.client_id}` + ); + return oidcStorage !== null ? User.fromStorageString(oidcStorage) : null; + }, + + signIn: async (popup: boolean, args?: SigninPopupArgs | SigninSilentArgs): Promise => { + const um: UserManager = OidcBroker.getUserManager(); + return popup ? await um.signinPopup(args) : await um.signinSilent(args); + }, + + signOut: async (): Promise => { + await OidcBroker.getUserManager().removeUser(); + } }; diff --git a/src/libs/config.js b/src/libs/config.js index 9d91f0cae..e9f0c0a73 100644 --- a/src/libs/config.js +++ b/src/libs/config.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import Auth from './auth/auth'; +import { Auth } from './auth/auth'; export const Config = { diff --git a/src/libs/storage.js b/src/libs/storage.js index 86129781e..c3bec380a 100644 --- a/src/libs/storage.js +++ b/src/libs/storage.js @@ -3,7 +3,6 @@ import { v4 as uuid } from 'uuid'; // Storage Variables const CurrentUser = 'CurrentUser'; // System user -const GoogleUser = 'Gapi'; // Google user info, including token const UserIsLogged = 'isLogged'; // User log status flag const UserSettings = 'UserSettings'; // Different user settings for saving statuses in the app const anonymousId = 'anonymousId'; @@ -50,14 +49,6 @@ export const Storage = { sessionStorage.setItem(UserSettings, JSON.stringify(userSettings)); }, - setGoogleData: data => { - sessionStorage.setItem(GoogleUser, JSON.stringify(data)); - }, - - getGoogleData: () => { - return sessionStorage.getItem(GoogleUser) ? JSON.parse(sessionStorage.getItem(GoogleUser)) : null; - }, - userIsLogged: () => { return sessionStorage.getItem(UserIsLogged) === 'true'; }, diff --git a/src/pages/BackgroundSignIn.jsx b/src/pages/BackgroundSignIn.jsx deleted file mode 100644 index ba835ead7..000000000 --- a/src/pages/BackgroundSignIn.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from 'react'; -import { User } from '../libs/ajax/User'; -import { Storage } from '../libs/storage'; -import { Navigation, setUserRoleStatuses } from '../libs/utils'; -import { useState, useEffect } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; -import { SpinnerComponent } from '../components/SpinnerComponent'; -import loadingImage from '../images/loading-indicator.svg'; - -export default function BackgroundSignIn(props) { - const location = useLocation(); - const history = useHistory(); - const queryParams = new URLSearchParams(location.search); - let token = queryParams.get('token'); - let { onSignIn, onError, bearerToken } = props; - token = bearerToken || (token || ''); - let [loading, setLoading] = useState(token && token !== ''); - let [accessToken, setAccessToken] = useState(token); - let [formToken, setFormToken] = useState(token); - let [invalidToken, setInvalidToken] = useState(false); - - useEffect(() => { - const getUser = async () => { - return await User.getMe(); - }; - - const redirect = (user) => { - Navigation.back(user, history); - if (onSignIn) - onSignIn(); - }; - - const setIsLogged = () => { - Storage.setUserIsLogged(true); - }; - - const performLogin = () => { - setLoading(true); - Storage.setGoogleData({ accessToken: accessToken }); - getUser().then( - user => { - user = Object.assign(user, setUserRoleStatuses(user, Storage)); - setIsLogged(); - setLoading(false); - redirect(user); - }, - error => { - const status = error.status; - switch (status) { - case 400: - if (onError) - onError(error); - setLoading(false); - break; - case 409: - // If the user exists, just log them in. - getUser().then( - user => { - user = Object.assign(user, setUserRoleStatuses(user, Storage)); - setIsLogged(); - redirect(user); - setLoading(false); - }, - () => { - Storage.clearStorage(); - setLoading(false); - }); - break; - case 401: - setInvalidToken(true); - setLoading(false); - break; - default: - setInvalidToken(true); - setLoading(false); - break; - } - }); - }; - - if (accessToken) - performLogin(); - return () => { }; - }, [accessToken, history, onError, onSignIn]); - - return ( -
- {loading - ?
- -
- :
{ - e.preventDefault(); - setAccessToken(formToken); - }} - > -
-
- {invalidToken && -
- The provided token is invalid. -
- } -
- -
-