Add passkey (WebAuthn) authentication with self-registration
Enable users to sign up and sign in using passkeys (biometrics/security keys) without needing a manually-created Authentik account. The existing SSO login remains as an alternative. Backend: - Add WebAuthn registration/authentication endpoints via py-webauthn - Issue HS256 JWTs for passkey users, with Redis-backed challenge storage - Dual JWT verification in auth middleware (issuer-based routing: passkey HS256 vs Authentik RS256) - PasskeyCredential model + migration making user.password nullable - UserRepository with full CRUD for users and credentials Frontend: - AuthUser type abstraction unifying OIDC and passkey users - Passkey service using @simplewebauthn/browser for WebAuthn ceremonies - LoginModal redesigned with Sign In / Sign Up tabs - Type migration from oidc-client-ts User to AuthUser across all services and components
This commit is contained in:
parent
95c0ddc4c6
commit
a8b7eace48
26 changed files with 1229 additions and 129 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import type { User } from 'oidc-client-ts';
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import './App.css';
|
||||
import { getUser } from './auth/authService';
|
||||
import { getStoredPasskeyUser } from './auth/passkeyService';
|
||||
import { fromOidcUser, type AuthUser } from './auth/types';
|
||||
import AlertError from './components/AlertError';
|
||||
import LoginModal from './components/LoginModal';
|
||||
import AuthCallback from './components/AuthCallback';
|
||||
|
|
@ -20,7 +21,7 @@ import { refreshListings, fetchTasksForUser, streamListingGeoJSON, type Streamin
|
|||
function App() {
|
||||
const [listingData, setListingData] = useState<GeoJSONFeatureCollection | null>(null);
|
||||
const [taskID, setTaskID] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [queryParameters, setQueryParameters] = useState<ParameterValues | null>(null);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [alertDialogIsOpen, setAlertDialogIsOpen] = useState(false);
|
||||
|
|
@ -41,10 +42,23 @@ function App() {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Load user data
|
||||
getUser().then(setUser);
|
||||
// Check passkey user first, then fall back to OIDC
|
||||
const passkeyUser = getStoredPasskeyUser();
|
||||
if (passkeyUser) {
|
||||
setUser(passkeyUser);
|
||||
} else {
|
||||
getUser().then((oidcUser) => {
|
||||
if (oidcUser) {
|
||||
setUser(fromOidcUser(oidcUser));
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePasskeyLogin = (passkeyUser: AuthUser) => {
|
||||
setUser(passkeyUser);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return;
|
||||
|
|
@ -122,7 +136,7 @@ function App() {
|
|||
}, [user, loadListings]);
|
||||
|
||||
if (!user) {
|
||||
return <LoginModal isOpen={user === null} />;
|
||||
return <LoginModal isOpen={user === null} onPasskeyLogin={handlePasskeyLogin} />;
|
||||
}
|
||||
|
||||
const onSubmit = async (action: 'fetch-data' | 'visualize', parameters: ParameterValues) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue