Skip to content

Commit 4109153

Browse files
os-zhuangclaude
andauthored
fix(plugin-auth,platform-objects): close @better-auth/oauth-provider 1.7 schema drift — restore platform SSO (#3080)
* fix(plugin-auth,platform-objects): close @better-auth/oauth-provider 1.7 schema drift — restore platform SSO The better-auth 1.7.0-rc.1 bump (4ab16f8 "pnpm upgrade") changed the oauth-provider model schemas, but the sys_oauth_* platform objects and the camelCase→snake_case fieldName mappings in auth-schema-config.ts were never updated. Token exchange then 500s with `table sys_oauth_access_token has no column named authorizationCodeId`, breaking platform SSO ("Continue with ObjectStack") end-to-end for every environment login — on SQLite AND Postgres control planes alike. - sys_oauth_access_token / sys_oauth_refresh_token: add authorization_code_id (indexed), resources, requested_user_info_claims, confirmation; access adds revoked; refresh adds rotated_at, rotation_replay_response, rotation_replay_expires_at. - sys_oauth_consent: add resources, requested_user_info_claims. - sys_oauth_application: add jwks, jwks_uri, backchannel_logout_uri, backchannel_logout_session_required, dpop_bound_access_tokens (1.7 DCR writes dpopBoundAccessTokens with a default — same crash class). - New objects for the three models 1.7 introduced: sys_oauth_resource, sys_oauth_client_resource (RFC 8707 resource indicators, used by the MCP OAuth track), sys_oauth_client_assertion (RFC 7523 jti replay prevention); registered in the auth manifest, SystemObjectName, i18n extract config, and buildOauthProviderPluginSchema(). - Mappings extended for every camelCase field; regenerated the platform-objects translation bundles (additive only). - NEW oauth-provider-schema-parity.test.ts: iterates the INSTALLED plugin's declared schema and fails when any writable column is missing from the platform objects — the next better-auth bump that adds fields breaks at test time, not in production. (This test is what surfaced the three brand-new 1.7 models above.) Companion changes: cloud registers the ≥1.7 social-flow callback format (/api/v1/auth/callback/<provider>) alongside the legacy one, and @object-ui/auth signs OIDC providers in via POST /sign-in/social. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(dogfood): OIDC authorization-code flow e2e — real kernel, real token exchange Drives authorize → code → token → userinfo against a bootStack kernel with OS_OIDC_PROVIDER_ENABLED, seeding the client row exactly the way cloud's seedPlatformSsoClient does (hashed secret, skip_consent, client_secret_basic, ≥1.7 /api/v1/auth/callback/<provider> redirect_uri). The token-exchange step is the hop that 500'd in production on the 1.7 schema drift; verified the test fails (2/3) when the authorization_code_id column is removed and passes (3/3) with the fix. Complements the static parity gate in plugin-auth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 5ffff3b commit 4109153

20 files changed

Lines changed: 1570 additions & 26 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/platform-objects': minor
3+
'@objectstack/plugin-auth': patch
4+
'@objectstack/spec': patch
5+
---
6+
7+
Close the `@better-auth/oauth-provider` 1.7 schema drift that broke platform
8+
SSO (token exchange 500: `table sys_oauth_access_token has no column named
9+
authorizationCodeId`).
10+
11+
- `sys_oauth_access_token` / `sys_oauth_refresh_token`: add
12+
`authorization_code_id`, `resources`, `requested_user_info_claims`,
13+
`confirmation` (+ access-token `revoked`; + refresh-token `rotated_at`,
14+
`rotation_replay_response`, `rotation_replay_expires_at`).
15+
- `sys_oauth_consent`: add `resources`, `requested_user_info_claims`.
16+
- `sys_oauth_application`: add `jwks`, `jwks_uri`, `backchannel_logout_uri`,
17+
`backchannel_logout_session_required`, `dpop_bound_access_tokens`.
18+
- New platform objects for the three models 1.7 introduced:
19+
`sys_oauth_resource`, `sys_oauth_client_resource`,
20+
`sys_oauth_client_assertion` (RFC 8707 resource indicators + RFC 7523
21+
client-assertion replay prevention), registered in the auth manifest and
22+
mapped in `buildOauthProviderPluginSchema()`.
23+
- All camelCase→snake_case `fieldName` mappings extended accordingly, and a
24+
new parity test (`oauth-provider-schema-parity.test.ts`) fails the build
25+
whenever a future better-auth bump introduces model fields our objects or
26+
mappings don't cover.

packages/platform-objects/scripts/i18n-extract.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ import {
4747
SysOauthAccessToken,
4848
SysOauthRefreshToken,
4949
SysOauthConsent,
50+
SysOauthResource,
51+
SysOauthClientResource,
52+
SysOauthClientAssertion,
5053
SysJwks,
5154
} from '../src/identity/index.js';
5255

@@ -144,6 +147,9 @@ export default defineStack({
144147
SysOauthAccessToken,
145148
SysOauthRefreshToken,
146149
SysOauthConsent,
150+
SysOauthResource,
151+
SysOauthClientResource,
152+
SysOauthClientAssertion,
147153
SysJwks,
148154

149155
// Security: RBAC moved to @objectstack/plugin-security, sharing to

packages/platform-objects/src/apps/translations/bundle-ownership.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ const OWNED_OBJECTS = new Set([
2222
'sys_member', 'sys_invitation', 'sys_team', 'sys_team_member', 'sys_business_unit',
2323
'sys_business_unit_member', 'sys_api_key', 'sys_two_factor', 'sys_device_code',
2424
'sys_user_preference', 'sys_oauth_application', 'sys_oauth_access_token',
25-
'sys_oauth_refresh_token', 'sys_oauth_consent', 'sys_jwks',
25+
'sys_oauth_refresh_token', 'sys_oauth_consent', 'sys_oauth_resource',
26+
'sys_oauth_client_resource', 'sys_oauth_client_assertion', 'sys_jwks',
2627
// audit / messaging-adjacent (still owned here)
2728
'sys_notification', 'sys_attachment', 'sys_email', 'sys_email_template',
2829
'sys_saved_report', 'sys_report_schedule', 'sys_job', 'sys_job_run', 'sys_job_queue',

packages/platform-objects/src/apps/translations/en.objects.generated.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,18 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
11871187
label: "Subject Type",
11881188
help: "OIDC subject type (e.g. public, pairwise)"
11891189
},
1190+
jwks: {
1191+
label: "JWKS",
1192+
help: "Client JSON Web Key Set (for private_key_jwt / signed-request verification)"
1193+
},
1194+
jwks_uri: {
1195+
label: "JWKS URI",
1196+
help: "URL of the client JSON Web Key Set"
1197+
},
1198+
dpop_bound_access_tokens: {
1199+
label: "DPoP-bound Access Tokens",
1200+
help: "Require access tokens issued to this client to be DPoP-bound (RFC 9449)"
1201+
},
11901202
disabled: {
11911203
label: "Disabled"
11921204
},
@@ -1198,6 +1210,14 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
11981210
label: "Enable End Session",
11991211
help: "Allow the client to call the OIDC end-session endpoint"
12001212
},
1213+
backchannel_logout_uri: {
1214+
label: "Back-channel Logout URI",
1215+
help: "OIDC back-channel logout endpoint of the client"
1216+
},
1217+
backchannel_logout_session_required: {
1218+
label: "Back-channel Logout Session Required",
1219+
help: "Whether the back-channel logout token must include a sid claim"
1220+
},
12011221
software_id: {
12021222
label: "Software ID"
12031223
},
@@ -1312,6 +1332,18 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
13121332
label: "Reference ID",
13131333
help: "Caller-supplied correlation identifier"
13141334
},
1335+
authorization_code_id: {
1336+
label: "Authorization Code ID",
1337+
help: "ID of the authorization-code grant this token originates from"
1338+
},
1339+
resources: {
1340+
label: "Resources",
1341+
help: "JSON-serialized list of RFC 8707 resource indicators bound to this token"
1342+
},
1343+
requested_user_info_claims: {
1344+
label: "Requested UserInfo Claims",
1345+
help: "JSON-serialized list of OIDC claims requested for the userinfo endpoint"
1346+
},
13151347
scopes: {
13161348
label: "Scopes",
13171349
help: "JSON-serialized list of scopes granted to this token"
@@ -1321,6 +1353,14 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
13211353
},
13221354
created_at: {
13231355
label: "Created At"
1356+
},
1357+
revoked: {
1358+
label: "Revoked At",
1359+
help: "Timestamp at which this access token was revoked"
1360+
},
1361+
confirmation: {
1362+
label: "Confirmation",
1363+
help: "JSON RFC 7800 cnf claim (e.g. DPoP key thumbprint) binding this token to a key"
13241364
}
13251365
}
13261366
},
@@ -1352,6 +1392,18 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
13521392
label: "Reference ID",
13531393
help: "Caller-supplied correlation identifier"
13541394
},
1395+
authorization_code_id: {
1396+
label: "Authorization Code ID",
1397+
help: "ID of the authorization-code grant this token chain originates from"
1398+
},
1399+
resources: {
1400+
label: "Resources",
1401+
help: "JSON-serialized list of RFC 8707 resource indicators bound to this token"
1402+
},
1403+
requested_user_info_claims: {
1404+
label: "Requested UserInfo Claims",
1405+
help: "JSON-serialized list of OIDC claims requested for the userinfo endpoint"
1406+
},
13551407
scopes: {
13561408
label: "Scopes",
13571409
help: "JSON-serialized list of scopes granted to this token"
@@ -1366,9 +1418,25 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
13661418
label: "Revoked At",
13671419
help: "Timestamp at which this refresh token was revoked"
13681420
},
1421+
rotated_at: {
1422+
label: "Rotated At",
1423+
help: "Timestamp at which this token was rotated (superseded by a new row)"
1424+
},
1425+
rotation_replay_response: {
1426+
label: "Rotation Replay Response",
1427+
help: "Cached token response replayed when the old token is re-presented within the reuse interval"
1428+
},
1429+
rotation_replay_expires_at: {
1430+
label: "Rotation Replay Expires At",
1431+
help: "End of the post-rotation reuse interval during which the replay response is served"
1432+
},
13691433
auth_time: {
13701434
label: "Auth Time",
13711435
help: "When the user originally authenticated for this token chain"
1436+
},
1437+
confirmation: {
1438+
label: "Confirmation",
1439+
help: "JSON RFC 7800 cnf claim (e.g. DPoP key thumbprint) binding this token to a key"
13721440
}
13731441
}
13741442
},
@@ -1392,6 +1460,14 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
13921460
label: "Reference ID",
13931461
help: "Caller-supplied correlation identifier"
13941462
},
1463+
resources: {
1464+
label: "Resources",
1465+
help: "JSON-serialized list of RFC 8707 resource indicators the consent covers"
1466+
},
1467+
requested_user_info_claims: {
1468+
label: "Requested UserInfo Claims",
1469+
help: "JSON-serialized list of OIDC claims the user consented to expose"
1470+
},
13951471
scopes: {
13961472
label: "Scopes",
13971473
help: "JSON-serialized list of scopes the user consented to"
@@ -1404,6 +1480,107 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
14041480
}
14051481
}
14061482
},
1483+
sys_oauth_resource: {
1484+
label: "OAuth Resource",
1485+
pluralLabel: "OAuth Resources",
1486+
description: "Registered OAuth protected resources (RFC 8707 resource indicators)",
1487+
fields: {
1488+
id: {
1489+
label: "ID"
1490+
},
1491+
identifier: {
1492+
label: "Identifier",
1493+
help: "Resource indicator URI presented in the RFC 8707 resource parameter"
1494+
},
1495+
name: {
1496+
label: "Name"
1497+
},
1498+
access_token_ttl: {
1499+
label: "Access Token TTL",
1500+
help: "Access-token lifetime in seconds for this resource (overrides the server default)"
1501+
},
1502+
refresh_token_ttl: {
1503+
label: "Refresh Token TTL",
1504+
help: "Refresh-token lifetime in seconds for this resource (overrides the server default)"
1505+
},
1506+
signing_algorithm: {
1507+
label: "Signing Algorithm",
1508+
help: "JWS algorithm used to sign access tokens for this resource"
1509+
},
1510+
signing_key_id: {
1511+
label: "Signing Key ID",
1512+
help: "Key id (kid) used to sign access tokens for this resource"
1513+
},
1514+
allowed_scopes: {
1515+
label: "Allowed Scopes",
1516+
help: "JSON-serialized list of scopes clients may request for this resource"
1517+
},
1518+
custom_claims: {
1519+
label: "Custom Claims",
1520+
help: "JSON object of extra claims stamped on access tokens for this resource"
1521+
},
1522+
dpop_bound_access_tokens_required: {
1523+
label: "DPoP Required",
1524+
help: "Require access tokens for this resource to be DPoP-bound (RFC 9449)"
1525+
},
1526+
disabled: {
1527+
label: "Disabled"
1528+
},
1529+
policy_version: {
1530+
label: "Policy Version",
1531+
help: "Monotonic version of the resource token policy"
1532+
},
1533+
metadata: {
1534+
label: "Metadata",
1535+
help: "JSON object of additional resource metadata"
1536+
},
1537+
created_at: {
1538+
label: "Created At"
1539+
},
1540+
updated_at: {
1541+
label: "Updated At"
1542+
}
1543+
}
1544+
},
1545+
sys_oauth_client_resource: {
1546+
label: "OAuth Client Resource",
1547+
pluralLabel: "OAuth Client Resources",
1548+
description: "Grants allowing an OAuth client to request tokens for a protected resource",
1549+
fields: {
1550+
id: {
1551+
label: "ID"
1552+
},
1553+
client_id: {
1554+
label: "Client ID",
1555+
help: "Foreign key to sys_oauth_application.client_id"
1556+
},
1557+
resource_id: {
1558+
label: "Resource ID",
1559+
help: "Foreign key to sys_oauth_resource.identifier"
1560+
},
1561+
metadata: {
1562+
label: "Metadata",
1563+
help: "JSON object of additional grant metadata"
1564+
},
1565+
created_at: {
1566+
label: "Created At"
1567+
}
1568+
}
1569+
},
1570+
sys_oauth_client_assertion: {
1571+
label: "OAuth Client Assertion",
1572+
pluralLabel: "OAuth Client Assertions",
1573+
description: "Consumed OAuth client-assertion JTIs (RFC 7523 replay prevention)",
1574+
fields: {
1575+
id: {
1576+
label: "ID"
1577+
},
1578+
expires_at: {
1579+
label: "Expires At",
1580+
help: "Assertion expiry — rows past this instant are safe to prune"
1581+
}
1582+
}
1583+
},
14071584
sys_jwks: {
14081585
label: "JWKS Key",
14091586
pluralLabel: "JWKS Keys",

0 commit comments

Comments
 (0)