Can authenticate using OpenID
This commit is contained in:
parent
94a4ab4f91
commit
d55718f4de
@ -3,14 +3,21 @@ import "./App.css";
|
|||||||
import { AuthApi } from "./api/AuthApi";
|
import { AuthApi } from "./api/AuthApi";
|
||||||
import { NotFoundRoute } from "./routes/NotFound";
|
import { NotFoundRoute } from "./routes/NotFound";
|
||||||
import { BaseLoginPage } from "./widgets/BaseLoginpage";
|
import { BaseLoginPage } from "./widgets/BaseLoginpage";
|
||||||
|
import { LoginRoute } from "./routes/auth/LoginRoute";
|
||||||
|
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [signedIn] = useAtom(AuthApi.authStatus);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
{AuthApi.SignedIn ? (
|
{signedIn ? (
|
||||||
<Route path="*" element={<p>signed in</p>} />
|
<Route path="*" element={<p>signed in</p>} />
|
||||||
) : (
|
) : (
|
||||||
<Route path="*" element={<BaseLoginPage />}>
|
<Route path="*" element={<BaseLoginPage />}>
|
||||||
|
<Route path="" element={<LoginRoute />} />
|
||||||
|
<Route path="oidc_cb" element={<OIDCCbRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
</Route>
|
</Route>
|
||||||
)}
|
)}
|
||||||
|
@ -1,8 +1,45 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
|
const TokenStateKey = "auth-token";
|
||||||
|
|
||||||
export class AuthApi {
|
export class AuthApi {
|
||||||
/**
|
/**
|
||||||
* Check out whether user is signed in or not
|
* Check out whether user is signed in or not
|
||||||
*/
|
*/
|
||||||
static get SignedIn(): boolean {
|
static get SignedIn(): boolean {
|
||||||
return false;
|
return sessionStorage.getItem(TokenStateKey) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static authStatus = atom(this.SignedIn);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start OpenID login
|
||||||
|
*
|
||||||
|
* @param id The ID of the OIDC provider to use
|
||||||
|
*/
|
||||||
|
static async StartOpenIDLogin(id: string): Promise<{ url: string }> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/auth/start_openid_login",
|
||||||
|
method: "POST",
|
||||||
|
jsonData: { provider: id },
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish OpenID login
|
||||||
|
*/
|
||||||
|
static async FinishOpenIDLogin(code: string, state: string): Promise<void> {
|
||||||
|
const res: { user_id: number; token: string } = (
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/auth/finish_openid_login",
|
||||||
|
method: "POST",
|
||||||
|
jsonData: { code: code, state: state },
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
|
||||||
|
sessionStorage.setItem(TokenStateKey, res.token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ export class ServerApi {
|
|||||||
/**
|
/**
|
||||||
* Get cached configuration
|
* Get cached configuration
|
||||||
*/
|
*/
|
||||||
static Config(): ServerConfig {
|
static get Config(): ServerConfig {
|
||||||
if (config === null) throw new Error("Missing configuration!");
|
if (config === null) throw new Error("Missing configuration!");
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
116
geneit_app/src/routes/auth/LoginRoute.tsx
Normal file
116
geneit_app/src/routes/auth/LoginRoute.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { Alert, CircularProgress } from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import * as React from "react";
|
||||||
|
import { AuthApi } from "../../api/AuthApi";
|
||||||
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login form
|
||||||
|
*/
|
||||||
|
export function LoginRoute(): React.ReactElement {
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const data = new FormData(event.currentTarget);
|
||||||
|
console.log({
|
||||||
|
email: data.get("email"),
|
||||||
|
password: data.get("password"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const authWithProvider = async (id: string) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const res = await AuthApi.StartOpenIDLogin(id);
|
||||||
|
window.location.href = res.url;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setError("Echec de l'initialisation de l'authentification OpenID !");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CircularProgress />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{error === null ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<Alert style={{ width: "100%" }} severity="error">
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Typography component="h2" variant="body1">
|
||||||
|
Connexion
|
||||||
|
</Typography>
|
||||||
|
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="email"
|
||||||
|
label="Adresse mail"
|
||||||
|
name="email"
|
||||||
|
autoComplete="email"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
name="password"
|
||||||
|
label="Mot de passe"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
sx={{ mt: 3, mb: 2 }}
|
||||||
|
>
|
||||||
|
Connexion
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs>
|
||||||
|
<Link href="#" variant="body2">
|
||||||
|
Mot de passe oublié
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Link href="#" variant="body2">
|
||||||
|
Créer un nouveau compte
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{ServerApi.Config.oidc_providers.map((p) => (
|
||||||
|
<Button
|
||||||
|
style={{ textAlign: "center", width: "100%", marginTop: "20px" }}
|
||||||
|
onClick={() => authWithProvider(p.id)}
|
||||||
|
>
|
||||||
|
Connection avec {p.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
55
geneit_app/src/routes/auth/OIDCCbRoute.tsx
Normal file
55
geneit_app/src/routes/auth/OIDCCbRoute.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Button, CircularProgress } from "@mui/material";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Link, useSearchParams } from "react-router-dom";
|
||||||
|
import { AuthApi } from "../../api/AuthApi";
|
||||||
|
import { useSetAtom } from "jotai";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenID login callback route
|
||||||
|
*/
|
||||||
|
export function OIDCCbRoute(): React.ReactElement {
|
||||||
|
const setAuth = useSetAtom(AuthApi.authStatus);
|
||||||
|
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const code = searchParams.get("code");
|
||||||
|
const state = searchParams.get("state");
|
||||||
|
|
||||||
|
const count = useRef("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
if (count.current === code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count.current = code!;
|
||||||
|
|
||||||
|
await AuthApi.FinishOpenIDLogin(code!, state!);
|
||||||
|
setAuth(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, [code, state]);
|
||||||
|
|
||||||
|
if (error)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>Echec de la finalisation de l'authentification !</p>
|
||||||
|
<Link to={"/"}>
|
||||||
|
<Button>Retour à l'accueil</Button>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CircularProgress />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -18,6 +18,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.allowed_origin(&AppConfig::get().website_origin)
|
.allowed_origin(&AppConfig::get().website_origin)
|
||||||
.allowed_methods(vec!["GET", "POST"])
|
.allowed_methods(vec!["GET", "POST"])
|
||||||
.allowed_header("X-Auth-Token")
|
.allowed_header("X-Auth-Token")
|
||||||
|
.allow_any_header()
|
||||||
.supports_credentials()
|
.supports_credentials()
|
||||||
.max_age(3600),
|
.max_age(3600),
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user