Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
141b278
oauth related express context chagnes
Zetazzz Jun 12, 2026
92eab0b
fixed ipm and types
Zetazzz Jun 14, 2026
4219d1f
refactor(express-context): clarify OAuth module loaders and follow-ups
Zetazzz Jun 15, 2026
3f9c00d
Add OAuth email verification handling and tests
Zetazzz Jun 16, 2026
dfd3da3
Migrate GraphQL OAuth routes and admin API integration
Zetazzz Jun 16, 2026
1e46998
remove outdated oauth express middleware
Zetazzz Jun 16, 2026
48eb234
abstract functions of pg interval and signed state
Zetazzz Jun 16, 2026
1729797
remove cross origin handoff token logic
Zetazzz Jun 16, 2026
5f5378c
apply loaders to app setting auth middleware
Zetazzz Jun 16, 2026
85f458d
Revert "apply loaders to app setting auth middleware"
Zetazzz Jun 16, 2026
db41a42
update followup
Zetazzz Jun 16, 2026
3c43994
Reapply "apply loaders to app setting auth middleware"
Zetazzz Jun 16, 2026
e610c85
update oauth followup after app settings loader reapply
Zetazzz Jun 16, 2026
d34c90a
handle app setting update in middleware
Zetazzz Jun 16, 2026
8a57dc8
regulate JWT claims usage
Zetazzz Jun 16, 2026
d472924
fix env snapshots
Zetazzz Jun 20, 2026
4044128
feat(oauth): add runtime provider resolver
Zetazzz Jul 2, 2026
ab93b95
fix(oauth): remove admin REST routes
Zetazzz Jul 2, 2026
71be2b7
feat(oauth): use runtime provider config
Zetazzz Jul 2, 2026
61f195f
feat(oauth): support pkce and token auth methods
Zetazzz Jul 2, 2026
d6ab87b
fix(oauth): harden pkce identity runtime
Zetazzz Jul 2, 2026
6e76ead
fix(oauth): validate callback state provider
Zetazzz Jul 6, 2026
580679e
refactor(oauth): clarify state secret config
Zetazzz Jul 6, 2026
8a97c02
fix(express-context): resolve platform database by dbname
Zetazzz Jul 6, 2026
fb58f39
docs(oauth): add local e2e test steps
Zetazzz Jul 7, 2026
5b9ebd2
fix(oauth): support legacy secrets metadata
Zetazzz Jul 27, 2026
8df7d66
fix(oauth): resolve auth settings by table id
Zetazzz Jul 27, 2026
0f0f298
fix(context): support legacy compute metadata
Zetazzz Jul 27, 2026
1a0a235
fix(graphile): support legacy function metadata
Zetazzz Jul 27, 2026
dd91f4f
fix(ci): sync OAuth package lockfile
Zetazzz Jul 27, 2026
6edb9a5
fix(oauth): align runtime with scoped routing
Zetazzz Jul 28, 2026
cc660c3
fix(oauth): resolve auth settings from metadata tables
Zetazzz Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions __fixtures__/seed/scoped/test-data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,31 @@

SET session_replication_role TO replica;

-- Current constructive-db module metadata resolves physical tables by stable
-- table id. The lightweight CNC fixture installs the metadata tables without
-- the full metaschema runtime package, so provide the same lookup contract for
-- scoped server integration tests.
CREATE SCHEMA IF NOT EXISTS metaschema;

DO $block$
BEGIN
IF to_regprocedure('metaschema.schema_and_table(uuid)') IS NULL THEN
EXECUTE $function$
CREATE FUNCTION metaschema.schema_and_table(target_table_id uuid)
RETURNS TABLE(schema_name text, table_name text)
LANGUAGE sql
STABLE
AS $$
SELECT s.schema_name, t.name
FROM metaschema_public.table t
JOIN metaschema_public.schema s ON s.id = t.schema_id
WHERE t.id = target_table_id
$$
$function$;
END IF;
END
$block$;

-- =====================================================
-- METASCHEMA DATA
-- =====================================================
Expand Down
23 changes: 23 additions & 0 deletions docs/plan/oauth/oauth-implementation-followup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# OAuth Implementation Follow-up

## Identity Auth Function Metadata

