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.
- package/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,50 +1,987 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oauth
|
|
3
|
-
description: OAuth 2.0
|
|
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
|
|
14
|
+
# OAuth
|
|
7
15
|
|
|
8
|
-
|
|
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.
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
496
|
+
### 4. Express Integration
|
|
497
|
+
|
|
27
498
|
```typescript
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
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)
|