better-auth-cognito-native 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.mjs ADDED
@@ -0,0 +1,679 @@
1
+ import { t as COGNITO_ERROR_CODES } from "./error-codes-O-Ljx2JX.mjs";
2
+ import { getPasskeyConfig } from "./config.mjs";
3
+ import { AliasExistsException, CodeMismatchException, CognitoIdentityProviderClient, CompleteWebAuthnRegistrationCommand, ConfirmForgotPasswordCommand, ConfirmSignUpCommand, ExpiredCodeException, ForgotPasswordCommand, GetUserCommand, GlobalSignOutCommand, InitiateAuthCommand, InvalidParameterException, InvalidPasswordException, LimitExceededException, NotAuthorizedException, ResendConfirmationCodeCommand, RespondToAuthChallengeCommand, SignUpCommand, StartWebAuthnRegistrationCommand, UpdateUserAttributesCommand, UserLambdaValidationException, UserNotConfirmedException, UserNotFoundException, UsernameExistsException, WebAuthnChallengeNotFoundException, WebAuthnConfigurationMissingException, WebAuthnNotEnabledException, WebAuthnOriginNotAllowedException, WebAuthnRelyingPartyMismatchException } from "@aws-sdk/client-cognito-identity-provider";
4
+ import { createRemoteJWKSet, jwtVerify } from "jose";
5
+ import { APIError, createAuthEndpoint, getSessionFromCtx } from "better-auth/api";
6
+ import { deleteSessionCookie, setSessionCookie } from "better-auth/cookies";
7
+ import { symmetricDecodeJWT, symmetricEncodeJWT } from "better-auth/crypto";
8
+ import * as z from "zod";
9
+ //#region src/routes.ts
10
+ const cognitoSignInBodySchema = z.object({
11
+ email: z.email(),
12
+ password: z.string().min(1),
13
+ rememberMe: z.boolean().optional()
14
+ });
15
+ const cognitoSignUpBodySchema = z.object({
16
+ email: z.email(),
17
+ password: z.string().min(1)
18
+ });
19
+ const cognitoConfirmSignUpBodySchema = z.object({
20
+ email: z.email(),
21
+ code: z.string().trim().min(1)
22
+ });
23
+ const cognitoResendConfirmationCodeBodySchema = z.object({ email: z.email() });
24
+ const cognitoForgotPasswordBodySchema = z.object({ email: z.email() });
25
+ const cognitoConfirmForgotPasswordBodySchema = z.object({
26
+ email: z.email(),
27
+ code: z.string().trim().min(1),
28
+ newPassword: z.string().min(1)
29
+ });
30
+ const cognitoNewPasswordBodySchema = z.object({
31
+ email: z.email(),
32
+ newPassword: z.string().min(1),
33
+ challengeSession: z.string().min(1),
34
+ rememberMe: z.boolean().optional()
35
+ });
36
+ const cognitoSignOutBodySchema = z.object({ redirectTo: z.string().optional() });
37
+ const cognitoStartPasskeySignInBodySchema = z.object({ email: z.email() });
38
+ const cognitoCompletePasskeySignInBodySchema = z.object({
39
+ email: z.email(),
40
+ credential: z.record(z.string(), z.unknown()),
41
+ session: z.string().min(1),
42
+ rememberMe: z.boolean().optional()
43
+ });
44
+ const cognitoCompletePasskeyRegistrationBodySchema = z.object({ credential: z.record(z.string(), z.unknown()) });
45
+ const cognitoUpdateUserAttributesBodySchema = z.object({ attributes: z.array(z.object({
46
+ Name: z.string().min(1),
47
+ Value: z.string()
48
+ })).min(1) });
49
+ const COGNITO_COOKIE = "cognito_tokens";
50
+ const COGNITO_COOKIE_DEFAULT_MAX_AGE = 3600;
51
+ const COGNITO_COOKIE_SUBJECT = "better-auth-cognito";
52
+ function isStatefulAuth(ctx) {
53
+ return !!ctx.context.options.database || !!ctx.context.options.secondaryStorage;
54
+ }
55
+ async function writeCognitoTokenCookie(ctx, payload) {
56
+ const cookie = ctx.context.createAuthCookie(COGNITO_COOKIE, { maxAge: COGNITO_COOKIE_DEFAULT_MAX_AGE });
57
+ const maxAge = typeof cookie.attributes.maxAge === "number" ? cookie.attributes.maxAge : COGNITO_COOKIE_DEFAULT_MAX_AGE;
58
+ const encoded = await symmetricEncodeJWT(payload, ctx.context.secretConfig, COGNITO_COOKIE_SUBJECT, maxAge);
59
+ ctx.setCookie(cookie.name, encoded, cookie.attributes);
60
+ }
61
+ async function readCognitoTokenCookie(ctx, userId) {
62
+ const cookie = ctx.context.createAuthCookie(COGNITO_COOKIE);
63
+ const raw = ctx.getCookie(cookie.name);
64
+ if (!raw) return null;
65
+ try {
66
+ const data = await symmetricDecodeJWT(raw, ctx.context.secretConfig, COGNITO_COOKIE_SUBJECT);
67
+ if (data?.userId === userId) return data;
68
+ } catch {}
69
+ return null;
70
+ }
71
+ function clearCognitoTokenCookie(ctx) {
72
+ const cognitoCookie = ctx.context.createAuthCookie(COGNITO_COOKIE);
73
+ ctx.setCookie(cognitoCookie.name, "", {
74
+ ...cognitoCookie.attributes,
75
+ maxAge: 0
76
+ });
77
+ }
78
+ async function getCognitoTokens(ctx, userId) {
79
+ if (!isStatefulAuth(ctx)) {
80
+ const fromCookie = await readCognitoTokenCookie(ctx, userId);
81
+ if (fromCookie) return {
82
+ accessToken: fromCookie.accessToken,
83
+ idToken: fromCookie.idToken
84
+ };
85
+ }
86
+ const account = (await ctx.context.internalAdapter.findAccounts(userId).catch(() => [])).find((a) => a.providerId === "cognito");
87
+ if (account) return {
88
+ accessToken: account.accessToken ?? null,
89
+ idToken: account.idToken ?? null
90
+ };
91
+ if (isStatefulAuth(ctx)) {
92
+ const fromCookie = await readCognitoTokenCookie(ctx, userId);
93
+ if (fromCookie) return {
94
+ accessToken: fromCookie.accessToken,
95
+ idToken: fromCookie.idToken
96
+ };
97
+ }
98
+ return {
99
+ accessToken: null,
100
+ idToken: null
101
+ };
102
+ }
103
+ async function getCognitoAccessToken(ctx, userId) {
104
+ const { accessToken } = await getCognitoTokens(ctx, userId);
105
+ return accessToken;
106
+ }
107
+ async function upsertCognitoAccount(ctx, userId, accountId, tokenData) {
108
+ const accountFields = {
109
+ providerId: "cognito",
110
+ accountId,
111
+ userId,
112
+ accessToken: tokenData.accessToken,
113
+ refreshToken: tokenData.refreshToken ?? null,
114
+ idToken: tokenData.idToken,
115
+ accessTokenExpiresAt: tokenData.accessTokenExpiresAt,
116
+ scope: "openid profile email"
117
+ };
118
+ if (!isStatefulAuth(ctx)) {
119
+ await writeCognitoTokenCookie(ctx, {
120
+ userId,
121
+ accessToken: tokenData.accessToken ?? null,
122
+ idToken: tokenData.idToken
123
+ });
124
+ return;
125
+ }
126
+ const existing = await ctx.context.internalAdapter.findAccounts(userId).then((accounts) => accounts.find((a) => a.providerId === "cognito")).catch(() => null);
127
+ if (existing) await ctx.context.internalAdapter.updateAccount(existing.id, accountFields);
128
+ else await ctx.context.internalAdapter.createAccount(accountFields);
129
+ }
130
+ function cognitoGetTokensEndpoint(pCtx) {
131
+ return createAuthEndpoint("/cognito/tokens", { method: "GET" }, async (ctx) => {
132
+ const activeSession = await getSessionFromCtx(ctx);
133
+ if (!activeSession) throw new APIError("UNAUTHORIZED");
134
+ const { accessToken, idToken } = await getCognitoTokens(ctx, activeSession.user.id);
135
+ return ctx.json({
136
+ accessToken,
137
+ idToken
138
+ });
139
+ });
140
+ }
141
+ function cognitoSignUpEndpoint(pCtx) {
142
+ return createAuthEndpoint("/cognito/sign-up", {
143
+ method: "POST",
144
+ body: cognitoSignUpBodySchema
145
+ }, async (ctx) => {
146
+ const { email, password } = ctx.body;
147
+ try {
148
+ const result = await pCtx.client.send(new SignUpCommand({
149
+ ClientId: pCtx.clientId,
150
+ Username: email,
151
+ Password: password,
152
+ UserAttributes: [{
153
+ Name: "email",
154
+ Value: email
155
+ }]
156
+ }));
157
+ return ctx.json({
158
+ userConfirmed: result.UserConfirmed,
159
+ userSub: result.UserSub,
160
+ codeDeliveryDetails: result.CodeDeliveryDetails ?? null
161
+ });
162
+ } catch (err) {
163
+ if (err instanceof UsernameExistsException) throw new APIError("CONFLICT", { message: COGNITO_ERROR_CODES.ACCOUNT_ALREADY_EXISTS });
164
+ if (err instanceof LimitExceededException) return ctx.json({
165
+ userConfirmed: false,
166
+ userSub: email,
167
+ codeDeliveryDetails: null
168
+ });
169
+ if (err instanceof InvalidPasswordException || err instanceof InvalidParameterException || err instanceof UserLambdaValidationException) throw new APIError("BAD_REQUEST", { message: err.message ?? COGNITO_ERROR_CODES.SIGN_UP_FAILED });
170
+ ctx.context.logger.error("cognito/sign-up: unexpected Cognito error", err);
171
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.SIGN_UP_FAILED });
172
+ }
173
+ });
174
+ }
175
+ function cognitoConfirmSignUpEndpoint(pCtx) {
176
+ return createAuthEndpoint("/cognito/confirm-sign-up", {
177
+ method: "POST",
178
+ body: cognitoConfirmSignUpBodySchema
179
+ }, async (ctx) => {
180
+ const { email, code } = ctx.body;
181
+ try {
182
+ await pCtx.client.send(new ConfirmSignUpCommand({
183
+ ClientId: pCtx.clientId,
184
+ Username: email,
185
+ ConfirmationCode: code
186
+ }));
187
+ return ctx.json({ confirmed: true });
188
+ } catch (err) {
189
+ if (err instanceof CodeMismatchException) throw new APIError("BAD_REQUEST", { message: COGNITO_ERROR_CODES.CONFIRMATION_CODE_MISMATCH });
190
+ if (err instanceof ExpiredCodeException) throw new APIError("BAD_REQUEST", { message: COGNITO_ERROR_CODES.CONFIRMATION_CODE_EXPIRED });
191
+ if (err instanceof UserNotFoundException) throw new APIError("NOT_FOUND", { message: COGNITO_ERROR_CODES.USER_NOT_FOUND });
192
+ if (err instanceof AliasExistsException || err instanceof InvalidParameterException || err instanceof NotAuthorizedException) throw new APIError("BAD_REQUEST", { message: err.message ?? COGNITO_ERROR_CODES.CONFIRMATION_FAILED });
193
+ ctx.context.logger.error("cognito/confirm-sign-up: unexpected Cognito error", err);
194
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.CONFIRMATION_FAILED });
195
+ }
196
+ });
197
+ }
198
+ function cognitoResendConfirmationCodeEndpoint(pCtx) {
199
+ return createAuthEndpoint("/cognito/resend-confirmation", {
200
+ method: "POST",
201
+ body: cognitoResendConfirmationCodeBodySchema
202
+ }, async (ctx) => {
203
+ const { email } = ctx.body;
204
+ try {
205
+ const result = await pCtx.client.send(new ResendConfirmationCodeCommand({
206
+ ClientId: pCtx.clientId,
207
+ Username: email
208
+ }));
209
+ return ctx.json({ codeDeliveryDetails: result.CodeDeliveryDetails ?? null });
210
+ } catch (err) {
211
+ if (err instanceof UserNotFoundException) throw new APIError("NOT_FOUND", { message: COGNITO_ERROR_CODES.USER_NOT_FOUND });
212
+ if (err instanceof InvalidParameterException || err instanceof LimitExceededException || err instanceof NotAuthorizedException) throw new APIError("BAD_REQUEST", { message: err.message ?? COGNITO_ERROR_CODES.RESEND_CONFIRMATION_FAILED });
213
+ ctx.context.logger.error("cognito/resend-confirmation: unexpected Cognito error", err);
214
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.RESEND_CONFIRMATION_FAILED });
215
+ }
216
+ });
217
+ }
218
+ function cognitoForgotPasswordEndpoint(pCtx) {
219
+ return createAuthEndpoint("/cognito/forgot-password", {
220
+ method: "POST",
221
+ body: cognitoForgotPasswordBodySchema
222
+ }, async (ctx) => {
223
+ const { email } = ctx.body;
224
+ try {
225
+ const result = await pCtx.client.send(new ForgotPasswordCommand({
226
+ ClientId: pCtx.clientId,
227
+ Username: email
228
+ }));
229
+ return ctx.json({ codeDeliveryDetails: result.CodeDeliveryDetails ?? null });
230
+ } catch (err) {
231
+ if (err instanceof UserNotFoundException) throw new APIError("NOT_FOUND", { message: COGNITO_ERROR_CODES.USER_NOT_FOUND });
232
+ if (err instanceof InvalidParameterException || err instanceof LimitExceededException || err instanceof NotAuthorizedException || err instanceof UserLambdaValidationException) throw new APIError("BAD_REQUEST", { message: err.message ?? COGNITO_ERROR_CODES.FORGOT_PASSWORD_FAILED });
233
+ ctx.context.logger.error("cognito/forgot-password: unexpected Cognito error", err);
234
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.FORGOT_PASSWORD_FAILED });
235
+ }
236
+ });
237
+ }
238
+ function cognitoConfirmForgotPasswordEndpoint(pCtx) {
239
+ return createAuthEndpoint("/cognito/confirm-forgot-password", {
240
+ method: "POST",
241
+ body: cognitoConfirmForgotPasswordBodySchema
242
+ }, async (ctx) => {
243
+ const { email, code, newPassword } = ctx.body;
244
+ try {
245
+ await pCtx.client.send(new ConfirmForgotPasswordCommand({
246
+ ClientId: pCtx.clientId,
247
+ Username: email,
248
+ ConfirmationCode: code,
249
+ Password: newPassword
250
+ }));
251
+ return ctx.json({ reset: true });
252
+ } catch (err) {
253
+ if (err instanceof CodeMismatchException) throw new APIError("BAD_REQUEST", { message: COGNITO_ERROR_CODES.CONFIRMATION_CODE_MISMATCH });
254
+ if (err instanceof ExpiredCodeException) throw new APIError("BAD_REQUEST", { message: COGNITO_ERROR_CODES.CONFIRMATION_CODE_EXPIRED });
255
+ if (err instanceof UserNotFoundException) throw new APIError("NOT_FOUND", { message: COGNITO_ERROR_CODES.USER_NOT_FOUND });
256
+ if (err instanceof InvalidPasswordException || err instanceof InvalidParameterException || err instanceof LimitExceededException || err instanceof NotAuthorizedException || err instanceof UserLambdaValidationException) throw new APIError("BAD_REQUEST", { message: err.message ?? COGNITO_ERROR_CODES.FORGOT_PASSWORD_CONFIRM_FAILED });
257
+ ctx.context.logger.error("cognito/confirm-forgot-password: unexpected Cognito error", err);
258
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.FORGOT_PASSWORD_CONFIRM_FAILED });
259
+ }
260
+ });
261
+ }
262
+ function cognitoSignInEndpoint(pCtx) {
263
+ return createAuthEndpoint("/cognito/sign-in", {
264
+ method: "POST",
265
+ body: cognitoSignInBodySchema
266
+ }, async (ctx) => {
267
+ const { email, password, rememberMe } = ctx.body;
268
+ let authResult;
269
+ try {
270
+ authResult = await pCtx.client.send(new InitiateAuthCommand({
271
+ AuthFlow: "USER_PASSWORD_AUTH",
272
+ ClientId: pCtx.clientId,
273
+ AuthParameters: {
274
+ USERNAME: email,
275
+ PASSWORD: password
276
+ }
277
+ }));
278
+ } catch (err) {
279
+ if (err instanceof NotAuthorizedException || err instanceof UserNotFoundException) throw new APIError("UNAUTHORIZED", { message: COGNITO_ERROR_CODES.INVALID_CREDENTIALS });
280
+ if (err instanceof UserNotConfirmedException) throw new APIError("FORBIDDEN", { message: COGNITO_ERROR_CODES.USER_NOT_CONFIRMED });
281
+ ctx.context.logger.error("cognito/sign-in: unexpected Cognito auth error", err);
282
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.AUTH_SERVICE_ERROR });
283
+ }
284
+ if (authResult.ChallengeName === "NEW_PASSWORD_REQUIRED") return ctx.json({
285
+ type: "NEW_PASSWORD_REQUIRED",
286
+ challengeSession: authResult.Session
287
+ });
288
+ const tokens = authResult.AuthenticationResult;
289
+ if (!tokens?.IdToken) {
290
+ ctx.context.logger.error("cognito/sign-in: no IdToken in Cognito response");
291
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.NO_TOKEN_FROM_COGNITO });
292
+ }
293
+ let claims;
294
+ try {
295
+ claims = await pCtx.verifyToken(tokens.IdToken);
296
+ } catch (err) {
297
+ ctx.context.logger.error("cognito/sign-in: token verification failed", err);
298
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.TOKEN_VERIFICATION_FAILED });
299
+ }
300
+ const existing = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: false }).catch(() => null);
301
+ const user = existing ? await ctx.context.internalAdapter.updateUser(existing.user.id, {
302
+ name: claims.name,
303
+ emailVerified: true
304
+ }) : await ctx.context.internalAdapter.createUser({
305
+ email,
306
+ name: claims.name,
307
+ emailVerified: true,
308
+ id: claims.sub
309
+ });
310
+ await setSessionCookie(ctx, {
311
+ session: await ctx.context.internalAdapter.createSession(user.id, rememberMe === false),
312
+ user
313
+ }, rememberMe === false);
314
+ await upsertCognitoAccount(ctx, user.id, claims.sub, {
315
+ accessToken: tokens.AccessToken,
316
+ refreshToken: tokens.RefreshToken,
317
+ idToken: tokens.IdToken,
318
+ accessTokenExpiresAt: new Date(Date.now() + 3600 * 1e3)
319
+ });
320
+ return ctx.json({ ok: true });
321
+ });
322
+ }
323
+ function cognitoNewPasswordEndpoint(pCtx) {
324
+ return createAuthEndpoint("/cognito/new-password", {
325
+ method: "POST",
326
+ body: cognitoNewPasswordBodySchema
327
+ }, async (ctx) => {
328
+ const { email, newPassword, challengeSession, rememberMe } = ctx.body;
329
+ let tokens;
330
+ try {
331
+ tokens = (await pCtx.client.send(new RespondToAuthChallengeCommand({
332
+ ClientId: pCtx.clientId,
333
+ ChallengeName: "NEW_PASSWORD_REQUIRED",
334
+ Session: challengeSession,
335
+ ChallengeResponses: {
336
+ USERNAME: email,
337
+ NEW_PASSWORD: newPassword
338
+ }
339
+ }))).AuthenticationResult;
340
+ } catch (err) {
341
+ throw new APIError("BAD_REQUEST", { message: err instanceof Error ? err.message : "Failed to set new password" });
342
+ }
343
+ if (!tokens?.IdToken) {
344
+ ctx.context.logger.error("cognito/new-password: no IdToken in Cognito response");
345
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.NO_TOKEN_FROM_COGNITO });
346
+ }
347
+ let claims;
348
+ try {
349
+ claims = await pCtx.verifyToken(tokens.IdToken);
350
+ } catch (err) {
351
+ ctx.context.logger.error("cognito/new-password: token verification failed", err);
352
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.TOKEN_VERIFICATION_FAILED });
353
+ }
354
+ const existing = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: false }).catch(() => null);
355
+ const user = existing ? await ctx.context.internalAdapter.updateUser(existing.user.id, {
356
+ name: claims.name,
357
+ emailVerified: true
358
+ }) : await ctx.context.internalAdapter.createUser({
359
+ email,
360
+ name: claims.name,
361
+ emailVerified: true,
362
+ id: claims.sub
363
+ });
364
+ await setSessionCookie(ctx, {
365
+ session: await ctx.context.internalAdapter.createSession(user.id, rememberMe === false),
366
+ user
367
+ }, rememberMe === false);
368
+ await upsertCognitoAccount(ctx, user.id, claims.sub, {
369
+ accessToken: tokens.AccessToken,
370
+ refreshToken: tokens.RefreshToken,
371
+ idToken: tokens.IdToken,
372
+ accessTokenExpiresAt: new Date(Date.now() + 3600 * 1e3)
373
+ });
374
+ return ctx.json({ ok: true });
375
+ });
376
+ }
377
+ function cognitoStartPasskeySignInEndpoint(pCtx) {
378
+ return createAuthEndpoint("/cognito/passkey/sign-in/start", {
379
+ method: "POST",
380
+ body: cognitoStartPasskeySignInBodySchema
381
+ }, async (ctx) => {
382
+ const { email } = ctx.body;
383
+ let result;
384
+ try {
385
+ result = await pCtx.client.send(new InitiateAuthCommand({
386
+ AuthFlow: "USER_AUTH",
387
+ ClientId: pCtx.clientId,
388
+ AuthParameters: {
389
+ USERNAME: email,
390
+ PREFERRED_CHALLENGE: "WEB_AUTHN"
391
+ }
392
+ }));
393
+ } catch (err) {
394
+ if (err instanceof UserNotFoundException || err instanceof NotAuthorizedException) throw new APIError("UNAUTHORIZED", { message: COGNITO_ERROR_CODES.USER_NOT_FOUND });
395
+ if (err instanceof WebAuthnNotEnabledException || err instanceof WebAuthnConfigurationMissingException) throw new APIError("BAD_REQUEST", { message: COGNITO_ERROR_CODES.PASSKEYS_NOT_ENABLED });
396
+ ctx.context.logger.error("cognito/passkey/sign-in/start: unexpected Cognito error", err);
397
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.AUTH_SERVICE_ERROR });
398
+ }
399
+ if (result.ChallengeName === "SELECT_CHALLENGE") {
400
+ if (!(result.AvailableChallenges ?? []).includes("WEB_AUTHN")) throw new APIError("BAD_REQUEST", { message: COGNITO_ERROR_CODES.NO_PASSKEYS_REGISTERED });
401
+ try {
402
+ result = await pCtx.client.send(new RespondToAuthChallengeCommand({
403
+ ClientId: pCtx.clientId,
404
+ ChallengeName: "SELECT_CHALLENGE",
405
+ Session: result.Session,
406
+ ChallengeResponses: {
407
+ USERNAME: email,
408
+ ANSWER: "WEB_AUTHN"
409
+ }
410
+ }));
411
+ } catch (err) {
412
+ ctx.context.logger.error("cognito/passkey/sign-in/start: SELECT_CHALLENGE respond failed", err);
413
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.AUTH_SERVICE_ERROR });
414
+ }
415
+ }
416
+ if (result.ChallengeName !== "WEB_AUTHN") throw new APIError("BAD_REQUEST", { message: `Passkey sign-in is not available (challenge: ${result.ChallengeName ?? "none"})` });
417
+ const rawOptions = result.ChallengeParameters?.CREDENTIAL_REQUEST_OPTIONS ?? result.ChallengeParameters?.PASSKEY_REQUEST_OPTIONS ?? Object.values(result.ChallengeParameters ?? {}).find((v) => typeof v === "string" && v.trimStart().startsWith("{"));
418
+ if (!rawOptions) {
419
+ ctx.context.logger.error(`cognito/passkey/sign-in/start: no credential request options. Keys: ${Object.keys(result.ChallengeParameters ?? {}).join(", ")}`);
420
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: `${COGNITO_ERROR_CODES.PASSKEY_NO_OPTIONS_RETURNED}. Keys: ${Object.keys(result.ChallengeParameters ?? {}).join(", ")}` });
421
+ }
422
+ return ctx.json({
423
+ session: result.Session,
424
+ requestOptions: JSON.parse(rawOptions)
425
+ });
426
+ });
427
+ }
428
+ function cognitoCompletePasskeySignInEndpoint(pCtx) {
429
+ return createAuthEndpoint("/cognito/passkey/sign-in/complete", {
430
+ method: "POST",
431
+ body: cognitoCompletePasskeySignInBodySchema
432
+ }, async (ctx) => {
433
+ const { email, credential, session, rememberMe } = ctx.body;
434
+ let result;
435
+ try {
436
+ result = await pCtx.client.send(new RespondToAuthChallengeCommand({
437
+ ClientId: pCtx.clientId,
438
+ ChallengeName: "WEB_AUTHN",
439
+ Session: session,
440
+ ChallengeResponses: {
441
+ USERNAME: email,
442
+ CREDENTIAL: JSON.stringify(credential)
443
+ }
444
+ }));
445
+ } catch (err) {
446
+ if (err instanceof NotAuthorizedException || err instanceof WebAuthnChallengeNotFoundException || err instanceof WebAuthnRelyingPartyMismatchException) throw new APIError("UNAUTHORIZED", { message: COGNITO_ERROR_CODES.PASSKEY_VERIFICATION_FAILED });
447
+ ctx.context.logger.error("cognito/passkey/sign-in/complete: unexpected Cognito error", err);
448
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.AUTH_SERVICE_ERROR });
449
+ }
450
+ const tokens = result.AuthenticationResult;
451
+ if (!tokens?.IdToken) {
452
+ ctx.context.logger.error("cognito/passkey/sign-in/complete: no IdToken in Cognito response");
453
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.NO_TOKEN_FROM_COGNITO });
454
+ }
455
+ let claims;
456
+ try {
457
+ claims = await pCtx.verifyToken(tokens.IdToken);
458
+ } catch (err) {
459
+ ctx.context.logger.error("cognito/passkey/sign-in/complete: token verification failed", err);
460
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.TOKEN_VERIFICATION_FAILED });
461
+ }
462
+ const existing = await ctx.context.internalAdapter.findUserByEmail(email, { includeAccounts: false }).catch(() => null);
463
+ const user = existing ? await ctx.context.internalAdapter.updateUser(existing.user.id, {
464
+ name: claims.name,
465
+ emailVerified: true
466
+ }) : await ctx.context.internalAdapter.createUser({
467
+ email,
468
+ name: claims.name,
469
+ emailVerified: true,
470
+ id: claims.sub
471
+ });
472
+ const sessionRecord = await ctx.context.internalAdapter.createSession(user.id, rememberMe === false);
473
+ await setSessionCookie(ctx, {
474
+ session: sessionRecord,
475
+ user
476
+ }, rememberMe === false);
477
+ await upsertCognitoAccount(ctx, user.id, claims.sub, {
478
+ accessToken: tokens.AccessToken,
479
+ refreshToken: tokens.RefreshToken,
480
+ idToken: tokens.IdToken,
481
+ accessTokenExpiresAt: new Date(Date.now() + 3600 * 1e3)
482
+ });
483
+ return ctx.json({
484
+ redirect: false,
485
+ token: sessionRecord.token,
486
+ user
487
+ });
488
+ });
489
+ }
490
+ function cognitoStartPasskeyRegistrationEndpoint(pCtx) {
491
+ return createAuthEndpoint("/cognito/passkey/register/start", {
492
+ method: "POST",
493
+ body: z.object({})
494
+ }, async (ctx) => {
495
+ if (!await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret)) throw new APIError("UNAUTHORIZED");
496
+ const activeSession = await getSessionFromCtx(ctx);
497
+ if (!activeSession) throw new APIError("UNAUTHORIZED");
498
+ const accessToken = await getCognitoAccessToken(ctx, activeSession.user.id);
499
+ if (!accessToken) throw new APIError("UNAUTHORIZED", { message: COGNITO_ERROR_CODES.NO_COGNITO_ACCESS_TOKEN });
500
+ let result;
501
+ try {
502
+ result = await pCtx.client.send(new StartWebAuthnRegistrationCommand({ AccessToken: accessToken }));
503
+ } catch (err) {
504
+ if (err instanceof WebAuthnNotEnabledException || err instanceof WebAuthnConfigurationMissingException) throw new APIError("BAD_REQUEST", { message: COGNITO_ERROR_CODES.PASSKEYS_NOT_ENABLED });
505
+ ctx.context.logger.error("cognito/passkey/register/start: unexpected Cognito error", err);
506
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.PASSKEY_REGISTRATION_START_FAILED });
507
+ }
508
+ return ctx.json({ credentialCreationOptions: result.CredentialCreationOptions });
509
+ });
510
+ }
511
+ function cognitoCompletePasskeyRegistrationEndpoint(pCtx) {
512
+ return createAuthEndpoint("/cognito/passkey/register/complete", {
513
+ method: "POST",
514
+ body: cognitoCompletePasskeyRegistrationBodySchema
515
+ }, async (ctx) => {
516
+ const { credential } = ctx.body;
517
+ if (!await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret)) throw new APIError("UNAUTHORIZED");
518
+ const activeSession = await getSessionFromCtx(ctx);
519
+ if (!activeSession) throw new APIError("UNAUTHORIZED");
520
+ const accessToken = await getCognitoAccessToken(ctx, activeSession.user.id);
521
+ if (!accessToken) throw new APIError("UNAUTHORIZED", { message: COGNITO_ERROR_CODES.NO_COGNITO_ACCESS_TOKEN });
522
+ try {
523
+ await pCtx.client.send(new CompleteWebAuthnRegistrationCommand({
524
+ AccessToken: accessToken,
525
+ Credential: credential
526
+ }));
527
+ } catch (err) {
528
+ if (err instanceof WebAuthnOriginNotAllowedException) throw new APIError("BAD_REQUEST", { message: COGNITO_ERROR_CODES.PASSKEY_ORIGIN_NOT_ALLOWED });
529
+ if (err instanceof WebAuthnRelyingPartyMismatchException) throw new APIError("BAD_REQUEST", { message: COGNITO_ERROR_CODES.RELYING_PARTY_MISMATCH });
530
+ ctx.context.logger.error("cognito/passkey/register/complete: unexpected Cognito error", err);
531
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.PASSKEY_REGISTRATION_FAILED });
532
+ }
533
+ return ctx.json({ ok: true });
534
+ });
535
+ }
536
+ function cognitoGetUserAttributesEndpoint(pCtx) {
537
+ return createAuthEndpoint("/cognito/user-attributes", { method: "GET" }, async (ctx) => {
538
+ const activeSession = await getSessionFromCtx(ctx);
539
+ if (!activeSession) throw new APIError("UNAUTHORIZED");
540
+ const accessToken = await getCognitoAccessToken(ctx, activeSession.user.id);
541
+ if (!accessToken) throw new APIError("UNAUTHORIZED", { message: COGNITO_ERROR_CODES.NO_COGNITO_ACCESS_TOKEN });
542
+ try {
543
+ const result = await pCtx.client.send(new GetUserCommand({ AccessToken: accessToken }));
544
+ return ctx.json({ attributes: result.UserAttributes ?? [] });
545
+ } catch (err) {
546
+ if (err instanceof NotAuthorizedException) throw new APIError("UNAUTHORIZED", { message: COGNITO_ERROR_CODES.NO_COGNITO_ACCESS_TOKEN });
547
+ ctx.context.logger.error("cognito/user-attributes GET: unexpected Cognito error", err);
548
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.GET_USER_ATTRIBUTES_FAILED });
549
+ }
550
+ });
551
+ }
552
+ function cognitoUpdateUserAttributesEndpoint(pCtx) {
553
+ return createAuthEndpoint("/cognito/user-attributes", {
554
+ method: "POST",
555
+ body: cognitoUpdateUserAttributesBodySchema
556
+ }, async (ctx) => {
557
+ const activeSession = await getSessionFromCtx(ctx);
558
+ if (!activeSession) throw new APIError("UNAUTHORIZED");
559
+ const accessToken = await getCognitoAccessToken(ctx, activeSession.user.id);
560
+ if (!accessToken) throw new APIError("UNAUTHORIZED", { message: COGNITO_ERROR_CODES.NO_COGNITO_ACCESS_TOKEN });
561
+ try {
562
+ await pCtx.client.send(new UpdateUserAttributesCommand({
563
+ AccessToken: accessToken,
564
+ UserAttributes: ctx.body.attributes
565
+ }));
566
+ const nameAttr = ctx.body.attributes.find((a) => a.Name === "name");
567
+ if (nameAttr && isStatefulAuth(ctx)) await ctx.context.internalAdapter.updateUser(activeSession.user.id, { name: nameAttr.Value }).catch((err) => {
568
+ ctx.context.logger.error("cognito/user-attributes POST: failed to sync name to DB", err);
569
+ });
570
+ return ctx.json({ updated: true });
571
+ } catch (err) {
572
+ if (err instanceof NotAuthorizedException) throw new APIError("UNAUTHORIZED", { message: COGNITO_ERROR_CODES.NO_COGNITO_ACCESS_TOKEN });
573
+ if (err instanceof InvalidParameterException || err instanceof AliasExistsException) throw new APIError("BAD_REQUEST", { message: err.message ?? COGNITO_ERROR_CODES.UPDATE_USER_ATTRIBUTES_FAILED });
574
+ ctx.context.logger.error("cognito/user-attributes POST: unexpected Cognito error", err);
575
+ throw new APIError("INTERNAL_SERVER_ERROR", { message: COGNITO_ERROR_CODES.UPDATE_USER_ATTRIBUTES_FAILED });
576
+ }
577
+ });
578
+ }
579
+ function cognitoSignOutEndpoint(pCtx) {
580
+ return createAuthEndpoint("/cognito/sign-out", {
581
+ method: "POST",
582
+ body: cognitoSignOutBodySchema.optional()
583
+ }, async (ctx) => {
584
+ const sessionCookieToken = await ctx.getSignedCookie(ctx.context.authCookies.sessionToken.name, ctx.context.secret);
585
+ if (!sessionCookieToken) {
586
+ deleteSessionCookie(ctx);
587
+ return ctx.json({ success: true });
588
+ }
589
+ const activeSession = await getSessionFromCtx(ctx);
590
+ const accessToken = activeSession ? await getCognitoAccessToken(ctx, activeSession.user.id) : null;
591
+ if (accessToken) try {
592
+ await pCtx.client.send(new GlobalSignOutCommand({ AccessToken: accessToken }));
593
+ } catch (error) {
594
+ ctx.context.logger.error("Failed to sign out from Cognito", error);
595
+ }
596
+ try {
597
+ await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
598
+ } catch (error) {
599
+ ctx.context.logger.error("Failed to delete session from database", error);
600
+ }
601
+ deleteSessionCookie(ctx);
602
+ clearCognitoTokenCookie(ctx);
603
+ return ctx.json({ success: true });
604
+ });
605
+ }
606
+ //#endregion
607
+ //#region src/index.ts
608
+ /**
609
+ * Better Auth server plugin for AWS Cognito.
610
+ *
611
+ * Registers endpoints:
612
+ * GET /cognito/tokens
613
+ * POST /cognito/sign-up
614
+ * POST /cognito/confirm-sign-up
615
+ * POST /cognito/resend-confirmation
616
+ * POST /cognito/forgot-password
617
+ * POST /cognito/confirm-forgot-password
618
+ * POST /cognito/sign-in
619
+ * POST /cognito/new-password
620
+ * POST /cognito/sign-out
621
+ * POST /cognito/passkey/sign-in/start
622
+ * POST /cognito/passkey/sign-in/complete
623
+ * POST /cognito/passkey/register/start
624
+ * POST /cognito/passkey/register/complete
625
+ */
626
+ const cognitoPlugin = (opts) => {
627
+ const client = new CognitoIdentityProviderClient({ region: opts.region });
628
+ const jwks = createRemoteJWKSet(new URL(`https://cognito-idp.${opts.region}.amazonaws.com/${opts.userPoolId}/.well-known/jwks.json`));
629
+ const issuer = `https://cognito-idp.${opts.region}.amazonaws.com/${opts.userPoolId}`;
630
+ const verifyToken = async (idToken) => {
631
+ const { payload } = await jwtVerify(idToken, jwks, {
632
+ issuer,
633
+ audience: opts.clientId
634
+ });
635
+ const profile = payload;
636
+ return {
637
+ ...profile,
638
+ name: `${profile.given_name ?? ""} ${profile.family_name ?? ""}`.trim() || profile.name || profile.email || "",
639
+ "cognito:groups": payload["cognito:groups"] ?? [],
640
+ idToken
641
+ };
642
+ };
643
+ const pCtx = {
644
+ client,
645
+ clientId: opts.clientId,
646
+ verifyToken
647
+ };
648
+ return {
649
+ id: "cognito",
650
+ endpoints: {
651
+ cognitoGetTokens: cognitoGetTokensEndpoint(pCtx),
652
+ cognitoGetUserAttributes: cognitoGetUserAttributesEndpoint(pCtx),
653
+ cognitoUpdateUserAttributes: cognitoUpdateUserAttributesEndpoint(pCtx),
654
+ cognitoSignUp: cognitoSignUpEndpoint(pCtx),
655
+ cognitoConfirmSignUp: cognitoConfirmSignUpEndpoint(pCtx),
656
+ cognitoResendConfirmationCode: cognitoResendConfirmationCodeEndpoint(pCtx),
657
+ cognitoForgotPassword: cognitoForgotPasswordEndpoint(pCtx),
658
+ cognitoConfirmForgotPassword: cognitoConfirmForgotPasswordEndpoint(pCtx),
659
+ cognitoSignIn: cognitoSignInEndpoint(pCtx),
660
+ cognitoNewPassword: cognitoNewPasswordEndpoint(pCtx),
661
+ cognitoStartPasskeySignIn: cognitoStartPasskeySignInEndpoint(pCtx),
662
+ cognitoCompletePasskeySignIn: cognitoCompletePasskeySignInEndpoint(pCtx),
663
+ cognitoStartPasskeyRegistration: cognitoStartPasskeyRegistrationEndpoint(pCtx),
664
+ cognitoCompletePasskeyRegistration: cognitoCompletePasskeyRegistrationEndpoint(pCtx),
665
+ cognitoSignOut: cognitoSignOutEndpoint(pCtx)
666
+ },
667
+ $ERROR_CODES: COGNITO_ERROR_CODES,
668
+ options: opts
669
+ };
670
+ };
671
+ /** Globally sign out from Cognito (invalidates the refresh token). Best-effort. */
672
+ async function cognitoGlobalSignOut(accessToken, opts) {
673
+ const client = new CognitoIdentityProviderClient({ region: opts.region });
674
+ try {
675
+ await client.send(new GlobalSignOutCommand({ AccessToken: accessToken }));
676
+ } catch {}
677
+ }
678
+ //#endregion
679
+ export { COGNITO_ERROR_CODES, cognitoGlobalSignOut, cognitoPlugin, getPasskeyConfig };