@draftlab/auth 0.0.1
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/dist/adapters/node.d.ts +18 -0
- package/dist/adapters/node.js +71 -0
- package/dist/allow-CixonwTW.d.ts +59 -0
- package/dist/allow-DX5cehSc.js +63 -0
- package/dist/allow.d.ts +2 -0
- package/dist/allow.js +4 -0
- package/dist/base-DRutbxgL.js +422 -0
- package/dist/client.d.ts +413 -0
- package/dist/client.js +209 -0
- package/dist/code-l_uvMR1j.d.ts +212 -0
- package/dist/core-8WTqfnb4.d.ts +129 -0
- package/dist/core-CncE5rPg.js +498 -0
- package/dist/core.d.ts +9 -0
- package/dist/core.js +14 -0
- package/dist/error-CWAdNAzm.d.ts +243 -0
- package/dist/error-DgAKK7b2.js +237 -0
- package/dist/error.d.ts +2 -0
- package/dist/error.js +3 -0
- package/dist/form-6XKM_cOk.js +61 -0
- package/dist/icon-Ci5uqGB_.js +192 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -0
- package/dist/keys-EEfxEGfO.js +140 -0
- package/dist/keys.d.ts +67 -0
- package/dist/keys.js +5 -0
- package/dist/oauth2-B7-6Z7Lc.js +155 -0
- package/dist/oauth2-DtKwtl8p.d.ts +176 -0
- package/dist/password-Cm0dRMwa.d.ts +385 -0
- package/dist/pkce-276Za_rZ.js +162 -0
- package/dist/pkce.d.ts +72 -0
- package/dist/pkce.js +3 -0
- package/dist/provider/code.d.ts +4 -0
- package/dist/provider/code.js +145 -0
- package/dist/provider/facebook.d.ts +137 -0
- package/dist/provider/facebook.js +85 -0
- package/dist/provider/github.d.ts +141 -0
- package/dist/provider/github.js +88 -0
- package/dist/provider/google.d.ts +113 -0
- package/dist/provider/google.js +62 -0
- package/dist/provider/oauth2.d.ts +4 -0
- package/dist/provider/oauth2.js +7 -0
- package/dist/provider/password.d.ts +4 -0
- package/dist/provider/password.js +366 -0
- package/dist/provider/provider.d.ts +3 -0
- package/dist/provider/provider.js +44 -0
- package/dist/provider-CwWMG-1l.d.ts +227 -0
- package/dist/random-SXMYlaVr.js +87 -0
- package/dist/random.d.ts +66 -0
- package/dist/random.js +3 -0
- package/dist/select-BjySLL8I.js +280 -0
- package/dist/storage/memory.d.ts +82 -0
- package/dist/storage/memory.js +127 -0
- package/dist/storage/storage.d.ts +2 -0
- package/dist/storage/storage.js +3 -0
- package/dist/storage/turso.d.ts +31 -0
- package/dist/storage/turso.js +117 -0
- package/dist/storage/unstorage.d.ts +38 -0
- package/dist/storage/unstorage.js +97 -0
- package/dist/storage-BEaqEPNQ.js +62 -0
- package/dist/storage-CxKerLlc.d.ts +162 -0
- package/dist/subject-DiQdRWGt.d.ts +62 -0
- package/dist/subject.d.ts +3 -0
- package/dist/subject.js +36 -0
- package/dist/theme-C9by7VXf.d.ts +209 -0
- package/dist/theme-CswaLtbW.js +120 -0
- package/dist/themes/theme.d.ts +2 -0
- package/dist/themes/theme.js +3 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +0 -0
- package/dist/ui/base.d.ts +43 -0
- package/dist/ui/base.js +4 -0
- package/dist/ui/code.d.ts +158 -0
- package/dist/ui/code.js +197 -0
- package/dist/ui/form.d.ts +31 -0
- package/dist/ui/form.js +3 -0
- package/dist/ui/icon.d.ts +98 -0
- package/dist/ui/icon.js +3 -0
- package/dist/ui/password.d.ts +54 -0
- package/dist/ui/password.js +300 -0
- package/dist/ui/select.d.ts +233 -0
- package/dist/ui/select.js +6 -0
- package/dist/util-CSdHUFOo.js +108 -0
- package/dist/util-ChlgVqPN.d.ts +72 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +3 -0
- package/package.json +63 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { base64url } from "jose";
|
|
2
|
+
|
|
3
|
+
//#region src/pkce.ts
|
|
4
|
+
/**
|
|
5
|
+
* Performs a timing-safe comparison of two strings to prevent timing attacks.
|
|
6
|
+
* This implementation is platform-agnostic, uses a constant-time algorithm,
|
|
7
|
+
* and correctly handles all Unicode characters by operating on their UTF-8 byte representation.
|
|
8
|
+
* It always takes a time proportional to the length of the expected string,
|
|
9
|
+
* regardless of where the strings differ, making it safe for comparing sensitive values.
|
|
10
|
+
*
|
|
11
|
+
* @param a - The first string to compare (often the expected, secret value).
|
|
12
|
+
* @param b - The second string to compare (often the user-provided value).
|
|
13
|
+
* @returns True if the strings are identical, false otherwise.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // Safe for comparing sensitive values like PKCE verifiers or tokens
|
|
18
|
+
* const isValid = await timingSafeCompare(receivedVerifier, expectedChallenge);
|
|
19
|
+
*
|
|
20
|
+
* // Safe for password hash verification
|
|
21
|
+
* const isValidPassword = timingSafeCompare(hashedInput, storedHash);
|
|
22
|
+
*
|
|
23
|
+
* // Returns false for different types or lengths without leaking timing info
|
|
24
|
+
* timingSafeCompare("abc", 123 as any); // false
|
|
25
|
+
* timingSafeCompare("abc", "abcd"); // false
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
const timingSafeCompare = (a, b) => {
|
|
29
|
+
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
30
|
+
const encoder = new TextEncoder();
|
|
31
|
+
const aBytes = encoder.encode(a);
|
|
32
|
+
const bBytes = encoder.encode(b);
|
|
33
|
+
let diff = aBytes.length ^ bBytes.length;
|
|
34
|
+
for (const [i, aByte] of aBytes.entries()) diff |= aByte ^ (bBytes[i] ?? 0);
|
|
35
|
+
return diff === 0;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Generates a cryptographically secure code verifier for PKCE.
|
|
39
|
+
* The verifier is a URL-safe base64-encoded string of random bytes.
|
|
40
|
+
*
|
|
41
|
+
* @param length - Length of the random buffer in bytes
|
|
42
|
+
* @returns Base64url-encoded verifier string
|
|
43
|
+
*/
|
|
44
|
+
const generateVerifier = (length) => {
|
|
45
|
+
const buffer = new Uint8Array(length);
|
|
46
|
+
crypto.getRandomValues(buffer);
|
|
47
|
+
return base64url.encode(buffer);
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Generates a code challenge from a verifier using the specified method.
|
|
51
|
+
* For 'S256', applies SHA-256 hash then base64url encoding.
|
|
52
|
+
* For 'plain', returns the verifier unchanged (not recommended for production).
|
|
53
|
+
*
|
|
54
|
+
* @param verifier - The code verifier string
|
|
55
|
+
* @param method - Challenge generation method
|
|
56
|
+
* @returns Promise resolving to the code challenge string
|
|
57
|
+
*/
|
|
58
|
+
const generateChallenge = async (verifier, method) => {
|
|
59
|
+
if (method === "plain") return verifier;
|
|
60
|
+
const encoder = new TextEncoder();
|
|
61
|
+
const data = encoder.encode(verifier);
|
|
62
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
63
|
+
return base64url.encode(new Uint8Array(hash));
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Generates a complete PKCE challenge for OAuth authorization requests.
|
|
67
|
+
* Creates a cryptographically secure verifier and corresponding S256 challenge.
|
|
68
|
+
* Validates that the generated verifier meets standard requirements (43-128 characters).
|
|
69
|
+
*
|
|
70
|
+
* @param length - Length of the random buffer in bytes (32-96 range to generate 43-128 character verifier)
|
|
71
|
+
* @returns Promise resolving to PKCE challenge data
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const pkce = await generatePKCE()
|
|
76
|
+
*
|
|
77
|
+
* // Use challenge in authorization URL
|
|
78
|
+
* authUrl.searchParams.set('code_challenge', pkce.challenge)
|
|
79
|
+
* authUrl.searchParams.set('code_challenge_method', pkce.method)
|
|
80
|
+
*
|
|
81
|
+
* // Store verifier for token exchange
|
|
82
|
+
* sessionStorage.setItem('code_verifier', pkce.verifier)
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @throws {RangeError} If length is outside valid range or generated verifier doesn't meet requirements
|
|
86
|
+
*/
|
|
87
|
+
const generatePKCE = async (length = 48) => {
|
|
88
|
+
if (!Number.isInteger(length) || length < 32 || length > 96) throw new RangeError("Random buffer length must be between 32 and 96 bytes (generates 43-128 character verifier)");
|
|
89
|
+
const verifier = generateVerifier(length);
|
|
90
|
+
if (verifier.length < 43 || verifier.length > 128) throw new Error("Generated verifier does not meet requirements");
|
|
91
|
+
if (!/^[A-Za-z0-9_-]+$/.test(verifier)) throw new Error("Generated verifier is not valid base64url format");
|
|
92
|
+
const challenge = await generateChallenge(verifier, "S256");
|
|
93
|
+
return {
|
|
94
|
+
verifier,
|
|
95
|
+
challenge,
|
|
96
|
+
method: "S256"
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Validates a PKCE code verifier against a previously generated challenge.
|
|
101
|
+
* Uses timing-safe comparison and timing normalization to prevent timing attacks.
|
|
102
|
+
* All validation paths take the same computational time regardless of input validity,
|
|
103
|
+
* making it resistant to timing-based side-channel attacks.
|
|
104
|
+
*
|
|
105
|
+
* @param verifier - The code verifier received from the client
|
|
106
|
+
* @param challenge - The code challenge stored during authorization
|
|
107
|
+
* @param method - The challenge method used during generation
|
|
108
|
+
* @returns Promise resolving to true if verifier matches challenge
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* // During token exchange
|
|
113
|
+
* const isValid = await validatePKCE(
|
|
114
|
+
* receivedVerifier,
|
|
115
|
+
* storedChallenge,
|
|
116
|
+
* 'S256'
|
|
117
|
+
* )
|
|
118
|
+
*
|
|
119
|
+
* if (!isValid) {
|
|
120
|
+
* throw new Error('Invalid PKCE verifier')
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
const validatePKCE = async (verifier, challenge, method = "S256") => {
|
|
125
|
+
const MIN_PROCESSING_TIME = 50;
|
|
126
|
+
const RANDOM_JITTER_MAX = 20;
|
|
127
|
+
const startTime = performance.now();
|
|
128
|
+
let isValid = false;
|
|
129
|
+
let hasEarlyFailure = false;
|
|
130
|
+
const normalizedVerifier = String(verifier || "");
|
|
131
|
+
const normalizedChallenge = String(challenge || "");
|
|
132
|
+
const validations = [
|
|
133
|
+
typeof verifier === "string" && typeof challenge === "string" && verifier && challenge,
|
|
134
|
+
normalizedVerifier.length >= 43 && normalizedVerifier.length <= 128,
|
|
135
|
+
normalizedChallenge.length >= 43 && normalizedChallenge.length <= 128,
|
|
136
|
+
/^[A-Za-z0-9_-]+$/.test(normalizedVerifier),
|
|
137
|
+
/^[A-Za-z0-9_-]+$/.test(normalizedChallenge)
|
|
138
|
+
];
|
|
139
|
+
hasEarlyFailure = !validations.every(Boolean);
|
|
140
|
+
const verifierToUse = hasEarlyFailure ? "dummyverifier_".repeat(6) : normalizedVerifier;
|
|
141
|
+
try {
|
|
142
|
+
const generatedChallenge = await generateChallenge(verifierToUse, method);
|
|
143
|
+
const challengeToCompare = hasEarlyFailure ? "dummychallenge_".repeat(6) : normalizedChallenge;
|
|
144
|
+
const comparisonResult = timingSafeCompare(generatedChallenge, challengeToCompare);
|
|
145
|
+
isValid = !hasEarlyFailure && comparisonResult;
|
|
146
|
+
} catch {
|
|
147
|
+
isValid = false;
|
|
148
|
+
}
|
|
149
|
+
const elapsed = performance.now() - startTime;
|
|
150
|
+
const remainingTime = Math.max(0, MIN_PROCESSING_TIME - elapsed);
|
|
151
|
+
if (remainingTime > 0 || elapsed < MIN_PROCESSING_TIME) {
|
|
152
|
+
const jitterArray = new Uint32Array(1);
|
|
153
|
+
crypto.getRandomValues(jitterArray);
|
|
154
|
+
const jitter = (jitterArray[0] ?? 0) / 4294967295 * RANDOM_JITTER_MAX;
|
|
155
|
+
const totalDelay = Math.max(remainingTime, MIN_PROCESSING_TIME - elapsed) + jitter;
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
157
|
+
}
|
|
158
|
+
return isValid;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
export { generatePKCE, validatePKCE };
|
package/dist/pkce.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/pkce.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* PKCE (Proof Key for Code Exchange) implementation for OAuth security.
|
|
4
|
+
* Provides protection against authorization code interception attacks by using
|
|
5
|
+
* dynamically generated code verifiers and challenges.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* PKCE challenge methods supported by the implementation.
|
|
9
|
+
*/
|
|
10
|
+
type PKCEMethod = "S256" | "plain";
|
|
11
|
+
/**
|
|
12
|
+
* Complete PKCE challenge data containing verifier, challenge, and method.
|
|
13
|
+
*/
|
|
14
|
+
interface PKCEChallenge {
|
|
15
|
+
/** The code verifier to be sent to the token endpoint */
|
|
16
|
+
readonly verifier: string;
|
|
17
|
+
/** The code challenge to be sent to the authorization endpoint */
|
|
18
|
+
readonly challenge: string;
|
|
19
|
+
/** The challenge method used */
|
|
20
|
+
readonly method: "S256";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generates a complete PKCE challenge for OAuth authorization requests.
|
|
24
|
+
* Creates a cryptographically secure verifier and corresponding S256 challenge.
|
|
25
|
+
* Validates that the generated verifier meets standard requirements (43-128 characters).
|
|
26
|
+
*
|
|
27
|
+
* @param length - Length of the random buffer in bytes (32-96 range to generate 43-128 character verifier)
|
|
28
|
+
* @returns Promise resolving to PKCE challenge data
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const pkce = await generatePKCE()
|
|
33
|
+
*
|
|
34
|
+
* // Use challenge in authorization URL
|
|
35
|
+
* authUrl.searchParams.set('code_challenge', pkce.challenge)
|
|
36
|
+
* authUrl.searchParams.set('code_challenge_method', pkce.method)
|
|
37
|
+
*
|
|
38
|
+
* // Store verifier for token exchange
|
|
39
|
+
* sessionStorage.setItem('code_verifier', pkce.verifier)
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @throws {RangeError} If length is outside valid range or generated verifier doesn't meet requirements
|
|
43
|
+
*/
|
|
44
|
+
declare const generatePKCE: (length?: number) => Promise<PKCEChallenge>;
|
|
45
|
+
/**
|
|
46
|
+
* Validates a PKCE code verifier against a previously generated challenge.
|
|
47
|
+
* Uses timing-safe comparison and timing normalization to prevent timing attacks.
|
|
48
|
+
* All validation paths take the same computational time regardless of input validity,
|
|
49
|
+
* making it resistant to timing-based side-channel attacks.
|
|
50
|
+
*
|
|
51
|
+
* @param verifier - The code verifier received from the client
|
|
52
|
+
* @param challenge - The code challenge stored during authorization
|
|
53
|
+
* @param method - The challenge method used during generation
|
|
54
|
+
* @returns Promise resolving to true if verifier matches challenge
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* // During token exchange
|
|
59
|
+
* const isValid = await validatePKCE(
|
|
60
|
+
* receivedVerifier,
|
|
61
|
+
* storedChallenge,
|
|
62
|
+
* 'S256'
|
|
63
|
+
* )
|
|
64
|
+
*
|
|
65
|
+
* if (!isValid) {
|
|
66
|
+
* throw new Error('Invalid PKCE verifier')
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
declare const validatePKCE: (verifier: string, challenge: string, method?: PKCEMethod) => Promise<boolean>;
|
|
71
|
+
//#endregion
|
|
72
|
+
export { generatePKCE, validatePKCE };
|
package/dist/pkce.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "../storage-CxKerLlc.js";
|
|
2
|
+
import "../provider-CwWMG-1l.js";
|
|
3
|
+
import { CodeProvider, CodeProviderConfig, CodeProviderError, CodeProviderOptions, CodeProviderState, CodeUserData } from "../code-l_uvMR1j.js";
|
|
4
|
+
export { CodeProvider, CodeProviderConfig, CodeProviderError, CodeProviderOptions, CodeProviderState, CodeUserData };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { generateUnbiasedDigits, timingSafeCompare } from "../random-SXMYlaVr.js";
|
|
2
|
+
|
|
3
|
+
//#region src/provider/code.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates a PIN code authentication provider.
|
|
6
|
+
* Implements a flexible claim-based authentication flow with PIN verification.
|
|
7
|
+
*
|
|
8
|
+
* @template Claims - Type of claims to collect (email, phone, username, etc.)
|
|
9
|
+
* @param config - PIN code provider configuration
|
|
10
|
+
* @returns Provider instance implementing PIN code authentication
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Email-based PIN authentication
|
|
15
|
+
* const emailCodeProvider = CodeProvider<{ email: string }>({
|
|
16
|
+
* length: 6,
|
|
17
|
+
* request: async (req, state, form, error) => {
|
|
18
|
+
* if (state.type === 'start') {
|
|
19
|
+
* return new Response(renderEmailForm(form?.get('email'), error))
|
|
20
|
+
* } else {
|
|
21
|
+
* return new Response(renderPinForm(state.claims.email, error, state.resend))
|
|
22
|
+
* }
|
|
23
|
+
* },
|
|
24
|
+
* sendCode: async (claims, code) => {
|
|
25
|
+
* if (!claims.email || !isValidEmail(claims.email)) {
|
|
26
|
+
* return {
|
|
27
|
+
* type: "invalid_claim",
|
|
28
|
+
* key: "email",
|
|
29
|
+
* value: "Invalid email address"
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* await emailService.send(claims.email, `Your verification code: ${code}`)
|
|
34
|
+
* }
|
|
35
|
+
* })
|
|
36
|
+
*
|
|
37
|
+
* // Multi-channel PIN authentication (email or phone)
|
|
38
|
+
* const flexibleCodeProvider = CodeProvider<{ email?: string; phone?: string }>({
|
|
39
|
+
* length: 4,
|
|
40
|
+
* request: async (req, state, form, error) => {
|
|
41
|
+
* if (state.type === 'start') {
|
|
42
|
+
* return new Response(renderContactForm(form, error))
|
|
43
|
+
* } else {
|
|
44
|
+
* const contact = state.claims.email || state.claims.phone
|
|
45
|
+
* return new Response(renderPinForm(contact, error))
|
|
46
|
+
* }
|
|
47
|
+
* },
|
|
48
|
+
* sendCode: async (claims, code) => {
|
|
49
|
+
* if (claims.email) {
|
|
50
|
+
* await emailService.send(claims.email, `PIN: ${code}`)
|
|
51
|
+
* } else if (claims.phone) {
|
|
52
|
+
* await smsService.send(claims.phone, `PIN: ${code}`)
|
|
53
|
+
* } else {
|
|
54
|
+
* return {
|
|
55
|
+
* type: "invalid_claim",
|
|
56
|
+
* key: "contact",
|
|
57
|
+
* value: "Provide either email or phone number"
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
* })
|
|
62
|
+
*
|
|
63
|
+
* // Usage in issuer
|
|
64
|
+
* export default issuer({
|
|
65
|
+
* providers: {
|
|
66
|
+
* email: emailCodeProvider,
|
|
67
|
+
* flexible: flexibleCodeProvider
|
|
68
|
+
* },
|
|
69
|
+
* success: async (ctx, value) => {
|
|
70
|
+
* if (value.provider === "code") {
|
|
71
|
+
* const email = value.claims.email
|
|
72
|
+
* const phone = value.claims.phone
|
|
73
|
+
*
|
|
74
|
+
* // Look up or create user based on verified claims
|
|
75
|
+
* const userId = await findOrCreateUser({ email, phone })
|
|
76
|
+
*
|
|
77
|
+
* return ctx.subject("user", { userId, email, phone })
|
|
78
|
+
* }
|
|
79
|
+
* }
|
|
80
|
+
* })
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
const CodeProvider = (config) => {
|
|
84
|
+
const codeLength = config.length || 6;
|
|
85
|
+
/**
|
|
86
|
+
* Generates a cryptographically secure PIN code.
|
|
87
|
+
*/
|
|
88
|
+
const generateCode = () => {
|
|
89
|
+
return generateUnbiasedDigits(codeLength);
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
type: "code",
|
|
93
|
+
init(routes, ctx) {
|
|
94
|
+
/**
|
|
95
|
+
* Transitions between authentication states and renders the appropriate UI.
|
|
96
|
+
*/
|
|
97
|
+
const transition = async (c, nextState, formData, error) => {
|
|
98
|
+
await ctx.set(c, "provider", 60 * 60 * 24, nextState);
|
|
99
|
+
const response = await config.request(c.request, nextState, formData, error);
|
|
100
|
+
return ctx.forward(c, response);
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* GET /authorize - Display initial claim collection form
|
|
104
|
+
*/
|
|
105
|
+
routes.get("/authorize", (c) => {
|
|
106
|
+
return transition(c, { type: "start" });
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* POST /authorize - Handle form submissions and state transitions
|
|
110
|
+
*/
|
|
111
|
+
routes.post("/authorize", async (c) => {
|
|
112
|
+
const formData = await c.formData();
|
|
113
|
+
const currentState = await ctx.get(c, "provider");
|
|
114
|
+
const action = formData.get("action")?.toString();
|
|
115
|
+
if (action === "request" || action === "resend") {
|
|
116
|
+
const code = generateCode();
|
|
117
|
+
const formEntries = Object.fromEntries(formData);
|
|
118
|
+
const { action: _,...claims } = formEntries;
|
|
119
|
+
const sendError = await config.sendCode(claims, code);
|
|
120
|
+
if (sendError) return transition(c, { type: "start" }, formData, sendError);
|
|
121
|
+
return transition(c, {
|
|
122
|
+
type: "code",
|
|
123
|
+
resend: action === "resend",
|
|
124
|
+
claims,
|
|
125
|
+
code
|
|
126
|
+
}, formData);
|
|
127
|
+
}
|
|
128
|
+
if (action === "verify" && currentState?.type === "code") {
|
|
129
|
+
const enteredCode = formData.get("code")?.toString();
|
|
130
|
+
if (!(currentState.code && enteredCode && timingSafeCompare(currentState.code, enteredCode))) return transition(c, {
|
|
131
|
+
...currentState,
|
|
132
|
+
resend: false
|
|
133
|
+
}, formData, { type: "invalid_code" });
|
|
134
|
+
await ctx.unset(c, "provider");
|
|
135
|
+
const successResponse = await ctx.success(c, { claims: currentState.claims });
|
|
136
|
+
return ctx.forward(c, successResponse);
|
|
137
|
+
}
|
|
138
|
+
return transition(c, { type: "start" }, formData);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
export { CodeProvider };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import "../storage-CxKerLlc.js";
|
|
2
|
+
import { Provider } from "../provider-CwWMG-1l.js";
|
|
3
|
+
import { Oauth2UserData, Oauth2WrappedConfig } from "../oauth2-DtKwtl8p.js";
|
|
4
|
+
|
|
5
|
+
//#region src/provider/facebook.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Configuration options for Facebook OAuth 2.0 provider.
|
|
9
|
+
* Extends the base OAuth 2.0 configuration with Facebook-specific documentation.
|
|
10
|
+
*/
|
|
11
|
+
interface FacebookConfig extends Oauth2WrappedConfig {
|
|
12
|
+
/**
|
|
13
|
+
* Facebook App ID from your Facebook App Dashboard.
|
|
14
|
+
* This is the public identifier for your Facebook application.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* {
|
|
19
|
+
* clientID: "1234567890123456"
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
readonly clientID: string;
|
|
24
|
+
/**
|
|
25
|
+
* Facebook App Secret from your Facebook App Dashboard.
|
|
26
|
+
* Keep this secure and never expose it to client-side code.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* {
|
|
31
|
+
* clientSecret: process.env.FACEBOOK_APP_SECRET
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
readonly clientSecret: string;
|
|
36
|
+
/**
|
|
37
|
+
* Facebook permissions to request during login.
|
|
38
|
+
* Determines what data your app can access from the user's Facebook account.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* {
|
|
43
|
+
* scopes: [
|
|
44
|
+
* "email", // User's email address
|
|
45
|
+
* "public_profile", // Basic profile info
|
|
46
|
+
* "user_friends", // User's friends list
|
|
47
|
+
* "user_posts" // User's timeline posts
|
|
48
|
+
* ]
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
readonly scopes: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Additional query parameters for Facebook OAuth authorization.
|
|
55
|
+
* Useful for Facebook-specific options like response type or display mode.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* {
|
|
60
|
+
* query: {
|
|
61
|
+
* display: "popup", // Show login in popup
|
|
62
|
+
* auth_type: "rerequest", // Force permission re-request
|
|
63
|
+
* state: "custom-state" // Custom state parameter
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
readonly query?: Record<string, string>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Creates a Facebook OAuth 2.0 authentication provider.
|
|
72
|
+
* Use this when you need access tokens to call Facebook Graph API on behalf of the user.
|
|
73
|
+
*
|
|
74
|
+
* @param config - Facebook OAuth 2.0 configuration
|
|
75
|
+
* @returns OAuth 2.0 provider configured for Facebook
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* // Basic Facebook authentication
|
|
80
|
+
* const basicFacebook = FacebookProvider({
|
|
81
|
+
* clientID: process.env.FACEBOOK_APP_ID,
|
|
82
|
+
* clientSecret: process.env.FACEBOOK_APP_SECRET,
|
|
83
|
+
* scopes: ["email", "public_profile"]
|
|
84
|
+
* })
|
|
85
|
+
*
|
|
86
|
+
* // Facebook with extended permissions
|
|
87
|
+
* const extendedFacebook = FacebookProvider({
|
|
88
|
+
* clientID: process.env.FACEBOOK_APP_ID,
|
|
89
|
+
* clientSecret: process.env.FACEBOOK_APP_SECRET,
|
|
90
|
+
* scopes: [
|
|
91
|
+
* "email",
|
|
92
|
+
* "public_profile",
|
|
93
|
+
* "user_friends",
|
|
94
|
+
* "user_posts",
|
|
95
|
+
* "user_photos"
|
|
96
|
+
* ],
|
|
97
|
+
* query: {
|
|
98
|
+
* display: "popup",
|
|
99
|
+
* auth_type: "rerequest" // Force permission approval
|
|
100
|
+
* }
|
|
101
|
+
* })
|
|
102
|
+
*
|
|
103
|
+
* // Using the access token for Graph API calls
|
|
104
|
+
* export default issuer({
|
|
105
|
+
* providers: { facebook: extendedFacebook },
|
|
106
|
+
* success: async (ctx, value) => {
|
|
107
|
+
* if (value.provider === "facebook") {
|
|
108
|
+
* const token = value.tokenset.access
|
|
109
|
+
*
|
|
110
|
+
* // Get user profile with custom fields
|
|
111
|
+
* const profileRes = await fetch(
|
|
112
|
+
* `https://graph.facebook.com/me?fields=id,name,email,picture.width(200),friends&access_token=${token}`
|
|
113
|
+
* )
|
|
114
|
+
* const profile = await profileRes.json()
|
|
115
|
+
*
|
|
116
|
+
* // Get user's posts (if permission granted)
|
|
117
|
+
* const postsRes = await fetch(
|
|
118
|
+
* `https://graph.facebook.com/me/posts?access_token=${token}`
|
|
119
|
+
* )
|
|
120
|
+
* const posts = await postsRes.json()
|
|
121
|
+
*
|
|
122
|
+
* return ctx.subject("user", {
|
|
123
|
+
* facebookId: profile.id,
|
|
124
|
+
* name: profile.name,
|
|
125
|
+
* email: profile.email,
|
|
126
|
+
* picture: profile.picture?.data?.url,
|
|
127
|
+
* friendsCount: profile.friends?.summary?.total_count || 0,
|
|
128
|
+
* postsCount: posts.data?.length || 0
|
|
129
|
+
* })
|
|
130
|
+
* }
|
|
131
|
+
* }
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare const FacebookProvider: (config: FacebookConfig) => Provider<Oauth2UserData>;
|
|
136
|
+
//#endregion
|
|
137
|
+
export { FacebookConfig, FacebookProvider };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import "../util-CSdHUFOo.js";
|
|
2
|
+
import "../error-DgAKK7b2.js";
|
|
3
|
+
import "../pkce-276Za_rZ.js";
|
|
4
|
+
import "../random-SXMYlaVr.js";
|
|
5
|
+
import { Oauth2Provider } from "../oauth2-B7-6Z7Lc.js";
|
|
6
|
+
|
|
7
|
+
//#region src/provider/facebook.ts
|
|
8
|
+
/**
|
|
9
|
+
* Creates a Facebook OAuth 2.0 authentication provider.
|
|
10
|
+
* Use this when you need access tokens to call Facebook Graph API on behalf of the user.
|
|
11
|
+
*
|
|
12
|
+
* @param config - Facebook OAuth 2.0 configuration
|
|
13
|
+
* @returns OAuth 2.0 provider configured for Facebook
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // Basic Facebook authentication
|
|
18
|
+
* const basicFacebook = FacebookProvider({
|
|
19
|
+
* clientID: process.env.FACEBOOK_APP_ID,
|
|
20
|
+
* clientSecret: process.env.FACEBOOK_APP_SECRET,
|
|
21
|
+
* scopes: ["email", "public_profile"]
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* // Facebook with extended permissions
|
|
25
|
+
* const extendedFacebook = FacebookProvider({
|
|
26
|
+
* clientID: process.env.FACEBOOK_APP_ID,
|
|
27
|
+
* clientSecret: process.env.FACEBOOK_APP_SECRET,
|
|
28
|
+
* scopes: [
|
|
29
|
+
* "email",
|
|
30
|
+
* "public_profile",
|
|
31
|
+
* "user_friends",
|
|
32
|
+
* "user_posts",
|
|
33
|
+
* "user_photos"
|
|
34
|
+
* ],
|
|
35
|
+
* query: {
|
|
36
|
+
* display: "popup",
|
|
37
|
+
* auth_type: "rerequest" // Force permission approval
|
|
38
|
+
* }
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* // Using the access token for Graph API calls
|
|
42
|
+
* export default issuer({
|
|
43
|
+
* providers: { facebook: extendedFacebook },
|
|
44
|
+
* success: async (ctx, value) => {
|
|
45
|
+
* if (value.provider === "facebook") {
|
|
46
|
+
* const token = value.tokenset.access
|
|
47
|
+
*
|
|
48
|
+
* // Get user profile with custom fields
|
|
49
|
+
* const profileRes = await fetch(
|
|
50
|
+
* `https://graph.facebook.com/me?fields=id,name,email,picture.width(200),friends&access_token=${token}`
|
|
51
|
+
* )
|
|
52
|
+
* const profile = await profileRes.json()
|
|
53
|
+
*
|
|
54
|
+
* // Get user's posts (if permission granted)
|
|
55
|
+
* const postsRes = await fetch(
|
|
56
|
+
* `https://graph.facebook.com/me/posts?access_token=${token}`
|
|
57
|
+
* )
|
|
58
|
+
* const posts = await postsRes.json()
|
|
59
|
+
*
|
|
60
|
+
* return ctx.subject("user", {
|
|
61
|
+
* facebookId: profile.id,
|
|
62
|
+
* name: profile.name,
|
|
63
|
+
* email: profile.email,
|
|
64
|
+
* picture: profile.picture?.data?.url,
|
|
65
|
+
* friendsCount: profile.friends?.summary?.total_count || 0,
|
|
66
|
+
* postsCount: posts.data?.length || 0
|
|
67
|
+
* })
|
|
68
|
+
* }
|
|
69
|
+
* }
|
|
70
|
+
* })
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
const FacebookProvider = (config) => {
|
|
74
|
+
return Oauth2Provider({
|
|
75
|
+
...config,
|
|
76
|
+
type: "facebook",
|
|
77
|
+
endpoint: {
|
|
78
|
+
authorization: "https://www.facebook.com/v18.0/dialog/oauth",
|
|
79
|
+
token: "https://graph.facebook.com/v18.0/oauth/access_token"
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
export { FacebookProvider };
|