Skip to content

Commit 0291368

Browse files
docs(sso): refresh RFC to reflect users table landing as #15
- Strike the "prerequisite blocker" section: the users table, sessions.user_id and user_type='user' all landed in #15. Quote the as-landed schema from src/lib/server/db/schema.ts rather than the org_users shape originally proposed. - Spell out the additive migration SSO still needs (auth_source, sso_subject, last_login_at on users; partial unique index on (org_id, sso_subject)). Drop role/status — out of scope for SSO. - Update the auth-flow pseudocode to reference users (not org_users) and to note last_login_at should be bumped on callback success. - Strike chunk 2 in the rollout plan (landed as #15) and renumber chunks 3-7 accordingly. - Add postguard-business#15 to the "Related" links and flag the refresh date in the status line. No prose rewrite; no change to the auth flow, security considerations, or open questions. The landed schema matched the proposal closely enough that the plan survives intact — the drift is mostly additive.
1 parent e4370dc commit 0291368

1 file changed

Lines changed: 66 additions & 34 deletions

File tree

docs/proposals/sso.md

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Proposal: Single Sign-On (SSO) for organizations
22

3-
Status: **draft, request for comments**
3+
Status: **draft, request for comments** (refreshed 2026-04-23 — reflects #15 landing)
44
Tracking issue: [encryption4all/postguard#143](https://github.com/encryption4all/postguard/issues/143)
5-
Related: [postguard#74](https://github.com/encryption4all/postguard/issues/74) (org user management), [postguard#146](https://github.com/encryption4all/postguard/issues/146) (AD / SCIM sync)
5+
Related: [postguard#74](https://github.com/encryption4all/postguard/issues/74) (org user management), [postguard#146](https://github.com/encryption4all/postguard/issues/146) (AD / SCIM sync), [postguard-business#15](https://github.com/encryption4all/postguard-business/pull/15) (users table — landed)
66

77
This document proposes the shape of SSO support in postguard-business before
88
any schema or code lands. The goal is to agree on the model and phasing so
@@ -34,35 +34,59 @@ authenticated, without re-proving identity attributes on every action.
3434
- **Custom-branded login pages per org.** A plain `/auth/sso/[orgSlug]`
3535
bounce is enough for v1.
3636

37-
## Prerequisite: multi-user organizations (blocks this work)
37+
## Prerequisite: multi-user organizations **landed in #15**
3838

39-
The current data model has one email per organization (`organizations.email`).
40-
There is no `org_users` table — you cannot log in "as Alice from Acme", only
41-
"as Acme". SSO only makes sense once an organization can have many users,
42-
so this proposal assumes issue #74 (part A, data model) lands first or in
43-
the same PR stack.
39+
The original version of this proposal called this out as a blocker. It is
40+
now done: PR [#15](https://github.com/encryption4all/postguard-business/pull/15)
41+
landed a `users` table and the session plumbing needed to log in as a
42+
specific person, not just "as the organization". SSO plugs into that same
43+
table rather than introducing a second `org_users` table.
4444

45-
Proposed minimal `org_users` shape (owned by #74, stated here only to fix
46-
the dependency):
45+
As-landed shape (`src/lib/server/db/schema.ts`):
4746

4847
```
49-
org_users (
48+
users (
5049
id uuid pk,
51-
org_id uuid references organizations,
52-
email varchar(256) not null,
53-
full_name varchar(256),
54-
role varchar(32) not null default 'member', -- 'owner' | 'admin' | 'member'
55-
status varchar(32) not null default 'active', -- 'invited' | 'active' | 'disabled'
56-
auth_source varchar(32) not null default 'yivi', -- 'yivi' | 'sso'
57-
sso_subject varchar(256), -- IdP 'sub' claim, for SSO users
58-
created_at timestamptz,
59-
last_login_at timestamptz,
60-
unique (org_id, email)
50+
email varchar(256) not null unique, -- globally unique (not per-org)
51+
full_name varchar(256) not null,
52+
phone varchar(32),
53+
org_id uuid not null references organizations on delete cascade,
54+
created_at timestamptz not null default now(),
55+
index idx_users_org (org_id)
6156
)
6257
```
6358

64-
`sessions.userType` gains `'user'` as a third value (alongside `'org'` and
65-
`'admin'`) and `sessions.orgUserId` references `org_users.id`.
59+
Sessions already carry `user_type` (`'org'` | `'admin'` | `'user'`) and
60+
`user_id` referencing `users.id`. The SSO callback creates a session row
61+
with `user_type='user'` the same way Yivi login does.
62+
63+
### Additive columns this proposal adds to `users`
64+
65+
SSO needs three columns that #15 did not land. They are additive, nullable,
66+
and default to Yivi behaviour so existing rows keep working:
67+
68+
```
69+
-- migration 0002_add-sso-fields.sql
70+
alter table users
71+
add column auth_source varchar(32) not null default 'yivi',
72+
-- 'yivi' | 'sso'
73+
add column sso_subject varchar(256),
74+
-- IdP 'sub' claim — only set for auth_source='sso' users
75+
add column last_login_at timestamptz;
76+
77+
create unique index users_sso_subject_unique
78+
on users (org_id, sso_subject)
79+
where sso_subject is not null;
80+
```
81+
82+
`role` / `status` are intentionally NOT part of this migration — #15 did
83+
not land them, and SSO does not need them (role gating is a follow-up for
84+
the admin/members work). Adding them here would widen scope.
85+
86+
**Email uniqueness note.** The landed `users.email` is globally unique,
87+
not per-org. That is fine for SSO: a single person works at one
88+
organization in PostGuard. If that ever changes, both the landed schema
89+
and this proposal have to evolve together.
6690

6791
## Data model for SSO
6892

@@ -120,9 +144,11 @@ User → /api/auth/sso/callback?code=…&state=…
120144
| 2. Exchange code at token endpoint
121145
| 3. Verify ID token signature against JWKS, iss/aud/exp checks
122146
| 4. Apply claim_mapping to extract email + sub + full_name
123-
| 5. Find org_users by (org_id, sso_subject); if miss, find by
124-
| (org_id, email); if miss and jit_provisioning, insert; else 403
125-
| 6. createSession(userType='user', orgUserId, orgId, attrs={sso:true})
147+
| 5. Find users by (org_id, sso_subject); if miss, find by
148+
| (org_id, email); if miss and jit_provisioning, insert with
149+
| auth_source='sso'; else 403
150+
| 6. createSession(userType='user', userId, orgId, attrs={sso:true}),
151+
| update users.last_login_at
126152
| 7. Set pg_session cookie (existing helper)
127153
| 8. Redirect to ?redirect= or /portal/dashboard
128154
```
@@ -199,17 +225,23 @@ Flag this for explicit review in the RFC discussion.
199225
Each chunk is one PR. Humans can stop the train at any step.
200226

201227
1. **This PR** — proposal only. No code. Merge or reject the plan.
202-
2. `feat(db): org_users and sso scaffolding` — schema migration only,
203-
tables empty, no routes. Pure data. (Part of / coordinated with #74.)
204-
3. `feat(auth): OIDC SSO callback``openid-client` dep, `/auth/sso/[slug]`,
228+
2. ~~`feat(db): users table and session scaffolding`~~**landed in #15.**
229+
The `users` table, `sessions.user_id`, `user_type='user'` and the
230+
members portal page all shipped. SSO only needs the additive migration
231+
in the next chunk.
232+
3. `feat(db): add SSO fields + org_identity_providers` — migration-only
233+
PR. Adds `auth_source`/`sso_subject`/`last_login_at` to `users`, a
234+
partial unique index on `(org_id, sso_subject)`, and the
235+
`org_identity_providers` table. No routes.
236+
4. `feat(auth): OIDC SSO callback``openid-client` dep, `/auth/sso/[slug]`,
205237
`/api/auth/sso/callback`, session creation path. `FF_SSO` gates
206238
everything. No admin UI yet; IdP config inserted via seed / db:studio
207239
for testing.
208-
4. `feat(portal): SSO login entry on /auth/login` — the small "sign in
240+
5. `feat(portal): SSO login entry on /auth/login` — the small "sign in
209241
with your organization" block.
210-
5. `feat(admin): SSO configuration page``(admin)/admin/organizations/[id]/sso`,
242+
6. `feat(admin): SSO configuration page``(admin)/admin/organizations/[id]/sso`,
211243
CRUD, test button, audit log.
212-
6. `feat(sso): SAML phase 2` — only if SAML is still wanted. Separate
244+
7. `feat(sso): SAML phase 2` — only if SAML is still wanted. Separate
213245
proposal amendment.
214246

215247
## Open questions (please answer in the PR thread)
@@ -230,5 +262,5 @@ Each chunk is one PR. Humans can stop the train at any step.
230262
add-ons are explicitly out-of-scope for this issue, and file a separate
231263
issue for desktop SSO.
232264

233-
If this plan looks right, chunks 2–5 are roughly one week of work total
234-
and can be reviewed independently.
265+
If this plan looks right, chunks 3–6 are roughly one week of work total
266+
and can be reviewed independently (chunk 2 already landed as #15).

0 commit comments

Comments
 (0)