diff --git a/API.md b/API.md index b9a4365..ba70477 100644 --- a/API.md +++ b/API.md @@ -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: " -} -``` - ---- - -### 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 -``` - -**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: " -} -``` - -#### `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: ` - -**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: " -} -``` - ---- - -### 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: " -} -``` - -#### `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: " -} -``` - ---- - -### 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 diff --git a/Caddyfile b/Caddyfile index a10d522..344d600 100644 --- a/Caddyfile +++ b/Caddyfile @@ -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} } diff --git a/frontend/src/components/DeviceLinkView.vue b/frontend/src/components/DeviceLinkView.vue index eb3001a..8f39b6d 100644 --- a/frontend/src/components/DeviceLinkView.vue +++ b/frontend/src/components/DeviceLinkView.vue @@ -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' } }) diff --git a/frontend/src/stores/API.md b/frontend/src/stores/API.md new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index 693b897..6c4f6f8 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -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'}) diff --git a/passkey/db/__init__.py b/passkey/db/__init__.py index 979644b..83cb18c 100644 --- a/passkey/db/__init__.py +++ b/passkey/db/__init__.py @@ -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"] diff --git a/passkey/fastapi/api.py b/passkey/fastapi/api.py index fe597aa..b38bf98 100644 --- a/passkey/fastapi/api.py +++ b/passkey/fastapi/api.py @@ -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)): diff --git a/passkey/fastapi/main.py b/passkey/fastapi/main.py index 2f7faa7..8f20d86 100644 --- a/passkey/fastapi/main.py +++ b/passkey/fastapi/main.py @@ -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 diff --git a/passkey/fastapi/ws.py b/passkey/fastapi/ws.py index be4e2ca..79be5c7 100644 --- a/passkey/fastapi/ws.py +++ b/passkey/fastapi/ws.py @@ -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)