@julr/sesame 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +470 -64
  3. package/build/authorize_controller-sCZIjnc-.js +251 -0
  4. package/build/{client_info_controller-BucHGx4u.js → client_info_controller-B5A3tpbk.js} +11 -4
  5. package/build/commands/sesame_client.d.ts +20 -0
  6. package/build/commands/sesame_key.d.ts +12 -0
  7. package/build/commands/sesame_purge.d.ts +0 -2
  8. package/build/commands/sesame_purge.js +12 -3
  9. package/build/configure-t9q3KonY.js +31 -0
  10. package/build/configure.js +1 -24
  11. package/build/consent_controller-ChBboaSd.js +96 -0
  12. package/build/consent_retry-DMkfhmk9.js +41 -0
  13. package/build/{decorate-BKZEjPRg.js → decorate-RQD1h28J.js} +3 -9
  14. package/build/id_token_service-BtBUiL_n.js +54 -0
  15. package/build/index.d.ts +2 -8
  16. package/build/index.js +59 -10
  17. package/build/{introspect_controller-6bRt9sZt.js → introspect_controller-LVYGWhMj.js} +28 -10
  18. package/build/issue_authorization_code-Dr4-bbMp.js +39 -0
  19. package/build/jwks_controller-4HUVS_HC.js +22 -0
  20. package/build/{main-EbeMS5S9.js → main-Dx9kZv07.js} +35 -43
  21. package/build/{metadata_controller-DeaMRnUr.js → metadata_controller-hwfmxlF7.js} +81 -6
  22. package/build/oauth_error-CvxtZvjp.js +189 -0
  23. package/build/providers/sesame_provider.d.ts +1 -1
  24. package/build/providers/sesame_provider.js +17 -6
  25. package/build/{register_controller-sIJ1rxdM.js → register_controller-Cdk3c32j.js} +44 -8
  26. package/build/revoke_controller-Daz4Yqfs.js +48 -0
  27. package/build/services/main.js +3 -3
  28. package/build/sesame_manager-CeEmlzba.js +515 -0
  29. package/build/sesame_manager-DQxvsTEq.js +2 -0
  30. package/build/src/actions/authorize.d.ts +46 -0
  31. package/build/src/actions/exchange_authorization_code.d.ts +34 -0
  32. package/build/src/actions/exchange_client_credentials.d.ts +28 -0
  33. package/build/src/actions/exchange_refresh_token.d.ts +59 -0
  34. package/build/src/actions/issue_authorization_code.d.ts +26 -0
  35. package/build/src/controllers/authorize_controller.d.ts +18 -17
  36. package/build/src/controllers/consent_controller.d.ts +5 -0
  37. package/build/src/controllers/jwks_controller.d.ts +14 -0
  38. package/build/src/controllers/metadata_controller.d.ts +10 -3
  39. package/build/src/controllers/register_controller.d.ts +30 -30
  40. package/build/src/controllers/token_controller.d.ts +8 -5
  41. package/build/src/controllers/userinfo_controller.d.ts +14 -0
  42. package/build/src/guard/guard.d.ts +1 -1
  43. package/build/src/guard/lucid.d.ts +8 -0
  44. package/build/src/guard/lucid.js +46 -0
  45. package/build/src/guard/main.d.ts +2 -9
  46. package/build/src/guard/main.js +2 -6
  47. package/build/src/guard/types.d.ts +0 -9
  48. package/build/src/guard/user_provider.d.ts +9 -1
  49. package/build/src/middleware/any_scope_middleware.js +10 -1
  50. package/build/src/middleware/scope_middleware.js +10 -1
  51. package/build/src/models/oauth_authorization_code.d.ts +1 -0
  52. package/build/src/models/oauth_pending_authorization_request.d.ts +1 -0
  53. package/build/src/oauth_error.d.ts +74 -74
  54. package/build/src/routes.d.ts +3 -1
  55. package/build/src/services/client_service.d.ts +5 -2
  56. package/build/src/services/id_token_service.d.ts +30 -0
  57. package/build/src/services/key_service.d.ts +20 -0
  58. package/build/src/sesame_manager.d.ts +56 -4
  59. package/build/src/storage/consent_retry.d.ts +4 -0
  60. package/build/src/storage/drivers/kysely.d.ts +166 -0
  61. package/build/src/storage/drivers/kysely.js +480 -0
  62. package/build/src/storage/drivers/lucid.d.ts +162 -0
  63. package/build/src/storage/drivers/lucid.js +549 -0
  64. package/build/src/storage/migrations/kysely.d.ts +6 -0
  65. package/build/src/storage/types.d.ts +197 -0
  66. package/build/src/storage/types.js +1 -0
  67. package/build/src/stores.d.ts +21 -0
  68. package/build/src/types.d.ts +120 -0
  69. package/build/src/types.js +2 -0
  70. package/build/stubs/config/sesame.stub +4 -1
  71. package/build/stubs/config/sesame_kysely.stub +35 -0
  72. package/build/stubs/main.ts +5 -0
  73. package/build/stubs/migrations/create_oauth_authorization_codes_table.stub +1 -0
  74. package/build/stubs/migrations/create_oauth_pending_authorization_requests_table.stub +1 -0
  75. package/build/stubs/migrations/create_oauth_refresh_tokens_table.stub +1 -1
  76. package/build/stubs/migrations/kysely/create_oauth_tables.stub +151 -0
  77. package/build/taze.config.d.ts +2 -0
  78. package/build/token_controller-DRatCQNT.js +492 -0
  79. package/build/token_service-DDQ3Dxaj.js +59 -0
  80. package/build/types-DEtn2Zdb.js +75 -0
  81. package/build/userinfo_controller-YwN26wK-.js +37 -0
  82. package/build/vite.config.d.ts +2 -0
  83. package/package.json +55 -54
  84. package/build/authorize_controller-YUfAy-R2.js +0 -138
  85. package/build/client_service-WTNMqWzY.js +0 -65
  86. package/build/consent_controller-Dprwd1ed.js +0 -85
  87. package/build/oauth_access_token-bsoM5KeU.js +0 -18
  88. package/build/oauth_client-BIoY5jBR.js +0 -24
  89. package/build/oauth_error-CnJ3L8tf.js +0 -94
  90. package/build/revoke_controller-D6isoQCi.js +0 -41
  91. package/build/sesame_manager-Bu4MHqZV.js +0 -4
  92. package/build/sesame_manager-DwDZy5Vy.js +0 -167
  93. package/build/src/grants/authorization_code_grant.d.ts +0 -23
  94. package/build/src/grants/client_credentials_grant.d.ts +0 -23
  95. package/build/src/grants/refresh_token_grant.d.ts +0 -27
  96. package/build/token_controller-DzcrLMyS.js +0 -194
  97. package/build/token_service-fhoA4slP.js +0 -31
