omgkit 2.1.0 → 2.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.
Files changed (56) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/SKILL_STANDARDS.md +743 -0
  3. package/plugin/skills/databases/mongodb/SKILL.md +797 -28
  4. package/plugin/skills/databases/postgresql/SKILL.md +494 -18
  5. package/plugin/skills/databases/prisma/SKILL.md +776 -30
  6. package/plugin/skills/databases/redis/SKILL.md +885 -25
  7. package/plugin/skills/devops/aws/SKILL.md +686 -28
  8. package/plugin/skills/devops/docker/SKILL.md +466 -18
  9. package/plugin/skills/devops/github-actions/SKILL.md +684 -29
  10. package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
  11. package/plugin/skills/frameworks/django/SKILL.md +920 -20
  12. package/plugin/skills/frameworks/express/SKILL.md +1361 -35
  13. package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
  14. package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
  15. package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
  16. package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
  17. package/plugin/skills/frameworks/rails/SKILL.md +594 -28
  18. package/plugin/skills/frameworks/react/SKILL.md +1006 -32
  19. package/plugin/skills/frameworks/spring/SKILL.md +528 -35
  20. package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
  21. package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
  22. package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
  23. package/plugin/skills/frontend/responsive/SKILL.md +847 -21
  24. package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
  25. package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
  26. package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
  27. package/plugin/skills/languages/javascript/SKILL.md +935 -31
  28. package/plugin/skills/languages/python/SKILL.md +489 -25
  29. package/plugin/skills/languages/typescript/SKILL.md +379 -30
  30. package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
  31. package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
  32. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
  33. package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
  34. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
  35. package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
  36. package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
  37. package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
  38. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
  39. package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
  40. package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
  41. package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
  42. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
  43. package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
  44. package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
  45. package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
  46. package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
  47. package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
  48. package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
  49. package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
  50. package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
  51. package/plugin/skills/security/better-auth/SKILL.md +1065 -28
  52. package/plugin/skills/security/oauth/SKILL.md +968 -31
  53. package/plugin/skills/security/owasp/SKILL.md +894 -33
  54. package/plugin/skills/testing/playwright/SKILL.md +764 -38
  55. package/plugin/skills/testing/pytest/SKILL.md +873 -36
  56. package/plugin/skills/testing/vitest/SKILL.md +980 -35
@@ -1,50 +1,987 @@
1
1
  ---
2
2
  name: oauth
3
- description: OAuth 2.0 / OIDC. Use for third-party authentication, social login.
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
4
12
  ---
5
13
 
6
- # OAuth Skill
14
+ # OAuth
7
15
 
8
- ## Authorization Code Flow
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
33
+
34
+ ```typescript
35
+ // lib/oauth/pkce.ts
36
+ import crypto from "crypto";
37
+
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)
46
+ 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
55
+ const state = crypto.randomBytes(16).toString("hex");
56
+
57
+ return { codeVerifier, codeChallenge, state };
58
+ }
59
+
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;
80
+ }
81
+
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
+ });
107
+
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);
111
+
112
+ const url = `${this.config.authorizationEndpoint}?${params.toString()}`;
113
+
114
+ return { url, pkce: this.pkce };
115
+ }
116
+
117
+ async exchangeCodeForTokens(
118
+ code: string,
119
+ codeVerifier: string
120
+ ): Promise<TokenResponse> {
121
+ const body = new URLSearchParams({
122
+ grant_type: "authorization_code",
123
+ code,
124
+ redirect_uri: this.config.redirectUri,
125
+ client_id: this.config.clientId,
126
+ 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
+ }
9
223
  ```
10
- 1. Redirect to provider
11
- /authorize?client_id=X&redirect_uri=Y&scope=Z&response_type=code
12
224
 
13
- 2. User authorizes
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
+ }
14
265
 
15
- 3. Callback with code
16
- /callback?code=ABC
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";
17
275
 
18
- 4. Exchange code for token
19
- POST /token
20
- { code, client_id, client_secret, redirect_uri }
276
+ 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
+ }
21
286
 
22
- 5. Use access token
23
- Authorization: Bearer <token>
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;
299
+ };
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
+ }
24
494
  ```
25
495
 
26
- ## Implementation
496
+ ### 4. Express Integration
497
+
27
498
  ```typescript
28
- // Redirect to OAuth provider
29
- app.get('/auth/google', (req, res) => {
30
- const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
31
- url.searchParams.set('client_id', CLIENT_ID);
32
- url.searchParams.set('redirect_uri', REDIRECT_URI);
33
- url.searchParams.set('response_type', 'code');
34
- url.searchParams.set('scope', 'openid email profile');
35
- res.redirect(url.toString());
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
+ },
517
+ };
518
+
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
+ }
36
610
  });
37
611
 
38
- // Handle callback
39
- app.get('/auth/callback', async (req, res) => {
40
- const { code } = req.query;
41
- const tokens = await exchangeCodeForTokens(code);
42
- // Create session
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
+ });
43
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",
845
+ };
846
+ ```
847
+
848
+ ## Use Cases
849
+
850
+ ### React Integration
851
+
852
+ ```typescript
853
+ // hooks/useOAuth.ts
854
+ import { useState, useCallback } from "react";
855
+
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;
937
+ }
938
+ }
939
+
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: "/",
947
+ });
948
+
949
+ res.setHeader("Set-Cookie", cookie);
950
+ }
44
951
  ```
45
952
 
46
953
  ## Best Practices
47
- - Use PKCE for public clients
48
- - Validate state parameter
49
- - Use short-lived tokens
50
- - Store tokens securely
954
+
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
980
+
981
+ ## References
982
+
983
+ - [OAuth 2.0 RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)
984
+ - [OAuth 2.0 PKCE RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)
985
+ - [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html)
986
+ - [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)