@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,189 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation } from "./_generated/server";
|
|
3
|
+
import * as bcrypt from "bcryptjs";
|
|
4
|
+
import { generateClientSecret } from "../lib/oauth.js";
|
|
5
|
+
import { OAUTH_CONSTANTS } from "./constants";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* OAuth Client Management Mutations
|
|
9
|
+
*
|
|
10
|
+
* Handles client registration, verification, and deletion.
|
|
11
|
+
* Uses bcryptjs (pure JavaScript implementation) for secure client secret hashing.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function isValidRedirectUri(uri: string): boolean {
|
|
15
|
+
let parsed: URL;
|
|
16
|
+
try {
|
|
17
|
+
parsed = new URL(uri);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (parsed.hash) return false;
|
|
23
|
+
|
|
24
|
+
const host = parsed.hostname.toLowerCase();
|
|
25
|
+
const isLoopback =
|
|
26
|
+
host === "localhost" ||
|
|
27
|
+
host === "127.0.0.1" ||
|
|
28
|
+
host === "::1";
|
|
29
|
+
|
|
30
|
+
if (parsed.protocol === "https:") return true;
|
|
31
|
+
if (parsed.protocol === "http:" && isLoopback) return true;
|
|
32
|
+
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register OAuth Client
|
|
38
|
+
*/
|
|
39
|
+
export const registerClient = mutation({
|
|
40
|
+
args: {
|
|
41
|
+
name: v.string(),
|
|
42
|
+
redirectUris: v.array(v.string()),
|
|
43
|
+
scopes: v.array(v.string()),
|
|
44
|
+
type: v.union(v.literal("confidential"), v.literal("public")),
|
|
45
|
+
// metadata
|
|
46
|
+
description: v.optional(v.string()),
|
|
47
|
+
website: v.optional(v.string()),
|
|
48
|
+
logoUrl: v.optional(v.string()),
|
|
49
|
+
tosUrl: v.optional(v.string()),
|
|
50
|
+
policyUrl: v.optional(v.string()),
|
|
51
|
+
isInternal: v.optional(v.boolean()),
|
|
52
|
+
},
|
|
53
|
+
handler: async (ctx, args) => {
|
|
54
|
+
if (args.redirectUris.length === 0) {
|
|
55
|
+
throw new Error("redirect_uris required");
|
|
56
|
+
}
|
|
57
|
+
const invalidRedirect = args.redirectUris.find((uri) => !isValidRedirectUri(uri));
|
|
58
|
+
if (invalidRedirect) {
|
|
59
|
+
throw new Error(`Invalid redirect_uri: ${invalidRedirect}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const clientId = crypto.randomUUID();
|
|
63
|
+
|
|
64
|
+
// Generate secret only if confidential
|
|
65
|
+
if (args.type === "confidential") {
|
|
66
|
+
// Generate plain secret using CSPrng
|
|
67
|
+
const clientSecret = generateClientSecret(OAUTH_CONSTANTS.CLIENT_SECRET_LENGTH);
|
|
68
|
+
|
|
69
|
+
// Hash the secret
|
|
70
|
+
const clientSecretHash = await bcrypt.hash(clientSecret, 10);
|
|
71
|
+
|
|
72
|
+
// Store the HASH, return the PLAIN secret once
|
|
73
|
+
await ctx.db.insert("oauthClients", {
|
|
74
|
+
name: args.name,
|
|
75
|
+
clientId,
|
|
76
|
+
clientSecret: clientSecretHash, // Store Hash!
|
|
77
|
+
type: args.type,
|
|
78
|
+
redirectUris: args.redirectUris,
|
|
79
|
+
allowedScopes: args.scopes,
|
|
80
|
+
createdAt: Date.now(),
|
|
81
|
+
description: args.description,
|
|
82
|
+
website: args.website,
|
|
83
|
+
logoUrl: args.logoUrl,
|
|
84
|
+
tosUrl: args.tosUrl,
|
|
85
|
+
policyUrl: args.policyUrl,
|
|
86
|
+
isInternal: args.isInternal,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
clientId,
|
|
91
|
+
clientSecret, // Return Plain!
|
|
92
|
+
clientIdIssuedAt: Math.floor(Date.now() / 1000),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Public client (no secret)
|
|
97
|
+
await ctx.db.insert("oauthClients", {
|
|
98
|
+
name: args.name,
|
|
99
|
+
clientId,
|
|
100
|
+
clientSecret: undefined,
|
|
101
|
+
type: args.type,
|
|
102
|
+
redirectUris: args.redirectUris,
|
|
103
|
+
allowedScopes: args.scopes,
|
|
104
|
+
createdAt: Date.now(),
|
|
105
|
+
description: args.description,
|
|
106
|
+
website: args.website,
|
|
107
|
+
logoUrl: args.logoUrl,
|
|
108
|
+
tosUrl: args.tosUrl,
|
|
109
|
+
policyUrl: args.policyUrl,
|
|
110
|
+
isInternal: args.isInternal,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
clientId,
|
|
115
|
+
clientIdIssuedAt: Math.floor(Date.now() / 1000),
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Verify Client Secret
|
|
122
|
+
*/
|
|
123
|
+
export const verifyClientSecret = mutation({
|
|
124
|
+
args: {
|
|
125
|
+
clientId: v.string(),
|
|
126
|
+
clientSecret: v.string(),
|
|
127
|
+
},
|
|
128
|
+
handler: async (ctx, args) => {
|
|
129
|
+
const client = await ctx.db
|
|
130
|
+
.query("oauthClients")
|
|
131
|
+
.withIndex("by_client_id", (q) => q.eq("clientId", args.clientId))
|
|
132
|
+
.unique();
|
|
133
|
+
|
|
134
|
+
if (!client || !client.clientSecret) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
return await bcrypt.compare(args.clientSecret, client.clientSecret);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error("Client Secret Verification Failed:", e);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Delete OAuth Client
|
|
149
|
+
*/
|
|
150
|
+
export const deleteClient = mutation({
|
|
151
|
+
args: {
|
|
152
|
+
clientId: v.string(),
|
|
153
|
+
},
|
|
154
|
+
handler: async (ctx, args) => {
|
|
155
|
+
const client = await ctx.db
|
|
156
|
+
.query("oauthClients")
|
|
157
|
+
.withIndex("by_client_id", (q) => q.eq("clientId", args.clientId))
|
|
158
|
+
.unique();
|
|
159
|
+
|
|
160
|
+
if (!client) {
|
|
161
|
+
throw new Error("Client not found");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Delete all tokens for this client
|
|
165
|
+
const tokens = await ctx.db
|
|
166
|
+
.query("oauthTokens")
|
|
167
|
+
.filter(q => q.eq(q.field("clientId"), args.clientId))
|
|
168
|
+
.collect();
|
|
169
|
+
|
|
170
|
+
for (const token of tokens) {
|
|
171
|
+
await ctx.db.delete(token._id);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Delete all codes for this client
|
|
175
|
+
const codes = await ctx.db
|
|
176
|
+
.query("oauthCodes")
|
|
177
|
+
.filter(q => q.eq(q.field("clientId"), args.clientId))
|
|
178
|
+
.collect();
|
|
179
|
+
|
|
180
|
+
for (const code of codes) {
|
|
181
|
+
await ctx.db.delete(code._id);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Delete the client
|
|
185
|
+
await ctx.db.delete(client._id);
|
|
186
|
+
|
|
187
|
+
return { success: true };
|
|
188
|
+
},
|
|
189
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 Provider Constants
|
|
3
|
+
*/
|
|
4
|
+
export const OAUTH_CONSTANTS = {
|
|
5
|
+
// Code & Token Expiry
|
|
6
|
+
CODE_EXPIRY_MS: 10 * 60 * 1000, // 10 minutes
|
|
7
|
+
ACCESS_TOKEN_EXPIRY_SECONDS: 3600, // 1 hour
|
|
8
|
+
ACCESS_TOKEN_EXPIRY: "1h",
|
|
9
|
+
ID_TOKEN_EXPIRY: "1h",
|
|
10
|
+
REFRESH_TOKEN_EXPIRY_MS: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
11
|
+
|
|
12
|
+
// Code Generation
|
|
13
|
+
AUTH_CODE_LENGTH: 32,
|
|
14
|
+
CLIENT_SECRET_LENGTH: 64,
|
|
15
|
+
|
|
16
|
+
// Supported Values
|
|
17
|
+
SUPPORTED_SCOPES: ["openid", "profile", "email", "offline_access"],
|
|
18
|
+
SUPPORTED_GRANT_TYPES: ["authorization_code", "refresh_token"],
|
|
19
|
+
SUPPORTED_RESPONSE_TYPES: ["code"],
|
|
20
|
+
SUPPORTED_CODE_CHALLENGE_METHODS: ["S256"],
|
|
21
|
+
|
|
22
|
+
// Keys
|
|
23
|
+
DEFAULT_KEY_ID: "default-key",
|
|
24
|
+
|
|
25
|
+
// CORS
|
|
26
|
+
CORS_MAX_AGE: "3600", // 1 hour
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* OAuth Error Codes (RFC 6749)
|
|
31
|
+
*/
|
|
32
|
+
export const OAUTH_ERROR_CODES = {
|
|
33
|
+
INVALID_REQUEST: "invalid_request",
|
|
34
|
+
INVALID_CLIENT: "invalid_client",
|
|
35
|
+
INVALID_GRANT: "invalid_grant",
|
|
36
|
+
UNAUTHORIZED_CLIENT: "unauthorized_client",
|
|
37
|
+
UNSUPPORTED_GRANT_TYPE: "unsupported_grant_type",
|
|
38
|
+
INVALID_SCOPE: "invalid_scope",
|
|
39
|
+
SERVER_ERROR: "server_error",
|
|
40
|
+
} as const;
|