Skip to content

Commit 6b07dbd

Browse files
committed
feat(auth): add support for OAuth refresh tokens
- Added SysOauthRefreshToken to the manifest and system object definitions. - Updated auth configuration schema to include new refresh token data-plane table. - Enhanced system object names to include OAUTH_REFRESH_TOKEN. - Updated pnpm-lock.yaml to include @better-auth/oauth-provider version 1.6.9. - Created sys_oauth_refresh_token object schema with necessary fields and indexes.
1 parent 6484ec1 commit 6b07dbd

15 files changed

Lines changed: 543 additions & 155 deletions

content/docs/references/system/auth-config.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ Advanced / low-level Better-Auth options
7676
| **twoFactor** | `boolean` || Enable 2FA |
7777
| **passkeys** | `boolean` || Enable Passkey support |
7878
| **magicLink** | `boolean` || Enable Magic Link login |
79+
| **oidcProvider** | `boolean` || Enable the OpenID Connect provider plugin (acts as an OIDC IdP) |
80+
| **deviceAuthorization** | `boolean` || Enable RFC 8628 Device Authorization Grant (CLI / TV-style login) |
7981

8082

8183
---

packages/platform-objects/src/identity/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ export { SysUserPreference } from './sys-user-preference.object.js';
2828
// ── OIDC Provider Objects ──────────────────────────────────────────────────
2929
export { SysOauthApplication } from './sys-oauth-application.object.js';
3030
export { SysOauthAccessToken } from './sys-oauth-access-token.object.js';
31+
export { SysOauthRefreshToken } from './sys-oauth-refresh-token.object.js';
3132
export { SysOauthConsent } from './sys-oauth-consent.object.js';

packages/platform-objects/src/identity/sys-oauth-access-token.object.ts

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
import { ObjectSchema, Field } from '@objectstack/spec/data';
44

