From b33bf24a31b3ec9c4ff264e4750dc8f4985f8369 Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Tue, 26 Mar 2024 10:21:18 -0400 Subject: [PATCH 01/11] Added oidcBroker and populated configs REBASE --- package-lock.json | 19 ++++++++++++++++ package.json | 1 + src/App.jsx | 7 ++++++ src/index.tsx | 35 ++++++++++++++++++----------- src/libs/oidcBroker.ts | 50 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 13 deletions(-) create mode 100644 src/libs/oidcBroker.ts diff --git a/package-lock.json b/package-lock.json index 7c698b73b..b846b4534 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,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", @@ -18637,6 +18638,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", @@ -36337,6 +36350,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 924687e64..b981a8236 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,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.tsx b/src/index.tsx index 5079ac2bd..c181a228c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,21 +1,30 @@ -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 {createRoot} from 'react-dom/client'; -import './index.css'; -import App from './App'; -import {unregister} from './registerServiceWorker'; -import {BrowserRouter} from 'react-router-dom'; +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"; const load = async () => { - unregister(); - const container = document.getElementById('root'); - const root = createRoot(container!); - root.render(); + unregister(); + OidcBroker.initializeAuth(); + const container = document.getElementById("root"); + const root = createRoot(container!); + root.render( + + + + + + ); }; await load(); 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 From de32b582e699b0cdc90f80780e787f100febf512 Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Tue, 2 Apr 2024 07:46:46 -0400 Subject: [PATCH 02/11] WIP appLoader REBASE --- src/appLoader.tsx | 16 +++++ src/components/SignInButton.tsx | 97 +++++++++++++++++--------- src/components/Spinner.tsx | 2 +- src/index.ts | 22 ++++++ src/libs/auth/RedirectFromOAuth.ts | 23 ++++++ src/libs/auth/oauth-redirect-loader.ts | 6 ++ src/libs/{ => auth}/oidcBroker.ts | 7 +- 7 files changed, 137 insertions(+), 36 deletions(-) create mode 100644 src/appLoader.tsx 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/SignInButton.tsx b/src/components/SignInButton.tsx index 6d1b89d62..73831c7a7 100644 --- a/src/components/SignInButton.tsx +++ b/src/components/SignInButton.tsx @@ -13,8 +13,7 @@ import ReactTooltip from 'react-tooltip'; import { GoogleIS } from '../libs/googleIS'; import eventList from '../libs/events'; import { StackdriverReporter } from '../libs/stackdriverReporter'; -import { History } from 'history'; -import CSS from 'csstype'; +import { AuthContextProps, useAuth } from 'react-oidc-context'; interface SignInButtonProps { customStyle: CSS.Properties | undefined; @@ -47,7 +46,7 @@ export const SignInButton = (props: SignInButtonProps) => { 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 @@ -117,7 +116,10 @@ export const SignInButton = (props: SignInButtonProps) => { const shouldRedirectTo = (page: string): boolean => page !== '/' && page !== '/home'; - const attemptSignInCheckToSAndRedirect = async (redirectTo:string, shouldRedirect: boolean) => { + const attemptSignInCheckToSAndRedirect = async ( + redirectTo, + shouldRedirect + ) => { await checkToSAndRedirect(shouldRedirect ? redirectTo : null); Metrics.identify(Storage.getAnonymousId()); Metrics.syncProfile(); @@ -139,7 +141,9 @@ export const SignInButton = (props: SignInButtonProps) => { 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: HttpError, redirectTo: string, shouldRedirect: boolean) => { @@ -147,13 +151,21 @@ export const SignInButton = (props: SignInButtonProps) => { 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; } }; @@ -183,37 +195,56 @@ export const SignInButton = (props: SignInButtonProps) => { } }; + // add sign in popup here const spinnerOrSignInButton = () => { - return (clientId === '' - ? Spinner() - : ( + ); }; return (
- {isEmpty(errorDisplay) - ?
- {spinnerOrSignInButton()} -
- :
+ {isEmpty(errorDisplay) ? ( +
{spinnerOrSignInButton()}
+ ) : ( +
{ description={(errorDisplay as ErrorInfo).description} />
- } + )}
); }; diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index d96ad078b..11a68c0c3 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -17,4 +17,4 @@ export function 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.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 8423c5a88a7c8af807801dfc746fdc66de26a1af Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Tue, 2 Apr 2024 11:57:59 -0400 Subject: [PATCH 03/11] Successful sign in REBASE --- src/App.jsx | 7 ++- src/appLoader.tsx | 14 ++--- src/components/SignInButton.tsx | 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 +++++++++++++------------- 7 files changed, 94 insertions(+), 86 deletions(-) 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/SignInButton.tsx b/src/components/SignInButton.tsx index 73831c7a7..562eac74f 100644 --- a/src/components/SignInButton.tsx +++ b/src/components/SignInButton.tsx @@ -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'; interface SignInButtonProps { customStyle: CSS.Properties | undefined; @@ -210,6 +210,9 @@ export const SignInButton = (props: SignInButtonProps) => { 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 c1d3bf389128861b04a4d146c2dd8e1434088139 Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Fri, 5 Apr 2024 13:15:19 -0400 Subject: [PATCH 04/11] Successful register/log in with b2c, next need to handle persistent session REBASE --- src/App.jsx | 10 +-- src/components/DuosHeader.jsx | 26 ++++--- src/components/SignInButton.tsx | 124 ++++++++------------------------ src/custom.d.ts | 10 +-- src/index.tsx | 30 -------- 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 +- 11 files changed, 126 insertions(+), 162 deletions(-) delete mode 100644 src/index.tsx 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 0e8054cef..c55a11a96 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: { @@ -142,7 +143,7 @@ const NavigationTabsComponent = (props) => { orientation, makeNotifications, navbarDuosIcon, duosLogoImage, DuosLogo, navbarDuosText, - currentUser, signOut, isLogged, + currentUser, signIn, signOut, isLogged, contactUsButton, showRequestModal, supportrequestModal, tabs, initialTab, initialSubTab, onSubtabChange @@ -226,7 +227,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, @@ -381,12 +391,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 ? '' : ; @@ -478,6 +484,7 @@ const DuosHeader = (props) => { navbarDuosText={navbarDuosText} currentUser={currentUser} isLogged={isLogged} + signIn={onSignIn} signOut={signOut} contactUsButton={contactUsButton} supportrequestModal={supportModal} @@ -524,6 +531,7 @@ const DuosHeader = (props) => { navbarDuosText={navbarDuosText} currentUser={currentUser} isLogged={isLogged} + signIn={onSignIn} signOut={signOut} contactUsButton={contactUsButton} supportrequestModal={supportModal} diff --git a/src/components/SignInButton.tsx b/src/components/SignInButton.tsx index 562eac74f..11a4c482e 100644 --- a/src/components/SignInButton.tsx +++ b/src/components/SignInButton.tsx @@ -1,19 +1,19 @@ -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'; +import CSS from 'csstype'; +import { useHistory } from 'react-router'; +import { OidcUser } from 'src/libs/auth/oidcBroker'; interface SignInButtonProps { customStyle: CSS.Properties | undefined; @@ -34,38 +34,13 @@ interface HttpError extends Error { status?: number; } -interface GoogleSuccessPayload { - accessToken: string; -} -declare global { - interface Window { google: any; } -} export const SignInButton = (props: SignInButtonProps) => { - const [clientId, setClientId] = useState(''); - const [errorDisplay, setErrorDisplay] = useState({}); - const { onSignIn, history, customStyle } = props; + 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. @@ -95,16 +70,14 @@ export const SignInButton = (props: SignInButtonProps) => { } }; - const onSuccess = async (response: GoogleSuccessPayload) => { - 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); } }; @@ -117,8 +90,8 @@ export const SignInButton = (props: SignInButtonProps) => { 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()); @@ -129,14 +102,14 @@ export const SignInButton = (props: SignInButtonProps) => { const handleRegistration = async (redirectTo: string, shouldRedirect: boolean) => { try { await registerAndRedirectNewUser(redirectTo, shouldRedirect); - } catch (error) { + } catch (error: unknown) { await handleErrors(error as HttpError, 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(); @@ -181,12 +154,11 @@ export const SignInButton = (props: SignInButtonProps) => { const onFailure = (response: any) => { Storage.clearStorage(); if (response.error === 'popup_closed_by_user') { - setErrorDisplay( - + setErrorDisplay( Sign-in cancelled ... - ); + ); setTimeout(() => { setErrorDisplay({}); }, 2000); @@ -195,65 +167,31 @@ export const SignInButton = (props: SignInButtonProps) => { } }; - // 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()}
) : (
+ description={(errorDisplay as ErrorInfo).description} />
)}
diff --git a/src/custom.d.ts b/src/custom.d.ts index 51c83b3a5..957f456d8 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -1,8 +1,8 @@ declare module '*.svg' { - export const ReactComponent: React.FunctionComponent< - React.SVGAttributes - >; + export const ReactComponent: React.FunctionComponent< + React.SVGAttributes + >; - const src: string; - export default src; + const src: string; + export default src; } diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index c181a228c..000000000 --- a/src/index.tsx +++ /dev/null @@ -1,30 +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"; - -const load = async () => { - unregister(); - OidcBroker.initializeAuth(); - const container = document.getElementById("root"); - const root = createRoot(container!); - root.render( - - - - - - ); -}; - -await load(); 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 9ac57d8aa..8d05980f7 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 1c034193148cb6bcf8db7054cf57d2d97b882ec1 Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Fri, 12 Apr 2024 18:20:38 -0400 Subject: [PATCH 05/11] 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 1752089dac6ffc955e1f9c51fe5e80fbde9ec4ed Mon Sep 17 00:00:00 2001 From: Sebastian Kobori Date: Wed, 17 Apr 2024 08:35:07 -0400 Subject: [PATCH 06/11] 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/SignInButton.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 b846b4534..7c698b73b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,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", @@ -18638,18 +18637,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", @@ -36350,12 +36337,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 b981a8236..924687e64 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,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 a3817ef39..578d38355 100644 --- a/src/Routes.jsx +++ b/src/Routes.jsx @@ -23,7 +23,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'; @@ -52,12 +51,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 c55a11a96..2b55ea6b1 100644 --- a/src/components/DuosHeader.jsx +++ b/src/components/DuosHeader.jsx @@ -350,6 +350,7 @@ const DuosHeader = (props) => { const signOut = () => { props.history.push('/home'); toggleDrawer(false); + props.onSignOut(); }; @@ -392,8 +393,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/SignInButton.tsx b/src/components/SignInButton.tsx index 11a4c482e..b3faea9e1 100644 --- a/src/components/SignInButton.tsx +++ b/src/components/SignInButton.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'; @@ -39,14 +38,13 @@ interface HttpError extends Error { export const SignInButton = (props: SignInButtonProps) => { 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); } @@ -174,7 +172,7 @@ export const SignInButton = (props: SignInButtonProps) => { 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. -
- } -
- -
-