- Consider making `sign_in_identity_function` and `sign_up_identity_function` metadata-driven. These functions are generated by the user auth module, but the current express-context config uses hardcoded function names. A future database/schema update could expose these function names through module metadata, similar to other auth functions.

## Loader Cache Invalidation and TTL Semantics

- Revisit `createModuleLoader` cache expiration and invalidation semantics. The current implementation uses `updateAgeOnGet: true`, so each cache hit refreshes the TTL and frequently accessed config may not expire while traffic continues. The reference branch changed the default to `false`, but the desired behavior should be decided together with explicit invalidation for writes performed by our own systems.

- Known changes made through this process, such as admin APIs updating auth settings or identity providers, should invalidate the relevant loader cache after the database write succeeds. The current auth settings update path invalidates `authSettingsLoader`, but identity provider admin writes do not yet invalidate the cached OAuth provider config. Recommended shape: loaders own read, transform, cache, and invalidate; services or repositories own validate, authorize, write, audit, and post-write invalidate. For example, `identityProvidersService.updateProvider(ctx, input)` writes the provider config and then calls a targeted invalidation such as `registry.invalidate(ctx.databaseId, "identityProviders")` or the equivalent loader-specific invalidation API.

- Unknown external changes, such as manual SQL, migrations, or another service updating module configuration, cannot be invalidated precisely by this process. Baseline approach: keep `updateAgeOnGet: false` so TTL remains a bounded staleness window, then reload from the database on the next read after TTL expiry. If stronger freshness is needed later, add an optional lightweight fingerprint probe near loader resolution, for example `getFingerprint(ctx)` using `updated_at`, version, or checksum plus `revalidateAfterMs` as the minimum interval between probes. This lets loaders detect external changes faster than full TTL expiry without fully reloading config on every request.

- Recommendation: set `updateAgeOnGet` to `false`, add explicit post-write invalidation for known admin writes, and rely on TTL as the fallback for unknown external changes. Add fingerprint probing only if TTL-based staleness becomes too slow in practice.

## API Service Cache Loader Snapshots

- Revisit `graphql/server/src/middleware/api.ts` storing resolved loader values inside the cached `ApiStructure`. The API resolver currently resolves mutable module settings such as `authSettings`, `corsOrigins`, `databaseSettings`, `pubkeyChallengeSettings`, and `webauthnSettings`, then stores the whole API structure in `svcCache`, whose TTL is effectively long-lived. This can bypass each loader's own TTL and invalidation path; for example, `updateAuthSettings()` invalidates `authSettingsLoader`, but middleware that reads `req.api.authSettings` could still see the old value from `svcCache`. Consider narrowing `svcCache` to stable API routing fields only, resolving mutable module settings through `ctx.useModule(...)` at use sites, or adding coordinated `svcCache` invalidation whenever loader-backed settings are updated.

## Env Config Consolidation

- Consider moving existing CAPTCHA and upload environment variables into the shared `@pgpmjs/env` config surface in a separate cleanup PR. The reference OAuth branch added `RECAPTCHA_SECRET_KEY` and `MAX_UPLOAD_FILE_SIZE` to `PgpmOptions`, but this OAuth migration keeps those existing middleware paths on direct `process.env` reads to avoid widening the PR scope beyond OAuth/server admin APIs.
252 changes: 252 additions & 0 deletions docs/plan/oauth/oauth-local-e2e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# OAuth Local E2E Test Steps

This is the short local runbook for validating the OAuth runtime on `feat/oauth-reorg`.

Do not commit real OAuth client secrets, `OAUTH_STATE_SECRET`, non-local database passwords, or reusable tokens. Export provider credentials from a local secret source.

## 1. Use the Target Branch

```bash
cd /Users/zeta/Projects/interweb/src/agents/constructive
git fetch origin
git checkout feat/oauth-reorg
git pull --ff-only
git status --short --branch
```

## 2. Prepare Local Secrets

Prepare two groups of environment variables:

- GraphQL server runtime: `OAUTH_STATE_SECRET`. The server requires this only when a configured OAuth flow starts, then uses it to sign and verify `oauth_state` / `oauth_pkce` cookies.
- Provider initialization SQL: `GITHUB_OAUTH_*` and `GOOGLE_OAUTH_*`. These are passed into the provider setup `psql` scripts below, then written or rotated into the database. The GraphQL server does not read these provider credential env vars directly.

