@auth-gate/core 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/dist/index.cjs +911 -0
- package/dist/index.d.cts +569 -0
- package/dist/index.d.ts +569 -0
- package/dist/index.mjs +891 -0
- package/package.json +29 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __defProps = Object.defineProperties;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
10
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
11
|
+
var __spreadValues = (a, b) => {
|
|
12
|
+
for (var prop in b || (b = {}))
|
|
13
|
+
if (__hasOwnProp.call(b, prop))
|
|
14
|
+
__defNormalProp(a, prop, b[prop]);
|
|
15
|
+
if (__getOwnPropSymbols)
|
|
16
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
17
|
+
if (__propIsEnum.call(b, prop))
|
|
18
|
+
__defNormalProp(a, prop, b[prop]);
|
|
19
|
+
}
|
|
20
|
+
return a;
|
|
21
|
+
};
|
|
22
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
23
|
+
var __export = (target, all) => {
|
|
24
|
+
for (var name in all)
|
|
25
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
26
|
+
};
|
|
27
|
+
var __copyProps = (to, from, except, desc) => {
|
|
28
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
29
|
+
for (let key of __getOwnPropNames(from))
|
|
30
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
31
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
32
|
+
}
|
|
33
|
+
return to;
|
|
34
|
+
};
|
|
35
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
36
|
+
|
|
37
|
+
// src/index.ts
|
|
38
|
+
var index_exports = {};
|
|
39
|
+
__export(index_exports, {
|
|
40
|
+
AuthGateClient: () => AuthGateClient,
|
|
41
|
+
AuthGateError: () => AuthGateError,
|
|
42
|
+
DEFAULT_COOKIE_NAME: () => DEFAULT_COOKIE_NAME,
|
|
43
|
+
InvalidStateError: () => InvalidStateError,
|
|
44
|
+
NONCE_COOKIE_NAME: () => NONCE_COOKIE_NAME,
|
|
45
|
+
REFRESH_TOKEN_COOKIE_NAME: () => REFRESH_TOKEN_COOKIE_NAME,
|
|
46
|
+
SESSION_MAX_AGE: () => SESSION_MAX_AGE,
|
|
47
|
+
STATE_TTL_MS: () => STATE_TTL_MS,
|
|
48
|
+
SUPPORTED_PROVIDERS: () => SUPPORTED_PROVIDERS,
|
|
49
|
+
SessionDecryptionError: () => SessionDecryptionError,
|
|
50
|
+
TokenVerificationError: () => TokenVerificationError,
|
|
51
|
+
createAuthGateClient: () => createAuthGateClient
|
|
52
|
+
});
|
|
53
|
+
module.exports = __toCommonJS(index_exports);
|
|
54
|
+
|
|
55
|
+
// src/crypto/encoding.ts
|
|
56
|
+
var encoder = new TextEncoder();
|
|
57
|
+
var decoder = new TextDecoder();
|
|
58
|
+
function toBase64Url(bytes) {
|
|
59
|
+
let binary = "";
|
|
60
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
61
|
+
binary += String.fromCharCode(bytes[i]);
|
|
62
|
+
}
|
|
63
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
64
|
+
}
|
|
65
|
+
function fromBase64Url(str) {
|
|
66
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
67
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
68
|
+
const binary = atob(padded);
|
|
69
|
+
const bytes = new Uint8Array(binary.length);
|
|
70
|
+
for (let i = 0; i < binary.length; i++) {
|
|
71
|
+
bytes[i] = binary.charCodeAt(i);
|
|
72
|
+
}
|
|
73
|
+
return bytes;
|
|
74
|
+
}
|
|
75
|
+
function toHex(bytes) {
|
|
76
|
+
let hex = "";
|
|
77
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
78
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
79
|
+
}
|
|
80
|
+
return hex;
|
|
81
|
+
}
|
|
82
|
+
function encodeUtf8(str) {
|
|
83
|
+
return encoder.encode(str);
|
|
84
|
+
}
|
|
85
|
+
function decodeUtf8(bytes) {
|
|
86
|
+
return decoder.decode(bytes);
|
|
87
|
+
}
|
|
88
|
+
function concat(...arrays) {
|
|
89
|
+
let length = 0;
|
|
90
|
+
for (const arr of arrays) length += arr.length;
|
|
91
|
+
const result = new Uint8Array(length);
|
|
92
|
+
let offset = 0;
|
|
93
|
+
for (const arr of arrays) {
|
|
94
|
+
result.set(arr, offset);
|
|
95
|
+
offset += arr.length;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
function constantTimeEqual(a, b) {
|
|
100
|
+
if (a.length !== b.length) return false;
|
|
101
|
+
let diff = 0;
|
|
102
|
+
for (let i = 0; i < a.length; i++) {
|
|
103
|
+
diff |= a[i] ^ b[i];
|
|
104
|
+
}
|
|
105
|
+
return diff === 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/crypto/keys.ts
|
|
109
|
+
async function hkdfDerive(secret, info, keylen) {
|
|
110
|
+
const ikm = encodeUtf8(secret);
|
|
111
|
+
const baseKey = await crypto.subtle.importKey("raw", ikm, "HKDF", false, [
|
|
112
|
+
"deriveBits"
|
|
113
|
+
]);
|
|
114
|
+
const bits = await crypto.subtle.deriveBits(
|
|
115
|
+
{
|
|
116
|
+
name: "HKDF",
|
|
117
|
+
hash: "SHA-256",
|
|
118
|
+
salt: new Uint8Array(0),
|
|
119
|
+
info: encodeUtf8(info)
|
|
120
|
+
},
|
|
121
|
+
baseKey,
|
|
122
|
+
keylen * 8
|
|
123
|
+
);
|
|
124
|
+
return new Uint8Array(bits);
|
|
125
|
+
}
|
|
126
|
+
async function deriveEncryptionKey(secret) {
|
|
127
|
+
return hkdfDerive(secret, "authgate-session-encryption", 32);
|
|
128
|
+
}
|
|
129
|
+
async function deriveStateKey(secret) {
|
|
130
|
+
return hkdfDerive(secret, "authgate-state-signing", 32);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/constants.ts
|
|
134
|
+
var DEFAULT_COOKIE_NAME = "__authgate";
|
|
135
|
+
var NONCE_COOKIE_NAME = "__authgate_nonce";
|
|
136
|
+
var REFRESH_TOKEN_COOKIE_NAME = "__authgate_refresh";
|
|
137
|
+
var SESSION_MAX_AGE = 604800;
|
|
138
|
+
var STATE_TTL_MS = 6e5;
|
|
139
|
+
var SUPPORTED_PROVIDERS = [
|
|
140
|
+
"google",
|
|
141
|
+
"github",
|
|
142
|
+
"discord",
|
|
143
|
+
"azure",
|
|
144
|
+
"apple"
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
// src/crypto/session.ts
|
|
148
|
+
var VERSION = 1;
|
|
149
|
+
async function encryptSession(key, user, maxAge = SESSION_MAX_AGE) {
|
|
150
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
151
|
+
const payload = {
|
|
152
|
+
user,
|
|
153
|
+
iat: now,
|
|
154
|
+
exp: now + maxAge
|
|
155
|
+
};
|
|
156
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
157
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
158
|
+
"raw",
|
|
159
|
+
key,
|
|
160
|
+
"AES-GCM",
|
|
161
|
+
false,
|
|
162
|
+
["encrypt"]
|
|
163
|
+
);
|
|
164
|
+
const plaintext = encodeUtf8(JSON.stringify(payload));
|
|
165
|
+
const ciphertextWithTag = new Uint8Array(
|
|
166
|
+
await crypto.subtle.encrypt(
|
|
167
|
+
{ name: "AES-GCM", iv, tagLength: 128 },
|
|
168
|
+
cryptoKey,
|
|
169
|
+
plaintext
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
const ciphertext = ciphertextWithTag.subarray(
|
|
173
|
+
0,
|
|
174
|
+
ciphertextWithTag.length - 16
|
|
175
|
+
);
|
|
176
|
+
const authTag = ciphertextWithTag.subarray(ciphertextWithTag.length - 16);
|
|
177
|
+
const result = concat(new Uint8Array([VERSION]), iv, authTag, ciphertext);
|
|
178
|
+
return toBase64Url(result);
|
|
179
|
+
}
|
|
180
|
+
async function decryptSessionPayload(key, cookieValue) {
|
|
181
|
+
try {
|
|
182
|
+
const data = fromBase64Url(cookieValue);
|
|
183
|
+
if (data.length < 30) return null;
|
|
184
|
+
const version = data[0];
|
|
185
|
+
if (version !== VERSION) return null;
|
|
186
|
+
const iv = data.subarray(1, 13);
|
|
187
|
+
const authTag = data.subarray(13, 29);
|
|
188
|
+
const ciphertext = data.subarray(29);
|
|
189
|
+
const ciphertextWithTag = concat(ciphertext, authTag);
|
|
190
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
191
|
+
"raw",
|
|
192
|
+
key,
|
|
193
|
+
"AES-GCM",
|
|
194
|
+
false,
|
|
195
|
+
["decrypt"]
|
|
196
|
+
);
|
|
197
|
+
const decrypted = new Uint8Array(
|
|
198
|
+
await crypto.subtle.decrypt(
|
|
199
|
+
{ name: "AES-GCM", iv, tagLength: 128 },
|
|
200
|
+
cryptoKey,
|
|
201
|
+
ciphertextWithTag
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
const payload = JSON.parse(decodeUtf8(decrypted));
|
|
205
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
206
|
+
if (payload.exp <= now) return null;
|
|
207
|
+
return payload;
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function decryptSession(key, cookieValue) {
|
|
213
|
+
var _a;
|
|
214
|
+
const payload = await decryptSessionPayload(key, cookieValue);
|
|
215
|
+
return (_a = payload == null ? void 0 : payload.user) != null ? _a : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/crypto/state.ts
|
|
219
|
+
async function createOAuthState(signingKey) {
|
|
220
|
+
const nonceBytes = crypto.getRandomValues(new Uint8Array(16));
|
|
221
|
+
const nonce = toHex(nonceBytes);
|
|
222
|
+
const timestamp = Date.now().toString();
|
|
223
|
+
const payload = `${nonce}.${timestamp}`;
|
|
224
|
+
const key = await crypto.subtle.importKey(
|
|
225
|
+
"raw",
|
|
226
|
+
signingKey,
|
|
227
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
228
|
+
false,
|
|
229
|
+
["sign"]
|
|
230
|
+
);
|
|
231
|
+
const sigBytes = new Uint8Array(
|
|
232
|
+
await crypto.subtle.sign("HMAC", key, encodeUtf8(payload))
|
|
233
|
+
);
|
|
234
|
+
const signature = toHex(sigBytes);
|
|
235
|
+
return {
|
|
236
|
+
state: `${payload}.${signature}`,
|
|
237
|
+
nonce
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
async function verifyOAuthState(state, nonce, signingKey) {
|
|
241
|
+
try {
|
|
242
|
+
const parts = state.split(".");
|
|
243
|
+
if (parts.length !== 3) return false;
|
|
244
|
+
const [stateNonce, timestamp, signature] = parts;
|
|
245
|
+
const nonceBuf = encodeUtf8(stateNonce);
|
|
246
|
+
const expectedNonceBuf = encodeUtf8(nonce);
|
|
247
|
+
if (nonceBuf.length !== expectedNonceBuf.length) return false;
|
|
248
|
+
if (!constantTimeEqual(nonceBuf, expectedNonceBuf)) return false;
|
|
249
|
+
const stateTime = parseInt(timestamp, 10);
|
|
250
|
+
if (isNaN(stateTime) || Date.now() - stateTime > STATE_TTL_MS) return false;
|
|
251
|
+
const payload = `${stateNonce}.${timestamp}`;
|
|
252
|
+
const key = await crypto.subtle.importKey(
|
|
253
|
+
"raw",
|
|
254
|
+
signingKey,
|
|
255
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
256
|
+
false,
|
|
257
|
+
["verify"]
|
|
258
|
+
);
|
|
259
|
+
const sigBytes = new Uint8Array(signature.length / 2);
|
|
260
|
+
for (let i = 0; i < signature.length; i += 2) {
|
|
261
|
+
sigBytes[i / 2] = parseInt(signature.substring(i, i + 2), 16);
|
|
262
|
+
}
|
|
263
|
+
return crypto.subtle.verify(
|
|
264
|
+
"HMAC",
|
|
265
|
+
key,
|
|
266
|
+
sigBytes,
|
|
267
|
+
encodeUtf8(payload)
|
|
268
|
+
);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/errors.ts
|
|
275
|
+
var AuthGateError = class extends Error {
|
|
276
|
+
constructor(message) {
|
|
277
|
+
super(message);
|
|
278
|
+
this.name = "AuthGateError";
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
var TokenVerificationError = class extends AuthGateError {
|
|
282
|
+
constructor(message = "Token verification failed") {
|
|
283
|
+
super(message);
|
|
284
|
+
this.name = "TokenVerificationError";
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
var SessionDecryptionError = class extends AuthGateError {
|
|
288
|
+
constructor(message = "Session decryption failed") {
|
|
289
|
+
super(message);
|
|
290
|
+
this.name = "SessionDecryptionError";
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
var InvalidStateError = class extends AuthGateError {
|
|
294
|
+
constructor(message = "Invalid OAuth state") {
|
|
295
|
+
super(message);
|
|
296
|
+
this.name = "InvalidStateError";
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/client.ts
|
|
301
|
+
var AuthGateClient = class {
|
|
302
|
+
/**
|
|
303
|
+
* @param config - Client configuration. See {@link AuthGateConfig}.
|
|
304
|
+
* @throws {@link AuthGateError} if `sessionSecret` is shorter than 32 characters.
|
|
305
|
+
*/
|
|
306
|
+
constructor(config) {
|
|
307
|
+
this.encryptionKey = null;
|
|
308
|
+
this.stateKey = null;
|
|
309
|
+
var _a, _b, _c;
|
|
310
|
+
if (!config.sessionSecret || config.sessionSecret.length < 32) {
|
|
311
|
+
throw new AuthGateError(
|
|
312
|
+
"sessionSecret must be at least 32 characters long"
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
this.config = __spreadProps(__spreadValues({}, config), {
|
|
316
|
+
cookieName: (_a = config.cookieName) != null ? _a : DEFAULT_COOKIE_NAME,
|
|
317
|
+
sessionMaxAge: (_b = config.sessionMaxAge) != null ? _b : SESSION_MAX_AGE,
|
|
318
|
+
callbackPath: (_c = config.callbackPath) != null ? _c : "/api/auth/callback"
|
|
319
|
+
});
|
|
320
|
+
this.keysReady = this.deriveKeys();
|
|
321
|
+
}
|
|
322
|
+
async deriveKeys() {
|
|
323
|
+
const [encKey, stKey] = await Promise.all([
|
|
324
|
+
deriveEncryptionKey(this.config.sessionSecret),
|
|
325
|
+
deriveStateKey(this.config.sessionSecret)
|
|
326
|
+
]);
|
|
327
|
+
this.encryptionKey = encKey;
|
|
328
|
+
this.stateKey = stKey;
|
|
329
|
+
}
|
|
330
|
+
async getEncryptionKey() {
|
|
331
|
+
await this.keysReady;
|
|
332
|
+
return this.encryptionKey;
|
|
333
|
+
}
|
|
334
|
+
async getStateKey() {
|
|
335
|
+
await this.keysReady;
|
|
336
|
+
return this.stateKey;
|
|
337
|
+
}
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Token verification
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
/**
|
|
342
|
+
* Verify a JWT token against the AuthGate API.
|
|
343
|
+
*
|
|
344
|
+
* Makes a server-side `POST /api/v1/token/verify` call using your API key.
|
|
345
|
+
* Times out after 5 seconds.
|
|
346
|
+
*
|
|
347
|
+
* @param token - The JWT token string from the OAuth callback or email/SMS flow.
|
|
348
|
+
* @returns A {@link TokenVerifyResult} with the user data if valid.
|
|
349
|
+
* @throws {@link TokenVerificationError} if the request times out.
|
|
350
|
+
*/
|
|
351
|
+
async verifyToken(token) {
|
|
352
|
+
const controller = new AbortController();
|
|
353
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
354
|
+
try {
|
|
355
|
+
const response = await fetch(
|
|
356
|
+
`${this.config.baseUrl}/api/v1/token/verify`,
|
|
357
|
+
{
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: {
|
|
360
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
361
|
+
"Content-Type": "application/json"
|
|
362
|
+
},
|
|
363
|
+
body: JSON.stringify({ token }),
|
|
364
|
+
signal: controller.signal
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
return { valid: false, error: `HTTP ${response.status}` };
|
|
369
|
+
}
|
|
370
|
+
return response.json();
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
373
|
+
throw new TokenVerificationError("Token verification timed out");
|
|
374
|
+
}
|
|
375
|
+
return { valid: false, error: "Network error" };
|
|
376
|
+
} finally {
|
|
377
|
+
clearTimeout(timeout);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// Session encryption
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
/**
|
|
384
|
+
* Encrypt user data into a session cookie value.
|
|
385
|
+
*
|
|
386
|
+
* Uses AES-256-GCM with a key derived from your session secret.
|
|
387
|
+
* The returned string is safe to store in an httpOnly cookie.
|
|
388
|
+
*
|
|
389
|
+
* @param user - The authenticated user to store.
|
|
390
|
+
* @returns Base64url-encoded encrypted session string.
|
|
391
|
+
*/
|
|
392
|
+
async encryptSession(user) {
|
|
393
|
+
const key = await this.getEncryptionKey();
|
|
394
|
+
return await encryptSession(key, user, this.config.sessionMaxAge);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Decrypt a session cookie and return the user if the session is valid.
|
|
398
|
+
*
|
|
399
|
+
* Returns `null` if the cookie is expired, tampered with, or encrypted
|
|
400
|
+
* with a different secret. All failure modes return `null` to prevent
|
|
401
|
+
* oracle attacks.
|
|
402
|
+
*
|
|
403
|
+
* @param cookieValue - The encrypted cookie string.
|
|
404
|
+
* @returns The user, or `null` if the session is invalid.
|
|
405
|
+
*/
|
|
406
|
+
async decryptSession(cookieValue) {
|
|
407
|
+
const key = await this.getEncryptionKey();
|
|
408
|
+
return await decryptSession(key, cookieValue);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Decrypt a session cookie and return the full payload (user + timestamps).
|
|
412
|
+
*
|
|
413
|
+
* Used internally by middleware to check `iat` for session revalidation.
|
|
414
|
+
*
|
|
415
|
+
* @param cookieValue - The encrypted cookie string.
|
|
416
|
+
* @returns The full session payload, or `null` if invalid.
|
|
417
|
+
* @internal
|
|
418
|
+
*/
|
|
419
|
+
async decryptSessionPayload(cookieValue) {
|
|
420
|
+
const key = await this.getEncryptionKey();
|
|
421
|
+
return await decryptSessionPayload(key, cookieValue);
|
|
422
|
+
}
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// OAuth state
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
/**
|
|
427
|
+
* Create a signed OAuth state parameter and nonce for CSRF protection.
|
|
428
|
+
*
|
|
429
|
+
* Store the `nonce` in a short-lived httpOnly cookie and pass `state`
|
|
430
|
+
* as a query parameter in the OAuth authorization URL.
|
|
431
|
+
*
|
|
432
|
+
* @returns `{ state, nonce }` — the state string and its corresponding nonce.
|
|
433
|
+
*/
|
|
434
|
+
async createState() {
|
|
435
|
+
const key = await this.getStateKey();
|
|
436
|
+
return await createOAuthState(key);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Verify an OAuth state parameter returned in the callback.
|
|
440
|
+
*
|
|
441
|
+
* @param state - The `state` query parameter from the callback.
|
|
442
|
+
* @param nonce - The nonce from the httpOnly cookie.
|
|
443
|
+
* @returns `true` if the state is valid and not expired.
|
|
444
|
+
*/
|
|
445
|
+
async verifyState(state, nonce) {
|
|
446
|
+
const key = await this.getStateKey();
|
|
447
|
+
return await verifyOAuthState(state, nonce, key);
|
|
448
|
+
}
|
|
449
|
+
// ---------------------------------------------------------------------------
|
|
450
|
+
// URL builders
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
/**
|
|
453
|
+
* Build the OAuth authorization URL for a given provider.
|
|
454
|
+
*
|
|
455
|
+
* @param provider - The OAuth provider (e.g. `"google"`, `"github"`).
|
|
456
|
+
* @param callbackUrl - Your app's callback URL where the user is redirected after auth.
|
|
457
|
+
* @param options - Optional parameters.
|
|
458
|
+
* @param options.linkTo - An existing user ID to link this OAuth account to.
|
|
459
|
+
* @returns The full authorization URL to redirect the user to.
|
|
460
|
+
* @throws {@link AuthGateError} if the provider is not in {@link SUPPORTED_PROVIDERS}.
|
|
461
|
+
*/
|
|
462
|
+
getOAuthUrl(provider, callbackUrl, options) {
|
|
463
|
+
if (!SUPPORTED_PROVIDERS.includes(provider)) {
|
|
464
|
+
throw new AuthGateError(`Unsupported provider: ${provider}`);
|
|
465
|
+
}
|
|
466
|
+
const url = new URL(`/api/proxy/${provider}`, this.config.baseUrl);
|
|
467
|
+
url.searchParams.set("project_id", this.config.projectId);
|
|
468
|
+
url.searchParams.set("callback_url", callbackUrl);
|
|
469
|
+
if (options == null ? void 0 : options.linkTo) {
|
|
470
|
+
url.searchParams.set("link_to", options.linkTo);
|
|
471
|
+
}
|
|
472
|
+
return url.toString();
|
|
473
|
+
}
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// Email auth
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
/**
|
|
478
|
+
* Register a new user with email and password.
|
|
479
|
+
*
|
|
480
|
+
* @param data - Signup data including email, password, optional name, and callback URL.
|
|
481
|
+
* @returns The raw `Response` from the AuthGate proxy.
|
|
482
|
+
*/
|
|
483
|
+
async emailSignup(data) {
|
|
484
|
+
return fetch(`${this.config.baseUrl}/api/proxy/email/signup`, {
|
|
485
|
+
method: "POST",
|
|
486
|
+
headers: { "Content-Type": "application/json" },
|
|
487
|
+
body: JSON.stringify({
|
|
488
|
+
email: data.email,
|
|
489
|
+
password: data.password,
|
|
490
|
+
name: data.name,
|
|
491
|
+
project_id: this.config.projectId,
|
|
492
|
+
callback_url: data.callbackUrl
|
|
493
|
+
})
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Sign in an existing user with email and password.
|
|
498
|
+
*
|
|
499
|
+
* The response may include `mfa_required: true` if the user has MFA enabled,
|
|
500
|
+
* in which case you should redirect to an MFA verification page.
|
|
501
|
+
*
|
|
502
|
+
* @param data - Sign-in data including email, password, and callback URL.
|
|
503
|
+
* @returns The raw `Response` from the AuthGate proxy.
|
|
504
|
+
*/
|
|
505
|
+
async emailSignin(data) {
|
|
506
|
+
return fetch(`${this.config.baseUrl}/api/proxy/email/signin`, {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: { "Content-Type": "application/json" },
|
|
509
|
+
body: JSON.stringify({
|
|
510
|
+
email: data.email,
|
|
511
|
+
password: data.password,
|
|
512
|
+
project_id: this.config.projectId,
|
|
513
|
+
callback_url: data.callbackUrl
|
|
514
|
+
})
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Send a password reset email to the user.
|
|
519
|
+
*
|
|
520
|
+
* @param data - The user's email and the URL to redirect to after clicking the reset link.
|
|
521
|
+
* @returns The raw `Response` from the AuthGate proxy.
|
|
522
|
+
*/
|
|
523
|
+
async emailForgotPassword(data) {
|
|
524
|
+
return fetch(`${this.config.baseUrl}/api/proxy/email/reset-password`, {
|
|
525
|
+
method: "POST",
|
|
526
|
+
headers: { "Content-Type": "application/json" },
|
|
527
|
+
body: JSON.stringify({
|
|
528
|
+
email: data.email,
|
|
529
|
+
project_id: this.config.projectId,
|
|
530
|
+
callback_url: data.callbackUrl
|
|
531
|
+
})
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Confirm a password reset with the token from the reset email.
|
|
536
|
+
*
|
|
537
|
+
* @param data - The reset token and the new password.
|
|
538
|
+
* @returns The raw `Response` from the AuthGate proxy.
|
|
539
|
+
*/
|
|
540
|
+
async emailResetPassword(data) {
|
|
541
|
+
return fetch(
|
|
542
|
+
`${this.config.baseUrl}/api/proxy/email/reset-password/confirm`,
|
|
543
|
+
{
|
|
544
|
+
method: "POST",
|
|
545
|
+
headers: { "Content-Type": "application/json" },
|
|
546
|
+
body: JSON.stringify({
|
|
547
|
+
token: data.token,
|
|
548
|
+
password: data.password,
|
|
549
|
+
project_id: this.config.projectId
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
// Magic link auth
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
/**
|
|
558
|
+
* Send a magic link email for passwordless authentication.
|
|
559
|
+
*
|
|
560
|
+
* @param data - The user's email and callback URL.
|
|
561
|
+
* @returns The raw `Response` from the AuthGate proxy.
|
|
562
|
+
*/
|
|
563
|
+
async magicLinkSend(data) {
|
|
564
|
+
return fetch(
|
|
565
|
+
`${this.config.baseUrl}/api/proxy/email/magic-link/send`,
|
|
566
|
+
{
|
|
567
|
+
method: "POST",
|
|
568
|
+
headers: { "Content-Type": "application/json" },
|
|
569
|
+
body: JSON.stringify({
|
|
570
|
+
email: data.email,
|
|
571
|
+
project_id: this.config.projectId,
|
|
572
|
+
callback_url: data.callbackUrl
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
// Email verification
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
/**
|
|
581
|
+
* Verify an email address using a one-time code.
|
|
582
|
+
*
|
|
583
|
+
* @param data - The user's email and the verification code.
|
|
584
|
+
* @returns The raw `Response` from the AuthGate proxy.
|
|
585
|
+
*/
|
|
586
|
+
async emailVerifyCode(data) {
|
|
587
|
+
return fetch(`${this.config.baseUrl}/api/proxy/email/verify`, {
|
|
588
|
+
method: "POST",
|
|
589
|
+
headers: { "Content-Type": "application/json" },
|
|
590
|
+
body: JSON.stringify({
|
|
591
|
+
email: data.email,
|
|
592
|
+
code: data.code,
|
|
593
|
+
project_id: this.config.projectId
|
|
594
|
+
})
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
// SMS auth
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
/**
|
|
601
|
+
* Send an SMS verification code to the user's phone number.
|
|
602
|
+
*
|
|
603
|
+
* @param data - The phone number (E.164 format) and callback URL.
|
|
604
|
+
* @returns The raw `Response` from the AuthGate proxy.
|
|
605
|
+
*/
|
|
606
|
+
async smsSendCode(data) {
|
|
607
|
+
return fetch(`${this.config.baseUrl}/api/proxy/sms/send-code`, {
|
|
608
|
+
method: "POST",
|
|
609
|
+
headers: { "Content-Type": "application/json" },
|
|
610
|
+
body: JSON.stringify({
|
|
611
|
+
phone: data.phone,
|
|
612
|
+
project_id: this.config.projectId,
|
|
613
|
+
callback_url: data.callbackUrl
|
|
614
|
+
})
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Verify an SMS code and authenticate the user.
|
|
619
|
+
*
|
|
620
|
+
* @param data - The phone number, verification code, and callback URL.
|
|
621
|
+
* @returns The raw `Response` from the AuthGate proxy.
|
|
622
|
+
*/
|
|
623
|
+
async smsVerifyCode(data) {
|
|
624
|
+
return fetch(`${this.config.baseUrl}/api/proxy/sms/verify-code`, {
|
|
625
|
+
method: "POST",
|
|
626
|
+
headers: { "Content-Type": "application/json" },
|
|
627
|
+
body: JSON.stringify({
|
|
628
|
+
phone: data.phone,
|
|
629
|
+
code: data.code,
|
|
630
|
+
project_id: this.config.projectId,
|
|
631
|
+
callback_url: data.callbackUrl
|
|
632
|
+
})
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
// Session management
|
|
637
|
+
// ---------------------------------------------------------------------------
|
|
638
|
+
/**
|
|
639
|
+
* Exchange a refresh token for a new JWT.
|
|
640
|
+
*
|
|
641
|
+
* Refresh tokens are single-use — each call returns a new JWT and
|
|
642
|
+
* invalidates the previous refresh token (rotation).
|
|
643
|
+
*
|
|
644
|
+
* @param refreshToken - The opaque refresh token.
|
|
645
|
+
* @returns A new JWT and its `expires_in` (seconds), or `null` if the refresh token is invalid.
|
|
646
|
+
*/
|
|
647
|
+
async refreshToken(refreshToken) {
|
|
648
|
+
try {
|
|
649
|
+
const response = await fetch(
|
|
650
|
+
`${this.config.baseUrl}/api/proxy/token/refresh`,
|
|
651
|
+
{
|
|
652
|
+
method: "POST",
|
|
653
|
+
headers: { "Content-Type": "application/json" },
|
|
654
|
+
body: JSON.stringify({
|
|
655
|
+
refresh_token: refreshToken,
|
|
656
|
+
project_id: this.config.projectId
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
);
|
|
660
|
+
if (!response.ok) return null;
|
|
661
|
+
return response.json();
|
|
662
|
+
} catch (e) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Revoke a session by its refresh token.
|
|
668
|
+
*
|
|
669
|
+
* After revocation, the refresh token can no longer be used to obtain
|
|
670
|
+
* new JWTs. The user will need to re-authenticate.
|
|
671
|
+
*
|
|
672
|
+
* @param refreshToken - The opaque refresh token to revoke.
|
|
673
|
+
* @returns `true` if the session was revoked, `false` on failure.
|
|
674
|
+
*/
|
|
675
|
+
async revokeSession(refreshToken) {
|
|
676
|
+
try {
|
|
677
|
+
const response = await fetch(
|
|
678
|
+
`${this.config.baseUrl}/api/proxy/sessions/revoke`,
|
|
679
|
+
{
|
|
680
|
+
method: "POST",
|
|
681
|
+
headers: { "Content-Type": "application/json" },
|
|
682
|
+
body: JSON.stringify({
|
|
683
|
+
refresh_token: refreshToken,
|
|
684
|
+
project_id: this.config.projectId
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
return response.ok;
|
|
689
|
+
} catch (e) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// ---------------------------------------------------------------------------
|
|
694
|
+
// MFA
|
|
695
|
+
// ---------------------------------------------------------------------------
|
|
696
|
+
/**
|
|
697
|
+
* Verify an MFA code to complete authentication.
|
|
698
|
+
*
|
|
699
|
+
* Called after a sign-in response includes `mfa_required: true`.
|
|
700
|
+
*
|
|
701
|
+
* @param data - The MFA challenge token, verification code, and method.
|
|
702
|
+
* @returns The raw `Response` — includes a JWT on success.
|
|
703
|
+
*/
|
|
704
|
+
async mfaVerify(data) {
|
|
705
|
+
return fetch(`${this.config.baseUrl}/api/proxy/mfa/verify`, {
|
|
706
|
+
method: "POST",
|
|
707
|
+
headers: { "Content-Type": "application/json" },
|
|
708
|
+
body: JSON.stringify(__spreadProps(__spreadValues({}, data), {
|
|
709
|
+
project_id: this.config.projectId
|
|
710
|
+
}))
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Begin TOTP (authenticator app) setup for the authenticated user.
|
|
715
|
+
*
|
|
716
|
+
* Requires a valid JWT in the `Authorization: Bearer` header.
|
|
717
|
+
*
|
|
718
|
+
* @param token - The user's current JWT.
|
|
719
|
+
* @returns The raw `Response` with TOTP secret, QR code, and URI.
|
|
720
|
+
*/
|
|
721
|
+
async mfaTotpSetup(token) {
|
|
722
|
+
return fetch(`${this.config.baseUrl}/api/proxy/mfa/totp/setup`, {
|
|
723
|
+
method: "POST",
|
|
724
|
+
headers: {
|
|
725
|
+
Authorization: `Bearer ${token}`,
|
|
726
|
+
"Content-Type": "application/json"
|
|
727
|
+
},
|
|
728
|
+
body: "{}"
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Confirm TOTP setup by verifying a code from the authenticator app.
|
|
733
|
+
*
|
|
734
|
+
* @param token - The user's current JWT.
|
|
735
|
+
* @param code - The 6-digit TOTP code from the authenticator app.
|
|
736
|
+
* @returns The raw `Response` — confirms enrollment on success.
|
|
737
|
+
*/
|
|
738
|
+
async mfaTotpVerifySetup(token, code) {
|
|
739
|
+
return fetch(`${this.config.baseUrl}/api/proxy/mfa/totp/verify-setup`, {
|
|
740
|
+
method: "POST",
|
|
741
|
+
headers: {
|
|
742
|
+
Authorization: `Bearer ${token}`,
|
|
743
|
+
"Content-Type": "application/json"
|
|
744
|
+
},
|
|
745
|
+
body: JSON.stringify({ code })
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Disable TOTP for the authenticated user.
|
|
750
|
+
*
|
|
751
|
+
* @param token - The user's current JWT.
|
|
752
|
+
* @param code - A valid TOTP code to confirm the action.
|
|
753
|
+
* @returns The raw `Response`.
|
|
754
|
+
*/
|
|
755
|
+
async mfaTotpDisable(token, code) {
|
|
756
|
+
return fetch(`${this.config.baseUrl}/api/proxy/mfa/totp`, {
|
|
757
|
+
method: "DELETE",
|
|
758
|
+
headers: {
|
|
759
|
+
Authorization: `Bearer ${token}`,
|
|
760
|
+
"Content-Type": "application/json"
|
|
761
|
+
},
|
|
762
|
+
body: JSON.stringify({ code })
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Enable SMS-based MFA for the authenticated user.
|
|
767
|
+
*
|
|
768
|
+
* Uses the phone number already associated with the user's account.
|
|
769
|
+
*
|
|
770
|
+
* @param token - The user's current JWT.
|
|
771
|
+
* @returns The raw `Response`.
|
|
772
|
+
*/
|
|
773
|
+
async mfaSmsEnable(token) {
|
|
774
|
+
return fetch(`${this.config.baseUrl}/api/proxy/mfa/sms/enable`, {
|
|
775
|
+
method: "POST",
|
|
776
|
+
headers: {
|
|
777
|
+
Authorization: `Bearer ${token}`,
|
|
778
|
+
"Content-Type": "application/json"
|
|
779
|
+
},
|
|
780
|
+
body: "{}"
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Disable SMS-based MFA for the authenticated user.
|
|
785
|
+
*
|
|
786
|
+
* @param token - The user's current JWT.
|
|
787
|
+
* @returns The raw `Response`.
|
|
788
|
+
*/
|
|
789
|
+
async mfaSmsDisable(token) {
|
|
790
|
+
return fetch(`${this.config.baseUrl}/api/proxy/mfa/sms`, {
|
|
791
|
+
method: "DELETE",
|
|
792
|
+
headers: {
|
|
793
|
+
Authorization: `Bearer ${token}`,
|
|
794
|
+
"Content-Type": "application/json"
|
|
795
|
+
},
|
|
796
|
+
body: "{}"
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Generate a new set of single-use MFA backup codes.
|
|
801
|
+
*
|
|
802
|
+
* Generating new codes invalidates all previously issued backup codes.
|
|
803
|
+
*
|
|
804
|
+
* @param token - The user's current JWT.
|
|
805
|
+
* @returns The raw `Response` with an array of backup codes.
|
|
806
|
+
*/
|
|
807
|
+
async mfaBackupCodesGenerate(token) {
|
|
808
|
+
return fetch(
|
|
809
|
+
`${this.config.baseUrl}/api/proxy/mfa/backup-codes/generate`,
|
|
810
|
+
{
|
|
811
|
+
method: "POST",
|
|
812
|
+
headers: {
|
|
813
|
+
Authorization: `Bearer ${token}`,
|
|
814
|
+
"Content-Type": "application/json"
|
|
815
|
+
},
|
|
816
|
+
body: "{}"
|
|
817
|
+
}
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Get the current MFA enrollment status for the authenticated user.
|
|
822
|
+
*
|
|
823
|
+
* @param token - The user's current JWT.
|
|
824
|
+
* @returns The raw `Response` with {@link MfaStatus} data.
|
|
825
|
+
*/
|
|
826
|
+
async mfaStatus(token) {
|
|
827
|
+
return fetch(`${this.config.baseUrl}/api/proxy/mfa/status`, {
|
|
828
|
+
method: "GET",
|
|
829
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Send an SMS code for MFA verification during the challenge flow.
|
|
834
|
+
*
|
|
835
|
+
* @param challenge - The MFA challenge token from the sign-in response.
|
|
836
|
+
* @returns The raw `Response`.
|
|
837
|
+
*/
|
|
838
|
+
async mfaSmsSendCode(challenge) {
|
|
839
|
+
return fetch(`${this.config.baseUrl}/api/proxy/mfa/sms/send-code`, {
|
|
840
|
+
method: "POST",
|
|
841
|
+
headers: { "Content-Type": "application/json" },
|
|
842
|
+
body: JSON.stringify({
|
|
843
|
+
challenge,
|
|
844
|
+
project_id: this.config.projectId
|
|
845
|
+
})
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
// ---------------------------------------------------------------------------
|
|
849
|
+
// Cookie config accessors
|
|
850
|
+
// ---------------------------------------------------------------------------
|
|
851
|
+
/** The name of the session cookie (default: `"__authgate"`). */
|
|
852
|
+
get cookieName() {
|
|
853
|
+
return this.config.cookieName;
|
|
854
|
+
}
|
|
855
|
+
/** The name of the OAuth nonce cookie (`"__authgate_nonce"`). */
|
|
856
|
+
get nonceCookieName() {
|
|
857
|
+
return NONCE_COOKIE_NAME;
|
|
858
|
+
}
|
|
859
|
+
/** The name of the refresh token cookie (`"__authgate_refresh"`). */
|
|
860
|
+
get refreshTokenCookieName() {
|
|
861
|
+
return REFRESH_TOKEN_COOKIE_NAME;
|
|
862
|
+
}
|
|
863
|
+
/** The callback path where OAuth redirects are handled (default: `"/api/auth/callback"`). */
|
|
864
|
+
get callbackPath() {
|
|
865
|
+
return this.config.callbackPath;
|
|
866
|
+
}
|
|
867
|
+
/** The AuthGate project ID. */
|
|
868
|
+
get projectId() {
|
|
869
|
+
return this.config.projectId;
|
|
870
|
+
}
|
|
871
|
+
/** The AuthGate service base URL. */
|
|
872
|
+
get baseUrl() {
|
|
873
|
+
return this.config.baseUrl;
|
|
874
|
+
}
|
|
875
|
+
/** Session lifetime in seconds. */
|
|
876
|
+
get sessionMaxAge() {
|
|
877
|
+
return this.config.sessionMaxAge;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Get cookie options for the session cookie.
|
|
881
|
+
*
|
|
882
|
+
* Returns `secure: true` in production, `httpOnly: true` always.
|
|
883
|
+
*/
|
|
884
|
+
getSessionCookieOptions() {
|
|
885
|
+
return {
|
|
886
|
+
httpOnly: true,
|
|
887
|
+
secure: process.env.NODE_ENV === "production",
|
|
888
|
+
sameSite: "lax",
|
|
889
|
+
maxAge: this.config.sessionMaxAge,
|
|
890
|
+
path: "/"
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Get cookie options for the OAuth nonce cookie.
|
|
895
|
+
*
|
|
896
|
+
* Short-lived (10 minutes) to match the state TTL.
|
|
897
|
+
*/
|
|
898
|
+
getNonceCookieOptions() {
|
|
899
|
+
return {
|
|
900
|
+
httpOnly: true,
|
|
901
|
+
secure: process.env.NODE_ENV === "production",
|
|
902
|
+
sameSite: "lax",
|
|
903
|
+
maxAge: 600,
|
|
904
|
+
// 10 minutes
|
|
905
|
+
path: "/"
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
function createAuthGateClient(config) {
|
|
910
|
+
return new AuthGateClient(config);
|
|
911
|
+
}
|