Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 83 additions & 4 deletions OPENAPI/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -471,14 +471,24 @@
},
"message": {
"type": "string",
"description": "Additional status information (for non-healthy states)",
"description": "Additional status information (for non-healthy states). Authenticated callers only.",
"example": "Document scan failed 3 time(s) in a row: connect ECONNREFUSED 172.18.0.2:8000"
},
"scanner": {
"$ref": "#/components/schemas/ScannerHealth"
"description": "Authenticated callers only.",
"allOf": [
{
"$ref": "#/components/schemas/ScannerHealth"
}
]
},
"paperless": {
"$ref": "#/components/schemas/PaperlessHealth"
"description": "Authenticated callers only.",
"allOf": [
{
"$ref": "#/components/schemas/PaperlessHealth"
}
]
}
}
},
Expand Down Expand Up @@ -6567,7 +6577,7 @@
"/health": {
"get": {
"summary": "System health check endpoint",
"description": "Reports database connectivity **and** whether the document scanner is\nactually able to work. Used by the Docker healthcheck and monitoring.\n\n`status` is `healthy` when the database is reachable and the scanner is\noperational, `degraded` when automatic processing is enabled but the\nscan loop is not armed or has failed `failureThreshold` runs in a row\n(for example because Paperless-ngx is unreachable), and `database_error`\nwhen the local database cannot be queried.\n\nA `degraded` state answers with HTTP 503 unless `HEALTHCHECK_STRICT=no`\nis configured, in which case the details are reported with HTTP 200.\nWhen automatic processing is switched off via\n`DISABLE_AUTOMATIC_PROCESSING=yes`, a missing scan loop is expected and\nnever reported as degraded.\n",
"description": "Reports database connectivity **and** whether the document scanner is\nactually able to work. Used by the Docker healthcheck and monitoring.\n\n`status` is `healthy` when the database is reachable and the scanner is\noperational, `degraded` when automatic processing is enabled but the\nscan loop is not armed or has failed `failureThreshold` runs in a row\n(for example because Paperless-ngx is unreachable), and `database_error`\nwhen the local database cannot be queried.\n\nA `degraded` state answers with HTTP 503 unless `HEALTHCHECK_STRICT=no`\nis configured, in which case the details are reported with HTTP 200.\nWhen automatic processing is switched off via\n`DISABLE_AUTOMATIC_PROCESSING=yes`, a missing scan loop is expected and\nnever reported as degraded.\n\nWithout authentication the response carries only `status` and\n`database`, with the same HTTP status codes. `message`, `scanner` and\n`paperless` hold raw error messages with internal host names and\nports, so they are returned only with a session (cookie or bearer\ntoken) or the `x-api-key` header.\n",
"tags": [
"System"
],
Expand Down Expand Up @@ -10254,6 +10264,75 @@
}
}
}
},
"/auth/oidc/login": {
"get": {
"summary": "Start single sign-on with the configured OpenID Connect provider",
"description": "Redirects the browser to the identity provider's authorization endpoint\n(authorization code flow with PKCE, state and nonce). Only available when\nOIDC_ENABLED=yes and OIDC_ISSUER_URL, OIDC_CLIENT_ID and OIDC_ADMIN_EMAIL\nare set.\n",
"tags": [
"Authentication"
],
"responses": {
"302": {
"description": "Redirect to the identity provider"
},
"404": {
"description": "Single sign-on is not enabled"
},
"502": {
"description": "The identity provider could not be reached; the login page is rendered with an error"
}
}
}
},
"/auth/oidc/callback": {
"get": {
"summary": "Complete single sign-on",
"description": "Redirect target registered with the identity provider. Exchanges the\nauthorization code, validates the ID token and signs in as the\nadministrator account when the identity's e-mail address equals\nOIDC_ADMIN_EMAIL. Sets the session cookie.\n",
"tags": [
"Authentication"
],
"parameters": [
{
"in": "query",
"name": "code",
"schema": {
"type": "string"
},
"description": "Authorization code issued by the provider"
},
{
"in": "query",
"name": "state",
"schema": {
"type": "string"
},
"description": "State value echoed back by the provider"
}
],
"responses": {
"302": {
"description": "Signed in, redirected to the dashboard",
"headers": {
"Set-Cookie": {
"schema": {
"type": "string",
"description": "HTTP-only cookie containing the session JWT"
}
}
}
},
"400": {
"description": "Missing or expired sign-in transaction, or the provider reported an error"
},
"403": {
"description": "The identity's e-mail address does not match OIDC_ADMIN_EMAIL"
},
"404": {
"description": "Single sign-on is not enabled"
}
}
}
}
},
"tags": [
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,23 @@ Then open [http://localhost:3000](http://localhost:3000) to complete setup.
- The Mistral OCR provider processes PDFs natively and ignores the `OCR_PDF_RENDER_*` settings
- The OCR queue can be worked through automatically instead of pressing **Process All Pending**: enable `OCR_AUTO_PROCESS_ENABLED` (default `no`) and configure `OCR_AUTO_PROCESS_INTERVAL` (cron, default `*/15 * * * *`), `OCR_AUTO_PROCESS_BATCH_SIZE` (documents per run, default `10`) and `OCR_AUTO_ANALYZE` (run AI analysis right after OCR, default `yes`). Runs are skipped while a document scan is active or while Paperless-ngx is unreachable, so queued documents are never marked as failed because of an outage

### Single Sign-On (OIDC)

Sign in with any OpenID Connect provider (Authentik, Authelia, Keycloak, Kanidm, Pocket ID, Zitadel, Entra ID, Google, ...) through a button on the login page. Zettelrobbe has one administrator account, and the identity whose e-mail address equals `OIDC_ADMIN_EMAIL` signs in as that account. Password login keeps working next to it.

```yaml
environment:
OIDC_ENABLED: 'yes'
OIDC_ISSUER_URL: https://auth.example.com/application/o/zettelrobbe/
OIDC_CLIENT_ID: zettelrobbe
OIDC_CLIENT_SECRET: ${ZETTELROBBE_OIDC_CLIENT_SECRET}
OIDC_REDIRECT_URI: https://zettelrobbe.example.com/auth/oidc/callback
OIDC_ADMIN_EMAIL: you@example.com # required
OIDC_BUTTON_TEXT: Sign in with Authentik # optional, also OIDC_BUTTON_ICON / _COLOR / _TEXT_COLOR
```

See [docs/sso-oidc.md](docs/sso-oidc.md) for all options, provider examples, button customization, security notes and troubleshooting.

### Container Images

| Image Tag | Size |
Expand Down
24 changes: 24 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,29 @@ const getCookieSecureMode = () => {
return 'auto';
};

/* Single sign-on through OpenID Connect. Read on every call rather than
snapshotted at load, like the other auth getters above, so a runtime
override or a test can change it without reloading the module. Off unless
OIDC_ENABLED is set; password login keeps working either way. */
const getOidcConfig = () => ({
enabled: parseEnvBoolean(process.env.OIDC_ENABLED, 'no') === 'yes',
issuerUrl: String(process.env.OIDC_ISSUER_URL || '').trim(),
clientId: String(process.env.OIDC_CLIENT_ID || '').trim(),
clientSecret: String(process.env.OIDC_CLIENT_SECRET || ''),
// Required: the identity whose e-mail matches signs in as the admin.
adminEmail: String(process.env.OIDC_ADMIN_EMAIL || '').trim(),
redirectUri: String(process.env.OIDC_REDIRECT_URI || '').trim(),
scopes: String(process.env.OIDC_SCOPES || 'openid profile email').trim(),
emailClaim: String(process.env.OIDC_EMAIL_CLAIM || 'email').trim() || 'email',
// client_secret_basic | client_secret_post | none; empty = automatic.
tokenAuthMethod: String(process.env.OIDC_TOKEN_AUTH_METHOD || '').trim(),
// Login button appearance.
buttonText: String(process.env.OIDC_BUTTON_TEXT || '').trim(),
buttonIcon: String(process.env.OIDC_BUTTON_ICON || '').trim(),
buttonColor: String(process.env.OIDC_BUTTON_COLOR || '').trim(),
buttonTextColor: String(process.env.OIDC_BUTTON_TEXT_COLOR || '').trim(),
});

// Initialize limit functions with defaults
const limitFunctions = {
activateTagging: parseEnvBoolean(process.env.ACTIVATE_TAGGING, 'yes'),
Expand Down Expand Up @@ -440,6 +463,7 @@ module.exports = {
getJwtSecret,
getTrustProxy,
getCookieSecureMode,
getOidcConfig,
isProtectedRuntimeEnvKey,
get apiKey() {
return getApiKey();
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ services:
# Update check (optional) – asks GitHub once a day whether a newer release
# exists. Set to 'no' for air-gapped installations; nothing else changes.
# - UPDATE_CHECK_ENABLED=no
# Single sign-on through OpenID Connect (optional), see docs/sso-oidc.md.
# The identity whose e-mail equals OIDC_ADMIN_EMAIL signs in as the admin.
# - OIDC_ENABLED=yes
# - OIDC_ISSUER_URL=https://auth.example.com/application/o/zettelrobbe/
# - OIDC_CLIENT_ID=your_client_id
# - OIDC_CLIENT_SECRET=your_client_secret
# - OIDC_REDIRECT_URI=https://zettelrobbe.example.com/auth/oidc/callback
# - OIDC_ADMIN_EMAIL=you@example.com
# - OIDC_BUTTON_TEXT=Sign in with SSO
ports:
- '3000:${PAPERLESS_AI_PORT:-3000}'
volumes:
Expand Down
Loading