Add OAuth2 authorization server for delegated read-only API access #6172

Open
poulpreben wants to merge 2 commits from poulpreben/bookstack:oauth-authorization-server into development
First-time contributor

Closes #5614

User story: As a user, I want to securely grant applications read access to BookStack on my behalf, without handing out long-lived API tokens. This unlocks use cases like MCP servers and CLI tools that authenticate as the actual user with their normal permissions.

What this adds:

  • GET/POST /oauth/authorize with a consent screen (uses the normal BookStack login, works with all auth methods)
  • POST /oauth/token with authorization code + refresh token grants, PKCE (S256) required
  • Bearer token support in the existing API guard; read scope enforced server-side (safe HTTP methods only)
  • Confidential clients (server apps, bcrypt secret) and public clients (CLIs, no secret) - both managed in Settings under "App connections", nothing via env vars
  • Refresh token rotation with reuse detection; users can revoke connections from their account page
  • Tokens stored hashed (SHA-256), auth codes are short-lived (60s) single-use cache entries - same pattern as the SAML2 ACS hand-off

Deliberately kept small: hand-rolled minimal OAuth2 following existing BookStack patterns (no league/oauth2-server dependency), one read scope for now, no dynamic client registration. Happy to adjust scope or approach to whatever fits the project best.

Screenshots

Consent screen (as seen by the authorizing user)
01-consent-screen.png

My Account: connected applications with revoke
02-my-account-connections.png

Settings: app connections list
03-settings-app-connections.png

Create form with confidential/public toggle
04-create-app-connection.png

One-time client secret reveal after creation
05-one-time-secret.png

Closes #5614 **User story:** As a user, I want to securely grant applications read access to BookStack on my behalf, without handing out long-lived API tokens. This unlocks use cases like MCP servers and CLI tools that authenticate as the actual user with their normal permissions. **What this adds:** - `GET/POST /oauth/authorize` with a consent screen (uses the normal BookStack login, works with all auth methods) - `POST /oauth/token` with authorization code + refresh token grants, PKCE (S256) required - Bearer token support in the existing API guard; `read` scope enforced server-side (safe HTTP methods only) - Confidential clients (server apps, bcrypt secret) and public clients (CLIs, no secret) - both managed in Settings under "App connections", nothing via env vars - Refresh token rotation with reuse detection; users can revoke connections from their account page - Tokens stored hashed (SHA-256), auth codes are short-lived (60s) single-use cache entries - same pattern as the SAML2 ACS hand-off **Deliberately kept small:** hand-rolled minimal OAuth2 following existing BookStack patterns (no league/oauth2-server dependency), one `read` scope for now, no dynamic client registration. Happy to adjust scope or approach to whatever fits the project best. <details> <summary><strong>Screenshots</strong></summary> **Consent screen (as seen by the authorizing user)** ![01-consent-screen.png](https://codeberg.org/attachments/db511ec4-66bb-4b32-979a-9d6dc1f42d4f) **My Account: connected applications with revoke** ![02-my-account-connections.png](https://codeberg.org/attachments/6061f29d-92aa-4806-9ac4-107119240fe8) **Settings: app connections list** ![03-settings-app-connections.png](https://codeberg.org/attachments/55621737-b36b-4086-9849-92fa108deb70) **Create form with confidential/public toggle** ![04-create-app-connection.png](https://codeberg.org/attachments/759dc270-a78c-4f87-9c5b-f7d92ef9f46a) **One-time client secret reveal after creation** ![05-one-time-secret.png](https://codeberg.org/attachments/a9e5d8d3-563c-4e8c-9222-5ad44f919f71) </details>
Add OAuth2 authorization server for delegated read-only API access
Some checks failed
analyse-php / build (pull_request) Has been cancelled
lint-php / build (pull_request) Has been cancelled
test-migrations / build (8.2) (pull_request) Has been cancelled
test-migrations / build (8.3) (pull_request) Has been cancelled
test-migrations / build (8.4) (pull_request) Has been cancelled
test-migrations / build (8.5) (pull_request) Has been cancelled
test-php / build (8.2) (pull_request) Has been cancelled
test-php / build (8.3) (pull_request) Has been cancelled
test-php / build (8.4) (pull_request) Has been cancelled
test-php / build (8.5) (pull_request) Has been cancelled
8ea887f799
Adds authorization code + refresh token grants with mandatory PKCE (S256),
allowing applications (MCP servers, CLIs) to call the API on behalf of a
user with their normal permissions, without long-lived static tokens.

- GET/POST /oauth/authorize with consent screen, using normal login
- POST /oauth/token, stateless, with refresh rotation + reuse detection
- Bearer token support in the existing API token guard
- read scope enforced server-side (safe HTTP methods only)
- Confidential and public client types, managed in settings UI
- Per-user connection revocation from the account security page
- Tokens stored as SHA-256 hashes; auth codes as short-lived cache entries

For #5614
OAuth: Harden delegated auth flows
Some checks are pending
analyse-php / build (pull_request) Blocked by required conditions
lint-php / build (pull_request) Blocked by required conditions
test-migrations / build (8.2) (pull_request) Blocked by required conditions
test-migrations / build (8.3) (pull_request) Blocked by required conditions
test-migrations / build (8.4) (pull_request) Blocked by required conditions
test-migrations / build (8.5) (pull_request) Blocked by required conditions
test-php / build (8.2) (pull_request) Blocked by required conditions
test-php / build (8.3) (pull_request) Blocked by required conditions
test-php / build (8.4) (pull_request) Blocked by required conditions
test-php / build (8.5) (pull_request) Blocked by required conditions
cf19d1e0ec
Some checks are pending
analyse-php / build (pull_request) Blocked by required conditions
lint-php / build (pull_request) Blocked by required conditions
test-migrations / build (8.2) (pull_request) Blocked by required conditions
test-migrations / build (8.3) (pull_request) Blocked by required conditions
test-migrations / build (8.4) (pull_request) Blocked by required conditions
test-migrations / build (8.5) (pull_request) Blocked by required conditions
test-php / build (8.2) (pull_request) Blocked by required conditions
test-php / build (8.3) (pull_request) Blocked by required conditions
test-php / build (8.4) (pull_request) Blocked by required conditions
test-php / build (8.5) (pull_request) Blocked by required conditions
This pull request can be merged automatically.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u oauth-authorization-server:poulpreben-oauth-authorization-server
git switch poulpreben-oauth-authorization-server

Merge

Merge the changes and update on Forgejo.
git switch development
git merge --no-ff poulpreben-oauth-authorization-server
git switch poulpreben-oauth-authorization-server
git rebase development
git switch development
git merge --ff-only poulpreben-oauth-authorization-server
git switch poulpreben-oauth-authorization-server
git rebase development
git switch development
git merge --no-ff poulpreben-oauth-authorization-server
git switch development
git merge --squash poulpreben-oauth-authorization-server
git switch development
git merge --ff-only poulpreben-oauth-authorization-server
git switch development
git merge poulpreben-oauth-authorization-server
git push origin development
Sign in to join this conversation.
No description provided.