Authentication, Signup & Approval
lk-wiz uses Cognito for identity — email/password or "Sign in with Google" — plus one extra gate on top: an admin has to approve your account before you can use the product. Signing up gets you a Cognito account; it doesn't get you into the app.
This page covers the account lifecycle and the approval gate. For the exact request/
response shapes, see API Reference → Auth. The authoritative
spec is specs/features/auth.md in the repo.
The lifecycle
signup ──▶ pending ──▶ approved (an admin clicks "Approve")
└──▶ rejected (an admin clicks "Reject")| Stage | What's true | Can you use the app? |
|---|---|---|
| Signup submitted | Cognito account requested; email not yet confirmed | No |
| Pending | Email confirmed (or Google login completed); you can log in | No — "Awaiting Approval" screen |
| Approved | An admin approved you | Yes |
| Rejected | An admin rejected you | No — your Cognito account is disabled outright |
The intended gate isn't this four-stage model directly — it's one boolean baked into your
login token, custom:approved. But as currently deployed that claim is not actually
checked by the REST API. See The approval gate.
Signing up
Email and password:
- Fill in the signup form. Behind a hidden reCAPTCHA v3 check — if your session looks automated, signup is rejected before Cognito is even involved.
- Cognito emails you a verification link.
- Click it. This is the moment your lk-wiz account actually comes into existence (a
database record gets created with
approved: false) — not the moment you submitted the form. - Every admin gets a Slack DM: "New user signup:
{your email}. Approve:{link}."
Google sign-in:
- Click "Sign in with Google." You're sent to Google's consent screen via the Cognito Hosted UI — lk-wiz never sees your Google password.
- First time signing in with a given Google account, your email is auto-verified (no confirmation email — Google already vouched for it), and the same account-creation + admin-notification step from above runs immediately.
- There is no reCAPTCHA check on this path — reCAPTCHA only applies to the email/password form.
Either way, you land in the same place next: pending.
Waiting for approval
Once you can log in but aren't approved yet, you're stuck on a dedicated "Awaiting Approval" screen — you can't reach ideas, posts, or any other page. That screen:
- Explains your account is pending review.
- Checks your approval status automatically every 30 seconds.
- Once approved, forces you through a fresh login (not just a page refresh) — this is necessary, not a UI quirk, because of how the approval gate actually works (next section).
Admins process approvals from an admin-only page — see User Approval for that side of the flow.
The approval gate
This is the single most important mechanic to understand about lk-wiz auth: your approval status is frozen into your login token at the moment it's issued. Nothing re-checks the database on every request.
Every time you log in (or your session silently refreshes), lk-wiz looks up your
account and stamps two things into the new token: custom:approved ("true" or
"false") and custom:role ("admin" or "member"). The frontend reads
custom:approved from that token to decide whether to show you the app or the "Awaiting
Approval" screen — not a live database read.
That's why:
- Getting approved doesn't unlock the UI until you get a new token. Just refreshing the page won't do it — you need to actually sign in again (which is what the Awaiting Approval screen's "check status" polling triggers automatically once it sees you're approved).
- If your role changes later (member → admin), the same rule applies — it won't show up until your next login.
Security gap — REST API is not approval-gated. The deployed API Gateway authorizer
is a native Cognito JWT authorizer that validates a token's signature, issuer, and
audience only — it does not check custom:approved. So an
authenticated-but-unapproved user can currently call the /api/* REST endpoints
directly, even though the web UI keeps them on the "Awaiting Approval" screen. A Lambda
authorizer that would enforce approval exists in the codebase but is not wired to any
route. Approval is enforced server-side on WebSocket collaboration tickets and on
admin operations. The client-side "Awaiting Approval" gate is a UX affordance, not a
security control. See Known gaps.
Roles
Everyone starts as a member. An admin can promote someone to admin (see
Admin → User Management) — admins can additionally approve/
reject signups and manage the workspace roster. Ordinary product features (ideas,
posts, review) don't distinguish member from admin at all; the role only matters to
admin-only pages.
Real-time collaboration and WebSockets
The live co-editing canvas connects over a WebSocket, which can't carry a normal login header the way a regular page request can. So instead, your browser first asks the API for a short-lived, single-purpose ticket (good for 8 hours), then hands that ticket to the WebSocket when it connects.
A ticket captures your approval/role status at the moment you requested it and
never re-checks it. Unlike the REST API, the WebSocket path does enforce approval:
requesting a ticket only needs a valid token (any authenticated user can mint one), but
the ticket records your approved status, and the WebSocket $connect handshake
rejects the connection if that status isn't "true". The ticket also isn't invalidated
after first use, so a single ticket can authenticate more than one connection attempt
within its 8-hour window.
Known gaps
Approval is not enforced on the REST API. The deployed API Gateway authorizer
(native Cognito JWT) validates only a token's signature, issuer, and audience — it does
not check custom:approved. An authenticated-but-unapproved user can call /api/* REST
routes directly. A Lambda approval-authorizer that would close this gap exists in the
codebase but is wired to zero routes. Approval is enforced server-side only on WebSocket
collaboration tickets and admin operations; the web UI's "Awaiting Approval" gate is
client-side only. See The approval gate for the full explanation.
Two things worth knowing if you're integrating against this system directly (not through the shipped web app):
- There's a fully working backend endpoint for exchanging an OAuth code for tokens that
also sets a secure,
HttpOnlysession cookie — but the shipped login page doesn't use it. It talks to Cognito's token endpoint directly from the browser instead and keeps tokens in the browser's local storage. Both mechanisms exist in the codebase; only one is actually wired up. - Your own profile endpoint (
GET /api/auth/me) only ever reports"approved"or"pending"— never"rejected"— even though the admin user list distinguishes all three. In practice this doesn't come up: a rejected account is disabled at the Cognito level and can't sign in to hit this endpoint at all.
See also
- API Reference → Auth — request/response shapes for signup, callback, profile, and WebSocket ticket endpoints.
- Admin → User Management — the approve/reject/role-change side of this flow.