@julr/sesame 0.6.0 → 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.
- package/README.md +69 -6
- package/build/{authorize_controller-BiycO4be.js → authorize_controller-sCZIjnc-.js} +10 -10
- package/build/{client_info_controller-AcOG8lWu.js → client_info_controller-B5A3tpbk.js} +3 -4
- package/build/commands/sesame_key.d.ts +1 -1
- package/build/commands/sesame_purge.js +2 -5
- package/build/{configure-DkDkIlt8.js → configure-t9q3KonY.js} +7 -3
- package/build/configure.js +1 -2
- package/build/{consent_controller-Dsdhv6-f.js → consent_controller-ChBboaSd.js} +17 -29
- package/build/consent_retry-DMkfhmk9.js +41 -0
- package/build/decorate-RQD1h28J.js +9 -0
- package/build/{id_token_service-CpTzOUDe.js → id_token_service-BtBUiL_n.js} +1 -1
- package/build/index.d.ts +2 -8
- package/build/index.js +38 -9
- package/build/{introspect_controller-DvOp9scr.js → introspect_controller-LVYGWhMj.js} +13 -9
- package/build/{issue_authorization_code-B9ERu1uO.js → issue_authorization_code-Dr4-bbMp.js} +2 -3
- package/build/{jwks_controller-keo4kBZc.js → jwks_controller-4HUVS_HC.js} +1 -5
- package/build/{main-DGBJhq3E.js → main-Dx9kZv07.js} +14 -52
- package/build/{metadata_controller-BVsTo0Gp.js → metadata_controller-hwfmxlF7.js} +4 -6
- package/build/providers/sesame_provider.d.ts +1 -1
- package/build/providers/sesame_provider.js +8 -8
- package/build/{register_controller-gbq7p8a5.js → register_controller-Cdk3c32j.js} +3 -6
- package/build/{revoke_controller-z_ghrEB7.js → revoke_controller-Daz4Yqfs.js} +15 -21
- package/build/services/main.js +1 -5
- package/build/{sesame_manager-DYUSZ0NC.js → sesame_manager-CeEmlzba.js} +78 -256
- package/build/sesame_manager-DQxvsTEq.js +2 -0
- package/build/src/actions/exchange_authorization_code.d.ts +5 -5
- package/build/src/actions/exchange_client_credentials.d.ts +3 -3
- package/build/src/actions/exchange_refresh_token.d.ts +4 -4
- package/build/src/actions/issue_authorization_code.d.ts +2 -2
- package/build/src/controllers/authorize_controller.d.ts +11 -11
- package/build/src/controllers/metadata_controller.d.ts +7 -7
- package/build/src/controllers/register_controller.d.ts +30 -30
- package/build/src/guard/guard.d.ts +1 -1
- package/build/src/guard/lucid.d.ts +8 -0
- package/build/src/guard/lucid.js +46 -0
- package/build/src/guard/main.d.ts +2 -9
- package/build/src/guard/main.js +2 -6
- package/build/src/guard/types.d.ts +0 -9
- package/build/src/guard/user_provider.d.ts +9 -1
- package/build/src/middleware/any_scope_middleware.js +1 -2
- package/build/src/middleware/scope_middleware.js +1 -2
- package/build/src/oauth_error.d.ts +74 -74
- package/build/src/services/client_service.d.ts +5 -2
- package/build/src/sesame_manager.d.ts +6 -5
- package/build/src/storage/consent_retry.d.ts +4 -0
- package/build/src/storage/drivers/kysely.d.ts +166 -0
- package/build/src/storage/drivers/kysely.js +480 -0
- package/build/src/storage/drivers/lucid.d.ts +162 -0
- package/build/src/storage/drivers/lucid.js +549 -0
- package/build/src/storage/migrations/kysely.d.ts +6 -0
- package/build/src/storage/types.d.ts +197 -0
- package/build/src/storage/types.js +1 -0
- package/build/src/stores.d.ts +21 -0
- package/build/src/types.d.ts +9 -1
- package/build/src/types.js +2 -0
- package/build/stubs/config/sesame.stub +4 -1
- package/build/stubs/config/sesame_kysely.stub +35 -0
- package/build/stubs/migrations/kysely/create_oauth_tables.stub +151 -0
- package/build/taze.config.d.ts +2 -0
- package/build/{token_controller-DyI7oy-U.js → token_controller-DRatCQNT.js} +74 -63
- package/build/types-DEtn2Zdb.js +75 -0
- package/build/{userinfo_controller-RLk8cN_o.js → userinfo_controller-YwN26wK-.js} +5 -8
- package/build/vite.config.d.ts +1 -1
- package/package.json +33 -17
- package/build/chunk-DF48asd8.js +0 -9
- package/build/oauth_access_token-Cz_5gNBx.js +0 -29
- package/build/oauth_client-BSanvSql.js +0 -63
- package/build/sesame_manager-B1Jgq1v2.js +0 -6
- /package/build/{oauth_error-C7UhDb2q.js → oauth_error-CvxtZvjp.js} +0 -0
- /package/build/{token_service-DwnfAR9F.js → token_service-DDQ3Dxaj.js} +0 -0
|
@@ -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
|
+
};
|
package/build/src/types.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { ConfigProvider } from '@adonisjs/core/types';
|
|
2
3
|
import type { JWK } from 'jose';
|
|
3
4
|
import type { OAuthUserProviderContract } from './guard/types.ts';
|
|
5
|
+
import type { OAuthClientRecord, SesameStore } from './storage/types.ts';
|
|
6
|
+
export type * from './storage/types.ts';
|
|
4
7
|
/**
|
|
5
8
|
* Augment this interface via module augmentation to enable
|
|
6
9
|
* type-safe scope names across the application.
|
|
@@ -131,7 +134,7 @@ export interface UpdateClientOptions {
|
|
|
131
134
|
* Includes the raw secret (only available at creation time).
|
|
132
135
|
*/
|
|
133
136
|
export interface CreateClientResult {
|
|
134
|
-
client:
|
|
137
|
+
client: OAuthClientRecord;
|
|
135
138
|
clientSecret: string | null;
|
|
136
139
|
}
|
|
137
140
|
/**
|
|
@@ -142,6 +145,10 @@ export interface CreateClientResult {
|
|
|
142
145
|
* page redirects for the authorization flow.
|
|
143
146
|
*/
|
|
144
147
|
export interface SesameConfig {
|
|
148
|
+
/**
|
|
149
|
+
* Store driver resolved by the service provider.
|
|
150
|
+
*/
|
|
151
|
+
store: ConfigProvider<SesameStore>;
|
|
145
152
|
/**
|
|
146
153
|
* The issuer URL (must be HTTPS in production).
|
|
147
154
|
* Used in JWT `iss` claim and discovery metadata.
|
|
@@ -256,6 +263,7 @@ export interface SesameConfig {
|
|
|
256
263
|
* Created by `defineConfig()` from user-supplied `SesameConfig`.
|
|
257
264
|
*/
|
|
258
265
|
export interface ResolvedSesameConfig {
|
|
266
|
+
store: ConfigProvider<SesameStore>;
|
|
259
267
|
issuer: string;
|
|
260
268
|
scopes: Record<string, string>;
|
|
261
269
|
defaultScopes: string[];
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{{{
|
|
2
|
-
exports
|
|
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,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
|
+
}
|