@@ -0,0 +1,197 @@
1
+ import type { DateTime } from 'luxon';
2
+ /**
3
+ * Stored OAuth client. `clientSecret` is a hash and must never be exposed
4
+ * in public responses.
5
+ */
6
+ export interface OAuthClientRecord {
7
+ id: string;
8
+ clientId: string;
9
+ clientSecret: string | null;
10
+ name: string;
11
+ redirectUris: string[];
12
+ scopes: string[];
13
+ grantTypes: string[];
14
+ isPublic: boolean;
15
+ isDisabled: boolean;
16
+ requirePkce: boolean;
17
+ type: string | null;
18
+ metadata: Record<string, any> | null;
19
+ userId: string | null;
20
+ createdAt: DateTime;
21
+ updatedAt: DateTime;
22
+ }
23
+ export interface OAuthAccessTokenRecord {
24
+ id: string;
25
+ tokenHash: string;
26
+ clientId: string;
27
+ userId: string | null;
28
+ scopes: string[];
29
+ expiresAt: DateTime;
30
+ revokedAt: DateTime | null;
31
+ createdAt: DateTime;
32
+ updatedAt: DateTime;
33
+ }
34
+ export interface OAuthRefreshTokenRecord {
35
+ id: string;
36
+ token: string;
37
+ accessTokenId: string;
38
+ clientId: string;
39
+ userId: string;
40
+ scopes: string[];
41
+ expiresAt: DateTime;
42
+ revokedAt: DateTime | null;
43
+ createdAt: DateTime;
44
+ updatedAt: DateTime;
45
+ }
46
+ export interface OAuthAuthorizationCodeRecord {
47
+ id: string;
48
+ code: string;
49
+ clientId: string;
50
+ userId: string;
51
+ scopes: string[];
52
+ redirectUri: string;
53
+ codeChallenge: string | null;
54
+ codeChallengeMethod: string | null;
55
+ nonce: string | null;
56
+ expiresAt: DateTime;
57
+ createdAt: DateTime;
58
+ updatedAt: DateTime;
59
+ }
60
+ export interface OAuthConsentRecord {
61
+ id: string;
62
+ clientId: string;
63
+ userId: string;
64
+ scopes: string[];
65
+ createdAt: DateTime;
66
+ updatedAt: DateTime;
67
+ }
68
+ export interface OAuthPendingAuthorizationRequestRecord {
69
+ id: string;
70
+ token: string;
71
+ userId: string;
72
+ clientId: string;
73
+ redirectUri: string;
74
+ scopes: string[];
75
+ state: string | null;
76
+ codeChallenge: string | null;
77
+ codeChallengeMethod: string | null;
78
+ nonce: string | null;
79
+ expiresAt: DateTime;
80
+ createdAt: DateTime;
81
+ }
82
+ type SesameOptionalCreateKeys<T> = Extract<keyof T, 'createdAt' | 'updatedAt'> | {
83
+ [K in keyof T]-?: null extends T[K] ? K : never;
84
+ }[keyof T];
85
+ /**
86
+ * Nullable fields default to null; timestamps default to the current time.
87
+ */
88
+ export type SesameCreateRecord<T> = Omit<T, SesameOptionalCreateKeys<T>> & Partial<Pick<T, SesameOptionalCreateKeys<T>>>;
89
+ /**
90
+ * Creation input for each persisted OAuth record.
91
+ */
92
+ export type CreateClientRecord = SesameCreateRecord<OAuthClientRecord>;
93
+ export type CreateAccessTokenRecord = SesameCreateRecord<OAuthAccessTokenRecord>;
94
+ export type CreateRefreshTokenRecord = SesameCreateRecord<OAuthRefreshTokenRecord>;
95
+ export type CreateAuthorizationCodeRecord = SesameCreateRecord<OAuthAuthorizationCodeRecord>;
96
+ export type CreatePendingAuthorizationRequestRecord = SesameCreateRecord<OAuthPendingAuthorizationRequestRecord>;
97
+ export type UpdateClientRecord = Partial<Pick<OAuthClientRecord, 'name' | 'redirectUris' | 'scopes' | 'grantTypes' | 'isDisabled' | 'requirePkce' | 'metadata'>>;
98
+ export interface SesamePurgeResult {
99
+ accessTokens: number;
100
+ refreshTokens: number;
101
+ authorizationCodes: number;
102
+ pendingRequests: number;
103
+ }
104
+ export interface IssueTokenPairOptions {
105
+ accessToken: CreateAccessTokenRecord;
106
+ refreshToken: CreateRefreshTokenRecord;
107
+ }
108
+ export interface ExchangeAuthorizationCodeOptions {
109
+ codeId: string;
110
+ accessToken: CreateAccessTokenRecord;
111
+ refreshToken: CreateRefreshTokenRecord | null;
112
+ }
113
+ export interface RotateRefreshTokenOptions extends IssueTokenPairOptions {
114
+ oldRefreshTokenId: string;
115
+ oldAccessTokenId: string;
116
+ revokedAt: DateTime;
117
+ }
118
+ export interface PurgeTokensOptions {
119
+ purgeRevoked: boolean;
120
+ purgeExpired: boolean;
121
+ cutoff: DateTime;
122
+ now: DateTime;
123
+ }
124
+ /**
125
+ * OAuth-specific persistence operations. Callers never build database predicates.
126
+ * Conditional exchanges and rotations return false when another request won.
127
+ */
128
+ export interface SesameStore {
129
+ findClient(clientId: string): Promise<OAuthClientRecord | null>;
130
+ listClients(options?: {
131
+ userId?: string;
132
+ }): Promise<OAuthClientRecord[]>;
133
+ createClient(data: CreateClientRecord): Promise<OAuthClientRecord>;
134
+ updateClient(options: {
135
+ id: string;
136
+ data: UpdateClientRecord;
137
+ }): Promise<void>;
138
+ updateClientSecret(options: {
139
+ id: string;
140
+ secret: string;
141
+ }): Promise<void>;
142
+ deleteClient(clientId: string): Promise<boolean>;
143
+ findAccessToken(options: {
144
+ hash: string;
145
+ clientId?: string;
146
+ }): Promise<OAuthAccessTokenRecord | null>;
147
+ createAccessToken(data: CreateAccessTokenRecord): Promise<void>;
148
+ revokeAccessToken(options: {
149
+ hash: string;
150
+ clientId: string;
151
+ now: DateTime;
152
+ }): Promise<boolean>;
153
+ findRefreshToken(options: {
154
+ hash: string;
155
+ clientId: string;
156
+ }): Promise<OAuthRefreshTokenRecord | null>;
157
+ revokeRefreshToken(options: {
158
+ hash: string;
159
+ clientId: string;
160
+ now: DateTime;
161
+ }): Promise<void>;
162
+ revokeTokenFamily(options: {
163
+ clientId: string;
164
+ userId: string;
165
+ now: DateTime;
166
+ }): Promise<void>;
167
+ findAuthorizationCode(options: {
168
+ code: string;
169
+ clientId: string;
170
+ }): Promise<OAuthAuthorizationCodeRecord | null>;
171
+ createAuthorizationCode(data: CreateAuthorizationCodeRecord): Promise<void>;
172
+ deleteAuthorizationCode(id: string): Promise<void>;
173
+ exchangeAuthorizationCode(options: ExchangeAuthorizationCodeOptions): Promise<boolean>;
174
+ findConsent(options: {
175
+ clientId: string;
176
+ userId: string;
177
+ }): Promise<OAuthConsentRecord | null>;
178
+ grantConsent(options: {
179
+ clientId: string;
180
+ userId: string;
181
+ scopes: string[];
182
+ }): Promise<void>;
183
+ createPendingAuthorizationRequest(data: CreatePendingAuthorizationRequestRecord): Promise<void>;
184
+ consumePendingAuthorizationRequest(options: {
185
+ token: string;
186
+ userId: string;
187
+ now: DateTime;
188
+ }): Promise<OAuthPendingAuthorizationRequestRecord | null>;
189
+ issueTokenPair(options: IssueTokenPairOptions): Promise<void>;
190
+ rotateRefreshToken(options: RotateRefreshTokenOptions): Promise<boolean>;
191
+ revokeAllForUser(options: {
192
+ userId: string;
193
+ now: DateTime;
194
+ }): Promise<void>;
195
+ purgeTokens(options: PurgeTokensOptions): Promise<SesamePurgeResult>;
196
+ }
197
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { ApplicationService, ConfigProvider } from '@adonisjs/core/types';
2
+ import type { Kysely } from 'kysely';
3
+ import type { SesameStore } from './storage/types.ts';
4
+ import type { KyselyDialect } from './storage/drivers/kysely.ts';
5
+ export interface KyselyStoreConfig<DB> {
6
+ connection: Kysely<DB> | ((app: ApplicationService) => Kysely<DB> | Promise<Kysely<DB>>);
7
+ dialect?: KyselyDialect;
8
+ }
9
+ /**
10
+ * Store driver factories resolved by the AdonisJS service provider.
11
+ */
12
+ export declare const stores: {
13
+ /**
14
+ * Use the application's Lucid connection for OAuth persistence.
15
+ */
16
+ lucid(): ConfigProvider<SesameStore>;
17
+ /**
18
+ * Use an existing Kysely connection for OAuth persistence.
19
+ */
20
+ kysely<DB>(options: KyselyStoreConfig<DB>): ConfigProvider<SesameStore>;
21
+ };
@@ -1,4 +1,9 @@
1
1
  import type { HttpContext } from '@adonisjs/core/http';
