@everystack/auth 0.1.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 +256 -0
- package/package.json +104 -0
- package/src/api.ts +65 -0
- package/src/cff/index.ts +157 -0
- package/src/client/index.tsx +139 -0
- package/src/context.ts +30 -0
- package/src/cookies.ts +93 -0
- package/src/index.ts +526 -0
- package/src/jwt/hmac-node.ts +11 -0
- package/src/jwt/hmac-web.ts +17 -0
- package/src/jwt/hmac.ts +10 -0
- package/src/jwt/index.ts +182 -0
- package/src/jwt/jwt.sql +177 -0
- package/src/oauth/index.ts +7 -0
- package/src/oauth/providers/apple.ts +43 -0
- package/src/oauth/providers/google.ts +49 -0
- package/src/oauth/types.ts +17 -0
- package/src/oauth/verify.ts +193 -0
- package/src/oauth.ts +6 -0
- package/src/passwords.ts +70 -0
- package/src/postgres.ts +504 -0
- package/src/refresh.ts +204 -0
- package/src/response.ts +50 -0
- package/src/schema.ts +224 -0
- package/src/sql.ts +261 -0
package/src/sql.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @everystack/auth/sql — Generate SQL for PostgreSQL auth functions.
|
|
3
|
+
*
|
|
4
|
+
* Creates 7 functions in the `auth` schema using pgcrypto:
|
|
5
|
+
* - auth.hash_password — bcrypt via crypt()/gen_salt()
|
|
6
|
+
* - auth.verify_password — bcrypt verification
|
|
7
|
+
* - auth.sign_jwt — HMAC-SHA256 JWT signing
|
|
8
|
+
* - auth.verify_jwt — JWT verification
|
|
9
|
+
* - auth.generate_refresh_token — opaque token with SHA-256 hash storage
|
|
10
|
+
* - auth.validate_refresh_token — one-time-use rotation
|
|
11
|
+
* - auth.revoke_refresh_tokens — delete all tokens for a user
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* import { generateAuthSQL } from '@everystack/auth/sql';
|
|
15
|
+
* const sql = generateAuthSQL();
|
|
16
|
+
* // Write to a migration file
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const PGCRYPTO = `CREATE EXTENSION IF NOT EXISTS pgcrypto;`;
|
|
20
|
+
|
|
21
|
+
const HASH_PASSWORD = `\
|
|
22
|
+
CREATE OR REPLACE FUNCTION auth.hash_password(p_password TEXT)
|
|
23
|
+
RETURNS TEXT
|
|
24
|
+
LANGUAGE plpgsql
|
|
25
|
+
SECURITY DEFINER
|
|
26
|
+
AS $$
|
|
27
|
+
BEGIN
|
|
28
|
+
RETURN crypt(p_password, gen_salt('bf', 10));
|
|
29
|
+
END;
|
|
30
|
+
$$;`;
|
|
31
|
+
|
|
32
|
+
const VERIFY_PASSWORD = `\
|
|
33
|
+
CREATE OR REPLACE FUNCTION auth.verify_password(p_password TEXT, p_hash TEXT)
|
|
34
|
+
RETURNS BOOLEAN
|
|
35
|
+
LANGUAGE plpgsql
|
|
36
|
+
SECURITY DEFINER
|
|
37
|
+
AS $$
|
|
38
|
+
DECLARE
|
|
39
|
+
v_normalized TEXT;
|
|
40
|
+
BEGIN
|
|
41
|
+
-- pgcrypto crypt() only supports $2a$ prefix; bcryptjs produces $2b$.
|
|
42
|
+
-- $2a$ and $2b$ are byte-identical algorithms — normalize prefix for compat.
|
|
43
|
+
v_normalized := regexp_replace(p_hash, '^\\$2[by]\\$', '$2a$');
|
|
44
|
+
RETURN crypt(p_password, v_normalized) = v_normalized;
|
|
45
|
+
END;
|
|
46
|
+
$$;`;
|
|
47
|
+
|
|
48
|
+
const SIGN_JWT = `\
|
|
49
|
+
CREATE OR REPLACE FUNCTION auth.sign_jwt(p_payload JSONB)
|
|
50
|
+
RETURNS TEXT
|
|
51
|
+
LANGUAGE plpgsql
|
|
52
|
+
SECURITY DEFINER
|
|
53
|
+
AS $$
|
|
54
|
+
DECLARE
|
|
55
|
+
v_header TEXT;
|
|
56
|
+
v_payload TEXT;
|
|
57
|
+
v_signing_input TEXT;
|
|
58
|
+
v_signature TEXT;
|
|
59
|
+
v_secret TEXT;
|
|
60
|
+
BEGIN
|
|
61
|
+
v_secret := current_setting('app.jwt_secret', true);
|
|
62
|
+
IF v_secret IS NULL OR v_secret = '' THEN
|
|
63
|
+
RAISE EXCEPTION 'app.jwt_secret not set'
|
|
64
|
+
USING HINT = 'Call SET LOCAL app.jwt_secret before auth.sign_jwt';
|
|
65
|
+
END IF;
|
|
66
|
+
|
|
67
|
+
-- Base64url encode header (replace strips MIME newlines from encode())
|
|
68
|
+
v_header := rtrim(translate(
|
|
69
|
+
replace(encode(convert_to('{"alg":"HS256","typ":"JWT"}', 'utf8'), 'base64'), E'\\n', ''),
|
|
70
|
+
'+/=', '-_ '
|
|
71
|
+
));
|
|
72
|
+
|
|
73
|
+
-- Base64url encode payload
|
|
74
|
+
v_payload := rtrim(translate(
|
|
75
|
+
replace(encode(convert_to(p_payload::text, 'utf8'), 'base64'), E'\\n', ''),
|
|
76
|
+
'+/=', '-_ '
|
|
77
|
+
));
|
|
78
|
+
|
|
79
|
+
v_signing_input := v_header || '.' || v_payload;
|
|
80
|
+
|
|
81
|
+
-- HMAC-SHA256 signature via pgcrypto
|
|
82
|
+
v_signature := rtrim(translate(
|
|
83
|
+
replace(encode(hmac(v_signing_input, v_secret, 'sha256'), 'base64'), E'\\n', ''),
|
|
84
|
+
'+/=', '-_ '
|
|
85
|
+
));
|
|
86
|
+
|
|
87
|
+
RETURN v_signing_input || '.' || v_signature;
|
|
88
|
+
END;
|
|
89
|
+
$$;`;
|
|
90
|
+
|
|
91
|
+
const VERIFY_JWT = `\
|
|
92
|
+
CREATE OR REPLACE FUNCTION auth.verify_jwt(p_token TEXT)
|
|
93
|
+
RETURNS JSONB
|
|
94
|
+
LANGUAGE plpgsql
|
|
95
|
+
SECURITY DEFINER
|
|
96
|
+
AS $$
|
|
97
|
+
DECLARE
|
|
98
|
+
v_parts TEXT[];
|
|
99
|
+
v_signing_input TEXT;
|
|
100
|
+
v_expected_sig TEXT;
|
|
101
|
+
v_payload_json JSONB;
|
|
102
|
+
v_exp BIGINT;
|
|
103
|
+
v_secret TEXT;
|
|
104
|
+
BEGIN
|
|
105
|
+
v_secret := current_setting('app.jwt_secret', true);
|
|
106
|
+
IF v_secret IS NULL OR v_secret = '' THEN
|
|
107
|
+
RAISE EXCEPTION 'app.jwt_secret not set'
|
|
108
|
+
USING HINT = 'Call SET LOCAL app.jwt_secret before auth.verify_jwt';
|
|
109
|
+
END IF;
|
|
110
|
+
|
|
111
|
+
v_parts := string_to_array(p_token, '.');
|
|
112
|
+
IF array_length(v_parts, 1) != 3 THEN
|
|
113
|
+
RETURN NULL;
|
|
114
|
+
END IF;
|
|
115
|
+
|
|
116
|
+
v_signing_input := v_parts[1] || '.' || v_parts[2];
|
|
117
|
+
|
|
118
|
+
-- Recompute expected signature (replace strips MIME newlines)
|
|
119
|
+
v_expected_sig := rtrim(translate(
|
|
120
|
+
replace(encode(hmac(v_signing_input, v_secret, 'sha256'), 'base64'), E'\\n', ''),
|
|
121
|
+
'+/=', '-_ '
|
|
122
|
+
));
|
|
123
|
+
|
|
124
|
+
IF v_parts[3] != v_expected_sig THEN
|
|
125
|
+
RETURN NULL;
|
|
126
|
+
END IF;
|
|
127
|
+
|
|
128
|
+
-- Decode payload: base64url -> base64 -> bytes -> text -> jsonb
|
|
129
|
+
v_payload_json := convert_from(
|
|
130
|
+
decode(
|
|
131
|
+
translate(v_parts[2], '-_', '+/') ||
|
|
132
|
+
repeat('=', (4 - length(v_parts[2]) % 4) % 4),
|
|
133
|
+
'base64'
|
|
134
|
+
),
|
|
135
|
+
'utf8'
|
|
136
|
+
)::jsonb;
|
|
137
|
+
|
|
138
|
+
-- Check expiration
|
|
139
|
+
v_exp := (v_payload_json->>'exp')::bigint;
|
|
140
|
+
IF v_exp IS NOT NULL AND v_exp < extract(epoch FROM now())::bigint THEN
|
|
141
|
+
RETURN NULL;
|
|
142
|
+
END IF;
|
|
143
|
+
|
|
144
|
+
RETURN v_payload_json;
|
|
145
|
+
END;
|
|
146
|
+
$$;`;
|
|
147
|
+
|
|
148
|
+
const GENERATE_REFRESH_TOKEN = `\
|
|
149
|
+
CREATE OR REPLACE FUNCTION auth.generate_refresh_token(p_user_id UUID)
|
|
150
|
+
RETURNS TEXT
|
|
151
|
+
LANGUAGE plpgsql
|
|
152
|
+
SECURITY DEFINER
|
|
153
|
+
AS $$
|
|
154
|
+
DECLARE
|
|
155
|
+
v_token TEXT;
|
|
156
|
+
v_hash TEXT;
|
|
157
|
+
v_family TEXT;
|
|
158
|
+
BEGIN
|
|
159
|
+
-- Clean up expired tokens for this user
|
|
160
|
+
DELETE FROM auth.refresh_tokens
|
|
161
|
+
WHERE user_id = p_user_id AND expires_at < now();
|
|
162
|
+
|
|
163
|
+
-- Generate random token (32 bytes, base64url encoded)
|
|
164
|
+
v_token := rtrim(translate(
|
|
165
|
+
encode(gen_random_bytes(32), 'base64'),
|
|
166
|
+
'+/=', '-_ '
|
|
167
|
+
));
|
|
168
|
+
|
|
169
|
+
-- Store SHA-256 hash, not the raw token
|
|
170
|
+
v_hash := encode(digest(v_token, 'sha256'), 'hex');
|
|
171
|
+
v_family := gen_random_uuid()::text;
|
|
172
|
+
|
|
173
|
+
INSERT INTO auth.refresh_tokens (user_id, token_hash, family_id, expires_at)
|
|
174
|
+
VALUES (p_user_id, v_hash, v_family, now() + interval '7 days');
|
|
175
|
+
|
|
176
|
+
RETURN v_token;
|
|
177
|
+
END;
|
|
178
|
+
$$;`;
|
|
179
|
+
|
|
180
|
+
const VALIDATE_REFRESH_TOKEN = `\
|
|
181
|
+
CREATE OR REPLACE FUNCTION auth.validate_refresh_token(p_token TEXT)
|
|
182
|
+
RETURNS JSONB
|
|
183
|
+
LANGUAGE plpgsql
|
|
184
|
+
SECURITY DEFINER
|
|
185
|
+
AS $$
|
|
186
|
+
DECLARE
|
|
187
|
+
v_hash TEXT;
|
|
188
|
+
v_existing RECORD;
|
|
189
|
+
v_new_token TEXT;
|
|
190
|
+
v_new_hash TEXT;
|
|
191
|
+
BEGIN
|
|
192
|
+
v_hash := encode(digest(p_token, 'sha256'), 'hex');
|
|
193
|
+
|
|
194
|
+
SELECT * INTO v_existing
|
|
195
|
+
FROM auth.refresh_tokens
|
|
196
|
+
WHERE token_hash = v_hash
|
|
197
|
+
FOR UPDATE
|
|
198
|
+
LIMIT 1;
|
|
199
|
+
|
|
200
|
+
IF v_existing IS NULL THEN
|
|
201
|
+
RAISE EXCEPTION 'invalid_token';
|
|
202
|
+
END IF;
|
|
203
|
+
|
|
204
|
+
IF v_existing.expires_at < now() THEN
|
|
205
|
+
DELETE FROM auth.refresh_tokens WHERE id = v_existing.id;
|
|
206
|
+
RAISE EXCEPTION 'token_expired';
|
|
207
|
+
END IF;
|
|
208
|
+
|
|
209
|
+
-- Delete old token (one-time use)
|
|
210
|
+
DELETE FROM auth.refresh_tokens WHERE id = v_existing.id;
|
|
211
|
+
|
|
212
|
+
-- Generate new token in same family
|
|
213
|
+
v_new_token := rtrim(translate(
|
|
214
|
+
encode(gen_random_bytes(32), 'base64'),
|
|
215
|
+
'+/=', '-_ '
|
|
216
|
+
));
|
|
217
|
+
v_new_hash := encode(digest(v_new_token, 'sha256'), 'hex');
|
|
218
|
+
|
|
219
|
+
INSERT INTO auth.refresh_tokens (user_id, token_hash, family_id, expires_at)
|
|
220
|
+
VALUES (v_existing.user_id, v_new_hash, v_existing.family_id, now() + interval '7 days');
|
|
221
|
+
|
|
222
|
+
RETURN jsonb_build_object(
|
|
223
|
+
'userId', v_existing.user_id,
|
|
224
|
+
'token', v_new_token,
|
|
225
|
+
'familyId', v_existing.family_id
|
|
226
|
+
);
|
|
227
|
+
END;
|
|
228
|
+
$$;`;
|
|
229
|
+
|
|
230
|
+
const REVOKE_REFRESH_TOKENS = `\
|
|
231
|
+
CREATE OR REPLACE FUNCTION auth.revoke_refresh_tokens(p_user_id UUID)
|
|
232
|
+
RETURNS VOID
|
|
233
|
+
LANGUAGE plpgsql
|
|
234
|
+
SECURITY DEFINER
|
|
235
|
+
AS $$
|
|
236
|
+
BEGIN
|
|
237
|
+
DELETE FROM auth.refresh_tokens WHERE user_id = p_user_id;
|
|
238
|
+
END;
|
|
239
|
+
$$;`;
|
|
240
|
+
|
|
241
|
+
export function generateAuthSQL(): string {
|
|
242
|
+
const parts = [
|
|
243
|
+
PGCRYPTO,
|
|
244
|
+
'',
|
|
245
|
+
HASH_PASSWORD,
|
|
246
|
+
'',
|
|
247
|
+
VERIFY_PASSWORD,
|
|
248
|
+
'',
|
|
249
|
+
SIGN_JWT,
|
|
250
|
+
'',
|
|
251
|
+
VERIFY_JWT,
|
|
252
|
+
'',
|
|
253
|
+
GENERATE_REFRESH_TOKEN,
|
|
254
|
+
'',
|
|
255
|
+
VALIDATE_REFRESH_TOKEN,
|
|
256
|
+
'',
|
|
257
|
+
REVOKE_REFRESH_TOKENS,
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
return parts.join('\n').trimEnd() + '\n';
|
|
261
|
+
}
|