Everything works. Minor adjustments on frontend and backend for the new API.
This commit is contained in:
parent
a987f47988
commit
30ab73d625
375
API.md
375
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: <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
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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>
|
||||
|
0
frontend/src/stores/API.md
Normal file
0
frontend/src/stores/API.md
Normal 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'})
|
||||
|
@ -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"]
|
||||
|
@ -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)):
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user