Public documentation for governed AI labor
SDKs/Governance/Connectors
Arx / Docs / POC User Onboarding Playbook

Documentation

POC User Onboarding Playbook

Project-Agent-trust-merge / poc/USER_ONBOARDING.md

Project-Agent-trust-merge repo-root 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.

  1. Open <http://localhost:3000> and click Sign up.
  2. Enter any email + a 12+ char password. Submit.
  3. The email lands in Mailhog at <http://localhost:8025> (no real SMTP

configured for the POC). Click the verification link.

  1. You're redirected back to the dashboard, signed in.

What this proves:

  • GoTrue → Postgres auth.users write 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

  1. In Okta admin, **Applications → Create App Integration → SCIM 2.0

Test App (OAuth Bearer Token)**.

  1. 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).

  1. Unique identifier field for users: userName.
  2. Supported provisioning actions: Push New Users, Push Profile

Updates, Push User Deactivation.

  1. Authentication mode: HTTP Header → Authorization: Bearer <POC_SCIM_BEARER>.
  2. Click Test Connector Configuration — should green-check.
  3. 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=false deactivates without deleting (audit-friendly).
  • DELETE removes both users and the GoTrue auth.users row.

---

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>.

  1. Grab the SP metadata: <http://localhost:8000/v1/saml/metadata>.

This is the XML Okta needs for trust setup.

  1. In Okta admin, Applications → Create App Integration → SAML 2.0.
  2. SP metadata: paste the entityID and AssertionConsumerService

Location URLs from step 1.

  • Single sign-on URL / ACS: http://localhost:8000/v1/saml/acs
  • Audience URI / SP Entity ID: matches entityID from the metadata
  • Name ID format: EmailAddress
  • Application username: Email
  1. Save. In the Sign On tab, click "View SAML setup instructions"

and copy the IdP metadata URL.

  1. 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"}' ```

  1. 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.