```bash
# GraphQL server runtime.
export OAUTH_STATE_SECRET="$(openssl rand -hex 32)"

# Provider initialization scripts only.
export GITHUB_OAUTH_CLIENT_ID="<github-client-id>"
export GITHUB_OAUTH_CLIENT_SECRET="<github-client-secret>"

export GOOGLE_OAUTH_CLIENT_ID="<google-client-id>"
export GOOGLE_OAUTH_CLIENT_SECRET="<google-client-secret>"
```

Provider callback URLs:

```text
GitHub: http://auth.localhost:3000/auth/github/callback
Google: http://localhost:3000/auth/google/callback
```

Google is tested through bare `localhost` because Google may reject `auth.localhost` as a local redirect URI.

## 3. Rebuild the Local Database

```bash
cd /Users/zeta/Projects/interweb/src/agents/constructive-db

pgpm docker start --image docker.io/constructiveio/postgres-plus:18 --recreate
eval "$(pgpm env)"

pgpm admin-users bootstrap --yes
pgpm admin-users add --test --yes

dropdb --if-exists constructive
createdb constructive

pgpm deploy --yes --database constructive --package constructive-local
```

## 4. Apply Local OAuth Test Setup

`PGPASSWORD` in the setup commands is only the local PostgreSQL connection password for `psql`. The OAuth routes use the same scoped routing plane as every other production route; do not seed `services_public` compatibility tables or rely on a default database.

```bash
PGPASSWORD=password psql -h localhost -U postgres -d constructive -v ON_ERROR_STOP=1 <<'SQL'
-- Local HTTP cookies.
UPDATE constructive_auth_private.app_settings_auth
SET allow_identity_sign_in = true,
allow_identity_sign_up = true,
oauth_require_verified_email = false,
cookie_secure = false;
SQL
```

Before starting the server, ensure the callback hostname is a real scoped
routing binding for the intended API/database. Verify it through the current
routing contract:

```sql
SELECT *
FROM routing_public.resolve_route('auth.localhost', '/', NULL);
```

If Google must use bare `localhost`, create that hostname through the current
routing/domain provisioning workflow and verify it with
`routing_public.resolve_route('localhost', '/', NULL)`. Do not insert a legacy
domain row directly.

## 5. Build and Start the Server

```bash
cd /Users/zeta/Projects/interweb/src/agents/constructive
pnpm install
pnpm build
```

```bash
# GraphQL server runtime environment.
PGHOST=localhost \
PGPORT=5432 \
PGUSER=postgres \
PGPASSWORD=password \
PGDATABASE=constructive \
NODE_USE_ENV_PROXY=1 \
NO_PROXY="localhost,127.0.0.1,::1,.localhost" \
OAUTH_STATE_SECRET="$OAUTH_STATE_SECRET" \
pnpm --filter @constructive-io/graphql-server dev
```

Expected:

```text
listening at http://localhost:3000
```

`PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, and `PGDATABASE` are the GraphQL server's database connection settings for this local run. `OAUTH_STATE_SECRET` is the OAuth state-cookie signing secret. `NODE_USE_ENV_PROXY` and `NO_PROXY` control Node's outbound HTTP behavior.

Keep `NODE_USE_ENV_PROXY=1` when the local network needs `HTTP_PROXY` / `HTTPS_PROXY`. Without it, Node's native `fetch` can fail during OAuth token exchange with `CALLBACK_FAILED` and `TypeError: fetch failed`.

## 6. Configure Providers

These commands use the provider initialization env vars from step 2. `psql` substitutes them into the setup script, and `rotate_identity_provider_platform_secret(...)` stores the provider client secret in the current database's `internal_secrets_module` table for the matching scope. The GraphQL server later resolves both provider and secret metadata by the routed `database_id`; it does not infer a platform database from `dbname` or read `GITHUB_OAUTH_*` / `GOOGLE_OAUTH_*`.

GitHub:

```bash
PGPASSWORD=password psql -h localhost -U postgres -d constructive \
-v ON_ERROR_STOP=1 \
-v github_client_id="$GITHUB_OAUTH_CLIENT_ID" \
-v github_client_secret="$GITHUB_OAUTH_CLIENT_SECRET" <<'SQL'
UPDATE constructive_auth_private.identity_providers
SET enabled = true,
client_id = :'github_client_id',
authorization_url = 'https://github.com/login/oauth/authorize',
token_url = 'https://github.com/login/oauth/access_token',
userinfo_url = 'https://api.github.com/user',
scopes = ARRAY['read:user', 'user:email'],
pkce_enabled = true
WHERE slug = 'github';