55
/**
6-
* sys_oauth_access_token — Issued OAuth/OIDC access + refresh token pair
6+
* sys_oauth_access_token — Issued OAuth/OIDC opaque access token
77
*
8-
* Backed by better-auth's `oidc-provider` plugin. One row per token issuance.
9-
* Tokens are short-lived; expired rows can be safely pruned.
8+
* Backed by `@better-auth/oauth-provider`'s `oauthAccessToken` model. One
9+
* row per opaque access token issuance. Tokens are short-lived; expired
10+
* rows can be safely pruned.
11+
*
12+
* Refresh tokens have been split into a sibling table — see
13+
* {@link SysOauthRefreshToken}. The optional `refresh_id` column links an
14+
* access token back to the refresh-token row that minted it.
1015
*
1116
* @namespace sys
1217
*/
@@ -16,8 +21,8 @@ export const SysOauthAccessToken = ObjectSchema.create({
1621
pluralLabel: 'OAuth Access Tokens',
1722
icon: 'ticket',
1823
isSystem: true,
19-
description: 'OAuth access and refresh tokens issued to client applications',
20-
compactLayout: ['client_id', 'user_id', 'access_token_expires_at'],
24+
description: 'Opaque OAuth access tokens issued to client applications',
25+
compactLayout: ['client_id', 'user_id', 'expires_at'],
2126

2227
fields: {
2328
id: Field.text({
@@ -26,26 +31,11 @@ export const SysOauthAccessToken = ObjectSchema.create({
2631
readonly: true,
2732
}),
2833

29-
access_token: Field.text({
30-
label: 'Access Token',
31-
required: true,
32-
maxLength: 1024,
33-
}),
34-
35-
refresh_token: Field.text({
36-
label: 'Refresh Token',
34+
token: Field.text({
35+
label: 'Token',
3736
required: true,
3837
maxLength: 1024,
39-
}),
40-
41-
access_token_expires_at: Field.datetime({
42-
label: 'Access Token Expires At',
43-
required: true,
44-
}),
45-
46-
refresh_token_expires_at: Field.datetime({
47-
label: 'Refresh Token Expires At',
48-
required: true,
38+
description: 'Opaque access token value',
4939
}),
5040

5141
client_id: Field.text({
@@ -54,36 +44,55 @@ export const SysOauthAccessToken = ObjectSchema.create({
5444
description: 'Foreign key to sys_oauth_application.client_id',
5545
}),
5646

47+
session_id: Field.text({
48+
label: 'Session ID',
49+
required: false,
50+
description: 'Foreign key to sys_session.id',
51+
}),
52+
5753
user_id: Field.text({
5854
label: 'User ID',
5955
required: false,
6056
description: 'Foreign key to sys_user.id',
6157
}),
6258

63-
scopes: Field.text({
64-
label: 'Scopes',
59+
refresh_id: Field.text({
60+
label: 'Refresh Token ID',
6561
required: false,
66-
maxLength: 1024,
62+
description: 'Foreign key to sys_oauth_refresh_token.id',
6763
}),
6864

69-
created_at: Field.datetime({
70-
label: 'Created At',
71-
defaultValue: 'NOW()',
72-
readonly: true,
65+
reference_id: Field.text({
66+
label: 'Reference ID',
67+
required: false,
68+
maxLength: 255,
69+
description: 'Caller-supplied correlation identifier',
7370
}),
7471

75-
updated_at: Field.datetime({
76-
label: 'Updated At',
72+
scopes: Field.textarea({
73+
label: 'Scopes',
74+
required: true,
75+
description: 'JSON-serialized list of scopes granted to this token',
76+
}),
77+
78+
expires_at: Field.datetime({
79+
label: 'Expires At',
80+
required: true,
81+
}),
82+
83+
created_at: Field.datetime({
84+
label: 'Created At',
7785
defaultValue: 'NOW()',
7886
readonly: true,
7987
}),
8088
},
8189

8290
indexes: [
83-
{ fields: ['access_token'], unique: true },
84-
{ fields: ['refresh_token'], unique: true },
91+
{ fields: ['token'], unique: true },
8592
{ fields: ['client_id'] },
93+
{ fields: ['session_id'] },
8694
{ fields: ['user_id'] },
95+
{ fields: ['refresh_id'] },
8796
],
8897

8998
enable: {

packages/platform-objects/src/identity/sys-oauth-application.object.ts

Lines changed: 145 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
55
/**
66
* sys_oauth_application — Registered OAuth/OIDC client application
77
*
8-
* Backed by better-auth's `oidc-provider` plugin. Each row represents an
9-
* external application that has been registered to authenticate users
10-
* against this ObjectStack server (acting as an OpenID Connect IdP).
8+
* Backed by `@better-auth/oauth-provider`'s `oauthClient` model. Each row
9+
* represents an external application that has been registered to authenticate
10+
* users against this ObjectStack server (acting as an OpenID Connect IdP).
11+
*
12+
* The table name is preserved from the deprecated `oidc-provider` plugin
13+
* (which used the `oauthApplication` model name) so existing data remains
14+
* accessible. The new model exposes a richer set of OAuth 2.1 / OIDC
15+
* registration fields — see RFC 7591 (Dynamic Client Registration) and
16+
* RFC 8414 (Authorization Server Metadata).
1117
*
1218
* @namespace sys
1319
*/
@@ -33,7 +39,7 @@ export const SysOauthApplication = ObjectSchema.create({
3339

3440
name: Field.text({
3541
label: 'Name',
36-
required: true,
42+
required: false,
3743
searchable: true,
3844
maxLength: 255,
3945
group: 'Identity',
@@ -46,6 +52,32 @@ export const SysOauthApplication = ObjectSchema.create({
4652
group: 'Identity',
4753
}),
4854

55+
uri: Field.url({
56+
label: 'Home URI',
57+
required: false,
58+
description: 'Public homepage of the registered client',
59+
group: 'Identity',
60+
}),
61+
62+
contacts: Field.textarea({
63+
label: 'Contacts',
64+
required: false,
65+
description: 'JSON-serialized list of contact email addresses',
66+
group: 'Identity',
67+
}),
68+
69+
tos: Field.url({
70+
label: 'Terms of Service',
71+
required: false,
72+
group: 'Identity',
73+
}),
74+
75+
policy: Field.url({
76+
label: 'Privacy Policy',
77+
required: false,
78+
group: 'Identity',
79+
}),
80+
4981
metadata: Field.textarea({
5082
label: 'Metadata',
5183
required: false,
@@ -71,35 +103,137 @@ export const SysOauthApplication = ObjectSchema.create({
71103
group: 'Credentials',
72104
}),
73105

74-
redirect_urls: Field.textarea({
75-
label: 'Redirect URLs',
106+
redirect_uris: Field.textarea({
107+
label: 'Redirect URIs',
76108
required: true,
77-
description: 'Comma-separated list of allowed redirect URIs',
109+
description: 'JSON-serialized list of allowed redirect URIs',
110+
group: 'Credentials',
111+
}),
112+
113+
post_logout_redirect_uris: Field.textarea({
114+
label: 'Post-logout Redirect URIs',
115+
required: false,
116+
description: 'JSON-serialized list of allowed post-logout redirect URIs',
78117
group: 'Credentials',
79118
}),
80119

81120
type: Field.select(['web', 'native', 'user-agent-based', 'public'], {
82121
label: 'Client Type',
83-
required: true,
122+
required: false,
84123
defaultValue: 'web',
85124
group: 'Credentials',
86125
}),
87126

127+
public: Field.boolean({
128+
label: 'Public Client',
129+
required: false,
130+
description: 'Marks the client as a public (non-confidential) OAuth client',
131+
group: 'Credentials',
132+
}),
133+
134+
require_pkce: Field.boolean({
135+
label: 'Require PKCE',
136+
required: false,
137+
group: 'Credentials',
138+
}),
139+
140+
token_endpoint_auth_method: Field.text({
141+
label: 'Token Endpoint Auth Method',
142+
required: false,
143+
maxLength: 64,
144+
description: 'e.g. client_secret_basic, client_secret_post, none',
145+
group: 'Credentials',
146+
}),
147+
148+
grant_types: Field.textarea({
149+
label: 'Grant Types',
150+
required: false,
151+
description: 'JSON-serialized list of allowed grant types',
152+
group: 'Credentials',
153+
}),
154+
155+
response_types: Field.textarea({
156+
label: 'Response Types',
157+
required: false,
158+
description: 'JSON-serialized list of allowed response types',
159+
group: 'Credentials',
160+
}),
161+
162+
scopes: Field.textarea({
163+
label: 'Allowed Scopes',
164+
required: false,
165+
description: 'JSON-serialized list of scopes the client may request',
166+
group: 'Credentials',
167+
}),
168+
169+
subject_type: Field.text({
170+
label: 'Subject Type',
171+
required: false,
172+
maxLength: 32,
173+
description: 'OIDC subject type (e.g. public, pairwise)',
174+
group: 'Credentials',
175+
}),
176+
177+
// ── Behaviour flags ──────────────────────────────────────────
88178
disabled: Field.boolean({
89179
label: 'Disabled',
90180
required: false,
91181
defaultValue: false,
92-
group: 'Credentials',
182+
group: 'Behaviour',
183+
}),
184+
185+
skip_consent: Field.boolean({
186+
label: 'Skip Consent',
187+
required: false,
188+
description: 'Treat as a trusted client and bypass the consent screen',
189+
group: 'Behaviour',
93190
}),
94191

95-
// ── Ownership ────────────────────────────────────────────────
192+
enable_end_session: Field.boolean({
193+
label: 'Enable End Session',
194+
required: false,
195+
description: 'Allow the client to call the OIDC end-session endpoint',
196+
group: 'Behaviour',
197+
}),
198+
199+
// ── Software statement (RFC 7591 §2.3) ───────────────────────
200+
software_id: Field.text({
201+
label: 'Software ID',
202+
required: false,
203+
maxLength: 255,
204+
group: 'Software',
205+
}),
206+
207+
software_version: Field.text({
208+
label: 'Software Version',
209+
required: false,
210+
maxLength: 64,
211+
group: 'Software',
212+
}),
213+
214+
software_statement: Field.textarea({
215+
label: 'Software Statement',
216+
required: false,
217+
description: 'Signed JWT asserting the client metadata (RFC 7591 §2.3)',
218+
group: 'Software',
219+
}),
220+
221+
// ── Ownership / system ───────────────────────────────────────
96222
user_id: Field.text({
97223
label: 'Owner User ID',
98224
required: false,
99225
description: 'User who registered this application',
100226
group: 'System',
101227
}),
102228

229+
reference_id: Field.text({
230+
label: 'Reference ID',
231+
required: false,
232+
maxLength: 255,
233+
description: 'Caller-supplied correlation identifier',
234+
group: 'System',
235+
}),
236+
103237
created_at: Field.datetime({
104238
label: 'Created At',
105239
defaultValue: 'NOW()',
@@ -118,6 +252,7 @@ export const SysOauthApplication = ObjectSchema.create({
118252
indexes: [
119253
{ fields: ['client_id'], unique: true },
120254
{ fields: ['user_id'] },
255+
{ fields: ['reference_id'] },
121256
],
122257

123258
enable: {

0 commit comments

Comments
 (0)