Files
ackify-ce/backend/openapi.yaml
Benjamin 68426bc882 feat: add PKCE support to OAuth2 flow for enhanced security
- Implement PKCE (Proof Key for Code Exchange) with S256 method
- Add crypto/pkce module with code verifier and challenge generation
- Modify OAuth flow to include code_challenge in authorization requests
- Update HandleCallback to validate code_verifier during token exchange
- Extend session lifetime from 7 to 30 days
- Add comprehensive unit tests for PKCE functions
- Maintain backward compatibility with fallback for non-PKCE sessions
- Add detailed logging for OAuth flow with PKCE tracking

PKCE enhances security by preventing authorization code interception
attacks, as recommended by OAuth 2.1 and OIDC standards.

feat: add encrypted refresh token storage with automatic cleanup

- Add oauth_sessions table for storing encrypted refresh tokens
- Implement AES-256-GCM encryption for refresh tokens using cookie secret
- Create OAuth session repository with full CRUD operations
- Add SessionWorker for automatic cleanup of expired sessions
- Configure cleanup to run every 24h for sessions older than 37 days
- Modify OAuth flow to store refresh tokens after successful authentication
- Track client IP and user agent for session security validation
- Link OAuth sessions to user sessions via session ID
- Add comprehensive encryption tests with security validations
- Integrate SessionWorker into server lifecycle with graceful shutdown

This enables persistent OAuth sessions with secure token storage,
reducing the need for frequent re-authentication from 7 to 30 days.
2025-10-26 02:32:10 +02:00

937 lines
23 KiB
YAML

