|
1 | 1 | /** |
2 | | - * Utility functions for authentication, token validation, and encryption. |
| 2 | + * Utility functions for authentication and token management. |
3 | 3 | */ |
4 | 4 |
|
5 | | -import { createHash } from "node:crypto"; |
6 | 5 | import type { Account } from "better-auth"; |
7 | | -import * as jose from "jose"; |
8 | 6 | import { cookies } from "next/headers"; |
9 | | -import { saveTokenCookie } from "./auth"; |
10 | 7 | import { |
11 | 8 | BETTER_AUTH_SECRET, |
12 | 9 | OIDC_TOKEN_COOKIE_NAME, |
13 | 10 | TOKEN_ONE_HOUR_MS, |
14 | 11 | } from "./constants"; |
| 12 | +import { saveTokenCookie } from "./cookie"; |
| 13 | +import { decrypt } from "./crypto"; |
15 | 14 | import type { OidcTokenData } from "./types"; |
16 | 15 |
|
17 | | -/** |
18 | | - * Derives encryption key from secret. |
19 | | - * Uses SHA-256 to derive exactly 32 bytes (256 bits) from the provided secret, |
20 | | - * ensuring compatibility with AES-256-GCM regardless of secret length. |
21 | | - */ |
22 | | -function getSecret(secret: string): Uint8Array { |
23 | | - // Hash the secret to get exactly 32 bytes for AES-256-GCM |
24 | | - return new Uint8Array(createHash("sha256").update(secret).digest()); |
25 | | -} |
26 | | - |
27 | | -/** |
28 | | - * Encrypts token data using JWE (JSON Web Encryption). |
29 | | - * Uses AES-256-GCM with direct key agreement (alg: 'dir'). |
30 | | - * Exported for testing purposes. |
31 | | - */ |
32 | | -export async function encrypt( |
33 | | - data: OidcTokenData, |
34 | | - secret: string, |
35 | | -): Promise<string> { |
36 | | - const key = getSecret(secret); |
37 | | - const plaintext = new TextEncoder().encode(JSON.stringify(data)); |
38 | | - return await new jose.CompactEncrypt(plaintext) |
39 | | - .setProtectedHeader({ alg: "dir", enc: "A256GCM" }) |
40 | | - .encrypt(key); |
41 | | -} |
42 | | - |
43 | | -/** |
44 | | - * Decrypts JWE token and returns parsed token data. |
45 | | - * Validates data structure after decryption. |
46 | | - * Exported for testing purposes. |
47 | | - */ |
48 | | -export async function decrypt( |
49 | | - jwe: string, |
50 | | - secret: string, |
51 | | -): Promise<OidcTokenData> { |
52 | | - try { |
53 | | - const key = getSecret(secret); |
54 | | - const { plaintext } = await jose.compactDecrypt(jwe, key); |
55 | | - const data = JSON.parse(new TextDecoder().decode(plaintext)); |
56 | | - |
57 | | - if (!isOidcTokenData(data)) { |
58 | | - throw new Error("Invalid token data structure"); |
59 | | - } |
60 | | - |
61 | | - return data; |
62 | | - } catch (error) { |
63 | | - if (error instanceof jose.errors.JWEDecryptionFailed) { |
64 | | - throw new Error("Token decryption failed - possible tampering"); |
65 | | - } |
66 | | - if (error instanceof jose.errors.JWEInvalid) { |
67 | | - throw new Error("Invalid JWE format"); |
68 | | - } |
69 | | - // Wrap unexpected errors to avoid exposing internal details |
70 | | - const message = error instanceof Error ? error.message : "Unknown error"; |
71 | | - throw new Error(`Token decryption error: ${message}`); |
72 | | - } |
73 | | -} |
74 | | - |
75 | | -/** |
76 | | - * Type guard to validate OidcTokenData structure at runtime. |
77 | | - * Used after decrypting token data from cookie to ensure data integrity. |
78 | | - * Note: idToken is not validated here as it's optional and not critical for token validation. |
79 | | - */ |
80 | | -export function isOidcTokenData(data: unknown): data is OidcTokenData { |
81 | | - if (typeof data !== "object" || data === null) { |
82 | | - return false; |
83 | | - } |
84 | | - |
85 | | - const obj = data as Record<string, unknown>; |
86 | | - |
87 | | - return ( |
88 | | - typeof obj.accessToken === "string" && |
89 | | - typeof obj.accessTokenExpiresAt === "number" && |
90 | | - typeof obj.userId === "string" && |
91 | | - (obj.refreshToken === undefined || typeof obj.refreshToken === "string") && |
92 | | - (obj.refreshTokenExpiresAt === undefined || |
93 | | - typeof obj.refreshTokenExpiresAt === "number") |
94 | | - ); |
95 | | -} |
| 16 | +// Re-export crypto functions for backwards compatibility |
| 17 | +export { decrypt, encrypt, isOidcTokenData } from "./crypto"; |
96 | 18 |
|
97 | 19 | /** |
98 | 20 | * Retrieves the OIDC ID token from HTTP-only cookie. |
@@ -153,17 +75,6 @@ export async function saveAccountToken(account: Account) { |
153 | 75 | userId: account.userId, |
154 | 76 | }; |
155 | 77 |
|
156 | | - console.log("[account] Token data to save:", JSON.stringify(account)); |
157 | | - |
158 | | - console.log("[Save Token] Token data to save:", { |
159 | | - hasAccessToken: !!tokenData.accessToken, |
160 | | - hasRefreshToken: !!tokenData.refreshToken, |
161 | | - accessTokenExpiresAt: new Date(accessTokenExpiresAt).toISOString(), |
162 | | - refreshTokenExpiresAt: tokenData.refreshTokenExpiresAt |
163 | | - ? new Date(tokenData.refreshTokenExpiresAt).toISOString() |
164 | | - : "none", |
165 | | - }); |
166 | | - |
167 | 78 | await saveTokenCookie(tokenData); |
168 | 79 |
|
169 | 80 | console.log("[Save Token] Token cookie saved successfully"); |
|
0 commit comments