@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.
- package/LICENSE.md +1 -1
- package/README.md +470 -64
- package/build/authorize_controller-sCZIjnc-.js +251 -0
- package/build/{client_info_controller-BucHGx4u.js → client_info_controller-B5A3tpbk.js} +11 -4
- package/build/commands/sesame_client.d.ts +20 -0
- package/build/commands/sesame_key.d.ts +12 -0
- package/build/commands/sesame_purge.d.ts +0 -2
- package/build/commands/sesame_purge.js +12 -3
- package/build/configure-t9q3KonY.js +31 -0
- package/build/configure.js +1 -24
- package/build/consent_controller-ChBboaSd.js +96 -0
- package/build/consent_retry-DMkfhmk9.js +41 -0
- package/build/{decorate-BKZEjPRg.js → decorate-RQD1h28J.js} +3 -9
- package/build/id_token_service-BtBUiL_n.js +54 -0
- package/build/index.d.ts +2 -8
- package/build/index.js +59 -10
- package/build/{introspect_controller-6bRt9sZt.js → introspect_controller-LVYGWhMj.js} +28 -10
- package/build/issue_authorization_code-Dr4-bbMp.js +39 -0
- package/build/jwks_controller-4HUVS_HC.js +22 -0
- package/build/{main-EbeMS5S9.js → main-Dx9kZv07.js} +35 -43
- package/build/{metadata_controller-DeaMRnUr.js → metadata_controller-hwfmxlF7.js} +81 -6
- package/build/oauth_error-CvxtZvjp.js +189 -0
- package/build/providers/sesame_provider.d.ts +1 -1
- package/build/providers/sesame_provider.js +17 -6
- package/build/{register_controller-sIJ1rxdM.js → register_controller-Cdk3c32j.js} +44 -8
- package/build/revoke_controller-Daz4Yqfs.js +48 -0
- package/build/services/main.js +3 -3
- package/build/sesame_manager-CeEmlzba.js +515 -0
- package/build/sesame_manager-DQxvsTEq.js +2 -0
- package/build/src/actions/authorize.d.ts +46 -0
- package/build/src/actions/exchange_authorization_code.d.ts +34 -0
- package/build/src/actions/exchange_client_credentials.d.ts +28 -0
- package/build/src/actions/exchange_refresh_token.d.ts +59 -0
- package/build/src/actions/issue_authorization_code.d.ts +26 -0
- package/build/src/controllers/authorize_controller.d.ts +18 -17
- package/build/src/controllers/consent_controller.d.ts +5 -0
- package/build/src/controllers/jwks_controller.d.ts +14 -0
- package/build/src/controllers/metadata_controller.d.ts +10 -3
- package/build/src/controllers/register_controller.d.ts +30 -30
- package/build/src/controllers/token_controller.d.ts +8 -5
- package/build/src/controllers/userinfo_controller.d.ts +14 -0
- 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 +10 -1
- package/build/src/middleware/scope_middleware.js +10 -1
- package/build/src/models/oauth_authorization_code.d.ts +1 -0
- package/build/src/models/oauth_pending_authorization_request.d.ts +1 -0
- package/build/src/oauth_error.d.ts +74 -74
- package/build/src/routes.d.ts +3 -1
- package/build/src/services/client_service.d.ts +5 -2
- package/build/src/services/id_token_service.d.ts +30 -0
- package/build/src/services/key_service.d.ts +20 -0
- package/build/src/sesame_manager.d.ts +56 -4
- 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 +120 -0
- 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/main.ts +5 -0
- package/build/stubs/migrations/create_oauth_authorization_codes_table.stub +1 -0
- package/build/stubs/migrations/create_oauth_pending_authorization_requests_table.stub +1 -0
- package/build/stubs/migrations/create_oauth_refresh_tokens_table.stub +1 -1
- package/build/stubs/migrations/kysely/create_oauth_tables.stub +151 -0
- package/build/taze.config.d.ts +2 -0
- package/build/token_controller-DRatCQNT.js +492 -0
- package/build/token_service-DDQ3Dxaj.js +59 -0
- package/build/types-DEtn2Zdb.js +75 -0
- package/build/userinfo_controller-YwN26wK-.js +37 -0
- package/build/vite.config.d.ts +2 -0
- package/package.json +55 -54
- package/build/authorize_controller-YUfAy-R2.js +0 -138
- package/build/client_service-WTNMqWzY.js +0 -65
- package/build/consent_controller-Dprwd1ed.js +0 -85
- package/build/oauth_access_token-bsoM5KeU.js +0 -18
- package/build/oauth_client-BIoY5jBR.js +0 -24
- package/build/oauth_error-CnJ3L8tf.js +0 -94
- package/build/revoke_controller-D6isoQCi.js +0 -41
- package/build/sesame_manager-Bu4MHqZV.js +0 -4
- package/build/sesame_manager-DwDZy5Vy.js +0 -167
- package/build/src/grants/authorization_code_grant.d.ts +0 -23
- package/build/src/grants/client_credentials_grant.d.ts +0 -23
- package/build/src/grants/refresh_token_grant.d.ts +0 -27
- package/build/token_controller-DzcrLMyS.js +0 -194
- 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
|
+
};
|
package/build/src/types.d.ts
CHANGED
|
@@ -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.
|
|
@@ -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
|
+
}
|
|
@@ -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.
|
|
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
|
+
}
|