Everything works. Minor adjustments on frontend and backend for the new API.

This commit is contained in:
Leo Vasanko 2025-08-02 07:41:42 -06:00
parent a987f47988
commit 30ab73d625
9 changed files with 43 additions and 391 deletions

375
API.md
View File

@ -4,361 +4,20 @@ This document describes all API endpoints available in the PassKey Auth FastAPI
## Base URL
- **Development**: `http://localhost:4401`
- All API endpoints are prefixed with `/auth`
## Authentication
The API uses JWT tokens stored in HTTP-only cookies for session management. Some endpoints require authentication via session cookies.
---
## HTTP API Endpoints
### User Management
#### `POST /auth/user-info`
Get detailed information about the current authenticated user and their credentials.
**Authentication**: Required (session cookie)
**Response**:
```json
{
"status": "success",
"user": {
"user_id": "string (UUID)",
"user_name": "string",
"created_at": "string (ISO 8601)",
"last_seen": "string (ISO 8601)",
"visits": "number"
},
"credentials": [
{
"credential_id": "string (hex)",
"aaguid": "string (UUID)",
"created_at": "string (ISO 8601)",
"last_used": "string (ISO 8601) | null",
"last_verified": "string (ISO 8601) | null",
"sign_count": "number",
"is_current_session": "boolean"
}
],
"aaguid_info": "object (AAGUID information)"
}
```
**Error Response**:
```json
{
"error": "Not authenticated" | "Failed to get user info: <error_message>"
}
```
---
### Session Management
#### `POST /auth/logout`
Log out the current user by clearing the session cookie.
**Authentication**: Not required
**Response**:
```json
{
"status": "success",
"message": "Logged out successfully"
}
```
#### `POST /auth/set-session`
Set session cookie using JWT token from request body or Authorization header.
**Authentication**: Not required
**Request Body** (alternative to Authorization header):
```json
{
"token": "string (JWT token)"
}
```
**Headers** (alternative to request body):
```
Authorization: Bearer <JWT_token>
```
**Response**:
```json
{
"status": "success",
"message": "Session cookie set successfully",
"user_id": "string (UUID)"
}
```
**Error Response**:
```json
{
"error": "No session token provided" | "Invalid or expired session token" | "Failed to set session: <error_message>"
}
```
#### `GET /auth/forward-auth`
Verification endpoint for use with Caddy forward_auth or Nginx auth_request.
**Authentication**: Required (session cookie)
**Success Response**:
- Status: `204 No Content`
- Headers: `x-auth-user-id: <user_id>`
**Error Response**:
- Status: `401 Unauthorized`
- Returns authentication app HTML page
- Headers: `www-authenticate: PrivateToken`
---
### Credential Management
#### `POST /auth/delete-credential`
Delete a specific passkey credential for the current user.
**Authentication**: Required (session cookie)
**Request Body**:
```json
{
"credential_id": "string (hex-encoded credential ID)"
}
```
**Response**:
```json
{
"status": "success",
"message": "Credential deleted successfully"
}
```
**Error Response**:
```json
{
"error": "Not authenticated" | "credential_id is required" | "Invalid credential_id format" | "Credential not found or access denied" | "Cannot delete current session credential" | "Cannot delete last remaining credential" | "Failed to delete credential: <error_message>"
}
```
---
### Device Addition
#### `POST /auth/create-device-link`
Generate a device addition link for authenticated users to add new passkeys to their account.
**Authentication**: Required (session cookie)
**Response**:
```json
{
"status": "success",
"message": "Device addition link generated successfully",
"addition_link": "string (URL)",
"expires_in_hours": 24
}
```
**Error Response**:
```json
{
"error": "Authentication required" | "Failed to create device addition link: <error_message>"
}
```
#### `POST /auth/validate-device-token`
Validate a device addition token and return associated user information.
**Authentication**: Not required
**Request Body**:
```json
{
"token": "string (device addition token)"
}
```
**Response**:
```json
{
"status": "success",
"valid": true,
"user_id": "string (UUID)",
"user_name": "string",
"token": "string (device addition token)"
}
```
**Error Response**:
```json
{
"error": "Device addition token is required" | "Invalid or expired device addition token" | "Device addition token has expired" | "Failed to validate device addition token: <error_message>"
}
```
---
### Static File Serving
#### `GET /auth/{passphrase}`
Handle passphrase-based authentication redirect with cookie setting.
**Parameters**:
- `passphrase`: String matching pattern `^\w+(\.\w+){2,}$` (e.g., "word1.word2.word3")
**Response**:
- Status: `303 See Other`
- Redirect to: `/`
- Sets temporary cookie: `auth-token` (expires in 2 seconds)
#### `GET /auth`
Serve the main authentication app.
**Response**: Returns the main `index.html` file for the authentication SPA.
#### `GET /auth/assets/{path}`
Serve static assets (CSS, JS, images) for the authentication app.
#### `GET /{path:path}`
Catch-all route for SPA routing. Serves `index.html` for all non-API routes when requesting HTML content.
**Response**:
- For HTML requests: Returns `index.html`
- For non-HTML requests: Returns `404 Not Found` JSON response
---
## WebSocket API Endpoints
All WebSocket endpoints are mounted under `/auth/ws/`.
### Registration
#### `WS /auth/ws/register_new`
Register a new user with a new passkey credential.
**Flow**:
1. Client connects to WebSocket
2. Server sends registration options
3. Client performs WebAuthn ceremony and sends response
4. Server validates and creates new user + credential
5. Server sends JWT token for session establishment
**Server Messages**:
```json
// Registration options
{
"rp": { "id": "localhost", "name": "Passkey Auth" },
"user": { "id": "base64", "name": "string", "displayName": "string" },
"challenge": "base64",
"pubKeyCredParams": [...],
"timeout": 60000,
"attestation": "none",
"authenticatorSelection": {...}
}
// Success response
{
"status": "success",
"message": "User registered successfully",
"token": "string (JWT)"
}
```
#### `WS /auth/ws/add_credential`
Add a new passkey credential to an existing authenticated user.
**Authentication**: Required (session cookie)
**Flow**:
1. Client connects with valid session
2. Server sends registration options for existing user
3. Client performs WebAuthn ceremony and sends response
4. Server validates and adds new credential
5. Server sends success confirmation
#### `WS /auth/ws/add_device_credential`
Add a new passkey credential using a device addition token.
**Flow**:
1. Client connects and sends device addition token
2. Server validates token and sends registration options
3. Client performs WebAuthn ceremony and sends response
4. Server validates, adds credential, and cleans up token
5. Server sends JWT token for session establishment
**Initial Client Message**:
```json
{
"token": "string (device addition token)"
}
```
### Authentication
#### `WS /auth/ws/authenticate`
Authenticate using existing passkey credentials.
**Flow**:
1. Client connects to WebSocket
2. Server sends authentication options
3. Client performs WebAuthn ceremony and sends response
4. Server validates credential and updates usage stats
5. Server sends JWT token for session establishment
**Server Messages**:
```json
// Authentication options
{
"challenge": "base64",
"timeout": 60000,
"rpId": "localhost",
"allowCredentials": [...] // Optional, for non-discoverable credentials
}
// Success response
{
"status": "success",
"message": "Authentication successful",
"token": "string (JWT)"
}
// Error response
{
"status": "error",
"message": "error description"
}
```
---
## Error Handling
All endpoints return consistent error responses:
```json
{
"error": "string (error description)"
}
```
## Security Features
- **HTTP-only Cookies**: Session tokens are stored in secure, HTTP-only cookies
- **CSRF Protection**: SameSite cookie attributes prevent CSRF attacks
- **Token Validation**: All JWT tokens are validated and automatically refreshed
- **Credential Isolation**: Users can only access and modify their own credentials
- **Time-based Expiration**: Device addition tokens expire after 24 hours
- **Rate Limiting**: WebSocket connections are limited and validated
## CORS and Headers
The application includes appropriate CORS headers and security headers for production use with reverse proxies like Caddy or Nginx.
- All endpoints are prefixed with `/auth/`
### HTTP Endpoints
GET /auth/ - Main authentication app
GET /auth/forward-auth - Authentication validation for Caddy/Nginx
POST /auth/validate - Token validation endpoint
POST /auth/user-info - Get authenticated user information
POST /auth/logout - Logout current user
POST /auth/set-session - Set session cookie from Authorization header
DELETE /auth/credential/{uuid} - Delete specific credential
POST /auth/create-link - Create device addition link
GET /auth/{reset_token} - Process reset token and redirect
### WebSocket Endpoints
WS /auth/ws/register - Register new user with passkey
WS /auth/ws/add_credential - Add new credential for existing user
WS /auth/ws/authenticate - Authenticate user with passkey

View File

@ -1,6 +1,5 @@
(auth) {
# Forward /auth to the authentication service
redir /auth /auth/ 302
# Forward /auth/ to the authentication service
@auth path /auth/*
handle @auth {
reverse_proxy localhost:4401
@ -9,7 +8,7 @@
# Check for authentication
forward_auth localhost:4401 {
uri /auth/forward-auth
copy_headers x-auth-user-id
copy_headers x-auth*
}
{block}
}

View File

@ -60,7 +60,8 @@ onMounted(async () => {
})
}
} catch (error) {
console.error('Failed to create link:', error)
authStore.showMessage(`Failed to create device link: ${error.message}`, 'error')
authStore.currentView = 'profile'
}
})
</script>

View File

View File

@ -79,6 +79,8 @@ export const useAuthStore = defineStore('auth', {
this.currentUser = result.user
this.currentCredentials = result.credentials || []
this.aaguidInfo = result.aaguid_info || {}
if (result.session_type === 'device addition') this.currentView = 'add-credential'
console.log('User info loaded:', result)
},
async deleteCredential(uuid) {
const response = await fetch(`/auth/credential/${uuid}`, {method: 'DELETE'})

View File

@ -5,7 +5,7 @@ This module provides dataclasses and database abstractions for managing
users, credentials, and sessions in a WebAuthn authentication system.
"""
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime
from uuid import UUID
@ -43,8 +43,8 @@ class Session:
key: bytes
user_uuid: UUID
expires: datetime
info: dict
credential_uuid: UUID | None = None
info: dict | None = None
__all__ = ["User", "Credential", "Session"]

View File

@ -81,6 +81,7 @@ def register_api_routes(app: FastAPI):
return {
"status": "success",
"session_type": s.info["type"],
"user": {
"user_uuid": str(u.user_uuid),
"user_name": u.user_name,
@ -91,8 +92,10 @@ def register_api_routes(app: FastAPI):
"credentials": credentials,
"aaguid_info": aaguid_info,
}
except Exception as e:
return {"error": f"Failed to get user info: {str(e)}"}
except ValueError as e:
return {"error": f"Failed to get user info: {e}"}
except Exception:
return {"error": "Failed to get user info"}
@app.post("/auth/logout")
async def api_logout(response: Response, auth=Cookie(None)):
@ -123,7 +126,7 @@ def register_api_routes(app: FastAPI):
except ValueError as e:
return {"error": str(e)}
except Exception as e:
return {"error": f"Failed to set session: {str(e)}"}
return {"error": f"Failed to set session: {e}"}
@app.delete("/auth/credential/{uuid}")
async def api_delete_credential(uuid: UUID, auth: str = Cookie(None)):

View File

@ -17,7 +17,6 @@ from pathlib import Path
from fastapi import Cookie, FastAPI, Request, Response
from fastapi.responses import (
FileResponse,
JSONResponse,
)
from fastapi.staticfiles import StaticFiles
@ -52,7 +51,12 @@ async def forward_authentication(request: Request, auth=Cookie(None)):
s = await session.get_session(auth)
# If authenticated, return a success response
if s.info and s.info["type"] == "authenticated":
return Response(status_code=204, headers={"x-auth-user": str(s.user_uuid)})
return Response(
status_code=204,
headers={
"x-auth-user-uuid": str(s.user_uuid),
},
)
# Serve the index.html of the authentication app if not authenticated
return FileResponse(
@ -68,21 +72,12 @@ app.mount(
)
@app.get("/auth")
@app.get("/auth/")
async def redirect_to_index():
"""Serve the main authentication app."""
return FileResponse(STATIC_DIR / "index.html")
# Catch-all route for SPA - serve index.html for all non-API routes
@app.get("/{path:path}")
async def spa_handler(request: Request, path: str):
"""Serve the Vue SPA for all routes (except API and static)"""
if "text/html" not in request.headers.get("accept", ""):
return JSONResponse({"error": "Not Found"}, status_code=404)
return FileResponse(STATIC_DIR / "index.html")
def main():
"""Entry point for the application"""
import uvicorn

View File

@ -20,7 +20,7 @@ from passkey.fastapi import session
from ..db import User, sql
from ..sansio import Passkey
from ..util.tokens import create_token, reset_key, session_key
from ..util.tokens import create_token, session_key
from .session import create_session, infodict
# Create a FastAPI subapp for WebSocket endpoints
@ -96,20 +96,13 @@ async def websocket_register_new(
@app.websocket("/add_credential")
async def websocket_register_add(ws: WebSocket, token: str | None = None):
async def websocket_register_add(ws: WebSocket, auth=Cookie(None)):
"""Register a new credential for an existing user."""
print(auth)
await ws.accept()
origin = ws.headers.get("origin")
try:
if not token:
await ws.send_json({"error": "Token is required"})
return
# If a token is provided, use it to look up the session
key = reset_key(token)
s = await sql.get_session(key)
if not s:
await ws.send_json({"error": "Invalid or expired token"})
return
s = await session.get_session(auth, reset_allowed=True)
user_uuid = s.user_uuid
# Get user information to get the user_name
@ -119,7 +112,7 @@ async def websocket_register_add(ws: WebSocket, token: str | None = None):
# WebAuthn registration
credential = await register_chat(
ws, user_uuid, user_name, challenge_ids, origin=origin
ws, user_uuid, user_name, challenge_ids, origin
)
# Store the new credential in the database
await sql.create_credential_for_user(credential)