Documentation
POC User Onboarding Playbook
Project-Agent / poc/USER_ONBOARDING.md
The POC stack supports four user-onboarding paths. Each exercises the same code as production. This document walks through all four so your IAM / IdP / security teams can verify each one against your real tools.
> Prerequisites. ./poc/bootstrap.sh has run successfully and the > stack is healthy (docker compose -f poc/docker-compose.poc.yml ps > shows everything running). Bootstrap also wrote poc/.env.poc, > which several flows below source for credentials.
| Flow | When to use | SIG-Lite controls | |---|---|---| | 1. Email signup | Smoke test; "can a real user sign up at all?" | E.4.1 | | 2. SCIM 2.0 | Automated provisioning from Okta / Entra ID | E.4.1, E.4.2, E.4.3 | | 3. SAML JIT | Federated login; user provisioned on first SSO | E.2.1, E.2.4 | | 4. GoTrue admin invite | One-off invites without IdP wiring | E.4.1 |
---
1. Email signup (manual)
Validates that the dashboard, GoTrue, and Postgres users table are all healthy and connected.
- Open <http://localhost:3000> and click Sign up.
- Enter any email + a 12+ char password. Submit.
- The email lands in Mailhog at <http://localhost:8025> (no real SMTP
configured for the POC). Click the verification link.
- You're redirected back to the dashboard, signed in.
What this proves:
- GoTrue → Postgres
auth.userswrite path works. - The post-signup trigger that populates the application
public.users
row (with org_id, default role) is firing.
- Email verification round-trips through the configured SMTP host.
To inspect:
``bash docker compose -f poc/docker-compose.poc.yml exec postgres \ psql -U postgres -d postgres -c \ "SELECT id, email, role, sso_provider, created_at FROM public.users ORDER BY created_at DESC LIMIT 5;" ``
---
2. SCIM 2.0 provisioning
Validates RFC 7644 endpoints — what your Okta / Entra tenant will hit when it auto-provisions users.
The SCIM bearer token was minted by the seeder and lives in poc/.env.poc as POC_SCIM_BEARER. To test by hand:
```bash source poc/.env.poc SCIM=http://localhost:8000/v1/scim H_AUTH="Authorization: Bearer $POC_SCIM_BEARER" H_CT="Content-Type: application/scim+json"
(a) Create user
curl -fsS -X POST "$SCIM/Users" -H "$H_AUTH" -H "$H_CT" -d '{ "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"], "userName":"jane.doe@acme.example", "name":{"givenName":"Jane","familyName":"Doe"}, "emails":[{"value":"jane.doe@acme.example","primary":true}], "active":true }' | jq .
(b) List users
curl -fsS "$SCIM/Users" -H "$H_AUTH" | jq '.Resources[] | {id, userName, active}'
(c) Deactivate via PATCH (Okta lifecycle deactivate path)
USER_ID=$(curl -fsS "$SCIM/Users?filter=userName eq \"jane.doe@acme.example\"" \ -H "$H_AUTH" | jq -r '.Resources[0].id') curl -fsS -X PATCH "$SCIM/Users/$USER_ID" -H "$H_AUTH" -H "$H_CT" -d '{ "schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations":[{"op":"replace","path":"active","value":false}] }' | jq .
(d) Delete (Okta deprovision)
curl -fsS -X DELETE "$SCIM/Users/$USER_ID" -H "$H_AUTH" -o /dev/null -w "DELETE %{http_code}\n" ```
The same flow is captured end-to-end in poc/scripts/scim-demo.sh — your IAM engineer can run that one script instead of typing the curls.
Wiring an Okta SCIM app
- In Okta admin, **Applications → Create App Integration → SCIM 2.0
Test App (OAuth Bearer Token)**.
- SCIM connector base URL:
http://<your-poc-host>:8000/v1/scim
(use your tunnel hostname if Okta is reaching the POC over the internet — ngrok http 8000 is fine for a sandbox).
- Unique identifier field for users:
userName. - Supported provisioning actions: Push New Users, Push Profile
Updates, Push User Deactivation.
- Authentication mode: HTTP Header →
Authorization: Bearer <POC_SCIM_BEARER>. - Click Test Connector Configuration — should green-check.
- Assign a test user to the app. Within ~30 seconds the user appears
under SELECT * FROM public.users. Suspend the user in Okta and confirm active flips to false.
What this proves:
- Bearer auth + bcrypt verification (see
arxsec-api/app/api/v1/scim.py:122). - Org-scoped row write via service role under RLS.
- PATCH
active=falsedeactivates without deleting (audit-friendly). - DELETE removes both
usersand the GoTrueauth.usersrow.
---
3. SAML JIT (just-in-time provisioning)
Federated login via SAML 2.0; the user record is created on first successful assertion.
Setup against an Okta dev tenant
Okta Developer Edition is free at <https://developer.okta.com/signup>.
- Grab the SP metadata: <http://localhost:8000/v1/saml/metadata>.
This is the XML Okta needs for trust setup.
- In Okta admin, Applications → Create App Integration → SAML 2.0.
- SP metadata: paste the
entityIDandAssertionConsumerService
Location URLs from step 1.
- Single sign-on URL / ACS:
http://localhost:8000/v1/saml/acs - Audience URI / SP Entity ID: matches
entityIDfrom the metadata - Name ID format:
EmailAddress - Application username:
Email
- Save. In the Sign On tab, click "View SAML setup instructions"
and copy the IdP metadata URL.
- Wire the IdP metadata into the POC (one-time per org):
```bash source poc/.env.poc ADMIN_TOKEN=$(curl -fsS -X POST http://localhost:9999/token?grant_type=password \ -H 'Content-Type: application/json' \ -d "{\"email\":\"$POC_ADMIN_EMAIL\",\"password\":\"$POC_ADMIN_PASSWORD\"}" \ | jq -r .access_token)
curl -fsS -X POST http://localhost:8000/v1/sso/saml/configure \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"metadata_url":"https://<your-okta>/app/.../sso/saml/metadata"}' ```
- Assign a test Okta user to the app and complete an SSO login at
<http://localhost:8000/v1/saml/login?org_id=$ORG_ID>. On success you land in the dashboard, and a fresh public.users row appears with sso_provider = 'saml'.
What this proves:
- SP metadata generation (see
arxsec-api/app/api/v1/saml.py:396). - Signed AuthnRequest emission and IdP-side validation.
- ACS signature verification, replay protection, audience checks
(see arxsec-api/app/core/saml_security.py).
- JIT user creation flowing through the same code as a manual signup,
with sso_subject and sso_provider populated.
If you don't have time for an Okta dev tenant, the file arxsec-api/tests/test_saml_signature.py::test_valid_signed_response_verifies exercises the same code path with a synthetic IdP.
---
4. GoTrue admin invite (no IdP wiring)
For one-off invites — useful when a customer wants to add a colleague mid-trial without standing up an IdP.
```bash source poc/.env.poc
Invites use GoTrue's admin API. The service-role JWT is in .env.poc.
curl -fsS -X POST "http://localhost:9999/admin/users" \ -H "Authorization: Bearer $POC_SUPABASE_SERVICE_ROLE_KEY" \ -H "Content-Type: application/json" \ -d '{ "email":"colleague@acme.example", "email_confirm":false, "user_metadata":{"invited_by":"poc-admin"} }'
Then trigger a magic link so they can set a password:
curl -fsS -X POST "http://localhost:9999/recover" \ -H "Content-Type: application/json" \ -d '{"email":"colleague@acme.example"}' ```
The magic link arrives in Mailhog at <http://localhost:8025>. Clicking it lands the user back at the dashboard, where they set a password.
What this proves:
- The same path the dashboard's "Invite member" button will eventually
use (the UI for it is on the roadmap; the API contract is stable).
- That outbound mail (Resend in production, Mailhog locally) is wired
end-to-end.
---
Cleaning up POC users
```bash
Drop everyone except the seeded admin/deployer/auditor:
docker compose -f poc/docker-compose.poc.yml exec postgres \ psql -U postgres -d postgres -c \ "DELETE FROM auth.users WHERE email NOT IN ( 'poc-admin@example.com','poc-deployer@example.com','poc-auditor@example.com' );" ```
Or just ./poc/teardown.sh && ./poc/bootstrap.sh for a full reset.