OIDC smoke test
End-to-end verification of the OIDC sign-in flow (web + iOS) using
navikt/mock-oauth2-server
as a drop-in identity provider. Stateless, no DB, “log in” is a
click-through “pick a user” page. Useful for confirming the wire
contract of /v1/auth/oidc/{begin, _jump, _native-done, finish}
without needing a real Authentik / Pocket ID / Keycloak running.
For a more realistic integration test (real users, passkeys, an admin UI), use Pocket ID instead - same admin form values, just with a real provider.
Boot the stack
Section titled “Boot the stack”# Boot Postgres + your usual storage profile + the mock IdP.docker compose --profile garage --profile mock-oidc up -d
# Verify the discovery doc resolves at the issuer URL we'll wire# into the admin form below.curl -s http://localhost:8080/default/.well-known/openid-configuration | jq .issuer# -> "http://localhost:8080/default"
pnpm dev # runs the app on http://localhost:3000The mock server publishes a default issuer default (you can pick
any path - default is the convention). The discovery doc lists
its own authorization / token / userinfo / JWKS endpoints, all
pointing at http://localhost:8080/default/....
Configure GiftWrapt
Section titled “Configure GiftWrapt”Sign in to GiftWrapt as an admin, navigate to /admin/auth, and fill in the OIDC card:
| Field | Value |
|---|---|
| Enable OIDC | on |
| Issuer URL | http://localhost:8080/default |
| Client ID | giftwrapt-mobile-test (any non-empty string) |
| Client Secret | dev-secret (any non-empty string) |
| Scopes | openid email profile (the default) |
| Button Text | Sign in with mock IdP |
| Match existing users by | email |
| Auto Register | on |
| Allowed Mobile Redirect URIs | wishlists://oauth |
mock-oauth2-server accepts any client_id / client_secret, so the
values above don’t need to be registered anywhere.
Save, then restart the dev server (pnpm dev’s
auth.ts reads OIDC config at module load via top-level await; a
hot reload won’t pick up the change).
Smoke test the web flow
Section titled “Smoke test the web flow”-
Open
http://localhost:3000/sign-inin any browser. -
Click Sign in with mock IdP.
-
The redirect lands on the mock server’s login form. Paste this JSON into the “Claims” textarea before submitting:
{ "email": "test@example.com", "name": "Test User", "email_verified": true }The default form returns an id_token with only
sub, which better-auth rejects with/api/auth/error?error=email_is_missing. Any email works; withMatch existing users by: emailset, it’ll link to that existing user (if present) or auto-register one (ifAuto Registeris on). -
Submit. The redirect chain hands back to
/api/auth/oauth2/callback/oidc, which exchanges the code, mints a session, redirects to the post-auth target.
If any step 4xx’s, the better-auth log line in the dev server’s
stderr names the exact problem (unsupported response_type,
state mismatch, etc).
Smoke test the iOS flow
Section titled “Smoke test the iOS flow”Same boot stack as above, plus:
- Open
wish-lists-ios/WishLists.xcodeprojand run the WishLists scheme on the iOS Simulator. - On the host-entry screen, type
http://localhost:3000. - Tap Continue. The capabilities probe should populate the methods step with email + password fields and the Sign in with mock IdP button.
- Tap the OIDC button.
ASWebAuthenticationSessionopens ontohttp://localhost:3000/api/mobile/v1/auth/oidc/_jump?token=..., which 302s to the mock IdP login page. - Pick a user, paste the same claims JSON as the web flow above
into the Claims textarea, submit. The chain runs through
better-auth’s callback, GiftWrapt’s
_native-done, and ends with a 302 towishlists://oauth?token=.... The auth session captures the redirect, hands control back to the app. - iOS posts
oidc/finish, gets the{ apiKey, user, device }envelope, andRootViewswaps to the authenticated tab view.
Common gotchas:
/api/auth/error?error=email_is_missingafter the mock IdP login: the default click-through form returns an id_token with onlysub. Paste the claims JSON above into the “Claims” textarea on the login form before submitting. (better-auth needsemailto find / create the local user.)- “Sign-in expired” on the iOS error banner: the begin TTL is 10 minutes, but if the dev server restarted between begin and finish, the in-memory state is gone. Sign in again.
- “redirect-not-allowed”: check the admin form has
wishlists://oauthliterally in the Allowed Mobile Redirect URIs textarea (one per line, no trailing whitespace). http://cookies blocked: Safari refuses to set theSecure-flagged session cookie on plain HTTP unless the site is onlocalhost(which it is). Other LAN hostnames needINSECURE_COOKIES=truein.env.local.
Tear down
Section titled “Tear down”docker compose --profile mock-oidc downOr to nuke everything (Postgres + storage + IdP):
docker compose down -vSwitching to Pocket ID later
Section titled “Switching to Pocket ID later”When ready to test against a real self-hosted IdP:
- Stop
mock-oidc(docker compose --profile mock-oidc down). - Boot Pocket ID per its docs.
- In Pocket ID’s admin: register a new OIDC client. Set the
redirect URI to
<server>/api/auth/oauth2/callback/oidc. Note the issued client_id + client_secret. - Update GiftWrapt’s
/admin/authform: replace the issuer URL, client_id, client_secret. Keepwishlists://oauthon the mobile-redirect-URIs list. - Restart the dev server. Re-run the iOS smoke test.
The wire contract is identical - any OIDC-compliant provider works.