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:
Viktor Barzin 2026-02-07 00:34:47 +00:00
parent 95c0ddc4c6
commit a8b7eace48
No known key found for this signature in database
GPG key ID: 0EB088298288D958
26 changed files with 1229 additions and 129 deletions

View file

@ -1,6 +1,6 @@
// Task service for fetching task status
import type { User } from 'oidc-client-ts';
import type { AuthUser } from '@/auth/types';
import type { TaskStatusResponse } from '@/types';
import { apiRequest } from './apiClient';
import { API_ENDPOINTS } from '@/constants';
@ -19,7 +19,7 @@ export interface ClearAllTasksResponse {
/**
* Fetch all active tasks for the current user
*/
export async function fetchTasksForUser(user: User): Promise<string[]> {
export async function fetchTasksForUser(user: AuthUser): Promise<string[]> {
return apiRequest<string[]>(user, API_ENDPOINTS.TASKS_FOR_USER);
}
@ -27,7 +27,7 @@ export async function fetchTasksForUser(user: User): Promise<string[]> {
* Fetch the status of a specific task
*/
export async function fetchTaskStatus(
user: User,
user: AuthUser,
taskId: string
): Promise<TaskStatusResponse> {
return apiRequest<TaskStatusResponse>(user, API_ENDPOINTS.TASK_STATUS, {
@ -39,7 +39,7 @@ export async function fetchTaskStatus(
* Cancel a running task
*/
export async function cancelTask(
user: User,
user: AuthUser,
taskId: string
): Promise<CancelTaskResponse> {
return apiRequest<CancelTaskResponse>(user, API_ENDPOINTS.CANCEL_TASK, {
@ -51,7 +51,7 @@ export async function cancelTask(
/**
* Clear all tasks for the current user
*/
export async function clearAllTasks(user: User): Promise<ClearAllTasksResponse> {
export async function clearAllTasks(user: AuthUser): Promise<ClearAllTasksResponse> {
return apiRequest<ClearAllTasksResponse>(user, API_ENDPOINTS.CLEAR_ALL_TASKS, {
method: 'POST',
});