clearauth 0.3.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/CHANGELOG.md +235 -0
- package/LICENSE +21 -0
- package/README.md +417 -0
- package/dist/auth/handler.d.ts +38 -0
- package/dist/auth/handler.js +483 -0
- package/dist/auth/handler.js.map +1 -0
- package/dist/auth/login.d.ts +69 -0
- package/dist/auth/login.js +103 -0
- package/dist/auth/login.js.map +1 -0
- package/dist/auth/register.d.ts +72 -0
- package/dist/auth/register.js +122 -0
- package/dist/auth/register.js.map +1 -0
- package/dist/auth/reset-password.d.ts +106 -0
- package/dist/auth/reset-password.js +213 -0
- package/dist/auth/reset-password.js.map +1 -0
- package/dist/auth/utils.d.ts +58 -0
- package/dist/auth/utils.js +121 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/auth/verify-email.d.ts +70 -0
- package/dist/auth/verify-email.js +137 -0
- package/dist/auth/verify-email.js.map +1 -0
- package/dist/createMechAuth.d.ts +178 -0
- package/dist/createMechAuth.js +215 -0
- package/dist/createMechAuth.js.map +1 -0
- package/dist/database/schema.d.ts +135 -0
- package/dist/database/schema.js +37 -0
- package/dist/database/schema.js.map +1 -0
- package/dist/edge.d.ts +4 -0
- package/dist/edge.js +6 -0
- package/dist/edge.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +44 -0
- package/dist/errors.js.map +1 -0
- package/dist/handler.d.ts +100 -0
- package/dist/handler.js +213 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +22 -0
- package/dist/logger.js +40 -0
- package/dist/logger.js.map +1 -0
- package/dist/mech-kysely.d.ts +22 -0
- package/dist/mech-kysely.js +88 -0
- package/dist/mech-kysely.js.map +1 -0
- package/dist/mech-sql-client.d.ts +85 -0
- package/dist/mech-sql-client.js +155 -0
- package/dist/mech-sql-client.js.map +1 -0
- package/dist/node.d.ts +4 -0
- package/dist/node.js +10 -0
- package/dist/node.js.map +1 -0
- package/dist/oauth/arctic-providers.d.ts +60 -0
- package/dist/oauth/arctic-providers.js +94 -0
- package/dist/oauth/arctic-providers.js.map +1 -0
- package/dist/oauth/callbacks.d.ts +155 -0
- package/dist/oauth/callbacks.js +286 -0
- package/dist/oauth/callbacks.js.map +1 -0
- package/dist/oauth/github.d.ts +47 -0
- package/dist/oauth/github.js +136 -0
- package/dist/oauth/github.js.map +1 -0
- package/dist/oauth/google.d.ts +49 -0
- package/dist/oauth/google.js +104 -0
- package/dist/oauth/google.js.map +1 -0
- package/dist/oauth/handler.d.ts +31 -0
- package/dist/oauth/handler.js +277 -0
- package/dist/oauth/handler.js.map +1 -0
- package/dist/password-hasher-argon2.d.ts +7 -0
- package/dist/password-hasher-argon2.js +16 -0
- package/dist/password-hasher-argon2.js.map +1 -0
- package/dist/password-hasher.d.ts +12 -0
- package/dist/password-hasher.js +115 -0
- package/dist/password-hasher.js.map +1 -0
- package/dist/react.d.ts +152 -0
- package/dist/react.js +296 -0
- package/dist/react.js.map +1 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/cors.d.ts +65 -0
- package/dist/utils/cors.js +152 -0
- package/dist/utils/cors.js.map +1 -0
- package/dist/utils/normalize-auth-path.d.ts +1 -0
- package/dist/utils/normalize-auth-path.js +8 -0
- package/dist/utils/normalize-auth-path.js.map +1 -0
- package/dist/validation.d.ts +23 -0
- package/dist/validation.js +70 -0
- package/dist/validation.js.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Callback Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for OAuth callback handling including:
|
|
5
|
+
* - State parameter validation (CSRF protection)
|
|
6
|
+
* - User upsert (create or update user by OAuth provider ID)
|
|
7
|
+
* - Session creation after successful OAuth
|
|
8
|
+
* - Error handling
|
|
9
|
+
*/
|
|
10
|
+
import { base64url } from 'oslo/encoding';
|
|
11
|
+
/**
|
|
12
|
+
* Generate a secure random session ID
|
|
13
|
+
* @param entropySize Number of bytes of entropy (default: 25 = 200 bits)
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
function generateSessionId(entropySize = 25) {
|
|
17
|
+
const bytes = new Uint8Array(entropySize);
|
|
18
|
+
crypto.getRandomValues(bytes);
|
|
19
|
+
return base64url.encode(bytes).replace(/=/g, '');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Upsert user from OAuth profile
|
|
23
|
+
*
|
|
24
|
+
* Creates a new user or updates an existing user based on the OAuth provider ID.
|
|
25
|
+
* Uses github_id or google_id column to identify existing users.
|
|
26
|
+
*
|
|
27
|
+
* @param db - Kysely database instance
|
|
28
|
+
* @param provider - OAuth provider name ('github' or 'google')
|
|
29
|
+
* @param profile - Normalized OAuth user profile
|
|
30
|
+
* @returns User record from database
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const user = await upsertOAuthUser(db, 'github', profile)
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export async function upsertOAuthUser(db, provider, profile) {
|
|
38
|
+
const providerIdColumn = provider === 'github' ? 'github_id' : 'google_id';
|
|
39
|
+
// Check if user exists by provider ID
|
|
40
|
+
const existingUser = await db
|
|
41
|
+
.selectFrom('users')
|
|
42
|
+
.selectAll()
|
|
43
|
+
.where(providerIdColumn, '=', profile.id)
|
|
44
|
+
.executeTakeFirst();
|
|
45
|
+
if (existingUser) {
|
|
46
|
+
// Update existing user with latest profile data
|
|
47
|
+
const updatedUser = await db
|
|
48
|
+
.updateTable('users')
|
|
49
|
+
.set({
|
|
50
|
+
email: profile.email,
|
|
51
|
+
name: profile.name,
|
|
52
|
+
avatar_url: profile.avatar_url,
|
|
53
|
+
email_verified: profile.email_verified ?? existingUser.email_verified,
|
|
54
|
+
})
|
|
55
|
+
.where('id', '=', existingUser.id)
|
|
56
|
+
.returningAll()
|
|
57
|
+
.executeTakeFirstOrThrow();
|
|
58
|
+
return updatedUser;
|
|
59
|
+
}
|
|
60
|
+
// Check if user exists by email (linking accounts)
|
|
61
|
+
const userByEmail = await db
|
|
62
|
+
.selectFrom('users')
|
|
63
|
+
.selectAll()
|
|
64
|
+
.where('email', '=', profile.email)
|
|
65
|
+
.executeTakeFirst();
|
|
66
|
+
if (userByEmail) {
|
|
67
|
+
// Link OAuth provider to existing email account
|
|
68
|
+
const updatedUser = await db
|
|
69
|
+
.updateTable('users')
|
|
70
|
+
.set({
|
|
71
|
+
[providerIdColumn]: profile.id,
|
|
72
|
+
name: profile.name || userByEmail.name,
|
|
73
|
+
avatar_url: profile.avatar_url || userByEmail.avatar_url,
|
|
74
|
+
email_verified: profile.email_verified || userByEmail.email_verified,
|
|
75
|
+
})
|
|
76
|
+
.where('id', '=', userByEmail.id)
|
|
77
|
+
.returningAll()
|
|
78
|
+
.executeTakeFirstOrThrow();
|
|
79
|
+
return updatedUser;
|
|
80
|
+
}
|
|
81
|
+
// Create new user
|
|
82
|
+
const newUser = {
|
|
83
|
+
email: profile.email,
|
|
84
|
+
email_verified: profile.email_verified ?? false,
|
|
85
|
+
password_hash: null, // OAuth-only user
|
|
86
|
+
[providerIdColumn]: profile.id,
|
|
87
|
+
name: profile.name,
|
|
88
|
+
avatar_url: profile.avatar_url,
|
|
89
|
+
};
|
|
90
|
+
const createdUser = await db
|
|
91
|
+
.insertInto('users')
|
|
92
|
+
.values(newUser)
|
|
93
|
+
.returningAll()
|
|
94
|
+
.executeTakeFirstOrThrow();
|
|
95
|
+
return createdUser;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create session for user
|
|
99
|
+
*
|
|
100
|
+
* Creates a new session record in the database and returns the session ID.
|
|
101
|
+
* Sessions expire after the configured duration (default: 30 days).
|
|
102
|
+
*
|
|
103
|
+
* @param db - Kysely database instance
|
|
104
|
+
* @param userId - User ID to create session for
|
|
105
|
+
* @param expiresInSeconds - Session expiration time in seconds (default: 2592000 = 30 days)
|
|
106
|
+
* @param context - Optional request context (IP address, user agent)
|
|
107
|
+
* @returns Session ID
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* const sessionId = await createSession(db, user.id, 2592000, { ipAddress, userAgent })
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export async function createSession(db, userId, expiresInSeconds = 2592000, // 30 days
|
|
115
|
+
context) {
|
|
116
|
+
// Generate secure random session ID
|
|
117
|
+
const sessionId = generateSessionId(25); // 200 bits of entropy
|
|
118
|
+
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
|
|
119
|
+
const newSession = {
|
|
120
|
+
id: sessionId,
|
|
121
|
+
user_id: userId,
|
|
122
|
+
expires_at: expiresAt,
|
|
123
|
+
ip_address: context?.ipAddress || null,
|
|
124
|
+
user_agent: context?.userAgent || null,
|
|
125
|
+
};
|
|
126
|
+
await db.insertInto('sessions').values(newSession).execute();
|
|
127
|
+
return sessionId;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Validate session
|
|
131
|
+
*
|
|
132
|
+
* Checks if a session exists and is not expired.
|
|
133
|
+
*
|
|
134
|
+
* @param db - Kysely database instance
|
|
135
|
+
* @param sessionId - Session ID to validate
|
|
136
|
+
* @returns User if session is valid, null otherwise
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* const user = await validateSession(db, sessionId)
|
|
141
|
+
* if (!user) {
|
|
142
|
+
* return new Response('Unauthorized', { status: 401 })
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export async function validateSession(db, sessionId) {
|
|
147
|
+
const result = await db
|
|
148
|
+
.selectFrom('sessions')
|
|
149
|
+
.innerJoin('users', 'users.id', 'sessions.user_id')
|
|
150
|
+
.selectAll('users')
|
|
151
|
+
.where('sessions.id', '=', sessionId)
|
|
152
|
+
.where('sessions.expires_at', '>', new Date())
|
|
153
|
+
.executeTakeFirst();
|
|
154
|
+
return result || null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Delete session (logout)
|
|
158
|
+
*
|
|
159
|
+
* Removes a session from the database.
|
|
160
|
+
*
|
|
161
|
+
* @param db - Kysely database instance
|
|
162
|
+
* @param sessionId - Session ID to delete
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* await deleteSession(db, sessionId)
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export async function deleteSession(db, sessionId) {
|
|
170
|
+
await db.deleteFrom('sessions').where('id', '=', sessionId).execute();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Delete all sessions for a user
|
|
174
|
+
*
|
|
175
|
+
* Removes all sessions for a specific user (useful for password changes, etc.)
|
|
176
|
+
*
|
|
177
|
+
* @param db - Kysely database instance
|
|
178
|
+
* @param userId - User ID to delete sessions for
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* await deleteAllUserSessions(db, userId)
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export async function deleteAllUserSessions(db, userId) {
|
|
186
|
+
await db.deleteFrom('sessions').where('user_id', '=', userId).execute();
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Clean up expired sessions
|
|
190
|
+
*
|
|
191
|
+
* Removes all expired sessions from the database.
|
|
192
|
+
* This should be run periodically as a background job.
|
|
193
|
+
*
|
|
194
|
+
* @param db - Kysely database instance
|
|
195
|
+
* @returns Number of sessions deleted
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* const deleted = await cleanupExpiredSessions(db)
|
|
200
|
+
* console.log(`Cleaned up ${deleted} expired sessions`)
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export async function cleanupExpiredSessions(db) {
|
|
204
|
+
const result = await db
|
|
205
|
+
.deleteFrom('sessions')
|
|
206
|
+
.where('expires_at', '<=', new Date())
|
|
207
|
+
.executeTakeFirst();
|
|
208
|
+
return Number(result.numDeletedRows ?? 0);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Parse cookie header
|
|
212
|
+
*
|
|
213
|
+
* Parses the Cookie header and returns a map of cookie names to values.
|
|
214
|
+
*
|
|
215
|
+
* @param cookieHeader - Cookie header string
|
|
216
|
+
* @returns Map of cookie names to values
|
|
217
|
+
*
|
|
218
|
+
* @internal
|
|
219
|
+
*/
|
|
220
|
+
export function parseCookies(cookieHeader) {
|
|
221
|
+
const cookies = {};
|
|
222
|
+
if (!cookieHeader) {
|
|
223
|
+
return cookies;
|
|
224
|
+
}
|
|
225
|
+
const pairs = cookieHeader.split(';');
|
|
226
|
+
for (const pair of pairs) {
|
|
227
|
+
const [name, value] = pair.trim().split('=');
|
|
228
|
+
if (name && value) {
|
|
229
|
+
cookies[name] = decodeURIComponent(value);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return cookies;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Create cookie header
|
|
236
|
+
*
|
|
237
|
+
* Creates a Set-Cookie header string with appropriate security attributes.
|
|
238
|
+
*
|
|
239
|
+
* @param name - Cookie name
|
|
240
|
+
* @param value - Cookie value
|
|
241
|
+
* @param options - Cookie options
|
|
242
|
+
* @returns Set-Cookie header string
|
|
243
|
+
*
|
|
244
|
+
* @internal
|
|
245
|
+
*/
|
|
246
|
+
export function createCookieHeader(name, value, options = {}) {
|
|
247
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
248
|
+
if (options.httpOnly !== false) {
|
|
249
|
+
parts.push('HttpOnly');
|
|
250
|
+
}
|
|
251
|
+
if (options.secure !== false) {
|
|
252
|
+
parts.push('Secure');
|
|
253
|
+
}
|
|
254
|
+
if (options.sameSite) {
|
|
255
|
+
parts.push(`SameSite=${options.sameSite.charAt(0).toUpperCase()}${options.sameSite.slice(1)}`);
|
|
256
|
+
}
|
|
257
|
+
if (options.path) {
|
|
258
|
+
parts.push(`Path=${options.path}`);
|
|
259
|
+
}
|
|
260
|
+
if (options.maxAge !== undefined) {
|
|
261
|
+
parts.push(`Max-Age=${options.maxAge}`);
|
|
262
|
+
}
|
|
263
|
+
if (options.expires) {
|
|
264
|
+
parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
265
|
+
}
|
|
266
|
+
return parts.join('; ');
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Create delete cookie header
|
|
270
|
+
*
|
|
271
|
+
* Creates a Set-Cookie header that deletes a cookie.
|
|
272
|
+
*
|
|
273
|
+
* @param name - Cookie name
|
|
274
|
+
* @param options - Cookie options (path, etc.)
|
|
275
|
+
* @returns Set-Cookie header string
|
|
276
|
+
*
|
|
277
|
+
* @internal
|
|
278
|
+
*/
|
|
279
|
+
export function createDeleteCookieHeader(name, options = {}) {
|
|
280
|
+
return createCookieHeader(name, '', {
|
|
281
|
+
...options,
|
|
282
|
+
maxAge: 0,
|
|
283
|
+
expires: new Date(0),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=callbacks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callbacks.js","sourceRoot":"","sources":["../../src/oauth/callbacks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Flow Implementation
|
|
3
|
+
*
|
|
4
|
+
* Handles GitHub OAuth authentication using Arctic.
|
|
5
|
+
* Implements authorization URL generation and callback handling.
|
|
6
|
+
*/
|
|
7
|
+
import type { ClearAuthConfig, OAuthCallbackResult } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generate GitHub OAuth authorization URL
|
|
10
|
+
*
|
|
11
|
+
* Creates the URL to redirect users to GitHub for authentication.
|
|
12
|
+
* Includes a state parameter for CSRF protection.
|
|
13
|
+
*
|
|
14
|
+
* @param config - Mech Auth configuration
|
|
15
|
+
* @returns Object containing the authorization URL and state parameter
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const { url, state } = await generateGitHubAuthUrl(config)
|
|
20
|
+
* // Store state in cookie for validation
|
|
21
|
+
* // Redirect user to url
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateGitHubAuthUrl(config: ClearAuthConfig): Promise<{
|
|
25
|
+
url: URL;
|
|
26
|
+
state: string;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Handle GitHub OAuth callback
|
|
30
|
+
*
|
|
31
|
+
* Validates the OAuth callback, exchanges the authorization code for tokens,
|
|
32
|
+
* and fetches the user's profile from GitHub.
|
|
33
|
+
*
|
|
34
|
+
* @param config - Mech Auth configuration
|
|
35
|
+
* @param code - Authorization code from GitHub
|
|
36
|
+
* @param storedState - State parameter stored in cookie
|
|
37
|
+
* @param returnedState - State parameter returned by GitHub
|
|
38
|
+
* @returns OAuth callback result with user profile and tokens
|
|
39
|
+
* @throws Error if state validation fails or API requests fail
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const result = await handleGitHubCallback(config, code, storedState, returnedState)
|
|
44
|
+
* // Use result.profile to create or update user
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function handleGitHubCallback(config: ClearAuthConfig, code: string, storedState: string, returnedState: string): Promise<OAuthCallbackResult>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Flow Implementation
|
|
3
|
+
*
|
|
4
|
+
* Handles GitHub OAuth authentication using Arctic.
|
|
5
|
+
* Implements authorization URL generation and callback handling.
|
|
6
|
+
*/
|
|
7
|
+
import { generateState } from 'arctic';
|
|
8
|
+
import { createGitHubProvider } from './arctic-providers.js';
|
|
9
|
+
/**
|
|
10
|
+
* Generate GitHub OAuth authorization URL
|
|
11
|
+
*
|
|
12
|
+
* Creates the URL to redirect users to GitHub for authentication.
|
|
13
|
+
* Includes a state parameter for CSRF protection.
|
|
14
|
+
*
|
|
15
|
+
* @param config - Mech Auth configuration
|
|
16
|
+
* @returns Object containing the authorization URL and state parameter
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const { url, state } = await generateGitHubAuthUrl(config)
|
|
21
|
+
* // Store state in cookie for validation
|
|
22
|
+
* // Redirect user to url
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function generateGitHubAuthUrl(config) {
|
|
26
|
+
const github = createGitHubProvider(config);
|
|
27
|
+
const state = generateState();
|
|
28
|
+
// Request user:email scope to access user's email addresses
|
|
29
|
+
const url = github.createAuthorizationURL(state, ['user:email']);
|
|
30
|
+
return { url, state };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Handle GitHub OAuth callback
|
|
34
|
+
*
|
|
35
|
+
* Validates the OAuth callback, exchanges the authorization code for tokens,
|
|
36
|
+
* and fetches the user's profile from GitHub.
|
|
37
|
+
*
|
|
38
|
+
* @param config - Mech Auth configuration
|
|
39
|
+
* @param code - Authorization code from GitHub
|
|
40
|
+
* @param storedState - State parameter stored in cookie
|
|
41
|
+
* @param returnedState - State parameter returned by GitHub
|
|
42
|
+
* @returns OAuth callback result with user profile and tokens
|
|
43
|
+
* @throws Error if state validation fails or API requests fail
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const result = await handleGitHubCallback(config, code, storedState, returnedState)
|
|
48
|
+
* // Use result.profile to create or update user
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export async function handleGitHubCallback(config, code, storedState, returnedState) {
|
|
52
|
+
// Validate state parameter (CSRF protection)
|
|
53
|
+
if (storedState !== returnedState) {
|
|
54
|
+
throw new Error('Invalid OAuth state parameter');
|
|
55
|
+
}
|
|
56
|
+
const github = createGitHubProvider(config);
|
|
57
|
+
// Exchange authorization code for access token
|
|
58
|
+
const tokens = await github.validateAuthorizationCode(code);
|
|
59
|
+
const accessToken = tokens.accessToken();
|
|
60
|
+
// Fetch user profile from GitHub API
|
|
61
|
+
const profile = await fetchGitHubUserProfile(accessToken);
|
|
62
|
+
return {
|
|
63
|
+
profile,
|
|
64
|
+
accessToken,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Fetch user profile from GitHub API
|
|
69
|
+
*
|
|
70
|
+
* Retrieves the user's profile information and email addresses from GitHub.
|
|
71
|
+
* If the user's email is not public, fetches it from the emails endpoint.
|
|
72
|
+
*
|
|
73
|
+
* @param accessToken - GitHub access token
|
|
74
|
+
* @returns Normalized user profile
|
|
75
|
+
* @throws Error if API requests fail or email cannot be found
|
|
76
|
+
*
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
async function fetchGitHubUserProfile(accessToken) {
|
|
80
|
+
// Fetch user profile
|
|
81
|
+
const userResponse = await fetch('https://api.github.com/user', {
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${accessToken}`,
|
|
84
|
+
Accept: 'application/vnd.github.v3+json',
|
|
85
|
+
'User-Agent': 'Mech-Auth',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
if (!userResponse.ok) {
|
|
89
|
+
throw new Error(`GitHub API error: ${userResponse.status} ${userResponse.statusText}`);
|
|
90
|
+
}
|
|
91
|
+
const user = await userResponse.json();
|
|
92
|
+
// If email is not public, fetch from emails endpoint
|
|
93
|
+
let email = user.email;
|
|
94
|
+
let emailVerified = false;
|
|
95
|
+
if (!email) {
|
|
96
|
+
const emailsResponse = await fetch('https://api.github.com/user/emails', {
|
|
97
|
+
headers: {
|
|
98
|
+
Authorization: `Bearer ${accessToken}`,
|
|
99
|
+
Accept: 'application/vnd.github.v3+json',
|
|
100
|
+
'User-Agent': 'Mech-Auth',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
if (!emailsResponse.ok) {
|
|
104
|
+
throw new Error(`GitHub API error (emails): ${emailsResponse.status} ${emailsResponse.statusText}`);
|
|
105
|
+
}
|
|
106
|
+
const emails = await emailsResponse.json();
|
|
107
|
+
// Find primary verified email
|
|
108
|
+
const primaryEmail = emails.find((e) => e.primary && e.verified);
|
|
109
|
+
if (primaryEmail) {
|
|
110
|
+
email = primaryEmail.email;
|
|
111
|
+
emailVerified = primaryEmail.verified;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Fall back to first verified email
|
|
115
|
+
const verifiedEmail = emails.find((e) => e.verified);
|
|
116
|
+
if (verifiedEmail) {
|
|
117
|
+
email = verifiedEmail.email;
|
|
118
|
+
emailVerified = verifiedEmail.verified;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
throw new Error('No verified email found in GitHub account');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!email) {
|
|
126
|
+
throw new Error('No email found in GitHub account');
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
id: user.id.toString(),
|
|
130
|
+
email,
|
|
131
|
+
name: user.name,
|
|
132
|
+
avatar_url: user.avatar_url,
|
|
133
|
+
email_verified: emailVerified,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/oauth/github.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth Flow Implementation
|
|
3
|
+
*
|
|
4
|
+
* Handles Google OAuth authentication using Arctic.
|
|
5
|
+
* Implements authorization URL generation and callback handling with PKCE.
|
|
6
|
+
*/
|
|
7
|
+
import type { ClearAuthConfig, OAuthCallbackResult } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generate Google OAuth authorization URL
|
|
10
|
+
*
|
|
11
|
+
* Creates the URL to redirect users to Google for authentication.
|
|
12
|
+
* Includes state and code verifier parameters for CSRF protection and PKCE.
|
|
13
|
+
*
|
|
14
|
+
* @param config - Mech Auth configuration
|
|
15
|
+
* @returns Object containing the authorization URL, state parameter, and code verifier
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const { url, state, codeVerifier } = await generateGoogleAuthUrl(config)
|
|
20
|
+
* // Store state and codeVerifier in cookies for validation
|
|
21
|
+
* // Redirect user to url
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateGoogleAuthUrl(config: ClearAuthConfig): Promise<{
|
|
25
|
+
url: URL;
|
|
26
|
+
state: string;
|
|
27
|
+
codeVerifier: string;
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Handle Google OAuth callback
|
|
31
|
+
*
|
|
32
|
+
* Validates the OAuth callback, exchanges the authorization code for tokens,
|
|
33
|
+
* and fetches the user's profile from Google.
|
|
34
|
+
*
|
|
35
|
+
* @param config - Mech Auth configuration
|
|
36
|
+
* @param code - Authorization code from Google
|
|
37
|
+
* @param storedState - State parameter stored in cookie
|
|
38
|
+
* @param returnedState - State parameter returned by Google
|
|
39
|
+
* @param codeVerifier - Code verifier stored in cookie (for PKCE)
|
|
40
|
+
* @returns OAuth callback result with user profile and tokens
|
|
41
|
+
* @throws Error if state validation fails or API requests fail
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const result = await handleGoogleCallback(config, code, storedState, returnedState, codeVerifier)
|
|
46
|
+
* // Use result.profile to create or update user
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function handleGoogleCallback(config: ClearAuthConfig, code: string, storedState: string, returnedState: string, codeVerifier: string): Promise<OAuthCallbackResult>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth Flow Implementation
|
|
3
|
+
*
|
|
4
|
+
* Handles Google OAuth authentication using Arctic.
|
|
5
|
+
* Implements authorization URL generation and callback handling with PKCE.
|
|
6
|
+
*/
|
|
7
|
+
import { generateState, generateCodeVerifier } from 'arctic';
|
|
8
|
+
import { createGoogleProvider } from './arctic-providers.js';
|
|
9
|
+
/**
|
|
10
|
+
* Generate Google OAuth authorization URL
|
|
11
|
+
*
|
|
12
|
+
* Creates the URL to redirect users to Google for authentication.
|
|
13
|
+
* Includes state and code verifier parameters for CSRF protection and PKCE.
|
|
14
|
+
*
|
|
15
|
+
* @param config - Mech Auth configuration
|
|
16
|
+
* @returns Object containing the authorization URL, state parameter, and code verifier
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const { url, state, codeVerifier } = await generateGoogleAuthUrl(config)
|
|
21
|
+
* // Store state and codeVerifier in cookies for validation
|
|
22
|
+
* // Redirect user to url
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function generateGoogleAuthUrl(config) {
|
|
26
|
+
const google = createGoogleProvider(config);
|
|
27
|
+
const state = generateState();
|
|
28
|
+
const codeVerifier = generateCodeVerifier();
|
|
29
|
+
// Request email and profile scopes
|
|
30
|
+
const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'email', 'profile']);
|
|
31
|
+
return { url, state, codeVerifier };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Handle Google OAuth callback
|
|
35
|
+
*
|
|
36
|
+
* Validates the OAuth callback, exchanges the authorization code for tokens,
|
|
37
|
+
* and fetches the user's profile from Google.
|
|
38
|
+
*
|
|
39
|
+
* @param config - Mech Auth configuration
|
|
40
|
+
* @param code - Authorization code from Google
|
|
41
|
+
* @param storedState - State parameter stored in cookie
|
|
42
|
+
* @param returnedState - State parameter returned by Google
|
|
43
|
+
* @param codeVerifier - Code verifier stored in cookie (for PKCE)
|
|
44
|
+
* @returns OAuth callback result with user profile and tokens
|
|
45
|
+
* @throws Error if state validation fails or API requests fail
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const result = await handleGoogleCallback(config, code, storedState, returnedState, codeVerifier)
|
|
50
|
+
* // Use result.profile to create or update user
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export async function handleGoogleCallback(config, code, storedState, returnedState, codeVerifier) {
|
|
54
|
+
// Validate state parameter (CSRF protection)
|
|
55
|
+
if (storedState !== returnedState) {
|
|
56
|
+
throw new Error('Invalid OAuth state parameter');
|
|
57
|
+
}
|
|
58
|
+
const google = createGoogleProvider(config);
|
|
59
|
+
// Exchange authorization code for access token (with PKCE)
|
|
60
|
+
const tokens = await google.validateAuthorizationCode(code, codeVerifier);
|
|
61
|
+
const accessToken = tokens.accessToken();
|
|
62
|
+
const refreshToken = tokens.refreshToken();
|
|
63
|
+
// Fetch user profile from Google API
|
|
64
|
+
const profile = await fetchGoogleUserProfile(accessToken);
|
|
65
|
+
return {
|
|
66
|
+
profile,
|
|
67
|
+
accessToken,
|
|
68
|
+
refreshToken,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fetch user profile from Google API
|
|
73
|
+
*
|
|
74
|
+
* Retrieves the user's profile information from Google's userinfo endpoint.
|
|
75
|
+
*
|
|
76
|
+
* @param accessToken - Google access token
|
|
77
|
+
* @returns Normalized user profile
|
|
78
|
+
* @throws Error if API request fails
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
async function fetchGoogleUserProfile(accessToken) {
|
|
83
|
+
// Fetch user profile from Google's userinfo endpoint
|
|
84
|
+
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${accessToken}`,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`Google API error: ${response.status} ${response.statusText}`);
|
|
91
|
+
}
|
|
92
|
+
const user = await response.json();
|
|
93
|
+
if (!user.email) {
|
|
94
|
+
throw new Error('No email found in Google account');
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
id: user.sub,
|
|
98
|
+
email: user.email,
|
|
99
|
+
name: user.name || null,
|
|
100
|
+
avatar_url: user.picture || null,
|
|
101
|
+
email_verified: user.email_verified,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=google.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google.js","sourceRoot":"","sources":["../../src/oauth/google.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAA;AAE5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AAe5D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,MAAuB;IAKjE,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAA;IAC3C,MAAM,KAAK,GAAG,aAAa,EAAE,CAAA;IAC7B,MAAM,YAAY,GAAG,oBAAoB,EAAE,CAAA;IAE3C,mCAAmC;IACnC,MAAM,GAAG,GAAG,MAAM,CAAC,sBAAsB,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAA;IAE9F,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,YAAY,EAAE,CAAA;AACrC,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAuB,EACvB,IAAY,EACZ,WAAmB,EACnB,aAAqB,EACrB,YAAoB;IAEpB,6CAA6C;IAC7C,IAAI,WAAW,KAAK,aAAa,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAA;IAClD,CAAC;IAED,MAAM,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAA;IAE3C,2DAA2D;IAC3D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;IACzE,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,EAAE,CAAA;IACxC,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,EAAE,CAAA;IAE1C,qCAAqC;IACrC,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,WAAW,CAAC,CAAA;IAEzD,OAAO;QACL,OAAO;QACP,WAAW;QACX,YAAY;KACb,CAAA;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,KAAK,UAAU,sBAAsB,CAAC,WAAmB;IACvD,qDAAqD;IACrD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,+CAA+C,EAAE;QAC5E,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,WAAW,EAAE;SACvC;KACF,CAAC,CAAA;IAEF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,qBAAqB,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;IAChF,CAAC;IAED,MAAM,IAAI,GAAe,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;IAE9C,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;IACrD,CAAC;IAED,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,GAAG;QACZ,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;QACvB,UAAU,EAAE,IAAI,CAAC,OAAO,IAAI,IAAI;QAChC,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth HTTP Request Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles OAuth-related HTTP requests for GitHub and Google authentication.
|
|
5
|
+
* Provides login initiation and callback handling endpoints.
|
|
6
|
+
*/
|
|
7
|
+
import type { ClearAuthConfig } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* OAuth Request Handler
|
|
10
|
+
*
|
|
11
|
+
* Main handler for OAuth-related requests. Routes requests to appropriate
|
|
12
|
+
* provider handlers based on URL path.
|
|
13
|
+
*
|
|
14
|
+
* @param request - HTTP request
|
|
15
|
+
* @param config - Clear Auth configuration
|
|
16
|
+
* @returns HTTP response
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* export default {
|
|
21
|
+
* async fetch(request: Request, env: Env) {
|
|
22
|
+
* const url = new URL(request.url)
|
|
23
|
+
* if (url.pathname.startsWith('/auth/oauth/')) {
|
|
24
|
+
* return handleOAuthRequest(request, config)
|
|
25
|
+
* }
|
|
26
|
+
* // ... other routes
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function handleOAuthRequest(request: Request, config: ClearAuthConfig): Promise<Response>;
|