Skip to content

Commit

Permalink
[REF] Switch experimental auth to auth0 (#380)
Browse files Browse the repository at this point in the history
* Install auth0 react library

* Replace Google Auth with Auth0

* Implement Navbar login handling

* Handle Logout

* Correct typos in two component tests

* Slightly unclear fix for Cypress component test issues

* Prettify the log in button

---------

Co-authored-by: Arman Jahanpour <77515879+rmanaem@users.noreply.github.com>
  • Loading branch information
surchs and rmanaem authored Dec 2, 2024
1 parent cdf0c6b commit 51b2613
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 104 deletions.
14 changes: 2 additions & 12 deletions cypress/component/AuthDialog.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import { GoogleOAuthProvider } from '@react-oauth/google';
import AuthDialog from '../../src/components/AuthDialog';

const props = {
onAuth: () => {},
onClose: () => {},
};

describe('AuthDialog', () => {
it('Displays a MUI dialog with the title and "sing in with google" button', () => {
cy.mount(
<GoogleOAuthProvider clientId="mock-client-id">
<AuthDialog open onClose={props.onClose} onAuth={props.onAuth} />
</GoogleOAuthProvider>
);
cy.mount(<AuthDialog open onClose={() => {}} />);
cy.get('[data-cy="auth-dialog"]').should('be.visible');
cy.get('[data-cy="auth-dialog"]').should('contain', 'You must log in');
cy.get('[data-cy="auth-dialog"]').within(() => {
cy.contains('Google');
cy.contains('Neurobagel');
});
cy.get('[data-cy="close-auth-dialog-button"]').should('be.visible');
});
Expand Down
13 changes: 1 addition & 12 deletions cypress/component/Navbar.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,12 @@ import Navbar from '../../src/components/Navbar';

const props = {
isLoggedIn: true,
name: 'john doe',
profilePic: 'johndoe.png',
onLogout: () => {},
onLogin: () => {},
};

describe('Navbar', () => {
it('Displays a MUI Toolbar with logo, title, subtitle, documentation link, and GitHub link', () => {
cy.mount(
<Navbar
isLoggedIn={props.isLoggedIn}
name={props.name}
profilePic={props.profilePic}
onLogout={props.onLogout}
onLogin={props.onLogin}
/>
);
cy.mount(<Navbar isLoggedIn={props.isLoggedIn} onLogin={props.onLogin} />);
cy.get("[data-cy='navbar']").should('be.visible');
cy.get("[data-cy='navbar'] img").should('exist');
cy.get("[data-cy='navbar'] h5").should('contain', 'Neurobagel Query');
Expand Down
5 changes: 5 additions & 0 deletions cypress/e2e/Feedback.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ describe('Feedback', () => {
it('Displays and closes small screen size dialog', () => {
cy.viewport(766, 500);
cy.visit('/');
// TODO: remove this
// Bit of a hacky way to close the auth dialog
// But we need to do it until we make auth an always-on feature
// Because the auth dialog will overlap a lot of the UI and thus fail the tests
cy.get('[data-cy="close-auth-dialog-button"]').click();
cy.get('[data-cy="small-screen-size-dialog"]').should('be.visible');
cy.get('[data-cy="close-small-screen-size-dialog-button"]').click();
cy.get('[data-cy="small-screen-size-dialog"]').should('not.exist');
Expand Down
28 changes: 18 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
"prepare": "husky install"
},
"dependencies": {
"@auth0/auth0-react": "^2.2.4",
"@emotion/react": "^11.13.5",
"@emotion/styled": "^11.13.0",
"@fontsource/roboto": "^5.1.0",
"@mui/icons-material": "^5.16.7",
"@mui/material": "^5.16.7",
"@react-oauth/google": "^0.12.1",
"axios": "^1.7.7",
"eslint-plugin-tsdoc": "^0.3.0",
"jwt-decode": "^4.0.0",
Expand Down
62 changes: 24 additions & 38 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { Alert, Grow, IconButton } from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery';
import CloseIcon from '@mui/icons-material/Close';
import { SnackbarKey, SnackbarProvider, closeSnackbar, enqueueSnackbar } from 'notistack';
import { jwtDecode } from 'jwt-decode';
import { googleLogout } from '@react-oauth/google';
import { useAuth0 } from '@auth0/auth0-react';
import { queryURL, baseAPIURL, nodesURL, enableAuth, enableChatbot } from './utils/constants';
import {
RetrievedAttributeOption,
Expand All @@ -18,7 +17,6 @@ import {
Pipelines,
NodeError,
QueryResponse,
GoogleJWT,
} from './utils/types';
import QueryForm from './components/QueryForm';
import ResultContainer from './components/ResultContainer';
Expand Down Expand Up @@ -60,11 +58,28 @@ function App() {
const [pipelineName, setPipelineName] = useState<FieldInput>(null);
const [searchParams, setSearchParams] = useSearchParams();

const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
const [openAuthDialog, setOpenAuthDialog] = useState(true);
const [name, setName] = useState<string>('');
const [profilePic, setProfilePic] = useState<string>('');
const [openAuthDialog, setOpenAuthDialog] = useState(false);
const [IDToken, setIDToken] = useState<string | undefined>('');
const { isAuthenticated, isLoading, getIdTokenClaims } = useAuth0();

// Extract the raw OIDC ID token from the Auth0 SDK
useEffect(() => {
if (enableAuth && !isLoading) {
if (isAuthenticated) {
(async () => {
const tokenClaims = await getIdTokenClaims();
// eslint-disable-next-line no-underscore-dangle
setIDToken(tokenClaims?.__raw);
})();
setOpenAuthDialog(false);
}
if (!isAuthenticated) {
setOpenAuthDialog(true);
} else {
setOpenAuthDialog(false);
}
}
}, [isAuthenticated, isLoading, getIdTokenClaims]);

const selectedNode: FieldInputOption[] = availableNodes
.filter((option) => searchParams.getAll('node').includes(option.NodeName))
Expand Down Expand Up @@ -415,45 +430,16 @@ function App() {
setLoading(false);
}

function login(credential: string | undefined) {
setIsLoggedIn(true);
setOpenAuthDialog(false);
const jwt: GoogleJWT = credential ? jwtDecode(credential) : ({} as GoogleJWT);
setIDToken(credential);
setName(jwt.given_name);
setProfilePic(jwt.picture);
}

function logout() {
googleLogout();
setIsLoggedIn(false);
setIDToken('');
setName('');
setProfilePic('');
}

return (
<>
{enableAuth && (
<AuthDialog
open={openAuthDialog}
onAuth={(credential) => login(credential)}
onClose={() => setOpenAuthDialog(false)}
/>
)}
<AuthDialog open={openAuthDialog} onClose={() => setOpenAuthDialog(false)} />
<SmallScreenSizeDialog open={isScreenSizeSmall} onClose={() => setIsScreenSizeSmall(false)} />
<SnackbarProvider
autoHideDuration={6000}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
maxSnack={7}
/>
<Navbar
isLoggedIn={isLoggedIn}
name={name}
profilePic={profilePic}
onLogout={() => logout()}
onLogin={() => setOpenAuthDialog(true)}
/>
<Navbar isLoggedIn={isAuthenticated} onLogin={() => setOpenAuthDialog(true)} />
{showAlert() && (
<>
<Grow in={!alertDismissed}>
Expand Down
20 changes: 8 additions & 12 deletions src/components/AuthDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,25 @@ import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { GoogleLogin } from '@react-oauth/google';
import { useAuth0 } from '@auth0/auth0-react';

function AuthDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
const { loginWithRedirect } = useAuth0();

function AuthDialog({
open,
onAuth,
onClose,
}: {
open: boolean;
onAuth: (credential: string | undefined) => void;
onClose: () => void;
}) {
return (
<Dialog open={open} onClose={onClose} data-cy="auth-dialog">
<DialogTitle>
You must log in to a trusted identity provider in order to query all available nodes!
</DialogTitle>
<DialogContent>
<div className="flex flex-col items-center justify-center">
<GoogleLogin onSuccess={(response) => onAuth(response.credential)} />
<Button variant="contained" onClick={() => loginWithRedirect()}>
Sign in to Neurobagel
</Button>
</div>
</DialogContent>
<DialogActions>
<Button onClick={onClose} data-cy="close-auth-dialog-button">
<Button variant="outlined" onClick={onClose} data-cy="close-auth-dialog-button">
Close
</Button>
</DialogActions>
Expand Down
31 changes: 14 additions & 17 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,17 @@ import Article from '@mui/icons-material/Article';
import Logout from '@mui/icons-material/Logout';
import Login from '@mui/icons-material/Login';
import Avatar from '@mui/material/Avatar';
import { useAuth0 } from '@auth0/auth0-react';
import { enableAuth } from '../utils/constants';
import logo from '../assets/logo.png';

function Navbar({
isLoggedIn,
name,
profilePic,
onLogout,
onLogin,
}: {
isLoggedIn: boolean;
name: string;
profilePic: string;
onLogout: () => void;
onLogin: () => void;
}) {
function Navbar({ isLoggedIn, onLogin }: { isLoggedIn: boolean; onLogin: () => void }) {
const [latestReleaseTag, setLatestReleaseTag] = useState('');
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const openAccountMenu = Boolean(anchorEl);

const { user, logout } = useAuth0();

useEffect(() => {
const GHApiURL = 'https://api.github.com/repos/neurobagel/query-tool/releases/latest';
axios
Expand Down Expand Up @@ -85,7 +76,11 @@ function Navbar({
{enableAuth && (
<>
<IconButton onClick={handleClick}>
<Avatar src={profilePic} sx={{ width: 30, height: 30 }} alt={name} />
<Avatar
src={user?.picture ?? ''}
sx={{ width: 30, height: 30 }}
alt={user?.name ?? ''}
/>
</IconButton>
<Menu
anchorEl={anchorEl}
Expand All @@ -103,15 +98,17 @@ function Navbar({
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<MenuItem>
<Avatar src={profilePic} alt={name} />
<Avatar src={user?.picture ?? ''} alt={user?.name ?? ''} />
</MenuItem>
</div>
{isLoggedIn ? (
<>
<MenuItem>
<Typography>Logged in as {name}</Typography>
<Typography>Logged in as {user?.name ?? ''}</Typography>
</MenuItem>
<MenuItem onClick={onLogout}>
<MenuItem
onClick={() => logout({ logoutParams: { returnTo: window.location.origin } })}
>
<ListItemIcon className="mr-[-8px]">
<Logout fontSize="small" />
</ListItemIcon>
Expand Down
16 changes: 14 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles';
import { GoogleOAuthProvider } from '@react-oauth/google';
import { Auth0Provider } from '@auth0/auth0-react';
import App from './App';
import { appBasePath, enableAuth, clientID } from './utils/constants';
import './index.css';
Expand All @@ -27,5 +27,17 @@ const app = (
);

ReactDOM.createRoot(document.getElementById('root')!).render(
enableAuth ? <GoogleOAuthProvider clientId={clientID}>{app}</GoogleOAuthProvider> : app
enableAuth ? (
<Auth0Provider
domain="neurobagel.ca.auth0.com" // TODO: Replace with customizable domain
clientId={clientID}
authorizationParams={{
redirect_uri: window.location.origin, // TODO: ensure that users end up where they started, including query params
}}
>
{app}
</Auth0Provider>
) : (
app
)
);
5 changes: 5 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ export default defineConfig(({ mode }) => {
},
plugins: [react()],
envPrefix: 'NB_',
// Excluding the Auth0 library from the bundle to avoid issues with
// Cypress component tests. TODO: understand why this is necessary
optimizeDeps: {
exclude: ['@auth0/auth0-react'],
},
};
});

0 comments on commit 51b2613

Please sign in to comment.