@codefox-inc/oauth-provider 0.2.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 +201 -0
- package/README.md +572 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/auth-config.d.ts +85 -0
- package/dist/client/auth-config.d.ts.map +1 -0
- package/dist/client/auth-config.js +81 -0
- package/dist/client/auth-config.js.map +1 -0
- package/dist/client/auth-helper.d.ts +81 -0
- package/dist/client/auth-helper.d.ts.map +1 -0
- package/dist/client/auth-helper.js +97 -0
- package/dist/client/auth-helper.js.map +1 -0
- package/dist/client/index.d.ts +189 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +230 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/routes.d.ts +94 -0
- package/dist/client/routes.d.ts.map +1 -0
- package/dist/client/routes.js +113 -0
- package/dist/client/routes.js.map +1 -0
- package/dist/component/_generated/api.d.ts +44 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +123 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/clientManagement.d.ts +39 -0
- package/dist/component/clientManagement.d.ts.map +1 -0
- package/dist/component/clientManagement.js +169 -0
- package/dist/component/clientManagement.js.map +1 -0
- package/dist/component/constants.d.ts +31 -0
- package/dist/component/constants.d.ts.map +1 -0
- package/dist/component/constants.js +36 -0
- package/dist/component/constants.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/handlers.d.ts +143 -0
- package/dist/component/handlers.d.ts.map +1 -0
- package/dist/component/handlers.js +624 -0
- package/dist/component/handlers.js.map +1 -0
- package/dist/component/mutations.d.ts +111 -0
- package/dist/component/mutations.d.ts.map +1 -0
- package/dist/component/mutations.js +459 -0
- package/dist/component/mutations.js.map +1 -0
- package/dist/component/queries.d.ts +127 -0
- package/dist/component/queries.d.ts.map +1 -0
- package/dist/component/queries.js +145 -0
- package/dist/component/queries.js.map +1 -0
- package/dist/component/schema.d.ts +116 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +77 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/token_security.d.ts +53 -0
- package/dist/component/token_security.d.ts.map +1 -0
- package/dist/component/token_security.js +91 -0
- package/dist/component/token_security.js.map +1 -0
- package/dist/lib/convex-types.d.ts +21 -0
- package/dist/lib/convex-types.d.ts.map +1 -0
- package/dist/lib/convex-types.js +2 -0
- package/dist/lib/convex-types.js.map +1 -0
- package/dist/lib/oauth.d.ts +123 -0
- package/dist/lib/oauth.d.ts.map +1 -0
- package/dist/lib/oauth.js +295 -0
- package/dist/lib/oauth.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +121 -0
- package/src/client/__tests__/auth-config.test.ts +244 -0
- package/src/client/__tests__/auth-helper.test.ts +273 -0
- package/src/client/__tests__/oauth-provider.test.ts +418 -0
- package/src/client/__tests__/routes.test.ts +428 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/auth-config.ts +157 -0
- package/src/client/auth-helper.ts +201 -0
- package/src/client/index.ts +326 -0
- package/src/client/routes.ts +251 -0
- package/src/component/__tests__/oauth.test.ts +3310 -0
- package/src/component/__tests__/rfc-compliance.test.ts +788 -0
- package/src/component/__tests__/token-security.test.ts +133 -0
- package/src/component/_generated/api.ts +60 -0
- package/src/component/_generated/component.ts +201 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/clientManagement.ts +189 -0
- package/src/component/constants.ts +40 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/handlers.ts +964 -0
- package/src/component/mutations.ts +531 -0
- package/src/component/queries.ts +165 -0
- package/src/component/schema.ts +92 -0
- package/src/component/token_security.ts +102 -0
- package/src/lib/__tests__/oauth-helpers.test.ts +143 -0
- package/src/lib/__tests__/oauth-jwt.test.ts +405 -0
- package/src/lib/convex-types.ts +37 -0
- package/src/lib/oauth.ts +412 -0
- package/src/react/index.ts +7 -0
- package/src/test.ts +21 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 RFC Compliance Tests
|
|
3
|
+
* Based on draft-ietf-oauth-v2-1-14
|
|
4
|
+
*
|
|
5
|
+
* This test suite validates compliance with OAuth 2.1 specification requirements.
|
|
6
|
+
* Each test maps to specific MUST/MUST NOT/SHOULD requirements from the RFC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { convexTest } from "convex-test";
|
|
10
|
+
import { expect, test, describe } from "vitest";
|
|
11
|
+
import { api, internal } from "../_generated/api";
|
|
12
|
+
import schema from "../schema";
|
|
13
|
+
|
|
14
|
+
const modules = import.meta.glob("../**/*.ts");
|
|
15
|
+
|
|
16
|
+
describe("OAuth 2.1 RFC Compliance", () => {
|
|
17
|
+
describe("Section 4.1.1 - PKCE Requirements", () => {
|
|
18
|
+
test("MUST support code_challenge and code_verifier parameters", async () => {
|
|
19
|
+
const t = convexTest(schema, modules);
|
|
20
|
+
|
|
21
|
+
// Register client
|
|
22
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
23
|
+
name: "Test Client",
|
|
24
|
+
type: "public",
|
|
25
|
+
redirectUris: ["https://example.com/callback"],
|
|
26
|
+
scopes: ["openid"],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Test that PKCE parameters are accepted
|
|
30
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
31
|
+
userId: "user123",
|
|
32
|
+
clientId: client.clientId,
|
|
33
|
+
scopes: ["openid"],
|
|
34
|
+
redirectUri: "https://example.com/callback",
|
|
35
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
36
|
+
codeChallengeMethod: "S256",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(authCode).toBeDefined();
|
|
40
|
+
expect(typeof authCode).toBe("string");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("MUST reject authorization requests without code_challenge from public clients", async () => {
|
|
44
|
+
const t = convexTest(schema, modules);
|
|
45
|
+
|
|
46
|
+
// Register public client
|
|
47
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
48
|
+
name: "Public Client",
|
|
49
|
+
type: "public",
|
|
50
|
+
redirectUris: ["https://example.com/callback"],
|
|
51
|
+
scopes: ["openid"],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Attempt to issue authorization code without PKCE (should fail for public clients)
|
|
55
|
+
await expect(
|
|
56
|
+
t.mutation(api.mutations.issueAuthorizationCode, {
|
|
57
|
+
userId: "user123",
|
|
58
|
+
clientId: client.clientId,
|
|
59
|
+
scopes: ["openid"],
|
|
60
|
+
redirectUri: "https://example.com/callback",
|
|
61
|
+
codeChallenge: "", // Empty code_challenge
|
|
62
|
+
codeChallengeMethod: "S256",
|
|
63
|
+
})
|
|
64
|
+
).rejects.toThrow();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("MUST support S256 code_challenge_method", async () => {
|
|
68
|
+
const t = convexTest(schema, modules);
|
|
69
|
+
|
|
70
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
71
|
+
name: "Test Client",
|
|
72
|
+
type: "public",
|
|
73
|
+
redirectUris: ["https://example.com/callback"],
|
|
74
|
+
scopes: ["openid"],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// S256 should be supported
|
|
78
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
79
|
+
userId: "user123",
|
|
80
|
+
clientId: client.clientId,
|
|
81
|
+
scopes: ["openid"],
|
|
82
|
+
redirectUri: "https://example.com/callback",
|
|
83
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
84
|
+
codeChallengeMethod: "S256",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(authCode).toBeDefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("SHOULD reject 'plain' code_challenge_method", async () => {
|
|
91
|
+
const t = convexTest(schema, modules);
|
|
92
|
+
|
|
93
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
94
|
+
name: "Test Client",
|
|
95
|
+
type: "public",
|
|
96
|
+
redirectUris: ["https://example.com/callback"],
|
|
97
|
+
scopes: ["openid"],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Plain method should be rejected or at minimum warned about
|
|
101
|
+
await expect(
|
|
102
|
+
t.mutation(api.mutations.issueAuthorizationCode, {
|
|
103
|
+
userId: "user123",
|
|
104
|
+
clientId: client.clientId,
|
|
105
|
+
scopes: ["openid"],
|
|
106
|
+
redirectUri: "https://example.com/callback",
|
|
107
|
+
codeChallenge: "test-verifier",
|
|
108
|
+
codeChallengeMethod: "plain",
|
|
109
|
+
})
|
|
110
|
+
).rejects.toThrow(/plain.*not.*support/i);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("Section 4.1.2 - Authorization Code Properties", () => {
|
|
115
|
+
test("Authorization code MUST expire shortly (10 minutes max RECOMMENDED)", async () => {
|
|
116
|
+
const t = convexTest(schema, modules);
|
|
117
|
+
|
|
118
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
119
|
+
name: "Test Client",
|
|
120
|
+
type: "public",
|
|
121
|
+
redirectUris: ["https://example.com/callback"],
|
|
122
|
+
scopes: ["openid"],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
126
|
+
userId: "user123",
|
|
127
|
+
clientId: client.clientId,
|
|
128
|
+
scopes: ["openid"],
|
|
129
|
+
redirectUri: "https://example.com/callback",
|
|
130
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
131
|
+
codeChallengeMethod: "S256",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Advance time by 11 minutes
|
|
135
|
+
await t.run(async (ctx) => {
|
|
136
|
+
await ctx.scheduler.runAfter(11 * 60 * 1000, internal.mutations.cleanupExpired, {});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Code should be expired - attempting to consume it should fail
|
|
140
|
+
// Note: This test may need adjustment based on actual expiration implementation
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("Authorization code MUST be bound to client_id, code_challenge, and redirect_uri", async () => {
|
|
144
|
+
const t = convexTest(schema, modules);
|
|
145
|
+
|
|
146
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
147
|
+
name: "Test Client",
|
|
148
|
+
type: "public",
|
|
149
|
+
redirectUris: ["https://example.com/callback"],
|
|
150
|
+
scopes: ["openid"],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
154
|
+
userId: "user123",
|
|
155
|
+
clientId: client.clientId,
|
|
156
|
+
scopes: ["openid"],
|
|
157
|
+
redirectUri: "https://example.com/callback",
|
|
158
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
159
|
+
codeChallengeMethod: "S256",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Attempt to use code with different redirect_uri should fail
|
|
163
|
+
await expect(
|
|
164
|
+
t.mutation(api.mutations.consumeAuthCode, {
|
|
165
|
+
code: authCode,
|
|
166
|
+
clientId: client.clientId,
|
|
167
|
+
redirectUri: "https://different.com/callback", // Wrong redirect_uri
|
|
168
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
169
|
+
})
|
|
170
|
+
).rejects.toThrow(/redirect.*uri/i);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("Section 4.1.3 - Token Endpoint (Authorization Code)", () => {
|
|
175
|
+
test("MUST return access token only once for a given authorization code", async () => {
|
|
176
|
+
const t = convexTest(schema, modules);
|
|
177
|
+
|
|
178
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
179
|
+
name: "Test Client",
|
|
180
|
+
type: "public",
|
|
181
|
+
redirectUris: ["https://example.com/callback"],
|
|
182
|
+
scopes: ["openid"],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
186
|
+
userId: "user123",
|
|
187
|
+
clientId: client.clientId,
|
|
188
|
+
scopes: ["openid"],
|
|
189
|
+
redirectUri: "https://example.com/callback",
|
|
190
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
191
|
+
codeChallengeMethod: "S256",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// First consumption should succeed
|
|
195
|
+
const result1 = await t.mutation(api.mutations.consumeAuthCode, {
|
|
196
|
+
code: authCode,
|
|
197
|
+
clientId: client.clientId,
|
|
198
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
199
|
+
redirectUri: "https://example.com/callback",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result1.userId).toBeDefined();
|
|
203
|
+
|
|
204
|
+
// Second consumption MUST fail
|
|
205
|
+
const result2: any = await t.mutation(api.mutations.consumeAuthCode, {
|
|
206
|
+
code: authCode,
|
|
207
|
+
clientId: client.clientId,
|
|
208
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
209
|
+
redirectUri: "https://example.com/callback",
|
|
210
|
+
});
|
|
211
|
+
expect(result2.error).toBe("authorization_code_reuse_detected");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("MUST verify code_verifier parameter is present if code_challenge was sent", async () => {
|
|
215
|
+
const t = convexTest(schema, modules);
|
|
216
|
+
|
|
217
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
218
|
+
name: "Test Client",
|
|
219
|
+
type: "public",
|
|
220
|
+
redirectUris: ["https://example.com/callback"],
|
|
221
|
+
scopes: ["openid"],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
225
|
+
userId: "user123",
|
|
226
|
+
clientId: client.clientId,
|
|
227
|
+
scopes: ["openid"],
|
|
228
|
+
redirectUri: "https://example.com/callback",
|
|
229
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
230
|
+
codeChallengeMethod: "S256",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Attempt to consume without code_verifier should fail
|
|
234
|
+
await expect(
|
|
235
|
+
t.mutation(api.mutations.consumeAuthCode, {
|
|
236
|
+
code: authCode,
|
|
237
|
+
clientId: client.clientId,
|
|
238
|
+
redirectUri: "https://example.com/callback",
|
|
239
|
+
codeVerifier: "", // Missing code_verifier
|
|
240
|
+
})
|
|
241
|
+
).rejects.toThrow();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("MUST verify code_verifier matches code_challenge", async () => {
|
|
245
|
+
const t = convexTest(schema, modules);
|
|
246
|
+
|
|
247
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
248
|
+
name: "Test Client",
|
|
249
|
+
type: "public",
|
|
250
|
+
redirectUris: ["https://example.com/callback"],
|
|
251
|
+
scopes: ["openid"],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
255
|
+
userId: "user123",
|
|
256
|
+
clientId: client.clientId,
|
|
257
|
+
scopes: ["openid"],
|
|
258
|
+
redirectUri: "https://example.com/callback",
|
|
259
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
260
|
+
codeChallengeMethod: "S256",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Wrong code_verifier should fail
|
|
264
|
+
await expect(
|
|
265
|
+
t.mutation(api.mutations.consumeAuthCode, {
|
|
266
|
+
code: authCode,
|
|
267
|
+
clientId: client.clientId,
|
|
268
|
+
redirectUri: "https://example.com/callback",
|
|
269
|
+
codeVerifier: "wrong-verifier-value",
|
|
270
|
+
})
|
|
271
|
+
).rejects.toThrow(/verifier/i);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("Section 2.3 & 4.1.1 - Redirect URI Validation", () => {
|
|
276
|
+
test("MUST use exact string comparison for redirect_uri validation", async () => {
|
|
277
|
+
const t = convexTest(schema, modules);
|
|
278
|
+
|
|
279
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
280
|
+
name: "Test Client",
|
|
281
|
+
type: "public",
|
|
282
|
+
redirectUris: ["https://example.com/callback"],
|
|
283
|
+
scopes: ["openid"],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Exact match should work
|
|
287
|
+
const authCode1 = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
288
|
+
userId: "user123",
|
|
289
|
+
clientId: client.clientId,
|
|
290
|
+
scopes: ["openid"],
|
|
291
|
+
redirectUri: "https://example.com/callback",
|
|
292
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
293
|
+
codeChallengeMethod: "S256",
|
|
294
|
+
});
|
|
295
|
+
expect(authCode1).toBeDefined();
|
|
296
|
+
|
|
297
|
+
// Different path should fail
|
|
298
|
+
await expect(
|
|
299
|
+
t.mutation(api.mutations.issueAuthorizationCode, {
|
|
300
|
+
userId: "user123",
|
|
301
|
+
clientId: client.clientId,
|
|
302
|
+
scopes: ["openid"],
|
|
303
|
+
redirectUri: "https://example.com/different",
|
|
304
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
305
|
+
codeChallengeMethod: "S256",
|
|
306
|
+
})
|
|
307
|
+
).rejects.toThrow(/redirect/i);
|
|
308
|
+
|
|
309
|
+
// Additional query parameter should fail (no substring matching)
|
|
310
|
+
await expect(
|
|
311
|
+
t.mutation(api.mutations.issueAuthorizationCode, {
|
|
312
|
+
userId: "user123",
|
|
313
|
+
clientId: client.clientId,
|
|
314
|
+
scopes: ["openid"],
|
|
315
|
+
redirectUri: "https://example.com/callback?extra=param",
|
|
316
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
317
|
+
codeChallengeMethod: "S256",
|
|
318
|
+
})
|
|
319
|
+
).rejects.toThrow(/redirect/i);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("MUST allow variable port numbers for localhost URIs (native apps)", async () => {
|
|
323
|
+
const t = convexTest(schema, modules);
|
|
324
|
+
|
|
325
|
+
// Register client with localhost redirect (ポート省略で登録)
|
|
326
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
327
|
+
name: "Native App",
|
|
328
|
+
type: "public",
|
|
329
|
+
redirectUris: ["http://127.0.0.1/callback"],
|
|
330
|
+
scopes: ["openid"],
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// 異なるポートでも許可されるべき
|
|
334
|
+
const authCode1 = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
335
|
+
userId: "user123",
|
|
336
|
+
clientId: client.clientId,
|
|
337
|
+
scopes: ["openid"],
|
|
338
|
+
redirectUri: "http://127.0.0.1:8080/callback",
|
|
339
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
340
|
+
codeChallengeMethod: "S256",
|
|
341
|
+
});
|
|
342
|
+
expect(authCode1).toBeDefined();
|
|
343
|
+
|
|
344
|
+
const authCode2 = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
345
|
+
userId: "user123",
|
|
346
|
+
clientId: client.clientId,
|
|
347
|
+
scopes: ["openid"],
|
|
348
|
+
redirectUri: "http://127.0.0.1:9090/callback",
|
|
349
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
350
|
+
codeChallengeMethod: "S256",
|
|
351
|
+
});
|
|
352
|
+
expect(authCode2).toBeDefined();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("Section 3.2.3 - Token Response", () => {
|
|
357
|
+
test("Refresh tokens MUST be bound to scope and resource servers", async () => {
|
|
358
|
+
const t = convexTest(schema, modules);
|
|
359
|
+
|
|
360
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
361
|
+
name: "Test Client",
|
|
362
|
+
type: "confidential",
|
|
363
|
+
redirectUris: ["https://example.com/callback"],
|
|
364
|
+
scopes: ["openid", "offline_access"],
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
368
|
+
userId: "user123",
|
|
369
|
+
clientId: client.clientId,
|
|
370
|
+
scopes: ["openid", "offline_access"],
|
|
371
|
+
redirectUri: "https://example.com/callback",
|
|
372
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
373
|
+
codeChallengeMethod: "S256",
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const result = await t.mutation(api.mutations.consumeAuthCode, {
|
|
377
|
+
code: authCode,
|
|
378
|
+
clientId: client.clientId,
|
|
379
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
380
|
+
redirectUri: "https://example.com/callback",
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(result.userId).toBeDefined();
|
|
384
|
+
// Note: This mutation returns userId and other metadata, not tokens directly
|
|
385
|
+
// Token issuance happens at handler level
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("Section 4.3 - Refresh Token", () => {
|
|
390
|
+
test("MUST maintain binding between refresh token and client", async () => {
|
|
391
|
+
const t = convexTest(schema, modules);
|
|
392
|
+
|
|
393
|
+
const client1 = await t.mutation(api.clientManagement.registerClient, {
|
|
394
|
+
name: "Client 1",
|
|
395
|
+
type: "confidential",
|
|
396
|
+
redirectUris: ["https://example.com/callback"],
|
|
397
|
+
scopes: ["openid", "offline_access"],
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Create another client to ensure token isolation
|
|
401
|
+
await t.mutation(api.clientManagement.registerClient, {
|
|
402
|
+
name: "Client 2",
|
|
403
|
+
type: "confidential",
|
|
404
|
+
redirectUris: ["https://example.com/callback"],
|
|
405
|
+
scopes: ["openid", "offline_access"],
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
409
|
+
userId: "user123",
|
|
410
|
+
clientId: client1.clientId,
|
|
411
|
+
scopes: ["openid", "offline_access"],
|
|
412
|
+
redirectUri: "https://example.com/callback",
|
|
413
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
414
|
+
codeChallengeMethod: "S256",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const result = await t.mutation(api.mutations.consumeAuthCode, {
|
|
418
|
+
code: authCode,
|
|
419
|
+
clientId: client1.clientId,
|
|
420
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
421
|
+
redirectUri: "https://example.com/callback",
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
expect(result.userId).toBeDefined();
|
|
425
|
+
// Note: Client binding is tested at handler level where tokens are issued
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("Public clients SHOULD implement refresh token rotation", async () => {
|
|
429
|
+
const t = convexTest(schema, modules);
|
|
430
|
+
|
|
431
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
432
|
+
name: "Public Client",
|
|
433
|
+
type: "public",
|
|
434
|
+
redirectUris: ["https://example.com/callback"],
|
|
435
|
+
scopes: ["openid", "offline_access"],
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
439
|
+
userId: "user123",
|
|
440
|
+
clientId: client.clientId,
|
|
441
|
+
scopes: ["openid", "offline_access"],
|
|
442
|
+
redirectUri: "https://example.com/callback",
|
|
443
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
444
|
+
codeChallengeMethod: "S256",
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const result1 = await t.mutation(api.mutations.consumeAuthCode, {
|
|
448
|
+
code: authCode,
|
|
449
|
+
clientId: client.clientId,
|
|
450
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
451
|
+
redirectUri: "https://example.com/callback",
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(result1.userId).toBeDefined();
|
|
455
|
+
// Note: Refresh token rotation is tested at handler level where tokens are issued
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe("Section 5.1 - Bearer Token Usage", () => {
|
|
460
|
+
test("MUST NOT send access token in URI query parameter", async () => {
|
|
461
|
+
// This is primarily a client requirement, but we can test that
|
|
462
|
+
// the resource server should ignore tokens in query parameters
|
|
463
|
+
|
|
464
|
+
// Note: This test verifies documentation/guidance rather than code behavior
|
|
465
|
+
// Implementation should document that query parameter tokens are not supported
|
|
466
|
+
expect(true).toBe(true); // Placeholder - adjust based on implementation
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe("Section 10 - OAuth 2.0 Differences", () => {
|
|
471
|
+
test("Implicit grant (response_type=token) MUST NOT be supported", async () => {
|
|
472
|
+
// OAuth 2.1 removes the implicit grant
|
|
473
|
+
// Authorization server should reject response_type=token
|
|
474
|
+
|
|
475
|
+
// Note: This test should verify that the handler rejects implicit flow
|
|
476
|
+
// Implementation-specific based on how authorization endpoint is exposed
|
|
477
|
+
expect(true).toBe(true); // Placeholder - adjust based on implementation
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("Password grant MUST NOT be supported", async () => {
|
|
481
|
+
// OAuth 2.1 removes the resource owner password credentials grant
|
|
482
|
+
|
|
483
|
+
// Note: Implementation does not expose password grant
|
|
484
|
+
expect(true).toBe(true); // Placeholder - verify no password grant implementation
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("RFC 6749 4.1.3: redirect_uri REQUIRED if included in authorization request", async () => {
|
|
488
|
+
const t = convexTest(schema, modules);
|
|
489
|
+
|
|
490
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
491
|
+
name: "OAuth 2.1 Client",
|
|
492
|
+
type: "public",
|
|
493
|
+
redirectUris: ["https://example.com/callback"],
|
|
494
|
+
scopes: ["openid"],
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
498
|
+
userId: "user123",
|
|
499
|
+
clientId: client.clientId,
|
|
500
|
+
scopes: ["openid"],
|
|
501
|
+
redirectUri: "https://example.com/callback",
|
|
502
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
503
|
+
codeChallengeMethod: "S256",
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// redirect_uri省略時はエラー
|
|
507
|
+
await expect(
|
|
508
|
+
t.mutation(api.mutations.consumeAuthCode, {
|
|
509
|
+
code: authCode,
|
|
510
|
+
clientId: client.clientId,
|
|
511
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
512
|
+
// redirect_uriを省略
|
|
513
|
+
})
|
|
514
|
+
).rejects.toThrow("redirect_uri_required");
|
|
515
|
+
|
|
516
|
+
// redirect_uri付きなら成功
|
|
517
|
+
const authCode2 = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
518
|
+
userId: "user123",
|
|
519
|
+
clientId: client.clientId,
|
|
520
|
+
scopes: ["openid"],
|
|
521
|
+
redirectUri: "https://example.com/callback",
|
|
522
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
523
|
+
codeChallengeMethod: "S256",
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const codeData = await t.mutation(api.mutations.consumeAuthCode, {
|
|
527
|
+
code: authCode2,
|
|
528
|
+
clientId: client.clientId,
|
|
529
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
530
|
+
redirectUri: "https://example.com/callback",
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(codeData.userId).toBeDefined();
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
describe("Section 4.2 - Client Credentials Grant", () => {
|
|
538
|
+
test("Client credentials grant MUST only be used by confidential clients", async () => {
|
|
539
|
+
const t = convexTest(schema, modules);
|
|
540
|
+
|
|
541
|
+
// Register public client
|
|
542
|
+
const publicClient = await t.mutation(api.clientManagement.registerClient, {
|
|
543
|
+
name: "Public Client",
|
|
544
|
+
type: "public",
|
|
545
|
+
redirectUris: ["https://example.com/callback"],
|
|
546
|
+
scopes: ["openid"],
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Public client should not be able to use client credentials grant
|
|
550
|
+
// Note: Implementation-specific - adjust based on actual client_credentials implementation
|
|
551
|
+
expect(publicClient.clientSecret).toBeUndefined();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
describe("Scope Validation", () => {
|
|
556
|
+
test("MUST validate requested scopes against client's allowed scopes", async () => {
|
|
557
|
+
const t = convexTest(schema, modules);
|
|
558
|
+
|
|
559
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
560
|
+
name: "Limited Client",
|
|
561
|
+
type: "public",
|
|
562
|
+
redirectUris: ["https://example.com/callback"],
|
|
563
|
+
scopes: ["openid"], // Only openid scope allowed
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Requesting disallowed scope should fail
|
|
567
|
+
await expect(
|
|
568
|
+
t.mutation(api.mutations.issueAuthorizationCode, {
|
|
569
|
+
userId: "user123",
|
|
570
|
+
clientId: client.clientId,
|
|
571
|
+
scopes: ["openid", "profile"], // profile not in allowed scopes
|
|
572
|
+
redirectUri: "https://example.com/callback",
|
|
573
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
574
|
+
codeChallengeMethod: "S256",
|
|
575
|
+
})
|
|
576
|
+
).rejects.toThrow(/scope/i);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("RFC Line 1251: New refresh token MUST have identical scope", async () => {
|
|
580
|
+
const t = convexTest(schema, modules);
|
|
581
|
+
|
|
582
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
583
|
+
name: "Test Client",
|
|
584
|
+
type: "confidential",
|
|
585
|
+
redirectUris: ["https://example.com/callback"],
|
|
586
|
+
scopes: ["openid", "profile", "offline_access"],
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// 最初にリフレッシュトークンを保存
|
|
590
|
+
const oldRefreshToken = "test-refresh-token-123";
|
|
591
|
+
await t.mutation(api.mutations.saveTokens, {
|
|
592
|
+
accessToken: "test-access-token",
|
|
593
|
+
refreshToken: oldRefreshToken,
|
|
594
|
+
clientId: client.clientId,
|
|
595
|
+
userId: "user123",
|
|
596
|
+
scopes: ["openid", "profile", "offline_access"],
|
|
597
|
+
expiresAt: Date.now() + 3600000,
|
|
598
|
+
refreshTokenExpiresAt: Date.now() + 2592000000,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// RTローテーションでスコープ縮小を試行 → エラー
|
|
602
|
+
await expect(
|
|
603
|
+
t.mutation(api.mutations.rotateRefreshToken, {
|
|
604
|
+
oldRefreshToken: oldRefreshToken,
|
|
605
|
+
accessToken: "new-access-token",
|
|
606
|
+
refreshToken: "new-refresh-token",
|
|
607
|
+
clientId: client.clientId,
|
|
608
|
+
userId: "user123",
|
|
609
|
+
scopes: ["openid", "profile"], // offline_accessを削除(縮小)
|
|
610
|
+
expiresAt: Date.now() + 3600000,
|
|
611
|
+
refreshTokenExpiresAt: Date.now() + 2592000000,
|
|
612
|
+
})
|
|
613
|
+
).rejects.toThrow(/scope/i);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("RFC Line 1217: refresh_token grant scope MUST NOT exceed original", async () => {
|
|
617
|
+
const t = convexTest(schema, modules);
|
|
618
|
+
|
|
619
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
620
|
+
name: "Test Client",
|
|
621
|
+
type: "confidential",
|
|
622
|
+
redirectUris: ["https://example.com/callback"],
|
|
623
|
+
scopes: ["openid", "profile", "email", "offline_access"],
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// 最初にリフレッシュトークンを保存(emailスコープなし)
|
|
627
|
+
const oldRefreshToken = "test-refresh-token-456";
|
|
628
|
+
await t.mutation(api.mutations.saveTokens, {
|
|
629
|
+
accessToken: "test-access-token",
|
|
630
|
+
refreshToken: oldRefreshToken,
|
|
631
|
+
clientId: client.clientId,
|
|
632
|
+
userId: "user123",
|
|
633
|
+
scopes: ["openid", "profile", "offline_access"], // emailは含まない
|
|
634
|
+
expiresAt: Date.now() + 3600000,
|
|
635
|
+
refreshTokenExpiresAt: Date.now() + 2592000000,
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Mutation level test: rotateRefreshToken with expanded scopes should fail
|
|
639
|
+
await expect(
|
|
640
|
+
t.mutation(api.mutations.rotateRefreshToken, {
|
|
641
|
+
oldRefreshToken: oldRefreshToken,
|
|
642
|
+
accessToken: "new-access-token",
|
|
643
|
+
refreshToken: "new-refresh-token",
|
|
644
|
+
clientId: client.clientId,
|
|
645
|
+
userId: "user123",
|
|
646
|
+
scopes: ["openid", "profile", "email", "offline_access"], // email追加(拡大)
|
|
647
|
+
expiresAt: Date.now() + 3600000,
|
|
648
|
+
refreshTokenExpiresAt: Date.now() + 2592000000,
|
|
649
|
+
})
|
|
650
|
+
).rejects.toThrow(/scope/i);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
describe("Section 4.1.3 - Authorization Code Replay Detection", () => {
|
|
655
|
+
test("RFC Line 1136: MUST deny authorization code reuse and SHOULD revoke tokens", async () => {
|
|
656
|
+
const t = convexTest(schema, modules);
|
|
657
|
+
|
|
658
|
+
// Register client
|
|
659
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
660
|
+
name: "Test Client",
|
|
661
|
+
type: "public",
|
|
662
|
+
redirectUris: ["https://example.com/callback"],
|
|
663
|
+
scopes: ["openid", "profile"],
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Issue authorization code
|
|
667
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
668
|
+
userId: "user123",
|
|
669
|
+
clientId: client.clientId,
|
|
670
|
+
scopes: ["openid", "profile"],
|
|
671
|
+
redirectUri: "https://example.com/callback",
|
|
672
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
673
|
+
codeChallengeMethod: "S256",
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// First use - should succeed
|
|
677
|
+
const codeData = await t.mutation(api.mutations.consumeAuthCode, {
|
|
678
|
+
code: authCode,
|
|
679
|
+
clientId: client.clientId,
|
|
680
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
681
|
+
redirectUri: "https://example.com/callback",
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
expect(codeData.userId).toBe("user123");
|
|
685
|
+
expect(codeData.codeHash).toBeDefined(); // Verify codeHash is returned
|
|
686
|
+
|
|
687
|
+
// Save tokens using the returned codeHash
|
|
688
|
+
await t.mutation(api.mutations.saveTokens, {
|
|
689
|
+
accessToken: "test-access-token",
|
|
690
|
+
refreshToken: "test-refresh-token",
|
|
691
|
+
clientId: client.clientId,
|
|
692
|
+
userId: "user123",
|
|
693
|
+
scopes: ["openid", "profile"],
|
|
694
|
+
expiresAt: Date.now() + 3600000,
|
|
695
|
+
refreshTokenExpiresAt: Date.now() + 2592000000,
|
|
696
|
+
authorizationCode: codeData.codeHash,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// Verify token was saved with correct authorizationCode
|
|
700
|
+
const tokensBefore = await t.run(async (ctx) => {
|
|
701
|
+
return await ctx.db
|
|
702
|
+
.query("oauthTokens")
|
|
703
|
+
.withIndex("by_authorization_code", (q) =>
|
|
704
|
+
q.eq("authorizationCode", codeData.codeHash)
|
|
705
|
+
)
|
|
706
|
+
.collect();
|
|
707
|
+
});
|
|
708
|
+
expect(tokensBefore.length).toBe(1); // Token should exist before replay
|
|
709
|
+
|
|
710
|
+
// Verify code is marked as used
|
|
711
|
+
const codeAfterFirstUse = await t.run(async (ctx) => {
|
|
712
|
+
const { hashToken } = await import("../token_security");
|
|
713
|
+
const hash = await hashToken(authCode);
|
|
714
|
+
return await ctx.db
|
|
715
|
+
.query("oauthCodes")
|
|
716
|
+
.withIndex("by_code", (q) => q.eq("code", hash))
|
|
717
|
+
.unique();
|
|
718
|
+
});
|
|
719
|
+
expect(codeAfterFirstUse?.usedAt).toBeDefined(); // Code should be marked as used
|
|
720
|
+
|
|
721
|
+
// Second use - should return error status (not throw, to allow token deletion to commit)
|
|
722
|
+
const secondUseResult: any = await t.mutation(api.mutations.consumeAuthCode, {
|
|
723
|
+
code: authCode,
|
|
724
|
+
clientId: client.clientId,
|
|
725
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
726
|
+
redirectUri: "https://example.com/callback",
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
expect(secondUseResult.error).toBe("authorization_code_reuse_detected");
|
|
730
|
+
expect(secondUseResult.revokedTokens).toBe(1);
|
|
731
|
+
|
|
732
|
+
// Verify that tokens were revoked
|
|
733
|
+
const tokensAfter = await t.run(async (ctx) => {
|
|
734
|
+
return await ctx.db
|
|
735
|
+
.query("oauthTokens")
|
|
736
|
+
.withIndex("by_authorization_code", (q) =>
|
|
737
|
+
q.eq("authorizationCode", codeData.codeHash)
|
|
738
|
+
)
|
|
739
|
+
.collect();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
expect(tokensAfter.length).toBe(0); // All tokens should be deleted
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("RFC Line 1136: Single use enforcement - code is marked as used", async () => {
|
|
746
|
+
const t = convexTest(schema, modules);
|
|
747
|
+
|
|
748
|
+
const client = await t.mutation(api.clientManagement.registerClient, {
|
|
749
|
+
name: "Test Client",
|
|
750
|
+
type: "public",
|
|
751
|
+
redirectUris: ["https://example.com/callback"],
|
|
752
|
+
scopes: ["openid"],
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const authCode = await t.mutation(api.mutations.issueAuthorizationCode, {
|
|
756
|
+
userId: "user123",
|
|
757
|
+
clientId: client.clientId,
|
|
758
|
+
scopes: ["openid"],
|
|
759
|
+
redirectUri: "https://example.com/callback",
|
|
760
|
+
codeChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
|
761
|
+
codeChallengeMethod: "S256",
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// First use
|
|
765
|
+
await t.mutation(api.mutations.consumeAuthCode, {
|
|
766
|
+
code: authCode,
|
|
767
|
+
clientId: client.clientId,
|
|
768
|
+
codeVerifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
|
769
|
+
redirectUri: "https://example.com/callback",
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Verify code is marked as used in database
|
|
773
|
+
const usedCode = await t.run(async (ctx) => {
|
|
774
|
+
const codeHash = await import("../token_security").then((m) =>
|
|
775
|
+
m.hashToken(authCode)
|
|
776
|
+
);
|
|
777
|
+
return await ctx.db
|
|
778
|
+
.query("oauthCodes")
|
|
779
|
+
.withIndex("by_code", (q) => q.eq("code", codeHash))
|
|
780
|
+
.unique();
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
expect(usedCode).toBeDefined();
|
|
784
|
+
expect(usedCode?.usedAt).toBeDefined();
|
|
785
|
+
expect(usedCode?.usedAt).toBeGreaterThan(0);
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
});
|