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.
- package/ReadMe.md +230 -16
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/express.d.ts +67 -0
- package/dist/middleware/express.d.ts.map +1 -1
- package/dist/middleware/express.js +69 -0
- package/dist/middleware/express.js.map +1 -1
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/strategies/discord.d.ts +99 -0
- package/dist/strategies/discord.d.ts.map +1 -0
- package/dist/strategies/discord.js +85 -0
- package/dist/strategies/discord.js.map +1 -0
- package/dist/strategies/github.d.ts +112 -0
- package/dist/strategies/github.d.ts.map +1 -0
- package/dist/strategies/github.js +110 -0
- package/dist/strategies/github.js.map +1 -0
- package/dist/strategies/google.d.ts +91 -0
- package/dist/strategies/google.d.ts.map +1 -0
- package/dist/strategies/google.js +77 -0
- package/dist/strategies/google.js.map +1 -0
- package/dist/strategies/index.d.ts +16 -0
- package/dist/strategies/index.d.ts.map +1 -1
- package/dist/strategies/index.js +10 -0
- package/dist/strategies/index.js.map +1 -1
- package/dist/strategies/magic-link.d.ts +141 -0
- package/dist/strategies/magic-link.d.ts.map +1 -0
- package/dist/strategies/magic-link.js +186 -0
- package/dist/strategies/magic-link.js.map +1 -0
- package/dist/strategies/microsoft.d.ts +127 -0
- package/dist/strategies/microsoft.d.ts.map +1 -0
- package/dist/strategies/microsoft.js +98 -0
- package/dist/strategies/microsoft.js.map +1 -0
- package/dist/strategies/oauth-base.d.ts +162 -0
- package/dist/strategies/oauth-base.d.ts.map +1 -0
- package/dist/strategies/oauth-base.js +243 -0
- package/dist/strategies/oauth-base.js.map +1 -0
- package/dist/strategies/password.d.ts +69 -6
- package/dist/strategies/password.d.ts.map +1 -1
- package/dist/strategies/password.js +73 -24
- package/dist/strategies/password.js.map +1 -1
- package/dist/strategies/twitter-x.d.ts +130 -0
- package/dist/strategies/twitter-x.d.ts.map +1 -0
- package/dist/strategies/twitter-x.js +275 -0
- package/dist/strategies/twitter-x.js.map +1 -0
- package/dist/strategies/username-password.d.ts +38 -0
- package/dist/strategies/username-password.d.ts.map +1 -0
- package/dist/strategies/username-password.js +61 -0
- package/dist/strategies/username-password.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Request, Response } from 'express';
|
|
2
|
+
import type { AuthResult, Strategy, HandshakeCallbacks, OAuthProfile } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for OAuth providers.
|
|
5
|
+
*/
|
|
6
|
+
export interface OAuthConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Strategy name (e.g., 'google', 'github', 'discord')
|
|
9
|
+
*/
|
|
10
|
+
name: string;
|
|
11
|
+
/**
|
|
12
|
+
* OAuth client ID from the provider
|
|
13
|
+
*/
|
|
14
|
+
clientId: string;
|
|
15
|
+
/**
|
|
16
|
+
* OAuth client secret from the provider
|
|
17
|
+
*/
|
|
18
|
+
clientSecret: string;
|
|
19
|
+
/**
|
|
20
|
+
* URL for the provider's authorization endpoint
|
|
21
|
+
*/
|
|
22
|
+
authorizeUrl: string;
|
|
23
|
+
/**
|
|
24
|
+
* URL for the provider's token endpoint
|
|
25
|
+
*/
|
|
26
|
+
tokenUrl: string;
|
|
27
|
+
/**
|
|
28
|
+
* URL for fetching user profile information
|
|
29
|
+
*/
|
|
30
|
+
userInfoUrl: string;
|
|
31
|
+
/**
|
|
32
|
+
* OAuth scopes to request
|
|
33
|
+
*/
|
|
34
|
+
scopes: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Your callback URL that the provider will redirect to
|
|
37
|
+
*/
|
|
38
|
+
redirectUri: string;
|
|
39
|
+
/**
|
|
40
|
+
* Cookie name for storing OAuth state (CSRF protection)
|
|
41
|
+
* @default 'oauth_state'
|
|
42
|
+
*/
|
|
43
|
+
stateCookieName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Additional authorization parameters to send
|
|
46
|
+
*/
|
|
47
|
+
authorizationParams?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Result from the redirect phase of OAuth authentication.
|
|
51
|
+
*/
|
|
52
|
+
export interface OAuthRedirectResult {
|
|
53
|
+
redirectUrl: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Token response from OAuth provider.
|
|
57
|
+
*/
|
|
58
|
+
export interface OAuthTokenResponse {
|
|
59
|
+
access_token: string;
|
|
60
|
+
token_type: string;
|
|
61
|
+
expires_in?: number;
|
|
62
|
+
refresh_token?: string;
|
|
63
|
+
scope?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Combined result type for OAuth authentication.
|
|
67
|
+
* Can be either a redirect URL (for initiation) or an auth result (for callback).
|
|
68
|
+
*/
|
|
69
|
+
export type OAuthAuthResult<TAccount> = AuthResult<TAccount> | OAuthRedirectResult;
|
|
70
|
+
/**
|
|
71
|
+
* Base class for OAuth2 authentication strategies.
|
|
72
|
+
*
|
|
73
|
+
* This class implements the standard OAuth2 authorization code flow:
|
|
74
|
+
* 1. **Redirect phase**: Generate state, build authorization URL, return redirect
|
|
75
|
+
* 2. **Callback phase**: Verify state, exchange code for token, fetch profile
|
|
76
|
+
*
|
|
77
|
+
* Subclasses should extend this and implement `mapProfile` to transform
|
|
78
|
+
* the provider's profile response into a standard OAuthProfile.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* class GoogleStrategy<TAccount> extends OAuthStrategy<TAccount> {
|
|
83
|
+
* constructor(options: GoogleOptions) {
|
|
84
|
+
* super({
|
|
85
|
+
* name: 'google',
|
|
86
|
+
* authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
87
|
+
* tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
88
|
+
* userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
|
|
89
|
+
* scopes: ['openid', 'email', 'profile'],
|
|
90
|
+
* ...options,
|
|
91
|
+
* });
|
|
92
|
+
* }
|
|
93
|
+
*
|
|
94
|
+
* protected mapProfile(data: unknown): OAuthProfile {
|
|
95
|
+
* const profile = data as GoogleProfile;
|
|
96
|
+
* return {
|
|
97
|
+
* id: profile.sub,
|
|
98
|
+
* email: profile.email ?? null,
|
|
99
|
+
* name: profile.name ?? null,
|
|
100
|
+
* picture: profile.picture ?? null,
|
|
101
|
+
* raw: profile,
|
|
102
|
+
* };
|
|
103
|
+
* }
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export declare abstract class OAuthStrategy<TAccount> implements Strategy<TAccount> {
|
|
108
|
+
readonly name: string;
|
|
109
|
+
protected readonly clientId: string;
|
|
110
|
+
protected readonly clientSecret: string;
|
|
111
|
+
protected readonly authorizeUrl: string;
|
|
112
|
+
protected readonly tokenUrl: string;
|
|
113
|
+
protected readonly userInfoUrl: string;
|
|
114
|
+
protected readonly scopes: string[];
|
|
115
|
+
protected readonly redirectUri: string;
|
|
116
|
+
protected readonly stateCookieName: string;
|
|
117
|
+
protected readonly authorizationParams: Record<string, string>;
|
|
118
|
+
constructor(config: OAuthConfig);
|
|
119
|
+
/**
|
|
120
|
+
* Authenticate using OAuth.
|
|
121
|
+
*
|
|
122
|
+
* @param callbacks - Handshake callbacks (findOrCreateFromOAuth is used)
|
|
123
|
+
* @param args - [req, res, phase] where phase is 'redirect' or 'callback'
|
|
124
|
+
* @returns Either a redirect URL or an auth result
|
|
125
|
+
*/
|
|
126
|
+
authenticate(callbacks: HandshakeCallbacks<TAccount>, ...args: unknown[]): Promise<AuthResult<TAccount>>;
|
|
127
|
+
/**
|
|
128
|
+
* Handle the redirect phase: generate state and return authorization URL.
|
|
129
|
+
*/
|
|
130
|
+
protected handleRedirect(_req: Request, res: Response): AuthResult<TAccount> & OAuthRedirectResult;
|
|
131
|
+
/**
|
|
132
|
+
* Handle the callback phase: verify state, exchange code, fetch profile.
|
|
133
|
+
*/
|
|
134
|
+
protected handleCallback(callbacks: HandshakeCallbacks<TAccount>, req: Request, res: Response): Promise<AuthResult<TAccount>>;
|
|
135
|
+
/**
|
|
136
|
+
* Exchange authorization code for access token.
|
|
137
|
+
*/
|
|
138
|
+
protected exchangeCodeForToken(code: string): Promise<OAuthTokenResponse | {
|
|
139
|
+
error: string;
|
|
140
|
+
}>;
|
|
141
|
+
/**
|
|
142
|
+
* Fetch user profile from the provider.
|
|
143
|
+
* Subclasses can override this for providers with non-standard profile endpoints.
|
|
144
|
+
*/
|
|
145
|
+
protected fetchUserProfile(accessToken: string): Promise<OAuthProfile | {
|
|
146
|
+
error: string;
|
|
147
|
+
}>;
|
|
148
|
+
/**
|
|
149
|
+
* Map the provider's profile response to a standard OAuthProfile.
|
|
150
|
+
* Subclasses must implement this to handle provider-specific profile formats.
|
|
151
|
+
*
|
|
152
|
+
* @param data - Raw profile data from the provider
|
|
153
|
+
* @param accessToken - Access token (some providers need this for additional API calls)
|
|
154
|
+
* @returns Standardized OAuthProfile
|
|
155
|
+
*/
|
|
156
|
+
protected abstract mapProfile(data: unknown, accessToken: string): OAuthProfile | Promise<OAuthProfile>;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Type guard to check if a result is an OAuth redirect result.
|
|
160
|
+
*/
|
|
161
|
+
export declare function isOAuthRedirect<TAccount>(result: AuthResult<TAccount> | OAuthRedirectResult): result is OAuthRedirectResult & AuthResult<TAccount>;
|
|
162
|
+
//# sourceMappingURL=oauth-base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-base.d.ts","sourceRoot":"","sources":["../../src/strategies/oauth-base.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Base class for OAuth2 authentication strategies.
|
|
4
|
+
*
|
|
5
|
+
* This class implements the standard OAuth2 authorization code flow:
|
|
6
|
+
* 1. **Redirect phase**: Generate state, build authorization URL, return redirect
|
|
7
|
+
* 2. **Callback phase**: Verify state, exchange code for token, fetch profile
|
|
8
|
+
*
|
|
9
|
+
* Subclasses should extend this and implement `mapProfile` to transform
|
|
10
|
+
* the provider's profile response into a standard OAuthProfile.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* class GoogleStrategy<TAccount> extends OAuthStrategy<TAccount> {
|
|
15
|
+
* constructor(options: GoogleOptions) {
|
|
16
|
+
* super({
|
|
17
|
+
* name: 'google',
|
|
18
|
+
* authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
19
|
+
* tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
20
|
+
* userInfoUrl: 'https://openidconnect.googleapis.com/v1/userinfo',
|
|
21
|
+
* scopes: ['openid', 'email', 'profile'],
|
|
22
|
+
* ...options,
|
|
23
|
+
* });
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* protected mapProfile(data: unknown): OAuthProfile {
|
|
27
|
+
* const profile = data as GoogleProfile;
|
|
28
|
+
* return {
|
|
29
|
+
* id: profile.sub,
|
|
30
|
+
* email: profile.email ?? null,
|
|
31
|
+
* name: profile.name ?? null,
|
|
32
|
+
* picture: profile.picture ?? null,
|
|
33
|
+
* raw: profile,
|
|
34
|
+
* };
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class OAuthStrategy {
|
|
40
|
+
name;
|
|
41
|
+
clientId;
|
|
42
|
+
clientSecret;
|
|
43
|
+
authorizeUrl;
|
|
44
|
+
tokenUrl;
|
|
45
|
+
userInfoUrl;
|
|
46
|
+
scopes;
|
|
47
|
+
redirectUri;
|
|
48
|
+
stateCookieName;
|
|
49
|
+
authorizationParams;
|
|
50
|
+
constructor(config) {
|
|
51
|
+
this.name = config.name;
|
|
52
|
+
this.clientId = config.clientId;
|
|
53
|
+
this.clientSecret = config.clientSecret;
|
|
54
|
+
this.authorizeUrl = config.authorizeUrl;
|
|
55
|
+
this.tokenUrl = config.tokenUrl;
|
|
56
|
+
this.userInfoUrl = config.userInfoUrl;
|
|
57
|
+
this.scopes = config.scopes;
|
|
58
|
+
this.redirectUri = config.redirectUri;
|
|
59
|
+
this.stateCookieName = config.stateCookieName ?? 'oauth_state';
|
|
60
|
+
this.authorizationParams = config.authorizationParams ?? {};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Authenticate using OAuth.
|
|
64
|
+
*
|
|
65
|
+
* @param callbacks - Handshake callbacks (findOrCreateFromOAuth is used)
|
|
66
|
+
* @param args - [req, res, phase] where phase is 'redirect' or 'callback'
|
|
67
|
+
* @returns Either a redirect URL or an auth result
|
|
68
|
+
*/
|
|
69
|
+
async authenticate(callbacks, ...args) {
|
|
70
|
+
const [req, res, phase] = args;
|
|
71
|
+
if (phase === 'redirect') {
|
|
72
|
+
return this.handleRedirect(req, res);
|
|
73
|
+
}
|
|
74
|
+
else if (phase === 'callback') {
|
|
75
|
+
return this.handleCallback(callbacks, req, res);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
return {
|
|
79
|
+
account: null,
|
|
80
|
+
error: `Invalid OAuth phase: ${phase}. Use 'redirect' or 'callback'`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Handle the redirect phase: generate state and return authorization URL.
|
|
86
|
+
*/
|
|
87
|
+
handleRedirect(_req, res) {
|
|
88
|
+
// Generate random state for CSRF protection
|
|
89
|
+
const state = randomBytes(32).toString('hex');
|
|
90
|
+
// Store state in a cookie (will be verified on callback)
|
|
91
|
+
// Using httpOnly and sameSite for security
|
|
92
|
+
res.cookie(this.stateCookieName, state, {
|
|
93
|
+
httpOnly: true,
|
|
94
|
+
secure: process.env.NODE_ENV === 'production',
|
|
95
|
+
sameSite: 'lax',
|
|
96
|
+
maxAge: 10 * 60 * 1000, // 10 minutes
|
|
97
|
+
});
|
|
98
|
+
// Build authorization URL
|
|
99
|
+
const params = new URLSearchParams({
|
|
100
|
+
client_id: this.clientId,
|
|
101
|
+
redirect_uri: this.redirectUri,
|
|
102
|
+
response_type: 'code',
|
|
103
|
+
scope: this.scopes.join(' '),
|
|
104
|
+
state,
|
|
105
|
+
...this.authorizationParams,
|
|
106
|
+
});
|
|
107
|
+
const redirectUrl = `${this.authorizeUrl}?${params.toString()}`;
|
|
108
|
+
// Return special result with redirect URL
|
|
109
|
+
// The caller should check for redirectUrl and redirect
|
|
110
|
+
return {
|
|
111
|
+
account: null,
|
|
112
|
+
redirectUrl,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Handle the callback phase: verify state, exchange code, fetch profile.
|
|
117
|
+
*/
|
|
118
|
+
async handleCallback(callbacks, req, res) {
|
|
119
|
+
// Get state and code from query params
|
|
120
|
+
const { state: returnedState, code, error, error_description } = req.query;
|
|
121
|
+
// Check for OAuth error response
|
|
122
|
+
if (error) {
|
|
123
|
+
return {
|
|
124
|
+
account: null,
|
|
125
|
+
error: error_description || error,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Verify state matches what we stored in the cookie
|
|
129
|
+
const storedState = req.cookies?.[this.stateCookieName];
|
|
130
|
+
// Clear the state cookie
|
|
131
|
+
res.clearCookie(this.stateCookieName);
|
|
132
|
+
if (!storedState || storedState !== returnedState) {
|
|
133
|
+
return {
|
|
134
|
+
account: null,
|
|
135
|
+
error: 'Invalid OAuth state. Possible CSRF attack or expired session.',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (!code) {
|
|
139
|
+
return {
|
|
140
|
+
account: null,
|
|
141
|
+
error: 'No authorization code received',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// Exchange code for token
|
|
145
|
+
const tokenResult = await this.exchangeCodeForToken(code);
|
|
146
|
+
if ('error' in tokenResult) {
|
|
147
|
+
return {
|
|
148
|
+
account: null,
|
|
149
|
+
error: tokenResult.error,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// Fetch user profile
|
|
153
|
+
const profileResult = await this.fetchUserProfile(tokenResult.access_token);
|
|
154
|
+
if ('error' in profileResult) {
|
|
155
|
+
return {
|
|
156
|
+
account: null,
|
|
157
|
+
error: profileResult.error,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// Check for required callback
|
|
161
|
+
if (!callbacks.findOrCreateFromOAuth) {
|
|
162
|
+
return {
|
|
163
|
+
account: null,
|
|
164
|
+
error: 'findOrCreateFromOAuth callback is required for OAuth strategies',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Let the application handle account creation/lookup
|
|
168
|
+
const account = await callbacks.findOrCreateFromOAuth(this.name, profileResult);
|
|
169
|
+
return {
|
|
170
|
+
account,
|
|
171
|
+
strategy: this.name,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Exchange authorization code for access token.
|
|
176
|
+
*/
|
|
177
|
+
async exchangeCodeForToken(code) {
|
|
178
|
+
try {
|
|
179
|
+
const params = new URLSearchParams({
|
|
180
|
+
client_id: this.clientId,
|
|
181
|
+
client_secret: this.clientSecret,
|
|
182
|
+
code,
|
|
183
|
+
grant_type: 'authorization_code',
|
|
184
|
+
redirect_uri: this.redirectUri,
|
|
185
|
+
});
|
|
186
|
+
const response = await fetch(this.tokenUrl, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: {
|
|
189
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
190
|
+
Accept: 'application/json',
|
|
191
|
+
},
|
|
192
|
+
body: params.toString(),
|
|
193
|
+
});
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
const errorData = await response.json().catch(() => ({}));
|
|
196
|
+
return {
|
|
197
|
+
error: errorData.error_description ||
|
|
198
|
+
errorData.error ||
|
|
199
|
+
`Token exchange failed: ${response.status}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return (await response.json());
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
return {
|
|
206
|
+
error: `Token exchange failed: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Fetch user profile from the provider.
|
|
212
|
+
* Subclasses can override this for providers with non-standard profile endpoints.
|
|
213
|
+
*/
|
|
214
|
+
async fetchUserProfile(accessToken) {
|
|
215
|
+
try {
|
|
216
|
+
const response = await fetch(this.userInfoUrl, {
|
|
217
|
+
headers: {
|
|
218
|
+
Authorization: `Bearer ${accessToken}`,
|
|
219
|
+
Accept: 'application/json',
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
return {
|
|
224
|
+
error: `Failed to fetch user profile: ${response.status}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const data = await response.json();
|
|
228
|
+
return this.mapProfile(data, accessToken);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
error: `Failed to fetch user profile: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Type guard to check if a result is an OAuth redirect result.
|
|
239
|
+
*/
|
|
240
|
+
export function isOAuthRedirect(result) {
|
|
241
|
+
return 'redirectUrl' in result;
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=oauth-base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-base.js","sourceRoot":"","sources":["../../src/strategies/oauth-base.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,17 +1,80 @@
|
|
|
1
1
|
import type { AuthResult, Strategy, HandshakeCallbacks } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Options for the simple password strategy.
|
|
4
|
+
*/
|
|
5
|
+
export interface PasswordStrategyOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Callback to verify the password.
|
|
8
|
+
* You handle how the password is verified (e.g., compare to env var, bcrypt hash).
|
|
9
|
+
*
|
|
10
|
+
* @param password - The password to verify
|
|
11
|
+
* @returns true if valid, false otherwise
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // Simple env var comparison
|
|
16
|
+
* verifyPassword: async (password) => password === process.env.APP_PASSWORD
|
|
17
|
+
*
|
|
18
|
+
* // Bcrypt comparison against hashed env var
|
|
19
|
+
* verifyPassword: async (password) => bcrypt.compare(password, process.env.APP_PASSWORD_HASH)
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
verifyPassword: (password: string) => Promise<boolean> | boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Simple password-only authentication strategy.
|
|
26
|
+
*
|
|
27
|
+
* Authenticates with just a password (no username/email). Useful for:
|
|
28
|
+
* - Self-hosted apps with a single admin password
|
|
29
|
+
* - Simple internal tools not exposed to the internet
|
|
30
|
+
* - Quick prototyping
|
|
31
|
+
*
|
|
32
|
+
* The strategy doesn't use findAccount since there's no user lookup -
|
|
33
|
+
* it just verifies the password. On success, it returns a minimal account
|
|
34
|
+
* object that you provide.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* // Simple password check against env var
|
|
39
|
+
* hs.use(new PasswordStrategy({
|
|
40
|
+
* verifyPassword: async (password) => password === process.env.ADMIN_PASSWORD,
|
|
41
|
+
* }));
|
|
42
|
+
*
|
|
43
|
+
* // With bcrypt
|
|
44
|
+
* hs.use(new PasswordStrategy({
|
|
45
|
+
* verifyPassword: async (password) => {
|
|
46
|
+
* return bcrypt.compare(password, process.env.ADMIN_PASSWORD_HASH!);
|
|
47
|
+
* },
|
|
48
|
+
* }));
|
|
49
|
+
*
|
|
50
|
+
* // In your login route
|
|
51
|
+
* app.post('/login', async (req, res) => {
|
|
52
|
+
* const result = await hs.authenticate('password', req.body.password);
|
|
53
|
+
* if (result.account) {
|
|
54
|
+
* login(req, result.account);
|
|
55
|
+
* res.redirect('/dashboard');
|
|
56
|
+
* } else {
|
|
57
|
+
* res.status(401).send('Invalid password');
|
|
58
|
+
* }
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
4
61
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
62
|
+
* @example HTML form
|
|
63
|
+
* ```html
|
|
64
|
+
* <form method="POST" action="/login">
|
|
65
|
+
* <input name="password" type="password" placeholder="Password" required />
|
|
66
|
+
* <button type="submit">Login</button>
|
|
67
|
+
* </form>
|
|
68
|
+
* ```
|
|
7
69
|
*/
|
|
8
70
|
export declare class PasswordStrategy<TAccount> implements Strategy<TAccount> {
|
|
9
71
|
readonly name = "password";
|
|
72
|
+
private readonly verifyPassword;
|
|
73
|
+
constructor(options: PasswordStrategyOptions);
|
|
10
74
|
/**
|
|
11
|
-
* Authenticate with
|
|
75
|
+
* Authenticate with password only.
|
|
12
76
|
*
|
|
13
|
-
* @param callbacks - Handshake callbacks (
|
|
14
|
-
* @param identifier - The account identifier (e.g., email)
|
|
77
|
+
* @param callbacks - Handshake callbacks (not used by this strategy)
|
|
15
78
|
* @param password - The password to verify
|
|
16
79
|
* @returns Authentication result with account or error
|
|
17
80
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../src/strategies/password.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE5E
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../src/strategies/password.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE5E;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;;;;;;;;;;;;OAeG;IACH,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CAClE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,qBAAa,gBAAgB,CAAC,QAAQ,CAAE,YAAW,QAAQ,CAAC,QAAQ,CAAC;IACnE,QAAQ,CAAC,IAAI,cAAc;IAE3B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4C;gBAE/D,OAAO,EAAE,uBAAuB;IAI5C;;;;;;OAMG;IACG,YAAY,CAChB,SAAS,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EACvC,GAAG,IAAI,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;CAsCjC"}
|
|
@@ -1,41 +1,90 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Simple password-only authentication strategy.
|
|
3
3
|
*
|
|
4
|
-
* Authenticates
|
|
5
|
-
*
|
|
4
|
+
* Authenticates with just a password (no username/email). Useful for:
|
|
5
|
+
* - Self-hosted apps with a single admin password
|
|
6
|
+
* - Simple internal tools not exposed to the internet
|
|
7
|
+
* - Quick prototyping
|
|
8
|
+
*
|
|
9
|
+
* The strategy doesn't use findAccount since there's no user lookup -
|
|
10
|
+
* it just verifies the password. On success, it returns a minimal account
|
|
11
|
+
* object that you provide.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // Simple password check against env var
|
|
16
|
+
* hs.use(new PasswordStrategy({
|
|
17
|
+
* verifyPassword: async (password) => password === process.env.ADMIN_PASSWORD,
|
|
18
|
+
* }));
|
|
19
|
+
*
|
|
20
|
+
* // With bcrypt
|
|
21
|
+
* hs.use(new PasswordStrategy({
|
|
22
|
+
* verifyPassword: async (password) => {
|
|
23
|
+
* return bcrypt.compare(password, process.env.ADMIN_PASSWORD_HASH!);
|
|
24
|
+
* },
|
|
25
|
+
* }));
|
|
26
|
+
*
|
|
27
|
+
* // In your login route
|
|
28
|
+
* app.post('/login', async (req, res) => {
|
|
29
|
+
* const result = await hs.authenticate('password', req.body.password);
|
|
30
|
+
* if (result.account) {
|
|
31
|
+
* login(req, result.account);
|
|
32
|
+
* res.redirect('/dashboard');
|
|
33
|
+
* } else {
|
|
34
|
+
* res.status(401).send('Invalid password');
|
|
35
|
+
* }
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example HTML form
|
|
40
|
+
* ```html
|
|
41
|
+
* <form method="POST" action="/login">
|
|
42
|
+
* <input name="password" type="password" placeholder="Password" required />
|
|
43
|
+
* <button type="submit">Login</button>
|
|
44
|
+
* </form>
|
|
45
|
+
* ```
|
|
6
46
|
*/
|
|
7
47
|
export class PasswordStrategy {
|
|
8
48
|
name = 'password';
|
|
49
|
+
verifyPassword;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.verifyPassword = options.verifyPassword;
|
|
52
|
+
}
|
|
9
53
|
/**
|
|
10
|
-
* Authenticate with
|
|
54
|
+
* Authenticate with password only.
|
|
11
55
|
*
|
|
12
|
-
* @param callbacks - Handshake callbacks (
|
|
13
|
-
* @param identifier - The account identifier (e.g., email)
|
|
56
|
+
* @param callbacks - Handshake callbacks (not used by this strategy)
|
|
14
57
|
* @param password - The password to verify
|
|
15
58
|
* @returns Authentication result with account or error
|
|
16
59
|
*/
|
|
17
60
|
async authenticate(callbacks, ...args) {
|
|
18
|
-
const [
|
|
19
|
-
if (!
|
|
20
|
-
return { account: null, error: '
|
|
61
|
+
const [password] = args;
|
|
62
|
+
if (!password) {
|
|
63
|
+
return { account: null, error: 'Password is required' };
|
|
21
64
|
}
|
|
22
|
-
|
|
23
|
-
|
|
65
|
+
// Verify the password using the provided callback
|
|
66
|
+
const isValid = await this.verifyPassword(password);
|
|
67
|
+
if (!isValid) {
|
|
68
|
+
return { account: null, error: 'Invalid password' };
|
|
24
69
|
}
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
70
|
+
// For password-only auth, we don't have a real account from the database.
|
|
71
|
+
// The user needs to provide a findAccount callback or we return a placeholder.
|
|
72
|
+
// Let's check if findAccount can find an account with a special identifier.
|
|
73
|
+
// Or we can just return a generic "authenticated" account.
|
|
74
|
+
// Try to find a default account (e.g., "admin" or the first account)
|
|
75
|
+
// If no findAccount provided or it returns null, create a minimal account marker
|
|
76
|
+
let account = null;
|
|
77
|
+
try {
|
|
78
|
+
// Try to get the default/admin account
|
|
79
|
+
account = await callbacks.findAccount('admin');
|
|
34
80
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
catch {
|
|
82
|
+
// findAccount might not be suitable for this use case
|
|
83
|
+
}
|
|
84
|
+
if (!account) {
|
|
85
|
+
// Return a minimal marker that indicates authentication succeeded
|
|
86
|
+
// The user can check for this in their app
|
|
87
|
+
account = { id: 'password-auth', authenticated: true };
|
|
39
88
|
}
|
|
40
89
|
return { account, strategy: this.name };
|
|
41
90
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/strategies/password.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/strategies/password.ts"],"names":[],"mappings":"AAyBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,MAAM,OAAO,gBAAgB;IAClB,IAAI,GAAG,UAAU,CAAC;IAEV,cAAc,CAA4C;IAE3E,YAAY,OAAgC;QAC1C,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAC/C,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAChB,SAAuC,EACvC,GAAG,IAAe;QAElB,MAAM,CAAC,QAAQ,CAAC,GAAG,IAAgB,CAAC;QAEpC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC;QAC1D,CAAC;QAED,kDAAkD;QAClD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QAEpD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC;QACtD,CAAC;QAED,0EAA0E;QAC1E,+EAA+E;QAC/E,4EAA4E;QAC5E,2DAA2D;QAE3D,qEAAqE;QACrE,iFAAiF;QACjF,IAAI,OAAO,GAAoB,IAAI,CAAC;QAEpC,IAAI,CAAC;YACH,uCAAuC;YACvC,OAAO,GAAG,MAAM,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACjD,CAAC;QAAC,MAAM,CAAC;YACP,sDAAsD;QACxD,CAAC;QAED,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,kEAAkE;YAClE,2CAA2C;YAC3C,OAAO,GAAG,EAAE,EAAE,EAAE,eAAe,EAAE,aAAa,EAAE,IAAI,EAAyB,CAAC;QAChF,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;IAC1C,CAAC;CACF"}
|