docs: add consent screen design
This commit is contained in:
parent
404fcac4dd
commit
16f3e039d9
1 changed files with 108 additions and 0 deletions
108
docs/plans/2026-02-18-consent-screen-design.md
Normal file
108
docs/plans/2026-02-18-consent-screen-design.md
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
# Consent Screen Design
|
||||||
|
|
||||||
|
**Goal:** Add a consent screen to the OIDC authorization flow so users can
|
||||||
|
approve (or partially approve) the scopes an RP requests before tokens are
|
||||||
|
issued. Consent decisions are persisted per user+client and only shown again
|
||||||
|
when the requested scopes change.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Consent is shown at scope level (not individual claims).
|
||||||
|
- `openid` is always granted (mandatory per OIDC spec).
|
||||||
|
- Users may uncheck optional scopes (`profile`, `email`, `phone`) for partial
|
||||||
|
consent — the flow proceeds with only the approved scopes.
|
||||||
|
- A "Deny" button returns `access_denied` to the RP.
|
||||||
|
- Consent is remembered per user+client. If the stored scopes cover all
|
||||||
|
requested scopes, consent is skipped silently.
|
||||||
|
- If the RP requests new scopes not previously approved, the consent screen
|
||||||
|
is shown again.
|
||||||
|
- The built-in `manage-app` client bypasses consent entirely (trusted
|
||||||
|
first-party).
|
||||||
|
- No other clients can be marked as trusted — all configured RP clients
|
||||||
|
require consent.
|
||||||
|
- Saved consents can be revoked from the (future) profile page.
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
New SQLite table in a new migration (`002_user_consents.sql`):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE user_consents (
|
||||||
|
userid TEXT NOT NULL REFERENCES users(userid) ON DELETE CASCADE,
|
||||||
|
client_id TEXT NOT NULL,
|
||||||
|
scopes TEXT NOT NULL, -- JSON array, e.g. '["openid", "profile"]'
|
||||||
|
created_at TEXT NOT NULL, -- ISO 8601
|
||||||
|
updated_at TEXT NOT NULL, -- ISO 8601
|
||||||
|
PRIMARY KEY (userid, client_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Change detection:** load stored consent for user+client. If the stored
|
||||||
|
scopes are a superset of (or equal to) the requested scopes, skip consent.
|
||||||
|
Otherwise show the consent screen. On approval the stored scopes are replaced
|
||||||
|
entirely with whatever the user checked.
|
||||||
|
|
||||||
|
## Authorization Flow
|
||||||
|
|
||||||
|
Current:
|
||||||
|
|
||||||
|
```
|
||||||
|
/authorization → login → _complete_authorization() → redirect with code
|
||||||
|
```
|
||||||
|
|
||||||
|
New:
|
||||||
|
|
||||||
|
```
|
||||||
|
/authorization → login → check consent → [consent screen] → _complete_authorization() → redirect with code
|
||||||
|
```
|
||||||
|
|
||||||
|
Detailed steps after authentication:
|
||||||
|
|
||||||
|
1. If `client_id == settings.manage_client_id`, skip consent — proceed
|
||||||
|
directly to `_complete_authorization()`.
|
||||||
|
2. Load stored consent from `user_consents` for this user+client.
|
||||||
|
3. If stored scopes cover all requested scopes, skip consent.
|
||||||
|
4. Otherwise store the parsed OIDC request in the session and redirect to
|
||||||
|
`GET /consent`.
|
||||||
|
5. The consent page shows the RP name (client_id), requested scopes with
|
||||||
|
checkboxes, and Allow / Deny buttons. `openid` is checked and disabled.
|
||||||
|
6. On "Allow": save approved scopes to `user_consents` (upsert), filter the
|
||||||
|
OIDC request scopes to the approved set, proceed to
|
||||||
|
`_complete_authorization()`.
|
||||||
|
7. On "Deny": redirect to the RP with `error=access_denied`.
|
||||||
|
|
||||||
|
## Consent Page
|
||||||
|
|
||||||
|
New template `consent.html` extending `base.html`. Uses a standard HTML form
|
||||||
|
(no HTMX needed — this is a full-page navigation step).
|
||||||
|
|
||||||
|
Scope descriptions shown to the user:
|
||||||
|
|
||||||
|
| Scope | Label |
|
||||||
|
|---|---|
|
||||||
|
| `openid` | Sign you in (required) |
|
||||||
|
| `profile` | Your name and profile information |
|
||||||
|
| `email` | Your email address |
|
||||||
|
| `phone` | Your phone number |
|
||||||
|
|
||||||
|
## Repository Layer
|
||||||
|
|
||||||
|
New `ConsentRepository` protocol and `SQLiteConsentRepository`, following the
|
||||||
|
existing repository pattern.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
|
||||||
|
- `get_consent(userid, client_id) -> Consent | None`
|
||||||
|
- `set_consent(userid, client_id, scopes) -> None` — upsert
|
||||||
|
- `delete_consent(userid, client_id) -> None` — for revocation
|
||||||
|
- `list_consents(userid) -> list[Consent]` — for profile page listing
|
||||||
|
|
||||||
|
`Consent` is a Pydantic model with `userid`, `client_id`, `scopes: list[str]`,
|
||||||
|
`created_at`, `updated_at`.
|
||||||
|
|
||||||
|
## Scope Filtering
|
||||||
|
|
||||||
|
When the user grants partial consent (e.g. approves `openid` + `profile` but
|
||||||
|
not `email`), the authorization request's scope list must be narrowed before
|
||||||
|
passing to idpyoidc. This ensures the issued tokens only contain claims for
|
||||||
|
the approved scopes.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue