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),
             )