openapi: 3.0.3
info:
title: Ackify API
description: |
REST API for Ackify - Document signature tracking with cryptographic Ed25519 signatures.
## Authentication
Most endpoints require OAuth2 authentication via session cookies.
Admin endpoints additionally require the user's email to be in the ACKIFY_ADMIN_EMAILS list.
## CSRF Protection
Write operations (POST, PUT, DELETE) require a CSRF token obtained from `GET /api/v1/csrf`.
Include the token in the `X-CSRF-Token` header.
version: 1.0.0
contact:
name: Ackify Support
url: https://github.com/btouchard/ackify-ce
license:
name: AGPL-3.0-or-later
url: https://www.gnu.org/licenses/agpl-3.0.html
servers:
- url: /api/v1
description: API v1
tags:
- name: Health
description: System health checks
- name: Auth
description: OAuth2 authentication endpoints
- name: Users
description: User information
- name: Documents
description: Document management (public)
- name: Signatures
description: Signature creation and retrieval
- name: Admin - Documents
description: Admin document management
- name: Admin - Signers
description: Admin expected signers management
- name: Admin - Reminders
description: Admin email reminder management
paths:
/health:
get:
summary: Health check
description: Returns the health status of the API
tags:
- Health
responses:
'200':
description: Service is healthy
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: ok
timestamp:
type: string
format: date-time
/csrf:
get:
summary: Get CSRF token
description: Returns a CSRF token required for write operations
tags:
- Auth
responses:
'200':
description: CSRF token generated
content:
application/json:
schema:
type: object
properties:
csrfToken:
type: string
example: abc123def456
/auth/start:
post:
summary: Start OAuth2 flow
description: Initiates OAuth2 authentication with the configured provider
tags:
- Auth
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
redirectTo:
type: string
description: URL to redirect to after successful authentication
example: /signatures
responses:
'302':
description: Redirect to OAuth provider
'400':
description: Invalid request
/auth/callback:
get:
summary: OAuth2 callback
description: Handles OAuth2 provider callback after user authentication
tags:
- Auth
parameters:
- name: code
in: query
required: true
schema:
type: string
- name: state
in: query
required: true
schema:
type: string
responses:
'302':
description: Redirect to application
'400':
description: Invalid callback parameters
'401':
description: Authentication failed
/auth/logout:
get:
summary: Logout
description: Logs out the current user and clears the session
tags:
- Auth
responses:
'302':
description: Redirect to home page
/auth/check:
get:
summary: Check authentication status
description: Checks if user has an active OAuth session (only available if ACKIFY_OAUTH_AUTO_LOGIN=true)
tags:
- Auth
responses:
'200':
description: Authentication status
content:
application/json:
schema:
type: object
properties:
authenticated:
type: boolean
/users/me:
get:
summary: Get current user
description: Returns information about the currently authenticated user
tags:
- Users
security:
- sessionAuth: []
responses:
'200':
description: Current user information
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'401':
description: Not authenticated
/documents:
get:
summary: List documents
description: Returns a paginated list of all documents
tags:
- Documents
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: List of documents
content:
application/json:
schema:
type: object
properties:
documents:
type: array
items:
$ref: '#/components/schemas/Document'
total:
type: integer
page:
type: integer
limit:
type: integer
post:
summary: Create document
description: Creates a new document with metadata
tags:
- Documents
security:
- csrfToken: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateDocumentRequest'
responses:
'201':
description: Document created
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
'400':
description: Invalid request
'403':
description: CSRF token missing or invalid
/documents/{docId}:
get:
summary: Get document
description: Returns document metadata and signature count
tags:
- Documents
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'200':
description: Document details
content:
application/json:
schema:
$ref: '#/components/schemas/DocumentWithCount'
'404':
description: Document not found
/documents/{docId}/signatures:
get:
summary: Get document signatures
description: Returns all signatures for a document
tags:
- Documents
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'200':
description: List of signatures
content:
application/json:
schema:
type: object
properties:
signatures:
type: array
items:
$ref: '#/components/schemas/Signature'
/documents/{docId}/signatures/status:
get:
summary: Get user signature status
description: Checks if the current user has signed this document
tags:
- Signatures
security:
- sessionAuth: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'200':
description: Signature status
content:
application/json:
schema:
type: object
properties:
hasSigned:
type: boolean
signature:
$ref: '#/components/schemas/Signature'
/documents/{docId}/expected-signers:
get:
summary: Get expected signers
description: Returns the list of expected signers for a document
tags:
- Documents
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'200':
description: List of expected signers
content:
application/json:
schema:
type: object
properties:
expectedSigners:
type: array
items:
$ref: '#/components/schemas/ExpectedSigner'
/documents/find-or-create:
get:
summary: Find or create document
description: Finds a document by reference, or creates it if it doesn't exist
tags:
- Documents
parameters:
- name: ref
in: query
required: true
schema:
type: string
description: Document reference (URL, path, or custom ID)
responses:
'200':
description: Document found or created
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
/signatures:
get:
summary: Get user signatures
description: Returns all signatures created by the current user
tags:
- Signatures
security:
- sessionAuth: []
responses:
'200':
description: List of user signatures
content:
application/json:
schema:
type: object
properties:
signatures:
type: array
items:
$ref: '#/components/schemas/Signature'
post:
summary: Create signature
description: Creates a cryptographic Ed25519 signature for a document
tags:
- Signatures
security:
- sessionAuth: []
- csrfToken: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateSignatureRequest'
responses:
'201':
description: Signature created
content:
application/json:
schema:
$ref: '#/components/schemas/Signature'
'400':
description: Invalid request
'409':
description: User has already signed this document
/admin/documents:
get:
summary: List all documents (admin)
description: Returns paginated list of all documents with admin metadata
tags:
- Admin - Documents
security:
- sessionAuth: []
- adminRole: []
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
'200':
description: List of documents
content:
application/json:
schema:
type: object
properties:
documents:
type: array
items:
$ref: '#/components/schemas/Document'
total:
type: integer
/admin/documents/{docId}:
get:
summary: Get document details (admin)
description: Returns detailed document information
tags:
- Admin - Documents
security:
- sessionAuth: []
- adminRole: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'200':
description: Document details
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
delete:
summary: Delete document (admin)
description: Soft deletes a document
tags:
- Admin - Documents
security:
- sessionAuth: []
- adminRole: []
- csrfToken: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'204':
description: Document deleted
'404':
description: Document not found
/admin/documents/{docId}/metadata:
put:
summary: Update document metadata (admin)
description: Updates document title, URL, checksum, and description
tags:
- Admin - Documents
security:
- sessionAuth: []
- adminRole: []
- csrfToken: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateDocumentMetadataRequest'
responses:
'200':
description: Metadata updated
content:
application/json:
schema:
$ref: '#/components/schemas/Document'
/admin/documents/{docId}/signers:
get:
summary: Get document with signers (admin)
description: Returns document with both expected and actual signers
tags:
- Admin - Signers
security:
- sessionAuth: []
- adminRole: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'200':
description: Document with signers
content:
application/json:
schema:
$ref: '#/components/schemas/DocumentWithSigners'
post:
summary: Add expected signer (admin)
description: Adds one or more expected signers to a document
tags:
- Admin - Signers
security:
- sessionAuth: []
- adminRole: []
- csrfToken: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AddExpectedSignerRequest'
responses:
'201':
description: Signer(s) added
'400':
description: Invalid request
/admin/documents/{docId}/signers/{email}:
delete:
summary: Remove expected signer (admin)
description: Removes an expected signer from a document
tags:
- Admin - Signers
security:
- sessionAuth: []
- adminRole: []
- csrfToken: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
- name: email
in: path
required: true
schema:
type: string
responses:
'204':
description: Signer removed
/admin/documents/{docId}/status:
get:
summary: Get document status (admin)
description: Returns completion statistics for a document
tags:
- Admin - Documents
security:
- sessionAuth: []
- adminRole: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'200':
description: Document status
content:
application/json:
schema:
$ref: '#/components/schemas/DocumentStatus'
/admin/documents/{docId}/reminders:
get:
summary: Get reminder history (admin)
description: Returns email reminder send history for a document
tags:
- Admin - Reminders
security:
- sessionAuth: []
- adminRole: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
responses:
'200':
description: Reminder history
content:
application/json:
schema:
type: object
properties:
reminders:
type: array
items:
$ref: '#/components/schemas/ReminderLog'
post:
summary: Send reminders (admin)
description: Sends email reminders to pending signers
tags:
- Admin - Reminders
security:
- sessionAuth: []
- adminRole: []
- csrfToken: []
parameters:
- name: docId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SendRemindersRequest'
responses:
'200':
description: Reminders queued
content:
application/json:
schema:
type: object
properties:
queued:
type: integer
description: Number of emails queued for sending
/openapi.json:
get:
summary: Get OpenAPI specification
description: Returns this OpenAPI specification in JSON format
tags:
- Health
responses:
'200':
description: OpenAPI spec
content:
application/json:
schema:
type: object
components:
securitySchemes:
sessionAuth:
type: apiKey
in: cookie
name: session
description: OAuth2 session cookie
csrfToken:
type: apiKey
in: header
name: X-CSRF-Token
description: CSRF protection token
adminRole:
type: http
scheme: bearer
description: Admin email must be in ACKIFY_ADMIN_EMAILS
schemas:
User:
type: object
properties:
id:
type: string
description: User unique identifier (OAuth sub claim)
email:
type: string
format: email
name:
type: string
isAdmin:
type: boolean
Document:
type: object
properties:
docId:
type: string
example: abc123
title:
type: string
url:
type: string
checksum:
type: string
checksumAlgorithm:
type: string
enum: [SHA-256, SHA-512, MD5]
description:
type: string
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
createdBy:
type: string
DocumentWithCount:
allOf:
- $ref: '#/components/schemas/Document'
- type: object
properties:
signatureCount:
type: integer
Signature:
type: object
properties:
id:
type: integer
format: int64
docId:
type: string
userSub:
type: string
userEmail:
type: string
format: email
userName:
type: string
signedAt:
type: string
format: date-time
payloadHash:
type: string
signature:
type: string
description: Ed25519 signature (hex-encoded)
nonce:
type: string
createdAt:
type: string
format: date-time
referer:
type: string
prevHash:
type: string
docChecksum:
type: string
ExpectedSigner:
type: object
properties:
id:
type: integer
format: int64
docId:
type: string
email:
type: string
format: email
name:
type: string
addedAt:
type: string
format: date-time
addedBy:
type: string
notes:
type: string
ReminderLog:
type: object
properties:
id:
type: integer
format: int64
docId:
type: string
recipientEmail:
type: string
format: email
sentAt:
type: string
format: date-time
sentBy:
type: string
templateUsed:
type: string
status:
type: string
enum: [sent, failed, bounced, queued]
errorMessage:
type: string
DocumentStatus:
type: object
properties:
docId:
type: string
expectedCount:
type: integer
signedCount:
type: integer
pendingCount:
type: integer
completionPercentage:
type: number
format: float
DocumentWithSigners:
allOf:
- $ref: '#/components/schemas/Document'
- type: object
properties:
expectedSigners:
type: array
items:
$ref: '#/components/schemas/ExpectedSigner'
signatures:
type: array
items:
$ref: '#/components/schemas/Signature'
CreateDocumentRequest:
type: object
required:
- reference
properties:
reference:
type: string
description: Document URL, path, or custom ID
title:
type: string
description:
type: string
UpdateDocumentMetadataRequest:
type: object
properties:
title:
type: string
url:
type: string
checksum:
type: string
checksumAlgorithm:
type: string
enum: [SHA-256, SHA-512, MD5]
description:
type: string
CreateSignatureRequest:
type: object
required:
- docId
properties:
docId:
type: string
referer:
type: string
description: Source service (Google Docs, GitHub, etc.)
docChecksum:
type: string
description: Document checksum at signing time
AddExpectedSignerRequest:
type: object
required:
- emails
properties:
emails:
type: array
items:
type: string
format: email
notes:
type: string
SendRemindersRequest:
type: object
properties:
emails:
type: array
items:
type: string
format: email
description: Specific emails to send to (omit to send to all pending)
docURL:
type: string
description: Custom document URL for email
locale:
type: string
enum: [en, fr, es, de, it]
default: en
description: Email language