SELECT constructive_auth_private.rotate_identity_provider_platform_secret(
(SELECT id FROM constructive_auth_private.identity_providers WHERE slug = 'github'),
:'github_client_secret'
);
SQL
```

Google:

```bash
PGPASSWORD=password psql -h localhost -U postgres -d constructive \
-v ON_ERROR_STOP=1 \
-v google_client_id="$GOOGLE_OAUTH_CLIENT_ID" \
-v google_client_secret="$GOOGLE_OAUTH_CLIENT_SECRET" <<'SQL'
UPDATE constructive_auth_private.identity_providers
SET enabled = true,
client_id = :'google_client_id',
authorization_url = 'https://accounts.google.com/o/oauth2/v2/auth',
token_url = 'https://oauth2.googleapis.com/token',
userinfo_url = 'https://openidconnect.googleapis.com/v1/userinfo',
scopes = ARRAY['openid', 'email', 'profile'],
pkce_enabled = true
WHERE slug = 'google';

SELECT constructive_auth_private.rotate_identity_provider_platform_secret(
(SELECT id FROM constructive_auth_private.identity_providers WHERE slug = 'google'),
:'google_client_secret'
);
SQL
```

Verify without printing secrets:

```bash
PGPASSWORD=password psql -h localhost -U postgres -d constructive -v ON_ERROR_STOP=1 <<'SQL'
SELECT slug,
enabled,
client_id IS NOT NULL AND client_id <> '' AS has_client_id,
client_secret_id IS NOT NULL AS has_client_secret,
scopes,
pkce_enabled
FROM constructive_auth_private.identity_providers
WHERE slug IN ('github', 'google')
ORDER BY slug;
SQL
```

## 7. Smoke Test

```bash
curl --noproxy '*' http://auth.localhost:3000/auth/providers
curl --noproxy '*' http://localhost:3000/auth/providers
```

Expected:

```json
{ "providers": ["github", "google"] }
```

Authorization redirects:

```bash
curl --noproxy '*' -i 'http://auth.localhost:3000/auth/github?redirect_uri=/auth/providers'
curl --noproxy '*' -i 'http://localhost:3000/auth/google?redirect_uri=/auth/providers'
```

Expected:

- GitHub redirects to `https://github.com/login/oauth/authorize`.
- Google redirects to `https://accounts.google.com/o/oauth2/v2/auth`.
- Both responses set `oauth_state`.
- Both responses set `oauth_pkce` when `pkce_enabled = true`.

## 8. Browser Test

GitHub:

```text
http://auth.localhost:3000/auth/github?redirect_uri=/auth/providers
```

Google:

```text
http://localhost:3000/auth/google?redirect_uri=/auth/providers
```

Expected result:

- Provider authorization completes.
- Browser redirects back to `/auth/providers`.
- The response shows the configured providers.
- Server logs include `Got profile`, `OAuth success`, and a successful cookie authentication.

Optional DB check:

```bash
PGPASSWORD=password psql -h localhost -U postgres -d constructive -x -c "
SELECT service,
identifier IS NOT NULL AS has_identifier,
details->>'email' AS email,
is_verified,
created_at
FROM constructive_user_identifiers_private.connected_accounts
WHERE service IN ('github', 'google')
ORDER BY created_at DESC
LIMIT 10;
"
```
1 change: 1 addition & 0 deletions graphql/env/__tests__/__snapshots__/merge.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ exports[`getEnvOptions merges pgpm defaults, graphql defaults, config, env, and
"useTx": false,
},
},
"oauth": {},
"pg": {
"database": "config-db",
"host": "override-host",
Expand Down
Loading
Loading