2
+ import type { ConfigProvider } from '@adonisjs/core/types';
3
+ import type { JWK } from 'jose';
4
+ import type { OAuthUserProviderContract } from './guard/types.ts';
5
+ import type { OAuthClientRecord, SesameStore } from './storage/types.ts';
6
+ export type * from './storage/types.ts';
2
7
  /**
3
8
  * Augment this interface via module augmentation to enable
4
9
  * type-safe scope names across the application.
@@ -47,6 +52,46 @@ export type InferScopes<T extends {
47
52
  * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
48
53
  */
49
54
  export declare const BUILTIN_SCOPES: Set<string>;
55
+ /**
56
+ * OIDC-recognized scopes that are accepted by server-level validation
57
+ * without needing to be declared in the config `scopes` map.
58
+ *
59
+ * Unlike `BUILTIN_SCOPES`, these still require explicit client authorization
60
+ * via `ClientService.validateClientScopes()`.
61
+ */
62
+ export declare const OIDC_SCOPES: Set<string>;
63
+ /**
64
+ * Protocol-managed claims that must never be overridden by `getOidcClaims()`.
65
+ */
66
+ export declare const RESERVED_OIDC_CLAIMS: Set<string>;
67
+ /**
68
+ * Interface for User models that provide OIDC claims.
69
+ * Implement this on your User model to include custom claims
70
+ * in id_tokens and /userinfo responses.
71
+ *
72
+ * If not implemented, only protocol-level claims (sub, iss, aud, exp, iat)
73
+ * are included.
74
+ */
75
+ export interface OidcSubject {
76
+ getOidcClaims(scopes: Scope[]): Record<string, unknown> | Promise<Record<string, unknown>>;
77
+ }
78
+ /**
79
+ * Collect OIDC claims based on the granted scopes.
80
+ *
81
+ * Maps each scope to its corresponding claims object and merges
82
+ * only the claims for scopes present in the granted set.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * getOidcClaims(scopes: Scope[]) {
87
+ * return collectOidcClaims(scopes, {
88
+ * profile: { name: this.fullName },
89
+ * email: { email: this.email },
90
+ * })
91
+ * }
92
+ * ```
93
+ */
94
+ export declare function collectOidcClaims(scopes: Scope[], claimsMap: Partial<Record<Scope, Record<string, unknown>>>): Record<string, unknown>;
50
95
  /**
51
96
  * Supported OAuth 2.1 grant types.
52
97
  *
@@ -59,6 +104,39 @@ export declare const BUILTIN_SCOPES: Set<string>;
59
104
  * @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
60
105
  */
