omgkit 2.2.0 → 2.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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/databases/mongodb/SKILL.md +60 -776
  3. package/plugin/skills/databases/prisma/SKILL.md +53 -744
  4. package/plugin/skills/databases/redis/SKILL.md +53 -860
  5. package/plugin/skills/devops/aws/SKILL.md +68 -672
  6. package/plugin/skills/devops/github-actions/SKILL.md +54 -657
  7. package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
  8. package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
  9. package/plugin/skills/frameworks/django/SKILL.md +87 -853
  10. package/plugin/skills/frameworks/express/SKILL.md +95 -1301
  11. package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
  12. package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
  13. package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
  14. package/plugin/skills/frameworks/react/SKILL.md +94 -962
  15. package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
  16. package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
  17. package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
  18. package/plugin/skills/frontend/responsive/SKILL.md +76 -799
  19. package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
  20. package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
  21. package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
  22. package/plugin/skills/languages/javascript/SKILL.md +106 -849
  23. package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
  24. package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
  25. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
  26. package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
  27. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
  28. package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
  29. package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
  30. package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
  31. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
  32. package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
  33. package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
  34. package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
  35. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
  36. package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
  37. package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
  38. package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
  39. package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
  40. package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
  41. package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
  42. package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
  43. package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
  44. package/plugin/skills/security/better-auth/SKILL.md +46 -1034
  45. package/plugin/skills/security/oauth/SKILL.md +80 -934
  46. package/plugin/skills/security/owasp/SKILL.md +78 -862
  47. package/plugin/skills/testing/playwright/SKILL.md +77 -700
  48. package/plugin/skills/testing/pytest/SKILL.md +73 -811
  49. package/plugin/skills/testing/vitest/SKILL.md +60 -920
  50. package/plugin/skills/tools/document-processing/SKILL.md +111 -838
  51. package/plugin/skills/tools/image-processing/SKILL.md +126 -659
  52. package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
  53. package/plugin/skills/tools/media-processing/SKILL.md +118 -735
  54. package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
  55. package/plugin/skills/SKILL_STANDARDS.md +0 -743
@@ -1,982 +1,129 @@
1
1
  ---
2
- name: oauth
3
- description: OAuth 2.0 and OpenID Connect implementation with authorization flows, token management, and security patterns
4
- category: security
5
- triggers:
6
- - oauth
7
- - oauth2
8
- - openid connect
9
- - oidc
10
- - social login
11
- - authorization
2
+ name: Implementing OAuth
3
+ description: Claude implements OAuth 2.0 and OpenID Connect authorization flows. Use when adding social login, integrating OAuth providers, managing tokens, or securing APIs with OAuth.
12
4
  ---
13
5
 
14
- # OAuth
6
+ # Implementing OAuth
15
7
 
16
- Enterprise-grade **OAuth 2.0 and OpenID Connect** implementation following industry best practices. This skill covers authorization flows, token management, provider integration, security patterns, and production-ready implementations used by top engineering teams.
17
-
18
- ## Purpose
19
-
20
- Build secure authorization systems:
21
-
22
- - Implement OAuth 2.0 authorization flows
23
- - Configure OpenID Connect for identity
24
- - Integrate social login providers
25
- - Manage access and refresh tokens
26
- - Implement PKCE for public clients
27
- - Handle token refresh and revocation
28
- - Secure API endpoints with OAuth
29
-
30
- ## Features
31
-
32
- ### 1. Authorization Code Flow with PKCE
8
+ ## Quick Start
33
9
 
34
10
  ```typescript
35
- // lib/oauth/pkce.ts
11
+ // lib/oauth/client.ts
36
12
  import crypto from "crypto";
37
13
 
38
- export interface PKCEPair {
39
- codeVerifier: string;
40
- codeChallenge: string;
41
- state: string;
42
- }
43
-
44
- export function generatePKCE(): PKCEPair {
45
- // Generate code verifier (43-128 characters)
14
+ export function generatePKCE() {
46
15
  const codeVerifier = crypto.randomBytes(32).toString("base64url");
47
-
48
- // Generate code challenge using SHA-256
49
- const codeChallenge = crypto
50
- .createHash("sha256")
51
- .update(codeVerifier)
52
- .digest("base64url");
53
-
54
- // Generate state for CSRF protection
16
+ const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
55
17
  const state = crypto.randomBytes(16).toString("hex");
56
-
57
18
  return { codeVerifier, codeChallenge, state };
58
19
  }
59
20
 
60
- // lib/oauth/client.ts
61
- import { PKCEPair, generatePKCE } from "./pkce";
62
-
63
- export interface OAuthConfig {
64
- clientId: string;
65
- clientSecret?: string;
66
- redirectUri: string;
67
- authorizationEndpoint: string;
68
- tokenEndpoint: string;
69
- userInfoEndpoint?: string;
70
- scopes: string[];
71
- }
72
-
73
- export interface TokenResponse {
74
- accessToken: string;
75
- tokenType: string;
76
- expiresIn: number;
77
- refreshToken?: string;
78
- idToken?: string;
79
- scope?: string;
21
+ export function buildAuthUrl(config: OAuthConfig, pkce: PKCEPair) {
22
+ const params = new URLSearchParams({
23
+ client_id: config.clientId,
24
+ redirect_uri: config.redirectUri,
25
+ response_type: "code",
26
+ scope: config.scopes.join(" "),
27
+ state: pkce.state,
28
+ code_challenge: pkce.codeChallenge,
29
+ code_challenge_method: "S256",
30
+ });
31
+ return `${config.authorizationEndpoint}?${params}`;
80
32
  }
33
+ ```
81
34
 
