@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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Helper for handling both Convex Auth and OAuth tokens
|
|
3
|
+
*
|
|
4
|
+
* This helper provides a unified way to get the current user
|
|
5
|
+
* regardless of whether they authenticated via Convex Auth (session)
|
|
6
|
+
* or OAuth token (MCP clients).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isOAuthToken as checkIsOAuthToken,
|
|
11
|
+
getOAuthClientId,
|
|
12
|
+
DEFAULT_OAUTH_ISSUER_PATTERN,
|
|
13
|
+
} from "../lib/oauth.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Context types (simplified for compatibility)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type QueryCtx = any;
|
|
20
|
+
|
|
21
|
+
type MutationCtx = any;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for the auth helper
|
|
25
|
+
*/
|
|
26
|
+
export interface AuthHelperConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Convex Auth provider names to check for subject ID lookup
|
|
29
|
+
* @example ["anonymous", "password", "google"]
|
|
30
|
+
*/
|
|
31
|
+
providers?: string[];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Custom function to get auth user ID from Convex Auth
|
|
35
|
+
* If not provided, you must pass it when calling methods
|
|
36
|
+
*/
|
|
37
|
+
getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Function to check if OAuth authorization is still valid
|
|
41
|
+
* Called for OAuth token requests to verify the authorization wasn't revoked
|
|
42
|
+
* @param ctx - Query/Mutation context
|
|
43
|
+
* @param userId - User ID from JWT
|
|
44
|
+
* @param clientId - Client ID from JWT (may be undefined if not in JWT)
|
|
45
|
+
* @returns true if authorization is valid, false if revoked
|
|
46
|
+
*/
|
|
47
|
+
checkAuthorization?: (ctx: QueryCtx | MutationCtx, userId: string, clientId?: string) => Promise<boolean>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* OAuth issuer URL pattern to identify OAuth tokens
|
|
51
|
+
* If the token's issuer contains this string, authorization check is enforced
|
|
52
|
+
* @example "/oauth"
|
|
53
|
+
*/
|
|
54
|
+
oauthIssuerPattern?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Auth Helper instance
|
|
59
|
+
*/
|
|
60
|
+
export interface AuthHelper {
|
|
61
|
+
/**
|
|
62
|
+
* Get the current user ID from either Convex Auth or OAuth token
|
|
63
|
+
* Returns null if not authenticated
|
|
64
|
+
*/
|
|
65
|
+
getCurrentUserId: (
|
|
66
|
+
ctx: QueryCtx | MutationCtx,
|
|
67
|
+
getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
|
|
68
|
+
) => Promise<string | null>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the current user document from the database
|
|
72
|
+
* Returns null if not authenticated or user not found
|
|
73
|
+
*/
|
|
74
|
+
getCurrentUser: <T>(
|
|
75
|
+
ctx: QueryCtx | MutationCtx,
|
|
76
|
+
getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
|
|
77
|
+
) => Promise<T | null>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Require authentication - throws if not authenticated
|
|
81
|
+
*/
|
|
82
|
+
requireAuth: (
|
|
83
|
+
ctx: QueryCtx | MutationCtx,
|
|
84
|
+
getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
|
|
85
|
+
) => Promise<string>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create an auth helper for handling both Convex Auth and OAuth tokens
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* import { createAuthHelper } from "@codefox-inc/oauth-provider";
|
|
94
|
+
* import { getAuthUserId } from "./auth";
|
|
95
|
+
*
|
|
96
|
+
* const authHelper = createAuthHelper({
|
|
97
|
+
* providers: ["anonymous"],
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* // In a query/mutation:
|
|
101
|
+
* const userId = await authHelper.getCurrentUserId(ctx, getAuthUserId);
|
|
102
|
+
* const user = await authHelper.getCurrentUser(ctx, getAuthUserId);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export function createAuthHelper(config: AuthHelperConfig = {}): AuthHelper {
|
|
106
|
+
const {
|
|
107
|
+
providers = ["anonymous"],
|
|
108
|
+
getAuthUserId: defaultGetAuthUserId,
|
|
109
|
+
checkAuthorization,
|
|
110
|
+
oauthIssuerPattern = DEFAULT_OAUTH_ISSUER_PATTERN,
|
|
111
|
+
} = config;
|
|
112
|
+
|
|
113
|
+
async function getCurrentUserId(
|
|
114
|
+
ctx: QueryCtx | MutationCtx,
|
|
115
|
+
getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
|
|
116
|
+
): Promise<string | null> {
|
|
117
|
+
const authFn = getAuthUserId ?? defaultGetAuthUserId;
|
|
118
|
+
|
|
119
|
+
// First, check if this is an OAuth token by looking at identity issuer
|
|
120
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
121
|
+
const isOAuth = checkIsOAuthToken(identity, oauthIssuerPattern);
|
|
122
|
+
|
|
123
|
+
// If this is an OAuth token, skip Convex Auth and enforce authorization check
|
|
124
|
+
if (isOAuth && identity?.subject) {
|
|
125
|
+
const validId = ctx.db.normalizeId("users", identity.subject);
|
|
126
|
+
if (validId) {
|
|
127
|
+
// OAuth tokens MUST pass authorization check
|
|
128
|
+
if (checkAuthorization) {
|
|
129
|
+
const clientId = getOAuthClientId(identity);
|
|
130
|
+
const isValid = await checkAuthorization(ctx, validId, clientId);
|
|
131
|
+
if (!isValid) {
|
|
132
|
+
// Authorization was revoked - reject access
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return validId;
|
|
137
|
+
}
|
|
138
|
+
// OAuth token but invalid user ID
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 1. Try Convex Auth (session-based, getAuthUserId)
|
|
143
|
+
if (authFn) {
|
|
144
|
+
const userIdOrSubject = await authFn(ctx);
|
|
145
|
+
if (userIdOrSubject) {
|
|
146
|
+
// Handle "userId|sessionId" format from some Convex Auth versions
|
|
147
|
+
const idToLookup = userIdOrSubject.includes("|")
|
|
148
|
+
? userIdOrSubject.split("|")[0]
|
|
149
|
+
: userIdOrSubject;
|
|
150
|
+
|
|
151
|
+
// Try as Convex ID
|
|
152
|
+
const validId = ctx.db.normalizeId("users", idToLookup);
|
|
153
|
+
if (validId) {
|
|
154
|
+
return validId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Try as Subject ID via authAccounts
|
|
158
|
+
for (const provider of providers) {
|
|
159
|
+
|
|
160
|
+
const account = await (ctx.db as any)
|
|
161
|
+
.query("authAccounts")
|
|
162
|
+
.withIndex("providerAndAccountId", (q: { eq: (field: string, value: string) => { eq: (field: string, value: string) => unknown } }) =>
|
|
163
|
+
q.eq("provider", provider).eq("providerAccountId", idToLookup)
|
|
164
|
+
)
|
|
165
|
+
.unique();
|
|
166
|
+
if (account) {
|
|
167
|
+
return account.userId;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function getCurrentUser<T>(
|
|
177
|
+
ctx: QueryCtx | MutationCtx,
|
|
178
|
+
getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
|
|
179
|
+
): Promise<T | null> {
|
|
180
|
+
const userId = await getCurrentUserId(ctx, getAuthUserId);
|
|
181
|
+
if (!userId) return null;
|
|
182
|
+
return ctx.db.get(userId) as Promise<T | null>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function requireAuth(
|
|
186
|
+
ctx: QueryCtx | MutationCtx,
|
|
187
|
+
getAuthUserId?: (ctx: QueryCtx | MutationCtx) => Promise<string | null>
|
|
188
|
+
): Promise<string> {
|
|
189
|
+
const userId = await getCurrentUserId(ctx, getAuthUserId);
|
|
190
|
+
if (!userId) {
|
|
191
|
+
throw new Error("Not authenticated");
|
|
192
|
+
}
|
|
193
|
+
return userId;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
getCurrentUserId,
|
|
198
|
+
getCurrentUser,
|
|
199
|
+
requireAuth,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import {
|
|
2
|
+
openIdConfigurationHandler,
|
|
3
|
+
jwksHandler,
|
|
4
|
+
tokenHandler,
|
|
5
|
+
userInfoHandler,
|
|
6
|
+
registerHandler,
|
|
7
|
+
authorizeHandler,
|
|
8
|
+
oauthProtectedResourceHandler,
|
|
9
|
+
} from "../component/handlers.js";
|
|
10
|
+
import type { OAuthComponentAPI } from "../component/handlers.js";
|
|
11
|
+
import type { OAuthConfig, UserProfile } from "../lib/oauth.js";
|
|
12
|
+
import type { RunQueryCtx, RunMutationCtx, RunActionCtx } from "../lib/convex-types.js";
|
|
13
|
+
|
|
14
|
+
// Re-export types and utilities
|
|
15
|
+
export type { OAuthConfig, UserProfile } from "../lib/oauth.js";
|
|
16
|
+
export {
|
|
17
|
+
OAuthError,
|
|
18
|
+
verifyAccessToken,
|
|
19
|
+
isOAuthToken,
|
|
20
|
+
getOAuthClientId,
|
|
21
|
+
DEFAULT_OAUTH_ISSUER_PATTERN,
|
|
22
|
+
} from "../lib/oauth.js";
|
|
23
|
+
export { OAUTH_CONSTANTS, OAUTH_ERROR_CODES } from "../component/constants.js";
|
|
24
|
+
|
|
25
|
+
// Auth helper for getCurrentUser pattern
|
|
26
|
+
export { createAuthHelper } from "./auth-helper.js";
|
|
27
|
+
export type { AuthHelper, AuthHelperConfig } from "./auth-helper.js";
|
|
28
|
+
|
|
29
|
+
// Route registration helper
|
|
30
|
+
export { registerOAuthRoutes } from "./routes.js";
|
|
31
|
+
export type { RegisterOAuthRoutesOptions } from "./routes.js";
|
|
32
|
+
|
|
33
|
+
// Auth config generator
|
|
34
|
+
export { generateAuthConfig, createAuthConfig } from "./auth-config.js";
|
|
35
|
+
export type { AuthConfig, AuthProvider, GenerateAuthConfigOptions } from "./auth-config.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* OAuth Provider Client Configuration
|
|
39
|
+
*/
|
|
40
|
+
export type OAuthProviderConfig = OAuthConfig;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* OAuth Provider SDK
|
|
44
|
+
*
|
|
45
|
+
* Usage:
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { OAuthProvider } from "@codefox-inc/oauth-provider";
|
|
48
|
+
* import { components } from "./_generated/api";
|
|
49
|
+
*
|
|
50
|
+
* const oauthProvider = new OAuthProvider(components.oauthProvider, {
|
|
51
|
+
* privateKey: process.env.OAUTH_PRIVATE_KEY!,
|
|
52
|
+
* publicKey: process.env.OAUTH_PUBLIC_KEY!,
|
|
53
|
+
* siteUrl: process.env.SITE_URL!,
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* // In http.ts
|
|
57
|
+
* http.route({
|
|
58
|
+
* path: "/oauth/.well-known/openid-configuration",
|
|
59
|
+
* method: "GET",
|
|
60
|
+
* handler: httpAction((ctx, req) => oauthProvider.handlers.openIdConfiguration(ctx, req)),
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export class OAuthProvider {
|
|
65
|
+
private config: OAuthProviderConfig;
|
|
66
|
+
private api: OAuthComponentAPI;
|
|
67
|
+
|
|
68
|
+
private component: any;
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
|
|
72
|
+
component: any,
|
|
73
|
+
config: OAuthProviderConfig
|
|
74
|
+
) {
|
|
75
|
+
this.config = config;
|
|
76
|
+
this.component = component;
|
|
77
|
+
this.api = this.createAPI(component);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getConfig(): OAuthProviderConfig {
|
|
81
|
+
return this.config;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
private createAPI(component: any): OAuthComponentAPI {
|
|
86
|
+
return {
|
|
87
|
+
queries: {
|
|
88
|
+
getClient: (ctx, args) => ctx.runQuery(component.queries.getClient, args),
|
|
89
|
+
getRefreshToken: (ctx, args) => ctx.runQuery(component.queries.getRefreshToken, args),
|
|
90
|
+
getTokensByUser: (ctx, args) => ctx.runQuery(component.queries.getTokensByUser, args),
|
|
91
|
+
},
|
|
92
|
+
mutations: {
|
|
93
|
+
issueAuthorizationCode: (ctx, args) =>
|
|
94
|
+
ctx.runMutation(component.mutations.issueAuthorizationCode, args),
|
|
95
|
+
consumeAuthCode: (ctx, args) =>
|
|
96
|
+
ctx.runMutation(component.mutations.consumeAuthCode, args),
|
|
97
|
+
saveTokens: (ctx, args) =>
|
|
98
|
+
ctx.runMutation(component.mutations.saveTokens, args),
|
|
99
|
+
rotateRefreshToken: (ctx, args) =>
|
|
100
|
+
ctx.runMutation(component.mutations.rotateRefreshToken, args),
|
|
101
|
+
upsertAuthorization: (ctx, args) =>
|
|
102
|
+
ctx.runMutation(component.mutations.upsertAuthorization, args),
|
|
103
|
+
updateAuthorizationLastUsed: (ctx, args) =>
|
|
104
|
+
ctx.runMutation(component.mutations.updateAuthorizationLastUsed, args),
|
|
105
|
+
},
|
|
106
|
+
clientManagement: {
|
|
107
|
+
registerClient: (ctx, args) =>
|
|
108
|
+
ctx.runMutation(component.clientManagement.registerClient, args),
|
|
109
|
+
verifyClientSecret: (ctx, args) =>
|
|
110
|
+
ctx.runMutation(component.clientManagement.verifyClientSecret, args),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* HTTP Handlers for mounting in http.ts
|
|
117
|
+
*
|
|
118
|
+
* Note: ctx expects Convex ActionCtx (HTTP Action context).
|
|
119
|
+
* RunActionCtx is used as the base type for compatibility.
|
|
120
|
+
*/
|
|
121
|
+
get handlers() {
|
|
122
|
+
return {
|
|
123
|
+
/**
|
|
124
|
+
* OpenID Connect Discovery
|
|
125
|
+
* Mount at: /oauth/.well-known/openid-configuration
|
|
126
|
+
*/
|
|
127
|
+
openIdConfiguration: (ctx: RunActionCtx, request: Request) =>
|
|
128
|
+
openIdConfigurationHandler(ctx as Parameters<typeof openIdConfigurationHandler>[0], request, this.config),
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Authorization Endpoint
|
|
132
|
+
* Mount at: /oauth/authorize
|
|
133
|
+
*/
|
|
134
|
+
authorize: (ctx: RunActionCtx, request: Request) =>
|
|
135
|
+
authorizeHandler(ctx as Parameters<typeof authorizeHandler>[0], request, this.config, this.api),
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* JWKS Endpoint
|
|
139
|
+
* Mount at: /oauth/.well-known/jwks.json
|
|
140
|
+
*/
|
|
141
|
+
jwks: (ctx: RunActionCtx, request: Request) =>
|
|
142
|
+
jwksHandler(ctx as Parameters<typeof jwksHandler>[0], request, this.config),
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Token Endpoint
|
|
146
|
+
* Mount at: /oauth/token
|
|
147
|
+
*/
|
|
148
|
+
token: (ctx: RunActionCtx, request: Request) =>
|
|
149
|
+
tokenHandler(ctx as Parameters<typeof tokenHandler>[0], request, this.config, this.api),
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* UserInfo Endpoint
|
|
153
|
+
* Mount at: /oauth/userinfo
|
|
154
|
+
* Requires getUserProfile callback
|
|
155
|
+
*/
|
|
156
|
+
userInfo: (ctx: RunActionCtx, request: Request, getUserProfile: (userId: string) => Promise<UserProfile | null>) =>
|
|
157
|
+
userInfoHandler(ctx as Parameters<typeof userInfoHandler>[0], request, this.config, getUserProfile),
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Dynamic Client Registration
|
|
161
|
+
* Mount at: /oauth/register
|
|
162
|
+
*/
|
|
163
|
+
register: (ctx: RunActionCtx, request: Request) =>
|
|
164
|
+
registerHandler(ctx as Parameters<typeof registerHandler>[0], request, this.config, this.api),
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Protected Resource Metadata
|
|
168
|
+
* Mount at: /.well-known/oauth-protected-resource
|
|
169
|
+
*/
|
|
170
|
+
protectedResource: (ctx: RunActionCtx, request: Request) =>
|
|
171
|
+
oauthProtectedResourceHandler(ctx as Parameters<typeof oauthProtectedResourceHandler>[0], request, this.config),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Issue Authorization Code
|
|
177
|
+
* Called from consent approval mutation
|
|
178
|
+
* Also creates/updates authorization record automatically
|
|
179
|
+
*/
|
|
180
|
+
async issueAuthorizationCode(ctx: RunMutationCtx, args: {
|
|
181
|
+
userId: string;
|
|
182
|
+
clientId: string;
|
|
183
|
+
scopes: string[];
|
|
184
|
+
redirectUri: string;
|
|
185
|
+
codeChallenge?: string;
|
|
186
|
+
codeChallengeMethod?: string;
|
|
187
|
+
nonce?: string;
|
|
188
|
+
}): Promise<string> {
|
|
189
|
+
if (!args.codeChallenge) {
|
|
190
|
+
throw new Error("codeChallenge required");
|
|
191
|
+
}
|
|
192
|
+
const codeChallengeMethod = args.codeChallengeMethod ?? "S256";
|
|
193
|
+
if (codeChallengeMethod !== "S256") {
|
|
194
|
+
throw new Error("codeChallengeMethod must be S256");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 1. Create/update authorization record (user consented)
|
|
198
|
+
await this.api.mutations.upsertAuthorization(ctx, {
|
|
199
|
+
userId: args.userId,
|
|
200
|
+
clientId: args.clientId,
|
|
201
|
+
scopes: args.scopes,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// 2. Issue the authorization code
|
|
205
|
+
return this.api.mutations.issueAuthorizationCode(ctx, {
|
|
206
|
+
...args,
|
|
207
|
+
codeChallenge: args.codeChallenge,
|
|
208
|
+
codeChallengeMethod,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get OAuth Client
|
|
214
|
+
*/
|
|
215
|
+
async getClient(ctx: RunQueryCtx, clientId: string) {
|
|
216
|
+
return this.api.queries.getClient(ctx, { clientId });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Register OAuth Client (for admin use)
|
|
221
|
+
*/
|
|
222
|
+
async registerClient(ctx: RunMutationCtx, args: {
|
|
223
|
+
name: string;
|
|
224
|
+
redirectUris: string[];
|
|
225
|
+
scopes: string[];
|
|
226
|
+
type: "confidential" | "public";
|
|
227
|
+
website?: string;
|
|
228
|
+
logoUrl?: string;
|
|
229
|
+
tosUrl?: string;
|
|
230
|
+
policyUrl?: string;
|
|
231
|
+
}) {
|
|
232
|
+
return this.api.clientManagement.registerClient(ctx, args);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get user's active tokens
|
|
237
|
+
*/
|
|
238
|
+
async getTokensByUser(ctx: RunQueryCtx, userId: string) {
|
|
239
|
+
return this.api.queries.getTokensByUser(ctx, { userId });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// -------------------------------------------------------------------------
|
|
243
|
+
// Authorization Management
|
|
244
|
+
// -------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get authorization for a specific user-client pair
|
|
248
|
+
* Returns null if user has not authorized this client
|
|
249
|
+
*/
|
|
250
|
+
async getAuthorization(ctx: RunQueryCtx, userId: string, clientId: string) {
|
|
251
|
+
return ctx.runQuery(this.component.queries.getAuthorization, { userId, clientId });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* List all authorized apps for a user
|
|
256
|
+
* Returns client info along with authorization details
|
|
257
|
+
*/
|
|
258
|
+
async listUserAuthorizations(ctx: RunQueryCtx, userId: string) {
|
|
259
|
+
return ctx.runQuery(this.component.queries.listUserAuthorizations, { userId });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create or update authorization when user grants consent
|
|
264
|
+
* Call this when user approves OAuth consent
|
|
265
|
+
*/
|
|
266
|
+
async upsertAuthorization(ctx: RunMutationCtx, args: {
|
|
267
|
+
userId: string;
|
|
268
|
+
clientId: string;
|
|
269
|
+
scopes: string[];
|
|
270
|
+
}) {
|
|
271
|
+
return ctx.runMutation(this.component.mutations.upsertAuthorization, args);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Revoke authorization and delete all associated tokens
|
|
276
|
+
* Call this when user wants to disconnect an app
|
|
277
|
+
*/
|
|
278
|
+
async revokeAuthorization(ctx: RunMutationCtx, userId: string, clientId: string) {
|
|
279
|
+
return ctx.runMutation(this.component.mutations.revokeAuthorization, { userId, clientId });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check if user has already authorized this client with sufficient scopes
|
|
284
|
+
* Useful for "skip consent" flow
|
|
285
|
+
*/
|
|
286
|
+
async hasAuthorization(ctx: RunQueryCtx, userId: string, clientId: string, requiredScopes: string[]): Promise<boolean> {
|
|
287
|
+
const auth = await this.getAuthorization(ctx, userId, clientId);
|
|
288
|
+
if (!auth) return false;
|
|
289
|
+
|
|
290
|
+
// Check if all required scopes are authorized
|
|
291
|
+
return requiredScopes.every(scope => auth.scopes.includes(scope));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check if authorization exists (for revocation check)
|
|
296
|
+
* Use this with createAuthHelper's checkAuthorization option
|
|
297
|
+
*/
|
|
298
|
+
async checkAuthorizationValid(ctx: RunQueryCtx, userId: string, clientId?: string): Promise<boolean> {
|
|
299
|
+
if (clientId) {
|
|
300
|
+
// Check specific client authorization
|
|
301
|
+
return ctx.runQuery(this.component.queries.hasAuthorization, { userId, clientId });
|
|
302
|
+
} else {
|
|
303
|
+
// Check if user has any authorization
|
|
304
|
+
return ctx.runQuery(this.component.queries.hasAnyAuthorization, { userId });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Create a checkAuthorization function for use with createAuthHelper
|
|
310
|
+
* This ensures revoked authorizations are rejected
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* const oauthProvider = new OAuthProvider(components.oauthProvider, config);
|
|
315
|
+
* const authHelper = createAuthHelper({
|
|
316
|
+
* providers: ["anonymous"],
|
|
317
|
+
* checkAuthorization: oauthProvider.createAuthorizationChecker(),
|
|
318
|
+
* });
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
createAuthorizationChecker() {
|
|
322
|
+
return async (ctx: RunQueryCtx, userId: string, clientId?: string): Promise<boolean> => {
|
|
323
|
+
return this.checkAuthorizationValid(ctx, userId, clientId);
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|