61
106
  export type GrantType = 'authorization_code' | 'refresh_token' | 'client_credentials';
107
+ /**
108
+ * Options for creating an OAuth client programmatically.
109
+ */
110
+ export interface CreateClientOptions {
111
+ name: string;
112
+ redirectUris: string[];
113
+ scopes?: string[];
114
+ grantTypes?: GrantType[];
115
+ isPublic?: boolean;
116
+ requirePkce?: boolean;
117
+ userId?: string;
118
+ metadata?: Record<string, any>;
119
+ }
120
+ /**
121
+ * Options for updating an existing OAuth client.
122
+ */
123
+ export interface UpdateClientOptions {
124
+ name?: string;
125
+ redirectUris?: string[];
126
+ scopes?: string[];
127
+ grantTypes?: GrantType[];
128
+ isDisabled?: boolean;
129
+ requirePkce?: boolean;
130
+ metadata?: Record<string, any>;
131
+ }
132
+ /**
133
+ * Result returned when creating a client.
134
+ * Includes the raw secret (only available at creation time).
135
+ */
136
+ export interface CreateClientResult {
137
+ client: OAuthClientRecord;
138
+ clientSecret: string | null;
139
+ }
62
140
  /**
63
141
  * User-facing configuration interface for Sésame.
64
142
  *
@@ -67,6 +145,10 @@ export type GrantType = 'authorization_code' | 'refresh_token' | 'client_credent
67
145
  * page redirects for the authorization flow.
68
146
  */
