handshake-auth 0.1.0 → 0.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 (54) hide show
  1. package/ReadMe.md +230 -16
  2. package/dist/index.d.ts +18 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/middleware/express.d.ts +67 -0
  7. package/dist/middleware/express.d.ts.map +1 -1
  8. package/dist/middleware/express.js +69 -0
  9. package/dist/middleware/express.js.map +1 -1
  10. package/dist/middleware/index.d.ts +2 -2
  11. package/dist/middleware/index.d.ts.map +1 -1
  12. package/dist/middleware/index.js +1 -1
  13. package/dist/middleware/index.js.map +1 -1
  14. package/dist/strategies/discord.d.ts +99 -0
  15. package/dist/strategies/discord.d.ts.map +1 -0
  16. package/dist/strategies/discord.js +85 -0
  17. package/dist/strategies/discord.js.map +1 -0
  18. package/dist/strategies/github.d.ts +112 -0
  19. package/dist/strategies/github.d.ts.map +1 -0
  20. package/dist/strategies/github.js +110 -0
  21. package/dist/strategies/github.js.map +1 -0
  22. package/dist/strategies/google.d.ts +91 -0
  23. package/dist/strategies/google.d.ts.map +1 -0
  24. package/dist/strategies/google.js +77 -0
  25. package/dist/strategies/google.js.map +1 -0
  26. package/dist/strategies/index.d.ts +16 -0
  27. package/dist/strategies/index.d.ts.map +1 -1
  28. package/dist/strategies/index.js +10 -0
  29. package/dist/strategies/index.js.map +1 -1
  30. package/dist/strategies/magic-link.d.ts +141 -0
  31. package/dist/strategies/magic-link.d.ts.map +1 -0
  32. package/dist/strategies/magic-link.js +186 -0
  33. package/dist/strategies/magic-link.js.map +1 -0
  34. package/dist/strategies/microsoft.d.ts +127 -0
  35. package/dist/strategies/microsoft.d.ts.map +1 -0
  36. package/dist/strategies/microsoft.js +98 -0
  37. package/dist/strategies/microsoft.js.map +1 -0
  38. package/dist/strategies/oauth-base.d.ts +162 -0
  39. package/dist/strategies/oauth-base.d.ts.map +1 -0
  40. package/dist/strategies/oauth-base.js +243 -0
  41. package/dist/strategies/oauth-base.js.map +1 -0
  42. package/dist/strategies/password.d.ts +69 -6
  43. package/dist/strategies/password.d.ts.map +1 -1
  44. package/dist/strategies/password.js +73 -24
  45. package/dist/strategies/password.js.map +1 -1
  46. package/dist/strategies/twitter-x.d.ts +130 -0
  47. package/dist/strategies/twitter-x.d.ts.map +1 -0
  48. package/dist/strategies/twitter-x.js +275 -0
  49. package/dist/strategies/twitter-x.js.map +1 -0
  50. package/dist/strategies/username-password.d.ts +38 -0
  51. package/dist/strategies/username-password.d.ts.map +1 -0
  52. package/dist/strategies/username-password.js +61 -0
  53. package/dist/strategies/username-password.js.map +1 -0
  54. package/package.json +2 -2
