@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
package/src/lib/oauth.ts
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SignJWT,
|
|
3
|
+
importPKCS8,
|
|
4
|
+
exportJWK,
|
|
5
|
+
jwtVerify,
|
|
6
|
+
importSPKI,
|
|
7
|
+
createLocalJWKSet
|
|
8
|
+
} from "jose";
|
|
9
|
+
import type { JWTPayload, KeyLike } from "jose";
|
|
10
|
+
import type { Auth } from "convex/server";
|
|
11
|
+
import type { RunActionCtx } from "./convex-types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* OAuth 2.1 Provider Configuration
|
|
15
|
+
*/
|
|
16
|
+
export interface OAuthConfig {
|
|
17
|
+
privateKey: string; // JWT_PRIVATE_KEY or OAUTH_PRIVATE_KEY (PEM)
|
|
18
|
+
jwks: string; // JWKS or OAUTH_JWKS (JSON) - for JWKS endpoint & token verification (REQUIRED)
|
|
19
|
+
keyId?: string; // JWT kid to use for signing (overrides JWKS kid)
|
|
20
|
+
siteUrl: string; // SITE_URL
|
|
21
|
+
convexSiteUrl?: string; // CONVEX_SITE_URL (optional)
|
|
22
|
+
allowedOrigins?: string; // ALLOWED_ORIGINS (comma-separated, optional)
|
|
23
|
+
allowedScopes?: string[]; // Allowed scopes for dynamic client registration
|
|
24
|
+
getUserId?: (ctx: RunActionCtx & { auth: Auth }, request: Request) => Promise<string | null> | string | null;
|
|
25
|
+
checkAuthorization?: (ctx: RunActionCtx & { auth: Auth }, userId: string, clientId?: string) => Promise<boolean>;
|
|
26
|
+
allowDynamicClientRegistration?: boolean;
|
|
27
|
+
prefix?: string; // OAuth endpoint prefix (default: "/oauth")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* User Profile for UserInfo endpoint
|
|
32
|
+
*/
|
|
33
|
+
export interface UserProfile {
|
|
34
|
+
sub: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
email?: string;
|
|
37
|
+
picture?: string;
|
|
38
|
+
email_verified?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Cache for keys to avoid re-parsing on every request
|
|
42
|
+
type JoseKey = Awaited<ReturnType<typeof importPKCS8>>;
|
|
43
|
+
type JoseJWK = Awaited<ReturnType<typeof exportJWK>>;
|
|
44
|
+
|
|
45
|
+
const keyCache = new Map<string, JoseKey>();
|
|
46
|
+
const jwkCache = new Map<string, JoseJWK>();
|
|
47
|
+
const jwksKeyCache = new Map<string, ReturnType<typeof createLocalJWKSet>>();
|
|
48
|
+
const DEFAULT_KEY_ID = "default-key";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reset key cache (for testing)
|
|
52
|
+
*/
|
|
53
|
+
export function resetKeysForTest() {
|
|
54
|
+
keyCache.clear();
|
|
55
|
+
jwkCache.clear();
|
|
56
|
+
jwksKeyCache.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get Private Key from PEM string
|
|
61
|
+
*/
|
|
62
|
+
async function getPrivateKey(privateKeyPEM: string): Promise<JoseKey> {
|
|
63
|
+
const cacheKey = `private:${privateKeyPEM}`;
|
|
64
|
+
const cached = keyCache.get(cacheKey);
|
|
65
|
+
if (cached) return cached;
|
|
66
|
+
|
|
67
|
+
const key = await importPKCS8(privateKeyPEM, "RS256");
|
|
68
|
+
keyCache.set(cacheKey, key);
|
|
69
|
+
return key;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get Public Key from PEM string
|
|
74
|
+
*/
|
|
75
|
+
async function getPublicKey(publicKeyPEM: string): Promise<JoseKey> {
|
|
76
|
+
const cacheKey = `public:${publicKeyPEM}`;
|
|
77
|
+
const cached = keyCache.get(cacheKey);
|
|
78
|
+
if (cached) return cached;
|
|
79
|
+
|
|
80
|
+
const key = await importSPKI(publicKeyPEM, "RS256", { extractable: true });
|
|
81
|
+
keyCache.set(cacheKey, key);
|
|
82
|
+
return key;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get Public JWK (for JWKS endpoint)
|
|
87
|
+
* @deprecated Use getJWKS instead
|
|
88
|
+
*/
|
|
89
|
+
export async function getPublicJWK(publicKeyPEM: string): Promise<JoseJWK> {
|
|
90
|
+
const cacheKey = `jwk:${publicKeyPEM}`;
|
|
91
|
+
const cached = jwkCache.get(cacheKey);
|
|
92
|
+
if (cached) return cached;
|
|
93
|
+
|
|
94
|
+
const key = await getPublicKey(publicKeyPEM);
|
|
95
|
+
const jwk = await exportJWK(key);
|
|
96
|
+
|
|
97
|
+
// Remove private fields
|
|
98
|
+
const { d: _d, p: _p, q: _q, dp: _dp, dq: _dq, qi: _qi, ...publicKey } = jwk;
|
|
99
|
+
|
|
100
|
+
const result = { ...publicKey, use: "sig", alg: "RS256", kid: "default-key" };
|
|
101
|
+
jwkCache.set(cacheKey, result);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get JWKS for the JWKS endpoint
|
|
107
|
+
* Adds kid: "default-key" to each key if not present (for compatibility with Convex Auth JWKS)
|
|
108
|
+
*/
|
|
109
|
+
export async function getJWKS(config: OAuthConfig): Promise<{ keys: JoseJWK[] }> {
|
|
110
|
+
const jwks = JSON.parse(config.jwks) as { keys: JoseJWK[] };
|
|
111
|
+
const keyId = getSigningKeyId(config);
|
|
112
|
+
jwks.keys = jwks.keys.map((key) => {
|
|
113
|
+
const {
|
|
114
|
+
d: _d,
|
|
115
|
+
p: _p,
|
|
116
|
+
q: _q,
|
|
117
|
+
dp: _dp,
|
|
118
|
+
dq: _dq,
|
|
119
|
+
qi: _qi,
|
|
120
|
+
oth: _oth,
|
|
121
|
+
k: _k,
|
|
122
|
+
...publicKey
|
|
123
|
+
} = key as JoseJWK & {
|
|
124
|
+
d?: string;
|
|
125
|
+
p?: string;
|
|
126
|
+
q?: string;
|
|
127
|
+
dp?: string;
|
|
128
|
+
dq?: string;
|
|
129
|
+
qi?: string;
|
|
130
|
+
oth?: unknown;
|
|
131
|
+
k?: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...publicKey,
|
|
136
|
+
kid: publicKey.kid ?? keyId,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return jwks;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ensureKidOnJwksKeys(keys: JoseJWK[], keyId: string): JoseJWK[] {
|
|
144
|
+
return keys.map((key) => ({
|
|
145
|
+
...key,
|
|
146
|
+
kid: key.kid ?? keyId,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getSigningKeyId(config: OAuthConfig): string {
|
|
151
|
+
if (config.keyId) return config.keyId;
|
|
152
|
+
try {
|
|
153
|
+
const jwks = JSON.parse(config.jwks) as { keys?: JoseJWK[] };
|
|
154
|
+
const kid = jwks.keys?.[0]?.kid;
|
|
155
|
+
if (typeof kid === "string" && kid.length > 0) {
|
|
156
|
+
return kid;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Fall through to default when jwks is invalid.
|
|
160
|
+
}
|
|
161
|
+
return DEFAULT_KEY_ID;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sign a JWT using the private key
|
|
166
|
+
*/
|
|
167
|
+
export async function sign(
|
|
168
|
+
payload: Record<string, unknown>,
|
|
169
|
+
subject: string,
|
|
170
|
+
audience: string,
|
|
171
|
+
expiresIn: string | number,
|
|
172
|
+
privateKeyPEM: string,
|
|
173
|
+
issuer?: string,
|
|
174
|
+
keyId: string = DEFAULT_KEY_ID
|
|
175
|
+
): Promise<string> {
|
|
176
|
+
const privateKey = await getPrivateKey(privateKeyPEM);
|
|
177
|
+
|
|
178
|
+
const jwt = new SignJWT(payload)
|
|
179
|
+
.setProtectedHeader({ alg: "RS256", kid: keyId })
|
|
180
|
+
.setIssuedAt()
|
|
181
|
+
.setSubject(subject)
|
|
182
|
+
.setAudience(audience)
|
|
183
|
+
.setExpirationTime(expiresIn);
|
|
184
|
+
|
|
185
|
+
if (issuer) {
|
|
186
|
+
jwt.setIssuer(issuer);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return jwt.sign(privateKey);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Verify Access Token using Public Key (PEM) or JWKS
|
|
194
|
+
*/
|
|
195
|
+
export async function verifyAccessToken(
|
|
196
|
+
token: string,
|
|
197
|
+
publicKeyOrConfig: string | OAuthConfig,
|
|
198
|
+
issuerUrl: string,
|
|
199
|
+
expectedAudience: string = "convex"
|
|
200
|
+
): Promise<JWTPayload> {
|
|
201
|
+
let publicKey: KeyLike | ReturnType<typeof createLocalJWKSet>;
|
|
202
|
+
|
|
203
|
+
if (typeof publicKeyOrConfig === "string") {
|
|
204
|
+
// Legacy: PEM string
|
|
205
|
+
publicKey = await getPublicKey(publicKeyOrConfig);
|
|
206
|
+
} else {
|
|
207
|
+
// New: OAuthConfig - get key from jwks or publicKey
|
|
208
|
+
publicKey = await getVerificationKey(publicKeyOrConfig);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const options = {
|
|
212
|
+
issuer: issuerUrl,
|
|
213
|
+
audience: expectedAudience,
|
|
214
|
+
};
|
|
215
|
+
const { payload } = typeof publicKey === "function"
|
|
216
|
+
? await jwtVerify(token, publicKey, options)
|
|
217
|
+
: await jwtVerify(token, publicKey, options);
|
|
218
|
+
|
|
219
|
+
return payload;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get verification key from config (JWKS)
|
|
224
|
+
*/
|
|
225
|
+
async function getVerificationKey(
|
|
226
|
+
config: OAuthConfig
|
|
227
|
+
): Promise<KeyLike | ReturnType<typeof createLocalJWKSet>> {
|
|
228
|
+
const cached = jwksKeyCache.get(config.jwks);
|
|
229
|
+
if (cached) return cached;
|
|
230
|
+
|
|
231
|
+
const jwks = JSON.parse(config.jwks) as { keys: JoseJWK[] };
|
|
232
|
+
if (!jwks.keys?.length) {
|
|
233
|
+
throw new Error("jwks must include at least one key");
|
|
234
|
+
}
|
|
235
|
+
const normalized = { keys: ensureKidOnJwksKeys(jwks.keys, getSigningKeyId(config)) };
|
|
236
|
+
const localJwks = createLocalJWKSet(normalized);
|
|
237
|
+
jwksKeyCache.set(config.jwks, localJwks);
|
|
238
|
+
return localJwks;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Generate a random code (Authorization Code)
|
|
243
|
+
*/
|
|
244
|
+
export function generateCode(length = 32): string {
|
|
245
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
246
|
+
let result = "";
|
|
247
|
+
const randomValues = new Uint32Array(length);
|
|
248
|
+
crypto.getRandomValues(randomValues);
|
|
249
|
+
for (let i = 0; i < length; i++) {
|
|
250
|
+
result += chars[randomValues[i] % chars.length];
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Generate a cryptographically strong Client Secret (hex string)
|
|
257
|
+
*/
|
|
258
|
+
export function generateClientSecret(length = 64): string {
|
|
259
|
+
const array = new Uint8Array(length);
|
|
260
|
+
crypto.getRandomValues(array);
|
|
261
|
+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get Issuer URL helper
|
|
266
|
+
*/
|
|
267
|
+
export function getIssuerUrl(config: OAuthConfig): string {
|
|
268
|
+
const issuerBaseUrl = config.convexSiteUrl ?? config.siteUrl;
|
|
269
|
+
const prefix = normalizePrefix(config.prefix);
|
|
270
|
+
return issuerBaseUrl + prefix;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Normalize OAuth prefix for consistent URL building.
|
|
275
|
+
* Ensures a leading slash, trims a trailing slash, and treats "/" as root ("").
|
|
276
|
+
*/
|
|
277
|
+
export function normalizePrefix(prefix?: string): string {
|
|
278
|
+
const raw = (prefix ?? "/oauth").trim();
|
|
279
|
+
if (!raw || raw === "/") return "";
|
|
280
|
+
let normalized = raw.startsWith("/") ? raw : `/${raw}`;
|
|
281
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
282
|
+
normalized = normalized.slice(0, -1);
|
|
283
|
+
}
|
|
284
|
+
return normalized;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* CORS Helper - Get allowed origin
|
|
289
|
+
*/
|
|
290
|
+
export function getAllowedOrigin(origin: string | null, config: OAuthConfig): string | null {
|
|
291
|
+
// 1. CLI / Non-browser clients (No Origin header)
|
|
292
|
+
if (!origin) return null;
|
|
293
|
+
|
|
294
|
+
// 2. Browser clients (Verified Origins)
|
|
295
|
+
const toOrigin = (value: string | undefined): string | null => {
|
|
296
|
+
if (!value) return null;
|
|
297
|
+
try {
|
|
298
|
+
return new URL(value).origin;
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const allowedList = (config.allowedOrigins || "")
|
|
304
|
+
.split(",")
|
|
305
|
+
.map((u) => toOrigin(u.trim()))
|
|
306
|
+
.filter((u): u is string => !!u);
|
|
307
|
+
const siteOrigin = toOrigin(config.siteUrl);
|
|
308
|
+
const convexOrigin = toOrigin(config.convexSiteUrl);
|
|
309
|
+
|
|
310
|
+
if (allowedList.includes(origin)) return origin;
|
|
311
|
+
if (siteOrigin && origin === siteOrigin) return origin;
|
|
312
|
+
if (convexOrigin && origin === convexOrigin) return origin;
|
|
313
|
+
|
|
314
|
+
// Allow Localhost (Development tools & Inspectors)
|
|
315
|
+
if (/^http:\/\/localhost(:\d+)?$/.test(origin)) return origin;
|
|
316
|
+
if (/^http:\/\/127\.0\.0\.1(:\d+)?$/.test(origin)) return origin;
|
|
317
|
+
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Create CORS headers
|
|
323
|
+
*/
|
|
324
|
+
export function createCorsHeaders(
|
|
325
|
+
origin: string | null,
|
|
326
|
+
config: OAuthConfig,
|
|
327
|
+
methods: string = "GET, POST, OPTIONS"
|
|
328
|
+
): Record<string, string> {
|
|
329
|
+
const headers: Record<string, string> = {
|
|
330
|
+
"Content-Type": "application/json",
|
|
331
|
+
"Access-Control-Allow-Methods": methods,
|
|
332
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, mcp-protocol-version",
|
|
333
|
+
};
|
|
334
|
+
const allowedOrigin = getAllowedOrigin(origin, config);
|
|
335
|
+
if (allowedOrigin) {
|
|
336
|
+
headers["Access-Control-Allow-Origin"] = allowedOrigin;
|
|
337
|
+
}
|
|
338
|
+
return headers;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Handle CORS preflight OPTIONS request
|
|
343
|
+
*/
|
|
344
|
+
export function handleCorsOptions(
|
|
345
|
+
request: Request,
|
|
346
|
+
config: OAuthConfig,
|
|
347
|
+
methods: string = "GET, POST, OPTIONS"
|
|
348
|
+
): Response | null {
|
|
349
|
+
if (request.method === "OPTIONS") {
|
|
350
|
+
const origin = request.headers.get("Origin");
|
|
351
|
+
return new Response(null, { headers: createCorsHeaders(origin, config, methods) });
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* OAuth Error Class
|
|
358
|
+
*/
|
|
359
|
+
export class OAuthError extends Error {
|
|
360
|
+
constructor(
|
|
361
|
+
public code: string,
|
|
362
|
+
message: string,
|
|
363
|
+
public statusCode: number = 400
|
|
364
|
+
) {
|
|
365
|
+
super(message);
|
|
366
|
+
this.name = "OAuthError";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
toResponse(headers: Record<string, string>): Response {
|
|
370
|
+
return new Response(
|
|
371
|
+
JSON.stringify({
|
|
372
|
+
error: this.code,
|
|
373
|
+
error_description: this.message,
|
|
374
|
+
}),
|
|
375
|
+
{ status: this.statusCode, headers }
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============================================================================
|
|
381
|
+
// OAuth Token Detection Helpers
|
|
382
|
+
// ============================================================================
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Default OAuth issuer pattern
|
|
386
|
+
* Used to identify OAuth tokens by checking if issuer URL contains this pattern
|
|
387
|
+
*/
|
|
388
|
+
export const DEFAULT_OAUTH_ISSUER_PATTERN = "/oauth";
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check if an identity is from an OAuth token
|
|
392
|
+
* @param identity - User identity from ctx.auth.getUserIdentity()
|
|
393
|
+
* @param issuerPattern - Pattern to match in issuer URL (default: "/oauth")
|
|
394
|
+
* @returns true if the identity is from an OAuth token
|
|
395
|
+
*/
|
|
396
|
+
export function isOAuthToken(
|
|
397
|
+
identity: { issuer?: string; subject?: string } | null | undefined,
|
|
398
|
+
issuerPattern: string = DEFAULT_OAUTH_ISSUER_PATTERN
|
|
399
|
+
): boolean {
|
|
400
|
+
return !!(identity?.issuer?.includes(issuerPattern) && identity?.subject);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Extract client ID from an OAuth token identity
|
|
405
|
+
* @param identity - User identity from ctx.auth.getUserIdentity()
|
|
406
|
+
* @returns Client ID if present, undefined otherwise
|
|
407
|
+
*/
|
|
408
|
+
export function getOAuthClientId(
|
|
409
|
+
identity: { cid?: string } | null | undefined
|
|
410
|
+
): string | undefined {
|
|
411
|
+
return identity?.cid;
|
|
412
|
+
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import type { TestConvex } from "convex-test";
|
|
3
|
+
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
|
+
import schema from "./component/schema.js";
|
|
5
|
+
// Note: schema.js extension is required for ESM compatibility
|
|
6
|
+
|
|
7
|
+
const modules = import.meta.glob("./component/**/*.ts");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register the OAuth Provider component with the test convex instance.
|
|
11
|
+
* @param t - The test convex instance, e.g. from calling `convexTest`.
|
|
12
|
+
* @param name - The name of the component, as registered in convex.config.ts.
|
|
13
|
+
*/
|
|
14
|
+
export function register(
|
|
15
|
+
t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
|
|
16
|
+
name: string = "oauthProvider",
|
|
17
|
+
) {
|
|
18
|
+
t.registerComponent(name, schema, modules);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default { register, schema, modules };
|