82
- export class OAuthClient {
83
- private config: OAuthConfig;
84
- private pkce: PKCEPair | null = null;
85
-
86
- constructor(config: OAuthConfig) {
87
- this.config = config;
88
- }
89
-
90
- generateAuthorizationUrl(options: {
91
- state?: string;
92
- nonce?: string;
93
- prompt?: "none" | "login" | "consent" | "select_account";
94
- loginHint?: string;
95
- } = {}): { url: string; pkce: PKCEPair } {
96
- this.pkce = generatePKCE();
97
-
98
- const params = new URLSearchParams({
99
- client_id: this.config.clientId,
100
- redirect_uri: this.config.redirectUri,
101
- response_type: "code",
102
- scope: this.config.scopes.join(" "),
103
- state: options.state || this.pkce.state,
104
- code_challenge: this.pkce.codeChallenge,
105
- code_challenge_method: "S256",
106
- });
35
+ ## Features
107
36
 
108
- if (options.nonce) params.set("nonce", options.nonce);
109
- if (options.prompt) params.set("prompt", options.prompt);
110
- if (options.loginHint) params.set("login_hint", options.loginHint);
37
+ | Feature | Description | Reference |
38
+ |---------|-------------|-----------|
39
+ | Authorization Code + PKCE | Secure flow for public/confidential clients | [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) |
40
+ | Token Management | Access/refresh token handling and storage | [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) |
41
+ | OpenID Connect | Identity layer with ID tokens and claims | [OIDC Core](https://openid.net/specs/openid-connect-core-1_0.html) |
42
+ | Provider Integration | Google, GitHub, Microsoft configurations | [OIDC Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) |
43
+ | JWT Validation | ID token signature and claims verification | [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) |
111
44
 
112
- const url = `${this.config.authorizationEndpoint}?${params.toString()}`;
45
+ ## Common Patterns
113
46
 
114
- return { url, pkce: this.pkce };
115
- }
47
+ ### Token Exchange
116
48
 
117
- async exchangeCodeForTokens(
118
- code: string,
119
- codeVerifier: string
120
- ): Promise<TokenResponse> {
121
- const body = new URLSearchParams({
49
+ ```typescript
50
+ async function exchangeCodeForTokens(code: string, codeVerifier: string) {
51
+ const response = await fetch(config.tokenEndpoint, {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
54
+ body: new URLSearchParams({
122
55
  grant_type: "authorization_code",
123
56
  code,
124
- redirect_uri: this.config.redirectUri,
125
- client_id: this.config.clientId,
57
+ redirect_uri: config.redirectUri,
58
+ client_id: config.clientId,
126
59
  code_verifier: codeVerifier,
127
- });
128
-
129
- // Add client secret for confidential clients
130
- if (this.config.clientSecret) {
131
- body.set("client_secret", this.config.clientSecret);
132
- }
133
-
134
- const response = await fetch(this.config.tokenEndpoint, {
135
- method: "POST",
136
- headers: {
137
- "Content-Type": "application/x-www-form-urlencoded",
138
- },
139
- body: body.toString(),
140
- });
141
-
142
- if (!response.ok) {
143
- const error = await response.json();
144
- throw new OAuthError(error.error, error.error_description);
145
- }
146
-
147
- const data = await response.json();
148
-
149
- return {
150
- accessToken: data.access_token,
151
- tokenType: data.token_type,
152
- expiresIn: data.expires_in,
153
- refreshToken: data.refresh_token,
154
- idToken: data.id_token,
155
- scope: data.scope,
156
- };
157
- }
158
-
159
- async refreshTokens(refreshToken: string): Promise<TokenResponse> {
160
- const body = new URLSearchParams({
161
- grant_type: "refresh_token",
162
- refresh_token: refreshToken,
163
- client_id: this.config.clientId,
164
- });
165
-
166
- if (this.config.clientSecret) {
167
- body.set("client_secret", this.config.clientSecret);
168
- }
169
-
170
- const response = await fetch(this.config.tokenEndpoint, {
171
- method: "POST",
172
- headers: {
173
- "Content-Type": "application/x-www-form-urlencoded",
174
- },
175
- body: body.toString(),
176
- });
177
-
178
- if (!response.ok) {
179
- const error = await response.json();
180
- throw new OAuthError(error.error, error.error_description);
181
- }
182
-
183
- const data = await response.json();
184
-
185
- return {
186
- accessToken: data.access_token,
187
- tokenType: data.token_type,
188
- expiresIn: data.expires_in,
189
- refreshToken: data.refresh_token || refreshToken,
190
- idToken: data.id_token,
191
- scope: data.scope,
192
- };
193
- }
194
-
195
- async getUserInfo(accessToken: string): Promise<Record<string, unknown>> {
196
- if (!this.config.userInfoEndpoint) {
197
- throw new Error("UserInfo endpoint not configured");
198
- }
199
-
200
- const response = await fetch(this.config.userInfoEndpoint, {
201
- headers: {
202
- Authorization: `Bearer ${accessToken}`,
203
- },
204
- });
205
-
206
- if (!response.ok) {
207
- throw new OAuthError("invalid_token", "Failed to fetch user info");
208
- }
209
-
210
- return response.json();
211
- }
212
- }
213
-
214
- export class OAuthError extends Error {
215
- constructor(
216
- public code: string,
217
- public description?: string
218
- ) {
219
- super(description || code);
220
- this.name = "OAuthError";
221
- }
222
- }
223
- ```
224
-
225
- ### 2. Provider Configuration
226
-
227
- ```typescript
228
- // lib/oauth/providers/google.ts
229
- import { OAuthConfig } from "../client";
230
-
231
- export function createGoogleConfig(options: {
232
- clientId: string;
233
- clientSecret: string;
234
- redirectUri: string;
235
- scopes?: string[];
236
- }): OAuthConfig {
237
- return {
238
- clientId: options.clientId,
239
- clientSecret: options.clientSecret,
240
- redirectUri: options.redirectUri,
241
- authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
242
- tokenEndpoint: "https://oauth2.googleapis.com/token",
243
- userInfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo",
244
- scopes: options.scopes || ["openid", "email", "profile"],
245
- };
246
- }
247
-
248
- // lib/oauth/providers/github.ts
249
- export function createGitHubConfig(options: {
250
- clientId: string;
251
- clientSecret: string;
252
- redirectUri: string;
253
- scopes?: string[];
254
- }): OAuthConfig {
255
- return {
256
- clientId: options.clientId,
257
- clientSecret: options.clientSecret,
258
- redirectUri: options.redirectUri,
259
- authorizationEndpoint: "https://github.com/login/oauth/authorize",
260
- tokenEndpoint: "https://github.com/login/oauth/access_token",
261
- userInfoEndpoint: "https://api.github.com/user",
262
- scopes: options.scopes || ["read:user", "user:email"],
263
- };
264
- }
265
-
266
- // lib/oauth/providers/microsoft.ts
267
- export function createMicrosoftConfig(options: {
268
- clientId: string;
269
- clientSecret: string;
270
- redirectUri: string;
271
- tenant?: string;
272
- scopes?: string[];
273
- }): OAuthConfig {
274
- const tenant = options.tenant || "common";
60
+ }),
61
+ });
275
62
 
63
+ const data = await response.json();
276
64
  return {
277
- clientId: options.clientId,
278
- clientSecret: options.clientSecret,
279
- redirectUri: options.redirectUri,
280
- authorizationEndpoint: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`,
281
- tokenEndpoint: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
282
- userInfoEndpoint: "https://graph.microsoft.com/oidc/userinfo",
283
- scopes: options.scopes || ["openid", "email", "profile", "User.Read"],
284
- };
285
- }
286
-
287
- // lib/oauth/providers/index.ts
288
- import { createGoogleConfig } from "./google";
289
- import { createGitHubConfig } from "./github";
290
- import { createMicrosoftConfig } from "./microsoft";
291
- import { OAuthClient } from "../client";
292
-
293
- export type Provider = "google" | "github" | "microsoft";
294
-
295
- export interface ProviderConfig {
296
- google?: {
297
- clientId: string;
298
- clientSecret: string;
65
+ accessToken: data.access_token,
66
+ refreshToken: data.refresh_token,
67
+ expiresIn: data.expires_in,
68
+ idToken: data.id_token,
299
69
  };
300
- github?: {
301
- clientId: string;
302
- clientSecret: string;
303
- };
304
- microsoft?: {
305
- clientId: string;
306
- clientSecret: string;
307
- tenant?: string;
308
- };
309
- }
310
-
311
- export function createOAuthClient(
312
- provider: Provider,
313
- config: ProviderConfig,
314
- redirectUri: string
315
- ): OAuthClient {
316
- switch (provider) {
317
- case "google":
318
- if (!config.google) throw new Error("Google config not provided");
319
- return new OAuthClient(
320
- createGoogleConfig({
321
- clientId: config.google.clientId,
322
- clientSecret: config.google.clientSecret,
323
- redirectUri,
324
- })
325
- );
326
-
327
- case "github":
328
- if (!config.github) throw new Error("GitHub config not provided");
329
- return new OAuthClient(
330
- createGitHubConfig({
331
- clientId: config.github.clientId,
332
- clientSecret: config.github.clientSecret,
333
- redirectUri,
334
- })
335
- );
336
-
337
- case "microsoft":
338
- if (!config.microsoft) throw new Error("Microsoft config not provided");
339
- return new OAuthClient(
340
- createMicrosoftConfig({
341
- clientId: config.microsoft.clientId,
342
- clientSecret: config.microsoft.clientSecret,
343
- redirectUri,
344
- tenant: config.microsoft.tenant,
345
- })
346
- );
347
-
348
- default:
349
- throw new Error(`Unknown provider: ${provider}`);
350
- }
351
- }
352
- ```
353
-
354
- ### 3. Token Validation and JWT
355
-
356
- ```typescript
357
- // lib/oauth/jwt.ts
358
- import jwt from "jsonwebtoken";
359
- import jwksClient from "jwks-rsa";
360
-
361
- export interface JWTClaims {
362
- iss: string;
363
- sub: string;
364
- aud: string | string[];
365
- exp: number;
366
- iat: number;
367
- nonce?: string;
368
- email?: string;
369
- email_verified?: boolean;
370
- name?: string;
371
- picture?: string;
372
- [key: string]: unknown;
373
- }
374
-
375
- export interface JWKSConfig {
376
- jwksUri: string;
377
- issuer: string;
378
- audience: string;
379
- }
380
-
381
- export class JWTValidator {
382
- private client: jwksClient.JwksClient;
383
- private issuer: string;
384
- private audience: string;
385
-
386
- constructor(config: JWKSConfig) {
387
- this.client = jwksClient({
388
- jwksUri: config.jwksUri,
389
- cache: true,
390
- cacheMaxEntries: 5,
391
- cacheMaxAge: 600000, // 10 minutes
392
- });
393
- this.issuer = config.issuer;
394
- this.audience = config.audience;
395
- }
396
-
397
- private getKey(
398
- header: jwt.JwtHeader,
399
- callback: jwt.SigningKeyCallback
400
- ): void {
401
- this.client.getSigningKey(header.kid, (err, key) => {
402
- if (err) {
403
- callback(err);
404
- return;
405
- }
406
- const signingKey = key?.getPublicKey();
407
- callback(null, signingKey);
408
- });
409
- }
410
-
411
- async validateIdToken(
412
- idToken: string,
413
- options: { nonce?: string } = {}
414
- ): Promise<JWTClaims> {
415
- return new Promise((resolve, reject) => {
416
- jwt.verify(
417
- idToken,
418
- (header, callback) => this.getKey(header, callback),
419
- {
420
- issuer: this.issuer,
421
- audience: this.audience,
422
- algorithms: ["RS256"],
423
- },
424
- (err, decoded) => {
425
- if (err) {
426
- reject(new Error(`Token validation failed: ${err.message}`));
427
- return;
428
- }
429
-
430
- const claims = decoded as JWTClaims;
431
-
432
- // Validate nonce if provided
433
- if (options.nonce && claims.nonce !== options.nonce) {
434
- reject(new Error("Nonce mismatch"));
435
- return;
436
- }
437
-
438
- resolve(claims);
439
- }
440
- );
441
- });
442
- }
443
- }
444
-
445
- // lib/oauth/token-store.ts
446
- export interface StoredToken {
447
- accessToken: string;
448
- refreshToken?: string;
449
- idToken?: string;
450
- expiresAt: number;
451
- tokenType: string;
452
- scope?: string;
453
- }
454
-
455
- export interface TokenStore {
456
- save(userId: string, token: StoredToken): Promise<void>;
457
- get(userId: string): Promise<StoredToken | null>;
458
- delete(userId: string): Promise<void>;
459
- }
460
-
461
- // Redis implementation
462
- import Redis from "ioredis";
463
-
464
- export class RedisTokenStore implements TokenStore {
465
- private redis: Redis;
466
- private prefix: string;
467
-
468
- constructor(redis: Redis, prefix = "oauth:token:") {
469
- this.redis = redis;
470
- this.prefix = prefix;
471
- }
472
-
473
- async save(userId: string, token: StoredToken): Promise<void> {
474
- const key = `${this.prefix}${userId}`;
475
- const ttl = Math.floor((token.expiresAt - Date.now()) / 1000);
476
-
477
- await this.redis.setex(key, ttl, JSON.stringify(token));
478
- }
479
-
480
- async get(userId: string): Promise<StoredToken | null> {
481
- const key = `${this.prefix}${userId}`;
482
- const data = await this.redis.get(key);
483
-
484
- if (!data) return null;
485
-
486
- return JSON.parse(data);
487
- }
488
-
489
- async delete(userId: string): Promise<void> {
490
- const key = `${this.prefix}${userId}`;
491
- await this.redis.del(key);
492
- }
493
70
  }
494
71
  ```
495
72
 
496
- ### 4. Express Integration
73
+ ### Provider Configuration
497
74
 
498
75
  ```typescript
499
- // routes/auth.ts
500
- import express from "express";
501
- import { OAuthClient, OAuthError } from "../lib/oauth/client";
502
- import { createOAuthClient, Provider } from "../lib/oauth/providers";
503
- import { JWTValidator } from "../lib/oauth/jwt";
504
- import { RedisTokenStore } from "../lib/oauth/token-store";
505
-
506
- const router = express.Router();
507
-
508
- const providerConfig = {
509
- google: {
510
- clientId: process.env.GOOGLE_CLIENT_ID!,
511
- clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
512
- },
513
- github: {
514
- clientId: process.env.GITHUB_CLIENT_ID!,
515
- clientSecret: process.env.GITHUB_CLIENT_SECRET!,
516
- },
76
+ // Google OAuth
77
+ const googleConfig = {
78
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
79
+ tokenEndpoint: "https://oauth2.googleapis.com/token",
80
+ userInfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo",
81
+ scopes: ["openid", "email", "profile"],
517
82
  };
518
83
 
519
- // Initiate OAuth flow
520
- router.get("/login/:provider", (req, res) => {
521
- const provider = req.params.provider as Provider;
522
- const redirectUri = `${process.env.BASE_URL}/auth/callback/${provider}`;
523
-
524
- try {
525
- const client = createOAuthClient(provider, providerConfig, redirectUri);
526
- const { url, pkce } = client.generateAuthorizationUrl();
527
-
528
- // Store PKCE and state in session
529
- req.session.oauth = {
530
- provider,
531
- state: pkce.state,
532
- codeVerifier: pkce.codeVerifier,
533
- };
534
-
535
- res.redirect(url);
536
- } catch (error) {
537
- res.status(400).json({ error: "Invalid provider" });
538
- }
539
- });
540
-
541
- // Handle OAuth callback
542
- router.get("/callback/:provider", async (req, res) => {
543
- const { code, state, error, error_description } = req.query;
544
-
545
- // Handle OAuth errors
546
- if (error) {
547
- return res.redirect(
548
- `/login?error=${encodeURIComponent(error_description as string || error as string)}`
549
- );
550
- }
551
-
552
- // Validate state
553
- if (!req.session.oauth || req.session.oauth.state !== state) {
554
- return res.redirect("/login?error=invalid_state");
555
- }
556
-
557
- const { provider, codeVerifier } = req.session.oauth;
558
- const redirectUri = `${process.env.BASE_URL}/auth/callback/${provider}`;
559
-
560
- try {
561
- const client = createOAuthClient(
562
- provider as Provider,
563
- providerConfig,
564
- redirectUri
565
- );
566
-
567
- // Exchange code for tokens
568
- const tokens = await client.exchangeCodeForTokens(
569
- code as string,
570
- codeVerifier
571
- );
572
-
573
- // Get user info
574
- const userInfo = await client.getUserInfo(tokens.accessToken);
575
-
576
- // Find or create user
577
- const user = await findOrCreateUser({
578
- provider,
579
- providerId: userInfo.sub || userInfo.id,
580
- email: userInfo.email,
581
- name: userInfo.name,
582
- picture: userInfo.picture,
583
- });
584
-
585
- // Store tokens
586
- const tokenStore = new RedisTokenStore(redis);
587
- await tokenStore.save(user.id, {
588
- accessToken: tokens.accessToken,
589
- refreshToken: tokens.refreshToken,
590
- idToken: tokens.idToken,
591
- expiresAt: Date.now() + tokens.expiresIn * 1000,
592
- tokenType: tokens.tokenType,
593
- scope: tokens.scope,
594
- });
595
-
596
- // Create session
597
- req.session.userId = user.id;
598
- delete req.session.oauth;
599
-
600
- res.redirect("/dashboard");
601
- } catch (error) {
602
- console.error("OAuth callback error:", error);
603
-
604
- if (error instanceof OAuthError) {
605
- return res.redirect(`/login?error=${encodeURIComponent(error.description || error.code)}`);
606
- }
607
-
608
- res.redirect("/login?error=authentication_failed");
609
- }
610
- });
611
-
612
- // Logout
613
- router.post("/logout", async (req, res) => {
614
- if (req.session.userId) {
615
- const tokenStore = new RedisTokenStore(redis);
616
- await tokenStore.delete(req.session.userId);
617
- }
618
-
619
- req.session.destroy((err) => {
620
- if (err) {
621
- return res.status(500).json({ error: "Logout failed" });
622
- }
623
- res.clearCookie("connect.sid");
624
- res.json({ success: true });
625
- });
626
- });
627
-
628
- export default router;
629
- ```
630
-
631
- ### 5. Token Refresh Middleware
632
-
633
- ```typescript
634
- // middleware/oauth.ts
635
- import { Request, Response, NextFunction } from "express";
636
- import { createOAuthClient, Provider } from "../lib/oauth/providers";
637
- import { RedisTokenStore, StoredToken } from "../lib/oauth/token-store";
638
-
639
- const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 minutes
640
-
641
- export async function ensureFreshToken(
642
- req: Request,
643
- res: Response,
644
- next: NextFunction
645
- ) {
646
- if (!req.session.userId) {
647
- return next();
648
- }
649
-
650
- const tokenStore = new RedisTokenStore(redis);
651
- const storedToken = await tokenStore.get(req.session.userId);
652
-
653
- if (!storedToken) {
654
- return next();
655
- }
656
-
657
- // Check if token needs refresh
658
- const timeUntilExpiry = storedToken.expiresAt - Date.now();
659
-
660
- if (timeUntilExpiry > TOKEN_REFRESH_THRESHOLD) {
661
- req.accessToken = storedToken.accessToken;
662
- return next();
663
- }
664
-
665
- // Refresh token if we have a refresh token
666
- if (!storedToken.refreshToken) {
667
- // Token expired and no refresh token
668
- delete req.session.userId;
669
- return res.redirect("/login?error=session_expired");
670
- }
671
-
672
- try {
673
- const user = await getUserById(req.session.userId);
674
- const provider = user.oauthProvider as Provider;
675
- const redirectUri = `${process.env.BASE_URL}/auth/callback/${provider}`;
676
-
677
- const client = createOAuthClient(provider, providerConfig, redirectUri);
678
- const newTokens = await client.refreshTokens(storedToken.refreshToken);
679
-
680
- // Store new tokens
681
- await tokenStore.save(req.session.userId, {
682
- accessToken: newTokens.accessToken,
683
- refreshToken: newTokens.refreshToken || storedToken.refreshToken,
684
- idToken: newTokens.idToken,
685
- expiresAt: Date.now() + newTokens.expiresIn * 1000,
686
- tokenType: newTokens.tokenType,
687
- scope: newTokens.scope,
688
- });
689
-
690
- req.accessToken = newTokens.accessToken;
691
- next();
692
- } catch (error) {
693
- console.error("Token refresh failed:", error);
694
- delete req.session.userId;
695
- await tokenStore.delete(req.session.userId);
696
- res.redirect("/login?error=session_expired");
697
- }
698
- }
699
-
700
- // middleware/requireAuth.ts
701
- export function requireAuth(
702
- req: Request,
703
- res: Response,
704
- next: NextFunction
705
- ) {
706
- if (!req.session.userId) {
707
- if (req.accepts("html")) {
708
- return res.redirect(`/login?returnTo=${encodeURIComponent(req.originalUrl)}`);
709
- }
710
- return res.status(401).json({ error: "Authentication required" });
711
- }
712
-
713
- next();
714
- }
715
-
716
- // Combine middlewares
717
- export const authMiddleware = [ensureFreshToken, requireAuth];
718
- ```
719
-
720
- ### 6. API Resource Protection
721
-
722
- ```typescript
723
- // middleware/oauth-scope.ts
724
- import { Request, Response, NextFunction } from "express";
725
-
726
- export function requireScope(...requiredScopes: string[]) {
727
- return async (req: Request, res: Response, next: NextFunction) => {
728
- const tokenStore = new RedisTokenStore(redis);
729
- const storedToken = await tokenStore.get(req.session.userId);
730
-
731
- if (!storedToken || !storedToken.scope) {
732
- return res.status(403).json({
733
- error: "insufficient_scope",
734
- message: "Required scopes not granted",
735
- required_scopes: requiredScopes,
736
- });
737
- }
738
-
739
- const grantedScopes = storedToken.scope.split(" ");
740
- const hasAllScopes = requiredScopes.every((scope) =>
741
- grantedScopes.includes(scope)
742
- );
743
-
744
- if (!hasAllScopes) {
745
- return res.status(403).json({
746
- error: "insufficient_scope",
747
- message: "Required scopes not granted",
748
- required_scopes: requiredScopes,
749
- granted_scopes: grantedScopes,
750
- });
751
- }
752
-
753
- next();
754
- };
755
- }
756
-
757
- // routes/api.ts
758
- import { authMiddleware } from "../middleware/oauth";
759
- import { requireScope } from "../middleware/oauth-scope";
760
-
761
- const router = express.Router();
762
-
763
- // Protected route - requires authentication
764
- router.get("/profile", authMiddleware, async (req, res) => {
765
- const user = await getUserById(req.session.userId);
766
- res.json(user);
767
- });
768
-
769
- // Protected route - requires specific scope
770
- router.get(
771
- "/emails",
772
- authMiddleware,
773
- requireScope("email", "read:user"),
774
- async (req, res) => {
775
- const emails = await getUserEmails(req.session.userId, req.accessToken);
776
- res.json(emails);
777
- }
778
- );
779
-
780
- // Protected route - requires admin scope
781
- router.get(
782
- "/admin/users",
783
- authMiddleware,
784
- requireScope("admin:read"),
785
- async (req, res) => {
786
- const users = await getAllUsers();
787
- res.json(users);
788
- }
789
- );
790
- ```
791
-
792
- ### 7. OpenID Connect Discovery
793
-
794
- ```typescript
795
- // lib/oauth/oidc-discovery.ts
796
- export interface OIDCConfiguration {
797
- issuer: string;
798
- authorization_endpoint: string;
799
- token_endpoint: string;
800
- userinfo_endpoint: string;
801
- jwks_uri: string;
802
- scopes_supported: string[];
803
- response_types_supported: string[];
804
- grant_types_supported: string[];
805
- subject_types_supported: string[];
806
- id_token_signing_alg_values_supported: string[];
807
- claims_supported: string[];
808
- }
809
-
810
- export async function discoverOIDCConfiguration(
811
- issuer: string
812
- ): Promise<OIDCConfiguration> {
813
- const wellKnownUrl = `${issuer}/.well-known/openid-configuration`;
814
-
815
- const response = await fetch(wellKnownUrl);
816
-
817
- if (!response.ok) {
818
- throw new Error(`Failed to discover OIDC configuration: ${response.status}`);
819
- }
820
-
821
- return response.json();
822
- }
823
-
824
- // Usage
825
- async function configureFromDiscovery(issuer: string) {
826
- const config = await discoverOIDCConfiguration(issuer);
827
-
828
- return new OAuthClient({
829
- clientId: process.env.CLIENT_ID!,
830
- clientSecret: process.env.CLIENT_SECRET!,
831
- redirectUri: process.env.REDIRECT_URI!,
832
- authorizationEndpoint: config.authorization_endpoint,
833
- tokenEndpoint: config.token_endpoint,
834
- userInfoEndpoint: config.userinfo_endpoint,
835
- scopes: ["openid", "email", "profile"],
836
- });
837
- }
838
-
839
- // Common OIDC issuers
840
- const OIDC_ISSUERS = {
841
- google: "https://accounts.google.com",
842
- microsoft: "https://login.microsoftonline.com/common/v2.0",
843
- okta: "https://your-org.okta.com",
844
- auth0: "https://your-tenant.auth0.com",
84
+ // GitHub OAuth
85
+ const githubConfig = {
86
+ authorizationEndpoint: "https://github.com/login/oauth/authorize",
87
+ tokenEndpoint: "https://github.com/login/oauth/access_token",
88
+ userInfoEndpoint: "https://api.github.com/user",
89
+ scopes: ["read:user", "user:email"],
845
90
  };
846
91
  ```
847
92
 
848
- ## Use Cases
849
-
850
- ### React Integration
93
+ ### Token Refresh Middleware
851
94
 
852
95
  ```typescript
853
- // hooks/useOAuth.ts
854
- import { useState, useCallback } from "react";
96
+ async function ensureFreshToken(req: Request, res: Response, next: NextFunction) {
97
+ const token = await tokenStore.get(req.session.userId);
98
+ if (!token) return next();
855
99
 
856
- export function useOAuth() {
857
- const [loading, setLoading] = useState(false);
858
- const [error, setError] = useState<string | null>(null);
859
-
860
- const login = useCallback(async (provider: string) => {
861
- setLoading(true);
862
- setError(null);
863
- window.location.href = `/auth/login/${provider}`;
864
- }, []);
865
-
866
- const logout = useCallback(async () => {
867
- setLoading(true);
868
- try {
869
- await fetch("/auth/logout", { method: "POST" });
870
- window.location.href = "/";
871
- } catch (err) {
872
- setError("Logout failed");
873
- } finally {
874
- setLoading(false);
875
- }
876
- }, []);
877
-
878
- return { login, logout, loading, error };
879
- }
880
-
881
- // components/LoginButtons.tsx
882
- export function LoginButtons() {
883
- const { login, loading } = useOAuth();
884
-
885
- return (
886
- <div className="space-y-2">
887
- <button onClick={() => login("google")} disabled={loading}>
888
- Continue with Google
889
- </button>
890
- <button onClick={() => login("github")} disabled={loading}>
891
- Continue with GitHub
892
- </button>
893
- <button onClick={() => login("microsoft")} disabled={loading}>
894
- Continue with Microsoft
895
- </button>
896
- </div>
897
- );
898
- }
899
- ```
900
-
901
- ### State Management with Cookies
902
-
903
- ```typescript
904
- // lib/oauth/state.ts
905
- import { serialize, parse } from "cookie";
906
- import crypto from "crypto";
907
-
908
- const STATE_COOKIE_NAME = "oauth_state";
909
- const STATE_MAX_AGE = 60 * 10; // 10 minutes
910
-
911
- export function setStateCookie(res: Response, state: string, codeVerifier: string) {
912
- const data = JSON.stringify({ state, codeVerifier });
913
- const encrypted = encrypt(data);
914
-
915
- const cookie = serialize(STATE_COOKIE_NAME, encrypted, {
916
- httpOnly: true,
917
- secure: process.env.NODE_ENV === "production",
918
- sameSite: "lax",
919
- maxAge: STATE_MAX_AGE,
920
- path: "/",
921
- });
922
-
923
- res.setHeader("Set-Cookie", cookie);
924
- }
925
-
926
- export function getStateCookie(req: Request): { state: string; codeVerifier: string } | null {
927
- const cookies = parse(req.headers.cookie || "");
928
- const encrypted = cookies[STATE_COOKIE_NAME];
929
-
930
- if (!encrypted) return null;
931
-
932
- try {
933
- const decrypted = decrypt(encrypted);
934
- return JSON.parse(decrypted);
935
- } catch {
936
- return null;
100
+ const timeUntilExpiry = token.expiresAt - Date.now();
101
+ if (timeUntilExpiry > 5 * 60 * 1000) {
102
+ req.accessToken = token.accessToken;
103
+ return next();
937
104
  }
938
- }
939
105
 
940
- export function clearStateCookie(res: Response) {
941
- const cookie = serialize(STATE_COOKIE_NAME, "", {
942
- httpOnly: true,
943
- secure: process.env.NODE_ENV === "production",
944
- sameSite: "lax",
945
- maxAge: 0,
946
- path: "/",
106
+ // Refresh token
107
+ const newTokens = await client.refreshTokens(token.refreshToken);
108
+ await tokenStore.save(req.session.userId, {
109
+ ...newTokens,
110
+ expiresAt: Date.now() + newTokens.expiresIn * 1000,
947
111
  });
948
-
949
- res.setHeader("Set-Cookie", cookie);
112
+ req.accessToken = newTokens.accessToken;
113
+ next();
950
114
  }
951
115
  ```
952
116
 
953
117
  ## Best Practices
954
118
 
955
- ### Do's
956
-
957
- - Always use PKCE for authorization code flow
958
- - Validate state parameter to prevent CSRF
959
- - Store tokens securely (encrypted, httpOnly cookies)
960
- - Implement token refresh before expiration
961
- - Use HTTPS for all OAuth endpoints
962
- - Validate ID token signatures with JWKS
963
- - Check token audience and issuer claims
964
- - Implement proper error handling
965
- - Log authentication events for auditing
966
- - Use short-lived access tokens
967
-
968
- ### Don'ts
969
-
970
- - Don't use implicit flow for new applications
971
- - Don't store tokens in localStorage
972
- - Don't expose client secrets in frontend code
973
- - Don't skip state validation
974
- - Don't ignore token expiration
975
- - Don't use long-lived access tokens
976
- - Don't trust unverified ID tokens
977
- - Don't log sensitive token data
978
- - Don't reuse authorization codes
979
- - Don't skip nonce validation for OIDC
119
+ | Do | Avoid |
120
+ |----|-------|
121
+ | Always use PKCE for authorization code flow | Using implicit flow for new apps |
122
+ | Validate state parameter to prevent CSRF | Storing tokens in localStorage |
123
+ | Store tokens securely (encrypted, httpOnly) | Exposing client secrets in frontend |
124
+ | Implement token refresh before expiration | Ignoring token expiration |
125
+ | Validate ID token signatures with JWKS | Trusting unverified ID tokens |
126
+ | Use short-lived access tokens | Reusing authorization codes |
980
127
 
981
128
  ## References
982
129
 
@@ -984,4 +131,3 @@ export function clearStateCookie(res: Response) {
984
131
  - [OAuth 2.0 PKCE RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)
985
132
  - [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
986
133
  - [OAuth 2.0 Security Best Practices](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics)
987
- - [OIDC Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html)