69
147
  export interface SesameConfig {
148
+ /**
149
+ * Store driver resolved by the service provider.
150
+ */
151
+ store: ConfigProvider<SesameStore>;
70
152
  /**
71
153
  * The issuer URL (must be HTTPS in production).
72
154
  * Used in JWT `iss` claim and discovery metadata.
@@ -100,6 +182,18 @@ export interface SesameConfig {
100
182
  * Defaults to '30d'.
101
183
  */
102
184
  refreshTokenTtl?: string;
185
+ /**
186
+ * Grace period (in seconds) during which a recently-rotated
187
+ * refresh token is still accepted instead of triggering
188
+ * replay-attack detection. Handles race conditions in
189
+ * multi-process clients (e.g. MCP SDK).
190
+ *
191
+ * Set to `0` to disable (strict rotation, no grace period).
192
+ * Defaults to `120` (2 minutes).
193
+ *
194
+ * @see https://auth0.com/docs/secure/tokens/refresh-tokens/configure-refresh-token-rotation
195
+ */
196
+ refreshTokenRotationGracePeriod?: number;
103
197
  /**
104
198
  * Access token TTL for the client_credentials grant (M2M).
105
199
  * Defaults to `accessTokenTtl`.
@@ -142,12 +236,34 @@ export interface SesameConfig {
142
236
  * Defaults to `false`.
143
237
  */
144
238
  allowPublicRegistration?: boolean;
239
+ /**
240
+ * JWK (JSON Web Key) for signing ID tokens.
241
+ * Must be an RSA private key in JWK format.
242
+ * Required together with `oidcProvider` when OIDC scopes (openid) are used.
243
+ * Passed via env var, parsed at boot, lives in memory.
244
+ */
245
+ jwk?: JWK;
246
+ /**
247
+ * User provider used by OIDC flows to resolve the subject for
248
+ * `id_token` emission and `/userinfo`.
249
+ *
250
+ * This is independent from `@adonisjs/auth` guards. The provider is
251
+ * configured once for the authorization server and must be stable for
252
+ * the issuer. Required together with `jwk` to enable OIDC.
253
+ */
254
+ oidcProvider?: OAuthUserProviderContract<unknown>;
255
+ /**
256
+ * ID token TTL as a string duration (e.g. '1h', '10m').
257
+ * Defaults to '1h'.
258
+ */
259
+ idTokenTtl?: string;
145
260
  }