@@ -0,0 +1,130 @@
1
+ import type { OAuthProfile, AuthResult, HandshakeCallbacks } from '../types.js';
2
+ import { OAuthStrategy } from './oauth-base.js';
3
+ /**
4
+ * Twitter/X user profile from the /2/users/me endpoint.
5
+ */
6
+ export interface TwitterXProfile {
7
+ data: {
8
+ id: string;
9
+ name: string;
10
+ username: string;
11
+ profile_image_url?: string;
12
+ description?: string;
13
+ location?: string;
14
+ url?: string;
15
+ verified?: boolean;
16
+ created_at?: string;
17
+ public_metrics?: {
18
+ followers_count: number;
19
+ following_count: number;
20
+ tweet_count: number;
21
+ listed_count: number;
22
+ };
23
+ };
24
+ }
25
+ /**
26
+ * Configuration options for Twitter/X OAuth strategy.
27
+ */
28
+ export interface TwitterXStrategyOptions {
29
+ /**
30
+ * Twitter/X OAuth 2.0 client ID
31
+ */
32
+ clientId: string;
33
+ /**
34
+ * Twitter/X OAuth 2.0 client secret
35
+ */
36
+ clientSecret: string;
37
+ /**
38
+ * Your callback URL (must match Twitter Developer Portal configuration)
39
+ */
40
+ redirectUri: string;
41
+ /**
42
+ * OAuth scopes to request
43
+ * @default ['tweet.read', 'users.read']
44
+ */
45
+ scopes?: string[];
46
+ }
47
+ /**
48
+ * Twitter/X OAuth 2.0 authentication strategy.
49
+ *
50
+ * Uses OAuth 2.0 with PKCE (Proof Key for Code Exchange) as required by
51
+ * Twitter's API v2.
52
+ *
53
+ * Note: Twitter does not provide email addresses through the OAuth API.
54
+ * The profile will have email set to null.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const hs = new Handshake({
59
+ * findAccount: async (email) => db.accounts.findByEmail(email),
60
+ * findOrCreateFromOAuth: async (provider, profile) => {
61
+ * // Note: Twitter doesn't provide email, use profile.id for lookup
62
+ * let account = await db.accounts.findByProviderId(provider, profile.id);
63
+ * if (!account) {
64
+ * account = await db.accounts.create({
65
+ * name: profile.name,
66
+ * providerId: profile.id,
67
+ * provider,
68
+ * // You may want to prompt user for email separately
69
+ * });
70
+ * }
71
+ * return account;
72
+ * },
73
+ * });
74
+ *
75
+ * hs.use(new TwitterXStrategy({
76
+ * clientId: process.env.TWITTER_CLIENT_ID!,
77
+ * clientSecret: process.env.TWITTER_CLIENT_SECRET!,
78
+ * redirectUri: 'http://localhost:3000/auth/twitter/callback',
79
+ * }));
80
+ *
81
+ * // Routes
82
+ * app.get('/auth/twitter', async (req, res) => {
83
+ * const result = await hs.authenticate('twitter-x', req, res, 'redirect');
84
+ * if ('redirectUrl' in result) {
85
+ * res.redirect(result.redirectUrl);
86
+ * }
87
+ * });
88
+ *
89
+ * app.get('/auth/twitter/callback', async (req, res) => {
90
+ * const result = await hs.authenticate('twitter-x', req, res, 'callback');
91
+ * if (result.account) {
92
+ * login(req, result.account);
93
+ * res.redirect('/dashboard');
94
+ * } else {
95
+ * res.status(401).send(result.error);
96
+ * }
97
+ * });
98
+ * ```
99
+ */
100
+ export declare class TwitterXStrategy<TAccount> extends OAuthStrategy<TAccount> {
101
+ private readonly codeVerifierCookieName;
102
+ constructor(options: TwitterXStrategyOptions);
103
+ /**
104
+ * Override authenticate to handle PKCE flow.
105
+ */
106
+ authenticate(callbacks: HandshakeCallbacks<TAccount>, ...args: unknown[]): Promise<AuthResult<TAccount>>;
107
+ /**
108
+ * Handle redirect with PKCE challenge.
109
+ */
110
+ private handleRedirectWithPKCE;
111
+ /**
112
+ * Handle callback with PKCE verification.
113
+ */
114
+ private handleCallbackWithPKCE;
115
+ /**
116
+ * Exchange code for token using PKCE.
117
+ */
118
+ private exchangeCodeWithPKCE;
119
+ /**
120
+ * Fetch user profile with additional fields.
121
+ */
122
+ protected fetchUserProfile(accessToken: string): Promise<OAuthProfile | {
123
+ error: string;
124
+ }>;
125
+ /**
126
+ * Map Twitter/X profile to standard OAuthProfile.
127
+ */
128
+ protected mapProfile(data: unknown, accessToken: string): OAuthProfile;
129
+ }
130
+ //# sourceMappingURL=twitter-x.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twitter-x.d.ts","sourceRoot":"","sources":["../../src/strategies/twitter-x.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,aAAa,EAA2C,MAAM,iBAAiB,CAAC;AAEzF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,cAAc,CAAC,EAAE;YACf,eAAe,EAAE,MAAM,CAAC;YACxB,eAAe,EAAE,MAAM,CAAC;YACxB,WAAW,EAAE,MAAM,CAAC;YACpB,YAAY,EAAE,MAAM,CAAC;SACtB,CAAC;KACH,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,qBAAa,gBAAgB,CAAC,QAAQ,CAAE,SAAQ,aAAa,CAAC,QAAQ,CAAC;IACrE,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA2B;gBAEtD,OAAO,EAAE,uBAAuB;IAc5C;;OAEG;IACG,YAAY,CAChB,SAAS,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EACvC,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAehC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;OAEG;YACW,sBAAsB;IAqFpC;;OAEG;YACW,oBAAoB;IA0ClC;;OAEG;cACa,gBAAgB,CAC9B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IA6B5C;;OAEG;IAEH,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,YAAY;CAYvE"}
@@ -0,0 +1,275 @@
1
+ import { randomBytes, createHash } from 'crypto';
2
+ import { OAuthStrategy } from './oauth-base.js';
3
+ /**
4
+ * Twitter/X OAuth 2.0 authentication strategy.
5
+ *
6
+ * Uses OAuth 2.0 with PKCE (Proof Key for Code Exchange) as required by
7
+ * Twitter's API v2.
8
+ *
9
+ * Note: Twitter does not provide email addresses through the OAuth API.
10
+ * The profile will have email set to null.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const hs = new Handshake({
15
+ * findAccount: async (email) => db.accounts.findByEmail(email),
16
+ * findOrCreateFromOAuth: async (provider, profile) => {
17
+ * // Note: Twitter doesn't provide email, use profile.id for lookup
18
+ * let account = await db.accounts.findByProviderId(provider, profile.id);
19
+ * if (!account) {
20
+ * account = await db.accounts.create({
21
+ * name: profile.name,
22
+ * providerId: profile.id,
23
+ * provider,
24
+ * // You may want to prompt user for email separately
25
+ * });
26
+ * }
27
+ * return account;
28
+ * },
29
+ * });
30
+ *
31
+ * hs.use(new TwitterXStrategy({
32
+ * clientId: process.env.TWITTER_CLIENT_ID!,
33
+ * clientSecret: process.env.TWITTER_CLIENT_SECRET!,
34
+ * redirectUri: 'http://localhost:3000/auth/twitter/callback',
35
+ * }));
36
+ *
37
+ * // Routes
38
+ * app.get('/auth/twitter', async (req, res) => {
39
+ * const result = await hs.authenticate('twitter-x', req, res, 'redirect');
40
+ * if ('redirectUrl' in result) {
41
+ * res.redirect(result.redirectUrl);
42
+ * }
43
+ * });
44
+ *
45
+ * app.get('/auth/twitter/callback', async (req, res) => {
46
+ * const result = await hs.authenticate('twitter-x', req, res, 'callback');
47
+ * if (result.account) {
48
+ * login(req, result.account);
49
+ * res.redirect('/dashboard');
50
+ * } else {
51
+ * res.status(401).send(result.error);
52
+ * }
53
+ * });
54
+ * ```
55
+ */
56
+ export class TwitterXStrategy extends OAuthStrategy {
57
+ codeVerifierCookieName = 'twitter_code_verifier';
58
+ constructor(options) {
59
+ super({
60
+ name: 'twitter-x',
61
+ clientId: options.clientId,
62
+ clientSecret: options.clientSecret,
63
+ redirectUri: options.redirectUri,
64
+ authorizeUrl: 'https://twitter.com/i/oauth2/authorize',
65
+ tokenUrl: 'https://api.twitter.com/2/oauth2/token',
66
+ userInfoUrl: 'https://api.twitter.com/2/users/me',
67
+ scopes: options.scopes ?? ['tweet.read', 'users.read'],
68
+ stateCookieName: 'twitter_oauth_state',
69
+ });
70
+ }
71
+ /**
72
+ * Override authenticate to handle PKCE flow.
73
+ */
74
+ async authenticate(callbacks, ...args) {
75
+ const [req, res, phase] = args;
76
+ if (phase === 'redirect') {
77
+ return this.handleRedirectWithPKCE(req, res);
78
+ }
79
+ else if (phase === 'callback') {
80
+ return this.handleCallbackWithPKCE(callbacks, req, res);
81
+ }
82
+ else {
83
+ return {
84
+ account: null,
85
+ error: `Invalid OAuth phase: ${phase}. Use 'redirect' or 'callback'`,
86
+ };
87
+ }
88
+ }
89
+ /**
90
+ * Handle redirect with PKCE challenge.
91
+ */
92
+ handleRedirectWithPKCE(_req, res) {
93
+ // Generate PKCE code verifier and challenge
94
+ const codeVerifier = randomBytes(32).toString('base64url');
95
+ const codeChallenge = createHash('sha256')
96
+ .update(codeVerifier)
97
+ .digest('base64url');
98
+ // Generate state for CSRF protection
99
+ const state = randomBytes(32).toString('hex');
100
+ // Store state and code verifier in cookies
101
+ const cookieOptions = {
102
+ httpOnly: true,
103
+ secure: process.env.NODE_ENV === 'production',
104
+ sameSite: 'lax',
105
+ maxAge: 10 * 60 * 1000, // 10 minutes
106
+ };
107
+ res.cookie(this.stateCookieName, state, cookieOptions);
108
+ res.cookie(this.codeVerifierCookieName, codeVerifier, cookieOptions);
109
+ // Build authorization URL with PKCE
110
+ const params = new URLSearchParams({
111
+ client_id: this.clientId,
112
+ redirect_uri: this.redirectUri,
113
+ response_type: 'code',
114
+ scope: this.scopes.join(' '),
115
+ state,
116
+ code_challenge: codeChallenge,
117
+ code_challenge_method: 'S256',
118
+ });
119
+ const redirectUrl = `${this.authorizeUrl}?${params.toString()}`;
120
+ return {
121
+ account: null,
122
+ redirectUrl,
123
+ };
124
+ }
125
+ /**
126
+ * Handle callback with PKCE verification.
127
+ */
128
+ async handleCallbackWithPKCE(callbacks, req, res) {
129
+ // Get state, code, and verifier
130
+ const { state: returnedState, code, error, error_description } = req.query;
131
+ // Check for OAuth error response
132
+ if (error) {
133
+ return {
134
+ account: null,
135
+ error: error_description || error,
136
+ };
137
+ }
138
+ // Verify state
139
+ const storedState = req.cookies?.[this.stateCookieName];
140
+ const codeVerifier = req.cookies?.[this.codeVerifierCookieName];
141
+ // Clear cookies
142
+ res.clearCookie(this.stateCookieName);
143
+ res.clearCookie(this.codeVerifierCookieName);
144
+ if (!storedState || storedState !== returnedState) {
145
+ return {
146
+ account: null,
147
+ error: 'Invalid OAuth state. Possible CSRF attack or expired session.',
148
+ };
149
+ }
150
+ if (!code) {
151
+ return {
152
+ account: null,
153
+ error: 'No authorization code received',
154
+ };
155
+ }
156
+ if (!codeVerifier) {
157
+ return {
158
+ account: null,
159
+ error: 'Missing PKCE code verifier. Session may have expired.',
160
+ };
161
+ }
162
+ // Exchange code for token with PKCE
163
+ const tokenResult = await this.exchangeCodeWithPKCE(code, codeVerifier);
164
+ if ('error' in tokenResult) {
165
+ return {
166
+ account: null,
167
+ error: tokenResult.error,
168
+ };
169
+ }
170
+ // Fetch user profile
171
+ const profileResult = await this.fetchUserProfile(tokenResult.access_token);
172
+ if ('error' in profileResult) {
173
+ return {
174
+ account: null,
175
+ error: profileResult.error,
176
+ };
177
+ }
178
+ // Check for required callback
179
+ if (!callbacks.findOrCreateFromOAuth) {
180
+ return {
181
+ account: null,
182
+ error: 'findOrCreateFromOAuth callback is required for OAuth strategies',
183
+ };
184
+ }
185
+ // Let the application handle account creation/lookup
186
+ const account = await callbacks.findOrCreateFromOAuth(this.name, profileResult);
187
+ return {
188
+ account,
189
+ strategy: this.name,
190
+ };
191
+ }
192
+ /**
193
+ * Exchange code for token using PKCE.
194
+ */
195
+ async exchangeCodeWithPKCE(code, codeVerifier) {
196
+ try {
197
+ // Twitter requires Basic auth for token exchange
198
+ const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
199
+ const params = new URLSearchParams({
200
+ code,
201
+ grant_type: 'authorization_code',
202
+ redirect_uri: this.redirectUri,
203
+ code_verifier: codeVerifier,
204
+ });
205
+ const response = await fetch(this.tokenUrl, {
206
+ method: 'POST',
207
+ headers: {
208
+ 'Content-Type': 'application/x-www-form-urlencoded',
209
+ Authorization: `Basic ${credentials}`,
210
+ Accept: 'application/json',
211
+ },
212
+ body: params.toString(),
213
+ });
214
+ if (!response.ok) {
215
+ const errorData = await response.json().catch(() => ({}));
216
+ return {
217
+ error: errorData.error_description ||
218
+ errorData.error ||
219
+ `Token exchange failed: ${response.status}`,
220
+ };
221
+ }
222
+ return (await response.json());
223
+ }
224
+ catch (err) {
225
+ return {
226
+ error: `Token exchange failed: ${err instanceof Error ? err.message : 'Unknown error'}`,
227
+ };
228
+ }
229
+ }
230
+ /**
231
+ * Fetch user profile with additional fields.
232
+ */
233
+ async fetchUserProfile(accessToken) {
234
+ try {
235
+ // Request additional user fields
236
+ const params = new URLSearchParams({
237
+ 'user.fields': 'id,name,username,profile_image_url,description',
238
+ });
239
+ const response = await fetch(`${this.userInfoUrl}?${params.toString()}`, {
240
+ headers: {
241
+ Authorization: `Bearer ${accessToken}`,
242
+ Accept: 'application/json',
243
+ },
244
+ });
245
+ if (!response.ok) {
246
+ return {
247
+ error: `Failed to fetch user profile: ${response.status}`,
248
+ };
249
+ }
250
+ const data = await response.json();
251
+ return this.mapProfile(data, accessToken);
252
+ }
253
+ catch (err) {
254
+ return {
255
+ error: `Failed to fetch user profile: ${err instanceof Error ? err.message : 'Unknown error'}`,
256
+ };
257
+ }
258
+ }
259
+ /**
260
+ * Map Twitter/X profile to standard OAuthProfile.
261
+ */
262
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
263
+ mapProfile(data, accessToken) {
264
+ const response = data;
265
+ const profile = response.data;
266
+ return {
267
+ id: profile.id,
268
+ email: null, // Twitter doesn't provide email through OAuth
269
+ name: profile.name,
270
+ picture: profile.profile_image_url ?? null,
271
+ raw: profile,
272
+ };
273
+ }
274
+ }
275
+ //# sourceMappingURL=twitter-x.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twitter-x.js","sourceRoot":"","sources":["../../src/strategies/twitter-x.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { AuthResult, Strategy, HandshakeCallbacks } from '../types.js';
2
+ /**
3
+ * Username/Password authentication strategy.
4
+ *
5
+ * Authenticates users with an identifier (e.g., email, username) and password.
6
+ * Uses the findAccount and verifyPassword callbacks from Handshake.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const hs = new Handshake({
11
+ * findAccount: async (email) => db.accounts.findByEmail(email),
12
+ * verifyPassword: async (account, password) => {
13
+ * return bcrypt.compare(password, account.passwordHash);
14
+ * },
15
+ * });
16
+ *
17
+ * hs.use(new UsernamePasswordStrategy());
18
+ *
19
+ * // In your login route
20
+ * const result = await hs.authenticate('username-password', email, password);
21
+ * if (result.account) {
22
+ * login(req, result.account);
23
+ * }
24
+ * ```
25
+ */
26
+ export declare class UsernamePasswordStrategy<TAccount> implements Strategy<TAccount> {
27
+ readonly name = "username-password";
28
+ /**
29
+ * Authenticate with identifier and password.
30
+ *
31
+ * @param callbacks - Handshake callbacks (findAccount, verifyPassword)
32
+ * @param identifier - The account identifier (e.g., email, username)
33
+ * @param password - The password to verify
34
+ * @returns Authentication result with account or error
35
+ */
36
+ authenticate(callbacks: HandshakeCallbacks<TAccount>, ...args: unknown[]): Promise<AuthResult<TAccount>>;
37
+ }
38
+ //# sourceMappingURL=username-password.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"username-password.d.ts","sourceRoot":"","sources":["../../src/strategies/username-password.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,wBAAwB,CAAC,QAAQ,CAAE,YAAW,QAAQ,CAAC,QAAQ,CAAC;IAC3E,QAAQ,CAAC,IAAI,uBAAuB;IAEpC;;;;;;;OAOG;IACG,YAAY,CAChB,SAAS,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EACvC,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;CAgCjC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Username/Password authentication strategy.
3
+ *
4
+ * Authenticates users with an identifier (e.g., email, username) and password.
5
+ * Uses the findAccount and verifyPassword callbacks from Handshake.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const hs = new Handshake({
10
+ * findAccount: async (email) => db.accounts.findByEmail(email),
11
+ * verifyPassword: async (account, password) => {
12
+ * return bcrypt.compare(password, account.passwordHash);
13
+ * },
14
+ * });
15
+ *
16
+ * hs.use(new UsernamePasswordStrategy());
17
+ *
18
+ * // In your login route
19
+ * const result = await hs.authenticate('username-password', email, password);
20
+ * if (result.account) {
21
+ * login(req, result.account);
22
+ * }
23
+ * ```
24
+ */
25
+ export class UsernamePasswordStrategy {
26
+ name = 'username-password';
27
+ /**
28
+ * Authenticate with identifier and password.
29
+ *
30
+ * @param callbacks - Handshake callbacks (findAccount, verifyPassword)
31
+ * @param identifier - The account identifier (e.g., email, username)
32
+ * @param password - The password to verify
33
+ * @returns Authentication result with account or error
34
+ */
35
+ async authenticate(callbacks, ...args) {
36
+ const [identifier, password] = args;
37
+ if (!identifier || !password) {
38
+ return { account: null, error: 'Identifier and password are required' };
39
+ }
40
+ if (!callbacks.verifyPassword) {
41
+ return { account: null, error: 'verifyPassword callback is required for username-password strategy' };
42
+ }
43
+ // Find the account
44
+ const account = await callbacks.findAccount(identifier);
45
+ // Always call verifyPassword even if account is null to prevent timing attacks.
46
+ // The verifyPassword callback should handle null accounts appropriately
47
+ // (e.g., by doing a dummy comparison that takes the same time).
48
+ // However, since the callback signature requires an account, we'll check first
49
+ // and rely on the overall operation timing being similar.
50
+ if (!account) {
51
+ return { account: null, error: 'Invalid credentials' };
52
+ }
53
+ // Verify the password
54
+ const isValid = await callbacks.verifyPassword(account, password);
55
+ if (!isValid) {
56
+ return { account: null, error: 'Invalid credentials' };
57
+ }
58
+ return { account, strategy: this.name };
59
+ }
60
+ }
61
+ //# sourceMappingURL=username-password.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"username-password.js","sourceRoot":"","sources":["../../src/strategies/username-password.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,wBAAwB;IAC1B,IAAI,GAAG,mBAAmB,CAAC;IAEpC;;;;;;;OAOG;IACH,KAAK,CAAC,YAAY,CAChB,SAAuC,EACvC,GAAG,IAAe;QAElB,MAAM,CAAC,UAAU,EAAE,QAAQ,CAAC,GAAG,IAAwB,CAAC;QAExD,IAAI,CAAC,UAAU,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC7B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAC;QAC1E,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,cAAc,EAAE,CAAC;YAC9B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,oEAAoE,EAAE,CAAC;QACxG,CAAC;QAED,mBAAmB;QACnB,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAExD,gFAAgF;QAChF,wEAAwE;QACxE,gEAAgE;QAChE,+EAA+E;QAC/E,0DAA0D;QAC1D,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;QACzD,CAAC;QAED,sBAAsB;QACtB,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAElE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;QACzD,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;IAC1C,CAAC;CACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "handshake-auth",
3
3
  "description": "Lightweight, storage-agnostic authentication for Express.js",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "repository": {
41
41
  "type": "git",
42
- "url": "git+https://github.com/chilts/handshake-auth.git"
42
+ "url": "https://codeberg.org/chilts/handshake-auth.git"
43
43
  },
44
44
  "author": {
45
45
  "name": "Andrew Chilton",