From d55718f4deed0b434c0f6cb06b38a4225a91be63 Mon Sep 17 00:00:00 2001
From: Pierre Hubert
Date: Fri, 9 Jun 2023 10:45:01 +0200
Subject: [PATCH] Can authenticate using OpenID
---
geneit_app/src/App.tsx | 9 +-
geneit_app/src/api/AuthApi.ts | 39 ++++++-
geneit_app/src/api/ServerApi.ts | 2 +-
geneit_app/src/routes/auth/LoginRoute.tsx | 116 +++++++++++++++++++++
geneit_app/src/routes/auth/OIDCCbRoute.tsx | 55 ++++++++++
geneit_backend/src/main.rs | 1 +
6 files changed, 219 insertions(+), 3 deletions(-)
create mode 100644 geneit_app/src/routes/auth/LoginRoute.tsx
create mode 100644 geneit_app/src/routes/auth/OIDCCbRoute.tsx
diff --git a/geneit_app/src/App.tsx b/geneit_app/src/App.tsx
index 062e5b2..2750f97 100644
--- a/geneit_app/src/App.tsx
+++ b/geneit_app/src/App.tsx
@@ -3,14 +3,21 @@ import "./App.css";
import { AuthApi } from "./api/AuthApi";
import { NotFoundRoute } from "./routes/NotFound";
import { BaseLoginPage } from "./widgets/BaseLoginpage";
+import { LoginRoute } from "./routes/auth/LoginRoute";
+import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
+import { useAtom } from "jotai";
function App() {
+ const [signedIn] = useAtom(AuthApi.authStatus);
+
return (
- {AuthApi.SignedIn ? (
+ {signedIn ? (
signed in
} />
) : (
}>
+ } />
+ } />
} />
)}
diff --git a/geneit_app/src/api/AuthApi.ts b/geneit_app/src/api/AuthApi.ts
index dfb47aa..1fb36af 100644
--- a/geneit_app/src/api/AuthApi.ts
+++ b/geneit_app/src/api/AuthApi.ts
@@ -1,8 +1,45 @@
+import { atom } from "jotai";
+import { APIClient } from "./ApiClient";
+
+const TokenStateKey = "auth-token";
+
export class AuthApi {
/**
* Check out whether user is signed in or not
*/
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 {
+ 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);
}
}
diff --git a/geneit_app/src/api/ServerApi.ts b/geneit_app/src/api/ServerApi.ts
index b25fdc0..34f62f9 100644
--- a/geneit_app/src/api/ServerApi.ts
+++ b/geneit_app/src/api/ServerApi.ts
@@ -40,7 +40,7 @@ export class ServerApi {
/**
* Get cached configuration
*/
- static Config(): ServerConfig {
+ static get Config(): ServerConfig {
if (config === null) throw new Error("Missing configuration!");
return config;
}
diff --git a/geneit_app/src/routes/auth/LoginRoute.tsx b/geneit_app/src/routes/auth/LoginRoute.tsx
new file mode 100644
index 0000000..98ff777
--- /dev/null
+++ b/geneit_app/src/routes/auth/LoginRoute.tsx
@@ -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(null);
+
+ const handleSubmit = (event: React.FormEvent) => {
+ 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 (
+ <>
+
+ >
+ );
+
+ return (
+ <>
+ {error === null ? (
+ <>>
+ ) : (
+
+ {error}
+
+ )}
+
+
+ Connexion
+
+
+
+
+
+
+
+
+
+ Mot de passe oublié
+
+
+
+
+ Créer un nouveau compte
+
+
+
+
+
+ {ServerApi.Config.oidc_providers.map((p) => (
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/geneit_app/src/routes/auth/OIDCCbRoute.tsx b/geneit_app/src/routes/auth/OIDCCbRoute.tsx
new file mode 100644
index 0000000..f7c6a27
--- /dev/null
+++ b/geneit_app/src/routes/auth/OIDCCbRoute.tsx
@@ -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 (
+ <>
+ Echec de la finalisation de l'authentification !
+
+
+
+ >
+ );
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs
index 1d58677..a1cf15a 100644
--- a/geneit_backend/src/main.rs
+++ b/geneit_backend/src/main.rs
@@ -18,6 +18,7 @@ async fn main() -> std::io::Result<()> {
.allowed_origin(&AppConfig::get().website_origin)
.allowed_methods(vec!["GET", "POST"])
.allowed_header("X-Auth-Token")
+ .allow_any_header()
.supports_credentials()
.max_age(3600),
)