Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Authentication/authorization tests #3056

Merged
merged 53 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
973aa97
Add universal required email config for authentication
apedroferreira Jan 3, 2024
ba2399d
Must have at least 1 verified email in Github
apedroferreira Jan 3, 2024
ebcbf55
Merge remote-tracking branch 'upstream/master' into auth-required-email
apedroferreira Jan 3, 2024
08791c6
Address review comments
apedroferreira Jan 4, 2024
ef5731b
Merge remote-tracking branch 'upstream/master' into auth-required-email
apedroferreira Jan 4, 2024
39c0f00
Refactor (review comments)
apedroferreira Jan 5, 2024
d7fc87c
Small refactor
apedroferreira Jan 5, 2024
07015a0
Load env before imports
apedroferreira Jan 8, 2024
f730414
Add spacing to navigation
apedroferreira Jan 8, 2024
ebce000
Azure AD auth provider (without role mapping)
apedroferreira Jan 12, 2024
60f031d
Use just size property in icon
apedroferreira Jan 12, 2024
f58dcc9
Add role mapping
apedroferreira Jan 16, 2024
43c9af0
Update schemas
apedroferreira Jan 16, 2024
1c06826
Better name
apedroferreira Jan 16, 2024
f6fdf95
Fix Azure icon
apedroferreira Jan 16, 2024
761f0ed
Merge remote-tracking branch 'upstream/master' into auth-azure-ad-pro…
apedroferreira Jan 16, 2024
71ea8a7
Disable feature flag
apedroferreira Jan 16, 2024
9cffb6a
Self-review
apedroferreira Jan 16, 2024
8f150de
Merge remote-tracking branch 'upstream/master' into auth-azure-ad-pro…
apedroferreira Jan 16, 2024
f550a02
Fix page blocking logic and default page
apedroferreira Jan 16, 2024
f725a21
More fixes
apedroferreira Jan 16, 2024
7d921c2
Better signout experience
apedroferreira Jan 16, 2024
af6835a
[WIP] Authentication tests
apedroferreira Jan 4, 2024
8bcf78b
Simplify error mesage logic
apedroferreira Jan 5, 2024
4d8fc43
Auth test without restricted domains
apedroferreira Jan 5, 2024
11a6704
Much more better
apedroferreira Jan 5, 2024
ec4c161
Add note to try a better way next
apedroferreira Jan 5, 2024
e1acc08
Best possible test without creating test users with public credentials
apedroferreira Jan 8, 2024
4efa00f
Small refactor
apedroferreira Jan 8, 2024
a1ca40c
Add credentials provider for testing
apedroferreira Jan 23, 2024
488da7d
Fix some scenarios with missing secret and unnecessary requests
apedroferreira Jan 24, 2024
68f9de7
More fixes
apedroferreira Jan 24, 2024
9d6b82a
Fix CSRF bullshit, add test with authentication, sign in, sign out an…
apedroferreira Jan 25, 2024
188ef58
Add roles test
apedroferreira Jan 25, 2024
55b35e7
Update test/integration/auth/basic.spec.ts
apedroferreira Jan 26, 2024
9baabfe
Better function name
apedroferreira Jan 26, 2024
ba3b0e3
Forgot this
apedroferreira Jan 26, 2024
74e9cb9
Merge remote-tracking branch 'upstream/master' into auth-tests
apedroferreira Jan 31, 2024
9affd28
Continue merge
apedroferreira Jan 31, 2024
e7b2931
Add some refactors from other PR
apedroferreira Jan 31, 2024
27f8e3e
Disable feature flag
apedroferreira Jan 31, 2024
3d7ad43
Fix tests
apedroferreira Jan 31, 2024
833feb7
Update @auth/core
apedroferreira Jan 31, 2024
e079ac1
Run install
apedroferreira Jan 31, 2024
f799ce5
Lint fixins
apedroferreira Jan 31, 2024
d4869b1
Prettier
apedroferreira Jan 31, 2024
6f9c719
Revert @auth/core version
apedroferreira Jan 31, 2024
3b8a69e
Remove all temporary fixtures
apedroferreira Feb 1, 2024
3a3d59e
Remove more unwanted things
apedroferreira Feb 1, 2024
4445f9d
Update @auth/core, fix error message
apedroferreira Feb 1, 2024
e7ff328
Merge remote-tracking branch 'upstream/master' into auth-tests
apedroferreira Feb 5, 2024
9d6c39f
Remove mysterious new folders
apedroferreira Feb 5, 2024
82781d2
Remove more stupid unwanted files and updates
apedroferreira Feb 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/schemas/v1/definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"properties": {
"provider": {
"type": "string",
"enum": ["github", "google", "azure-ad"],
"enum": ["github", "google", "azure-ad", "credentials"],
"description": "Unique identifier for this authentication provider."
},
"roles": {
Expand Down
2 changes: 1 addition & 1 deletion packages/toolpad-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
}
},
"dependencies": {
"@auth/core": "0.24.0",
"@auth/core": "0.20.0",
apedroferreira marked this conversation as resolved.
Show resolved Hide resolved
"@emotion/cache": "11.11.0",
"@emotion/react": "11.11.3",
"@emotion/server": "11.11.0",
Expand Down
285 changes: 207 additions & 78 deletions packages/toolpad-app/src/runtime/SignInPage.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,72 @@
import * as React from 'react';
import { Alert, Snackbar, Stack, Typography, useTheme } from '@mui/material';
import {
Alert,
Button,
Divider,
Snackbar,
Stack,
TextField,
Typography,
useTheme,
} from '@mui/material';
import GitHubIcon from '@mui/icons-material/GitHub';
import PasswordIcon from '@mui/icons-material/Password';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { LoadingButton } from '@mui/lab';
import { useSearchParams } from 'react-router-dom';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
import { AuthProvider, AuthContext } from './useAuth';
import productIconDark from '../../public/product-icon-dark.svg';
import productIconLight from '../../public/product-icon-light.svg';

const AUTH_ERROR_URL_PARAM = 'error';

type CredentialsFormInputs = {
username: string;
password: string;
};

const azureIconSvg = (
<svg viewBox="0 0 59.242 47.271" width={18} height={18} xmlns="http://www.w3.org/2000/svg">
<path
d="m32.368 0-17.468 15.145-14.9 26.75h13.437zm2.323 3.543-7.454 21.008 14.291 17.956-27.728 4.764h45.442z"
fill="currentColor"
/>
</svg>
);

export default function SignInPage() {
const theme = useTheme();
const [urlParams] = useSearchParams();

const { signIn, isSigningIn } = React.useContext(AuthContext);

const [errorSnackbarMessage, setErrorSnackbarMessage] = React.useState<string>('');
const [errorSnackbarMessage, setErrorSnackbarMessage] = React.useState<React.ReactNode>('');
const [latestSelectedProvider, setLatestSelectedProvider] = React.useState<AuthProvider | null>(
null,
);

const [isCredentialsSignIn, setIsCredentialsSignIn] = React.useState(false);

const { authProviders } = React.useContext(AuthContext);

const handleSignIn = React.useCallback(
(provider: AuthProvider) => () => {
setLatestSelectedProvider(provider);
signIn(provider);
},
(provider: AuthProvider, payload?: Record<string, unknown>, isLocalProvider?: boolean) =>
() => {
setLatestSelectedProvider(provider);
signIn(provider, payload, isLocalProvider);
},
[signIn],
);

const handleCredentialsSignIn = React.useCallback(() => {
setIsCredentialsSignIn(true);
}, []);

const handleCredentialsBack = React.useCallback(() => {
setIsCredentialsSignIn(false);
}, []);

React.useEffect(() => {
const authError = urlParams.get(AUTH_ERROR_URL_PARAM);

Expand All @@ -39,6 +76,8 @@ export default function SignInPage() {
setErrorSnackbarMessage(
'There was an error with your authentication provider configuration.',
);
} else if (authError === 'MissingSecretError') {
setErrorSnackbarMessage('Missing secret for authentication. Please provide a secret.');
} else if (authError) {
setErrorSnackbarMessage('An authentication error occurred.');
}
Expand All @@ -48,6 +87,21 @@ export default function SignInPage() {
setErrorSnackbarMessage('');
}, []);

const { handleSubmit: handleCredentialsSubmit, control: credentialsFormControl } =
useForm<CredentialsFormInputs>({
defaultValues: {
username: '',
password: '',
},
});

const onCredentialsSubmit: SubmitHandler<CredentialsFormInputs> = React.useCallback(
(data) => {
handleSignIn('credentials', data, true)();
},
[handleSignIn],
);

const productIcon = theme.palette.mode === 'dark' ? productIconDark : productIconLight;

return (
Expand All @@ -66,78 +120,153 @@ export default function SignInPage() {
You must be authenticated to use this app.
</Typography>
<Stack sx={{ width: 300 }} gap={2}>
{authProviders.includes('github') ? (
<LoadingButton
variant="contained"
onClick={handleSignIn('github')}
startIcon={<GitHubIcon />}
loading={isSigningIn && latestSelectedProvider === 'github'}
disabled={isSigningIn}
loadingPosition="start"
size="large"
fullWidth
sx={{
backgroundColor: '#24292F',
}}
>
Sign in with GitHub
</LoadingButton>
) : null}
{authProviders.includes('google') ? (
<LoadingButton
variant="contained"
onClick={handleSignIn('google')}
startIcon={
<img
alt="Google logo"
loading="lazy"
height="18"
width="18"
src="https://authjs.dev/img/providers/google.svg"
style={{ marginLeft: '2px', marginRight: '2px' }}
/>
}
loading={isSigningIn && latestSelectedProvider === 'google'}
disabled={isSigningIn}
loadingPosition="start"
size="large"
fullWidth
sx={{
backgroundColor: '#fff',
color: '#000',
'&:hover': {
color: theme.palette.primary.contrastText,
},
}}
>
Sign in with Google
</LoadingButton>
) : null}
{authProviders.includes('azure-ad') ? (
<LoadingButton
variant="contained"
onClick={handleSignIn('azure-ad')}
startIcon={
<img
alt="Microsoft Azure logo"
loading="lazy"
height="18"
width="18"
src="https://authjs.dev/img/providers/azure.svg"
/>
}
loading={isSigningIn && latestSelectedProvider === 'azure-ad'}
disabled={isSigningIn}
loadingPosition="start"
size="large"
fullWidth
sx={{
backgroundColor: '##0072c6',
}}
>
Sign in with Azure AD
</LoadingButton>
) : null}
{isCredentialsSignIn ? (
<React.Fragment>
<Stack direction="row" alignItems="center">
<Button onClick={handleCredentialsBack}>
<ArrowBackIcon />
<Typography variant="button" sx={{ ml: 1 }}>
Back
</Typography>
</Button>
</Stack>
<form onSubmit={handleCredentialsSubmit(onCredentialsSubmit)}>
<Stack direction="column" gap={2}>
<Controller
name="username"
rules={{ required: 'Username is required' }}
control={credentialsFormControl}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<TextField
helperText={error ? error.message : null}
error={!!error}
onChange={onChange}
value={value}
fullWidth
label="Username"
variant="outlined"
/>
)}
/>
<Controller
name="password"
rules={{ required: 'Password is required' }}
control={credentialsFormControl}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<TextField
type="password"
helperText={error ? error.message : null}
error={!!error}
onChange={onChange}
value={value}
fullWidth
label="Password"
variant="outlined"
/>
)}
/>
<LoadingButton
variant="contained"
type="submit"
loading={isSigningIn && latestSelectedProvider === 'credentials'}
disabled={isSigningIn}
size="large"
fullWidth
>
Sign in
</LoadingButton>
</Stack>
</form>
</React.Fragment>
) : (
<React.Fragment>
{authProviders.includes('github') ? (
<LoadingButton
variant="contained"
onClick={handleSignIn('github')}
startIcon={<GitHubIcon />}
loading={isSigningIn && latestSelectedProvider === 'github'}
disabled={isSigningIn}
loadingPosition="start"
size="large"
fullWidth
sx={{
backgroundColor: '#24292F',
}}
>
Sign in with GitHub
</LoadingButton>
) : null}
{authProviders.includes('google') ? (
<LoadingButton
variant="contained"
onClick={handleSignIn('google')}
startIcon={
<img
alt="Google logo"
loading="lazy"
height="18"
width="18"
src="https://authjs.dev/img/providers/google.svg"
style={{ marginLeft: '2px', marginRight: '2px' }}
/>
}
loading={isSigningIn && latestSelectedProvider === 'google'}
disabled={isSigningIn}
loadingPosition="start"
size="large"
fullWidth
sx={{
backgroundColor: '#fff',
color: '#000',
'&:hover': {
color: theme.palette.primary.contrastText,
},
}}
>
Sign in with Google
</LoadingButton>
) : null}
{authProviders.includes('azure-ad') ? (
<LoadingButton
variant="contained"
onClick={handleSignIn('azure-ad')}
startIcon={azureIconSvg}
loading={isSigningIn && latestSelectedProvider === 'azure-ad'}
disabled={isSigningIn}
loadingPosition="start"
size="large"
fullWidth
sx={{
backgroundColor: '#0072c6',
}}
>
Sign in with Azure AD
</LoadingButton>
) : null}
{authProviders.includes('credentials') ? (
<React.Fragment>
{authProviders.length > 1 ? (
<Divider>
<Typography variant="caption">OR</Typography>
</Divider>
) : null}
<LoadingButton
variant="contained"
onClick={handleCredentialsSignIn}
startIcon={<PasswordIcon />}
loading={isSigningIn && latestSelectedProvider === 'credentials'}
disabled={isSigningIn}
loadingPosition="start"
size="large"
fullWidth
>
Sign in with credentials
</LoadingButton>
</React.Fragment>
) : null}
</React.Fragment>
)}
</Stack>
</Stack>
<Snackbar
Expand Down
13 changes: 4 additions & 9 deletions packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1477,10 +1477,9 @@ function PageNotFound() {
interface RenderedPagesProps {
pages: appDom.PageNode[];
defaultPage: appDom.PageNode;
hasAuthentication?: boolean;
}

function RenderedPages({ pages, defaultPage, hasAuthentication = false }: RenderedPagesProps) {
function RenderedPages({ pages, defaultPage }: RenderedPagesProps) {
const { search } = useLocation();

const defaultPageNavigation = <Navigate to={`/pages/${defaultPage.name}${search}`} replace />;
Expand All @@ -1497,7 +1496,7 @@ function RenderedPages({ pages, defaultPage, hasAuthentication = false }: Render
/>
);

if (!IS_RENDERED_IN_CANVAS && hasAuthentication) {
if (!IS_RENDERED_IN_CANVAS) {
pageContent = (
<RequireAuthorization
allowAll={page.attributes.authorization?.allowAll ?? true}
Expand Down Expand Up @@ -1597,11 +1596,7 @@ function ToolpadAppLayout({ dom, basename, clipped }: ToolpadAppLayoutProps) {
clipped={clipped}
basename={basename}
>
<RenderedPages
pages={pages}
defaultPage={authFilteredPages[0] ?? pages[0]}
hasAuthentication={hasAuthentication}
/>
<RenderedPages pages={pages} defaultPage={authFilteredPages[0] ?? pages[0]} />
</AppLayout>
);
}
Expand Down Expand Up @@ -1632,7 +1627,7 @@ export default function ToolpadApp({ rootRef, basename, state }: ToolpadAppProps
(window as any).toggleDevtools = () => toggleDevtools();
}, [toggleDevtools]);

const authContext = useAuth({ dom, basename });
const authContext = useAuth({ dom, basename, isRenderedInCanvas: IS_RENDERED_IN_CANVAS });

const appHost = useNonNullableContext(AppHostContext);
const showPreviewHeader: boolean = !!appHost?.isPreview && !IS_RENDERED_IN_CANVAS;
Expand Down
Loading
Loading