146
261
  /**
147
262
  * Fully resolved configuration with defaults applied.
148
263
  * Created by `defineConfig()` from user-supplied `SesameConfig`.
149
264
  */
150
265
  export interface ResolvedSesameConfig {
266
+ store: ConfigProvider<SesameStore>;
151
267
  issuer: string;
152
268
  scopes: Record<string, string>;
153
269
  defaultScopes: string[];
@@ -155,12 +271,16 @@ export interface ResolvedSesameConfig {
155
271
  accessTokenTtl: string;
156
272
  clientCredentialsAccessTokenTtl: string;
157
273
  refreshTokenTtl: string;
274
+ refreshTokenRotationGracePeriod: number;
158
275
  authorizationCodeTtl: string;
159
276
  authorizationRequestTtl: string;
160
277
  loginPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
161
278
  consentPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
162
279
  allowDynamicRegistration: boolean;
163
280
  allowPublicRegistration: boolean;
281
+ jwk?: JWK;
282
+ oidcProvider?: OAuthUserProviderContract<unknown>;
283
+ idTokenTtl: string;
164
284
  }
165
285
  /**
166
286
  * OAuth 2.0 Authorization Server Metadata as defined by RFC 8414.
@@ -0,0 +1,2 @@
1
+ import { i as collectOidcClaims, n as OIDC_SCOPES, r as RESERVED_OIDC_CLAIMS, t as BUILTIN_SCOPES } from "../types-DEtn2Zdb.js";
2
+ export { BUILTIN_SCOPES, OIDC_SCOPES, RESERVED_OIDC_CLAIMS, collectOidcClaims };
@@ -1,12 +1,15 @@
1
1
  {{{
2
- exports: { defineConfig: 'import { defineConfig } from \'@julr/sesame\'' }
2
+ exports({ to: app.configPath('sesame.ts') })
3
3
  }}}
4
4
  import env from '#start/env'
5
+ import { defineConfig, stores } from '@julr/sesame'
5
6
  import type { InferScopes } from '@julr/sesame/types'
6
7
 
7
8
  const sesameConfig = defineConfig({
8
9
  issuer: env.get('APP_URL'),
9
10
 
11
+ store: stores.lucid(),
12
+
10
13
  scopes: {
11
14
  // Define your available scopes here
12
15
  // 'read': 'Read access',
@@ -0,0 +1,35 @@
1
+ {{{
2
+ exports({ to: app.configPath('sesame.ts') })
3
+ }}}
4
+ import env from '#start/env'
5
+ import { defineConfig, stores } from '@julr/sesame'
6
+ import type { InferScopes } from '@julr/sesame/types'
7
+
8
+ const sesameConfig = defineConfig({
9
+ issuer: env.get('APP_URL'),
10
+
11
+ // Replace this import with your application's Kysely instance.
12
+ store: stores.kysely({
13
+ connection: async () => {
14
+ const { db } = await import('#services/kysely')
15
+ return db
16
+ },
17
+ }),
18
+
19
+ scopes: {},
20
+ defaultScopes: [],
21
+ grantTypes: ['authorization_code', 'refresh_token'],
22
+ accessTokenTtl: '1h',
23
+ refreshTokenTtl: '30d',
24
+ authorizationCodeTtl: '10m',
25
+ loginPage: '/login',
26
+ consentPage: '/oauth/consent',
27
+ allowDynamicRegistration: false,
28
+ allowPublicRegistration: false,
29
+ })
30
+
31
+ export default sesameConfig
32
+
33
+ declare module '@julr/sesame/types' {
34
+ interface SesameScopes extends InferScopes<typeof sesameConfig> {}
35
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Path to the root directory where the stubs are stored. We use
3
+ * this path within commands and the configure hook
4
+ */
5
+ export const stubsRoot = import.meta.dirname
@@ -15,6 +15,7 @@ export default class extends BaseSchema {
15
15
  table.text('redirect_uri').notNullable()
16
16
  table.string('code_challenge').nullable()
17
17
  table.string('code_challenge_method').nullable()
18
+ table.string('nonce').nullable()
18
19
  table.timestamp('expires_at').notNullable()
19
20
  table.timestamp('created_at').notNullable()
20
21
  table.timestamp('updated_at').notNullable()
@@ -21,6 +21,7 @@ export default class extends BaseSchema {
21
21
  table.string('state').nullable()
22
22
  table.string('code_challenge').nullable()
23
23
  table.string('code_challenge_method').nullable()
24
+ table.string('nonce').nullable()
24
25
  table.timestamp('expires_at').notNullable()
25
26
  table.timestamp('created_at').notNullable()
26
27
  })
@@ -9,7 +9,7 @@ export default class extends BaseSchema {
9
9
  this.schema.createTable(this.tableName, (table) => {
10
10
  table.uuid('id').primary()
11
11
  table.string('token').notNullable().index()
12
- table.string('access_token_id').notNullable()
12
+ table.uuid('access_token_id').notNullable()
13
13
  table.string('client_id').notNullable().references('client_id').inTable('oauth_clients').onDelete('CASCADE')
14
14
  table.string('user_id').notNullable()
15
15
  table.json('scopes').notNullable()
@@ -0,0 +1,151 @@
1
+ {{{
2
+ exports({
3
+ to: app.makePath('database/kysely_migrations/create_oauth_tables.ts')
4
+ })
5
+ }}}
6
+
7
+ import type { Kysely } from 'kysely'
8
+
9
+ /**
10
+ * Kysely migration for Sesame's OAuth tables.
11
+ */
12
+ export async function up(db: Kysely<any>): Promise<void> {
13
+ // Clients own OAuth grants and define the allowed redirects, scopes, and grant types.
14
+ await db.schema
15
+ .createTable('oauth_clients')
16
+ .addColumn('id', 'varchar(36)', (column) => column.primaryKey())
17
+ .addColumn('client_id', 'varchar(255)', (column) => column.notNull().unique())
18
+ .addColumn('client_secret', 'text')
19
+ .addColumn('name', 'varchar(255)', (column) => column.notNull())
20
+ .addColumn('redirect_uris', 'json', (column) => column.notNull())
21
+ .addColumn('scopes', 'json', (column) => column.notNull())
22
+ .addColumn('grant_types', 'json', (column) => column.notNull())
23
+ .addColumn('is_public', 'boolean', (column) => column.notNull().defaultTo(false))
24
+ .addColumn('is_disabled', 'boolean', (column) => column.notNull().defaultTo(false))
25
+ .addColumn('require_pkce', 'boolean', (column) => column.notNull().defaultTo(true))
26
+ .addColumn('type', 'varchar(255)')
27
+ .addColumn('metadata', 'json')
28
+ .addColumn('user_id', 'varchar(255)')
29
+ .addColumn('created_at', 'timestamp', (column) => column.notNull())
30
+ .addColumn('updated_at', 'timestamp', (column) => column.notNull())
31
+ .execute()
32
+
33
+ // Access tokens are stored by hash so a leaked database never exposes bearer tokens.
34
+ await db.schema
35
+ .createTable('oauth_access_tokens')
36
+ .addColumn('id', 'varchar(36)', (column) => column.primaryKey())
37
+ .addColumn('token_hash', 'varchar(255)', (column) => column.notNull().unique())
38
+ .addColumn('client_id', 'varchar(255)', (column) =>
39
+ column.notNull().references('oauth_clients.client_id').onDelete('cascade')
40
+ )
41
+ .addColumn('user_id', 'varchar(255)')
42
+ .addColumn('scopes', 'json', (column) => column.notNull())
43
+ .addColumn('expires_at', 'timestamp', (column) => column.notNull())
44
+ .addColumn('revoked_at', 'timestamp')
45
+ .addColumn('created_at', 'timestamp', (column) => column.notNull())
46
+ .addColumn('updated_at', 'timestamp', (column) => column.notNull())
47
+ .execute()
48
+
49
+ // Refresh tokens point to their access token so rotation can revoke the old pair.
50
+ await db.schema
51
+ .createTable('oauth_refresh_tokens')
52
+ .addColumn('id', 'varchar(36)', (column) => column.primaryKey())
53
+ .addColumn('token', 'varchar(255)', (column) => column.notNull())
54
+ .addColumn('access_token_id', 'varchar(36)', (column) => column.notNull())
55
+ .addColumn('client_id', 'varchar(255)', (column) =>
56
+ column.notNull().references('oauth_clients.client_id').onDelete('cascade')
57
+ )
58
+ .addColumn('user_id', 'varchar(255)', (column) => column.notNull())
59
+ .addColumn('scopes', 'json', (column) => column.notNull())
60
+ .addColumn('expires_at', 'timestamp', (column) => column.notNull())
61
+ .addColumn('revoked_at', 'timestamp')
62
+ .addColumn('created_at', 'timestamp', (column) => column.notNull())
63
+ .addColumn('updated_at', 'timestamp', (column) => column.notNull())
64
+ .execute()
65
+ // Token lookup is part of the refresh grant's hot path.
66
+ await db.schema
67
+ .createIndex('oauth_refresh_tokens_token_idx')
68
+ .on('oauth_refresh_tokens')
69
+ .column('token')
70
+ .execute()
71
+
72
+ // Authorization codes are short-lived, single-use credentials for the code grant.
73
+ await db.schema
74
+ .createTable('oauth_authorization_codes')
75
+ .addColumn('id', 'varchar(36)', (column) => column.primaryKey())
76
+ .addColumn('code', 'varchar(255)', (column) => column.notNull())
77
+ .addColumn('client_id', 'varchar(255)', (column) =>
78
+ column.notNull().references('oauth_clients.client_id').onDelete('cascade')
79
+ )
80
+ .addColumn('user_id', 'varchar(255)', (column) => column.notNull())
81
+ .addColumn('scopes', 'json', (column) => column.notNull())
82
+ .addColumn('redirect_uri', 'text', (column) => column.notNull())
83
+ .addColumn('code_challenge', 'varchar(255)')
84
+ .addColumn('code_challenge_method', 'varchar(255)')
85
+ .addColumn('nonce', 'varchar(255)')
86
+ .addColumn('expires_at', 'timestamp', (column) => column.notNull())
87
+ .addColumn('created_at', 'timestamp', (column) => column.notNull())
88
+ .addColumn('updated_at', 'timestamp', (column) => column.notNull())
89
+ .execute()
90
+ // Locate the authorization code submitted to the token endpoint.
91
+ await db.schema
92
+ .createIndex('oauth_authorization_codes_code_idx')
93
+ .on('oauth_authorization_codes')
94
+ .column('code')
95
+ .execute()
96
+
97
+ // A consent records the scopes approved by one user for one client.
98
+ await db.schema
99
+ .createTable('oauth_consents')
100
+ .addColumn('id', 'varchar(36)', (column) => column.primaryKey())
101
+ .addColumn('client_id', 'varchar(255)', (column) =>
102
+ column.notNull().references('oauth_clients.client_id').onDelete('cascade')
103
+ )
104
+ .addColumn('user_id', 'varchar(255)', (column) => column.notNull())
105
+ .addColumn('scopes', 'json', (column) => column.notNull())
106
+ .addColumn('created_at', 'timestamp', (column) => column.notNull())
107
+ .addColumn('updated_at', 'timestamp', (column) => column.notNull())
108
+ .execute()
109
+ // Only one consent record may exist for a client and user pair.
110
+ await db.schema
111
+ .createIndex('oauth_consents_client_id_user_id_unique')
112
+ .on('oauth_consents')
113
+ .columns(['client_id', 'user_id'])
114
+ .unique()
115
+ .execute()
116
+
117
+ // Pending requests carry the authorization context across the consent redirect.
118
+ await db.schema
119
+ .createTable('oauth_pending_authorization_requests')
120
+ .addColumn('id', 'varchar(36)', (column) => column.primaryKey())
121
+ .addColumn('token', 'varchar(255)', (column) => column.notNull())
122
+ .addColumn('client_id', 'varchar(255)', (column) =>
123
+ column.notNull().references('oauth_clients.client_id').onDelete('cascade')
124
+ )
125
+ .addColumn('user_id', 'varchar(255)', (column) => column.notNull())
126
+ .addColumn('scopes', 'json', (column) => column.notNull())
127
+ .addColumn('redirect_uri', 'text', (column) => column.notNull())
128
+ .addColumn('state', 'varchar(255)')
129
+ .addColumn('code_challenge', 'varchar(255)')
130
+ .addColumn('code_challenge_method', 'varchar(255)')
131
+ .addColumn('nonce', 'varchar(255)')
132
+ .addColumn('expires_at', 'timestamp', (column) => column.notNull())
133
+ .addColumn('created_at', 'timestamp', (column) => column.notNull())
134
+ .execute()
135
+ // Resolve the opaque pending-request token when the consent form returns.
136
+ await db.schema
137
+ .createIndex('oauth_pending_authorization_requests_token_idx')
138
+ .on('oauth_pending_authorization_requests')
139
+ .column('token')
140
+ .execute()
141
+ }
142
+
143
+ export async function down(db: Kysely<any>): Promise<void> {
144
+ // Drop children before clients to respect the client foreign keys.
145
+ await db.schema.dropTable('oauth_pending_authorization_requests').execute()
146
+ await db.schema.dropTable('oauth_consents').execute()
147
+ await db.schema.dropTable('oauth_authorization_codes').execute()
148
+ await db.schema.dropTable('oauth_refresh_tokens').execute()
149
+ await db.schema.dropTable('oauth_access_tokens').execute()
150
+ await db.schema.dropTable('oauth_clients').execute()
151
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: Partial<import("taze").CheckOptions>;
2
+ export default _default;