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