diff --git a/packages/core/src/@types/session.ts b/packages/core/src/@types/session.ts index 90a9b6dd..366d7e95 100644 --- a/packages/core/src/@types/session.ts +++ b/packages/core/src/@types/session.ts @@ -282,6 +282,11 @@ export interface SessionStrategy { * @unstable This API is experimental and may change in future releases. */ signUp(payload: Record, request: Request): Promise + /** + * Sign in a user with the given credentials and request. Returns the session token on success. + * @unstable This API is experimental and may change in future releases. + */ + signInCredentials(payload: Record, request: Request, redirectTo?: string): Promise signIn( oauth: string, request: Request, diff --git a/packages/core/src/api/signInCredentials.ts b/packages/core/src/api/signInCredentials.ts index 77efe971..8efdd99b 100644 --- a/packages/core/src/api/signInCredentials.ts +++ b/packages/core/src/api/signInCredentials.ts @@ -1,4 +1,5 @@ import { AuraAuthError } from "@/shared/errors.ts" +import { getErrorName } from "@/shared/utils.ts" import { HeadersBuilder } from "@aura-stack/router" import { secureApiHeaders } from "@/shared/headers.ts" import { createCSRF, hashPassword, verifyPassword } from "@/shared/crypto.ts" @@ -35,7 +36,7 @@ export const signInCredentials = async ({ if (!session) { throw new AuraAuthError({ code: "AUTH_CREDENTIALS_INVALID" }) } - const sessionToken = await sessionStrategy.createSession(session, request) + const sessionToken = await sessionStrategy.signInCredentials(session, request) const csrfToken = await createCSRF(ctx.jose) logger?.log("CREDENTIALS_SIGN_IN_SUCCESS") @@ -70,6 +71,9 @@ export const signInCredentials = async ({ "An error occurred during credentials sign-in.", 401 ) + const error_type = getErrorName(error) + const error_code = error instanceof AuraAuthError ? error.code : "UNKNOWN_ERROR" + const error_message = error instanceof Error ? error.message : String(error) const headers = new Headers(secureApiHeaders) const invalidCredentials: SignInCredentialsAPIReturn = { success: false, @@ -84,13 +88,23 @@ export const signInCredentials = async ({ if (error instanceof AuraAuthError && error.code === "AUTH_CREDENTIALS_INVALID") { logger?.log("INVALID_CREDENTIALS", { severity: "warning", - structuredData: { path: "/signIn/credentials" }, + structuredData: { + path: "/signIn/credentials", + error_type, + error_code, + error_message, + }, }) return invalidCredentials } logger?.log("CREDENTIALS_SIGN_IN_FAILED", { severity: "error", - structuredData: { path: "/signIn/credentials" }, + structuredData: { + path: "/signIn/credentials", + error_type, + error_code, + error_message, + }, }) return invalidCredentials } diff --git a/packages/core/src/session/stateful/index.ts b/packages/core/src/session/stateful/index.ts index 1ccb0d02..ce8cab0b 100644 --- a/packages/core/src/session/stateful/index.ts +++ b/packages/core/src/session/stateful/index.ts @@ -8,6 +8,7 @@ import { revokeSession } from "@/session/stateful/revokeSession.ts" import { destroySession } from "@/session/stateful/destroySession.ts" import { refreshSession } from "@/session/stateful/refreshSession.ts" import { refreshUserInfo } from "@/session/stateful/refreshUserInfo.ts" +import { signInCredentials } from "@/session/stateful/signInCredentials.ts" import { getProviderTokens } from "@/session/stateful/getProviderTokens.ts" import { isProviderConnected } from "@/session/stateful/isProviderConnected.ts" import type { SessionStrategy, User, InternalStatefulContext } from "@/@types/index.ts" @@ -16,17 +17,18 @@ export const createStatefulStrategy = ( ctx: InternalStatefulContext ): SessionStrategy => { return { - refreshUserInfo: refreshUserInfo(ctx), + signUp: signUp(ctx), + signIn: signIn(ctx), getSession: getSession(ctx), + revokeToken: revokeToken(ctx), + oauthCallback: oauthCallback(ctx), createSession: createSession(ctx), - refreshSession: refreshSession(ctx), revokeSession: revokeSession(ctx), - revokeToken: revokeToken(ctx), destroySession: destroySession(ctx), + refreshSession: refreshSession(ctx), + refreshUserInfo: refreshUserInfo(ctx), + signInCredentials: signInCredentials(ctx), getProviderTokens: getProviderTokens(ctx), isProviderConnected: isProviderConnected(ctx), - signIn: signIn(ctx), - oauthCallback: oauthCallback(ctx), - signUp: signUp(ctx), } } diff --git a/packages/core/src/session/stateful/signInCredentials.ts b/packages/core/src/session/stateful/signInCredentials.ts new file mode 100644 index 00000000..1fd2c1fe --- /dev/null +++ b/packages/core/src/session/stateful/signInCredentials.ts @@ -0,0 +1,96 @@ +import { AuraAuthError } from "@/shared/errors.ts" +import { createHash, createSecretValue } from "@/shared/crypto.ts" +import { createDevice as __createDevice } from "@/shared/utils/session-strategy.ts" +import type { InternalStatefulContext } from "@/@types/config.ts" + +export const signInCredentials = ({ ctx, cookies, cookieManager }: InternalStatefulContext) => { + const { logger, sessionConfig } = ctx + const createDevice = __createDevice({ ctx, cookies, cookieManager }) + + return async (payload: Record, request: Request): Promise => { + logger?.log("STATEFUL_SIGN_IN_CREDENTIALS_START", { + structuredData: { + strategy: "stateful", + operation: "signInCredentials", + }, + }) + + if (ctx.identity.skipValidation) { + logger?.log("IDENTITY_VALIDATION_DISABLED", { + structuredData: { + identity_validation_disabled: true, + }, + }) + } + + const validatedPayload = ctx.identity.skipValidation ? payload : await ctx.identity.schemaRegistry.parse(payload) + logger?.log("STATEFUL_PAYLOAD_VALIDATION", { + structuredData: { + validation_skipped: ctx.identity.skipValidation || false, + has_email: Boolean(validatedPayload?.email) || false, + }, + }) + + const { sub } = validatedPayload + + const user = await sessionConfig.adapter.getUserById(sub) + if (!user) { + throw new AuraAuthError({ code: "AUTH_CREDENTIALS_INVALID" }) + } + + const device = await createDevice(user.id, request) + + const secretValue = createSecretValue(64) + logger?.log("STATEFUL_TOKEN_GENERATED", { + structuredData: { + token_length: secretValue.length, + }, + }) + + const tokenHash = await createHash(secretValue) + logger?.log("STATEFUL_TOKEN_HASHED", { + structuredData: { + hash_length: tokenHash.length, + }, + }) + + const expiresAt = new Date(Date.now() + 60 * 60 * 24 * 15 * 1000) + logger?.log("STATEFUL_SESSION_EXPIRATION_SET", { + structuredData: { + expires_at: expiresAt?.toISOString(), + max_age_days: 15, + }, + }) + + const dbSession = await sessionConfig.adapter.createSession({ + id: createSecretValue(32), + userId: user.id, + deviceId: device.id, + authenticatedWith: "credentials", + status: "active", + mfaState: "none", + tokenHash, + expiresAt, + metadata: null, + }) + + logger?.log("STATEFUL_SESSION_CREATED", { + structuredData: { + session_id: dbSession.id, + user_id: dbSession.userId, + status: dbSession.status, + expires_at: dbSession?.expiresAt?.toISOString(), + }, + }) + + logger?.log("STATEFUL_SIGN_IN_CREDENTIALS_SUCCESS", { + structuredData: { + session_id: dbSession.id, + user_id: dbSession.userId, + token_returned: true, + }, + }) + + return tokenHash + } +} diff --git a/packages/core/src/session/stateless/index.ts b/packages/core/src/session/stateless/index.ts index f680701b..44895b58 100644 --- a/packages/core/src/session/stateless/index.ts +++ b/packages/core/src/session/stateless/index.ts @@ -7,6 +7,7 @@ import { createSession } from "@/session/stateless/createSession.ts" import { destroySession } from "@/session/stateless/destroySession.ts" import { refreshSession } from "@/session/stateless/refreshSession.ts" import { refreshUserInfo } from "@/session/stateless/refreshUserInfo.ts" +import { signInCredentials } from "@/session/stateless/signInCredentials.ts" import { getProviderTokens } from "@/session/stateless/getProviderTokens.ts" import { isProviderConnected } from "@/session/stateless/isProviderConnected.ts" import type { SessionStrategy, User, InternalStatelessContext } from "@/@types/index.ts" @@ -21,17 +22,18 @@ export const createStatelessStrategy = ( } return { + revokeSession, + signIn: signIn(ctx), + signUp: signUp(ctx), getSession: getSession(ctx), + revokeToken: revokeToken(ctx), createSession: createSession(ctx), - getProviderTokens: getProviderTokens(ctx), + oauthCallback: oauthCallback(ctx), refreshSession: refreshSession(ctx), - revokeSession, - revokeToken: revokeToken(ctx), - isProviderConnected: isProviderConnected(ctx), - refreshUserInfo: refreshUserInfo(ctx), destroySession: destroySession(ctx), - signIn: signIn(ctx), - oauthCallback: oauthCallback(ctx), - signUp: signUp(ctx), + refreshUserInfo: refreshUserInfo(ctx), + getProviderTokens: getProviderTokens(ctx), + signInCredentials: signInCredentials(ctx), + isProviderConnected: isProviderConnected(ctx), } } diff --git a/packages/core/src/session/stateless/signInCredentials.ts b/packages/core/src/session/stateless/signInCredentials.ts new file mode 100644 index 00000000..574947fa --- /dev/null +++ b/packages/core/src/session/stateless/signInCredentials.ts @@ -0,0 +1,10 @@ +import { createSession as __createSession } from "@/session/stateless/createSession.ts" +import type { InternalStatelessContext, TypedJWTPayload, User } from "@/@types/index.ts" + +export const signInCredentials = (ctx: InternalStatelessContext) => { + const createSession = __createSession(ctx) + + return async (payload: Record, _request: Request): Promise => { + return await createSession(payload as TypedJWTPayload) + } +} diff --git a/packages/core/src/shared/logger.ts b/packages/core/src/shared/logger.ts index 73b9aaad..820858af 100644 --- a/packages/core/src/shared/logger.ts +++ b/packages/core/src/shared/logger.ts @@ -837,6 +837,18 @@ export const logMessages = { msgId: "SIGN_UP_ERROR", message: "Error occurred during user sign-up process", }, + STATEFUL_SIGN_IN_CREDENTIALS_START: { + facility: 4, + severity: "debug", + msgId: "STATEFUL_SIGN_IN_CREDENTIALS_START", + message: "Starting stateful sign-in with credentials", + }, + STATEFUL_SIGN_IN_CREDENTIALS_SUCCESS: { + facility: 4, + severity: "info", + msgId: "STATEFUL_SIGN_IN_CREDENTIALS_SUCCESS", + message: "Stateful sign-in with credentials completed successfully", + }, } as const export const createLogEntry = (key: T, overrides?: Partial): SyslogOptions => { diff --git a/packages/core/test/actions/signIn/signInCredentials/stateful.test.ts b/packages/core/test/actions/signIn/signInCredentials/stateful.test.ts index a9020fa1..7d8b7d0b 100644 --- a/packages/core/test/actions/signIn/signInCredentials/stateful.test.ts +++ b/packages/core/test/actions/signIn/signInCredentials/stateful.test.ts @@ -20,14 +20,12 @@ describe("signInCredentials action", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const { handlers } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createSession: createSessionMock, @@ -57,13 +55,6 @@ describe("signInCredentials action", async () => { email: "johndoe@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "johndoe@example.com", - name: "johndoe", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), @@ -192,14 +183,12 @@ describe("signInCredentials action", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const { handlers } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createSession: createSessionMock, @@ -231,13 +220,6 @@ describe("signInCredentials action", async () => { email: "alice@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "alice@example.com", - name: "alice", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), @@ -260,14 +242,12 @@ describe("signInCredentials action", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const { handlers } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createSession: createSessionMock, @@ -299,13 +279,6 @@ describe("signInCredentials action", async () => { email: "alice@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "alice@example.com", - name: "alice", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), @@ -328,14 +301,12 @@ describe("signInCredentials action", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const { handlers } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createSession: createSessionMock, @@ -367,13 +338,6 @@ describe("signInCredentials action", async () => { email: "alice@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "alice@example.com", - name: "alice", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), @@ -396,14 +360,12 @@ describe("signInCredentials action", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const { handlers } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createSession: createSessionMock, @@ -435,13 +397,6 @@ describe("signInCredentials action", async () => { email: "alice@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "alice@example.com", - name: "alice", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), diff --git a/packages/core/test/api/stateful/signInCredentials.test.ts b/packages/core/test/api/stateful/signInCredentials.test.ts index 1c2c8794..815e506e 100644 --- a/packages/core/test/api/stateful/signInCredentials.test.ts +++ b/packages/core/test/api/stateful/signInCredentials.test.ts @@ -20,14 +20,12 @@ describe("signInCredentials API", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const { api } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createDevice: createDeviceMock, @@ -56,13 +54,6 @@ describe("signInCredentials API", async () => { email: "johndoe@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "johndoe@example.com", - name: "johndoe", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), @@ -238,14 +229,12 @@ describe("signInCredentials API", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const { api } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createDevice: createDeviceMock, @@ -277,13 +266,6 @@ describe("signInCredentials API", async () => { email: "johndoe@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "johndoe@example.com", - name: "johndoe", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), @@ -308,14 +290,12 @@ describe("signInCredentials API", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const { api } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createDevice: createDeviceMock, @@ -347,13 +327,6 @@ describe("signInCredentials API", async () => { email: "johndoe@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "johndoe@example.com", - name: "johndoe", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), @@ -378,14 +351,12 @@ describe("signInCredentials API", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const { api } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createDevice: createDeviceMock, @@ -417,13 +388,6 @@ describe("signInCredentials API", async () => { email: "johndoe@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "johndoe@example.com", - name: "johndoe", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), @@ -448,14 +412,12 @@ describe("signInCredentials API", async () => { vi.spyOn(module, "createSchemaRegistry").mockReturnValue(registry) const updateUserMock = vi.fn() - const getUserByIdMock = vi.fn().mockReturnValue(null) - const createUserMock = vi.fn().mockReturnValue(userEntity) + const getUserByIdMock = vi.fn().mockReturnValue(userEntity) const createDeviceMock = vi.fn().mockResolvedValue(deviceEntity) const createSessionMock = vi.fn().mockReturnValue(sessionEntityWithUser) const getDeviceByFingerprintMock = vi.fn().mockReturnValue(null) const { api } = authInstance({ - createUser: createUserMock, updateUser: updateUserMock, getUserById: getUserByIdMock, createDevice: createDeviceMock, @@ -487,13 +449,6 @@ describe("signInCredentials API", async () => { email: "johndoe@example.com", image: "https://example.com/image.jpg", }) - expect(createUserMock).toHaveBeenCalledWith({ - id: "user-123", - email: "johndoe@example.com", - name: "johndoe", - image: "https://example.com/image.jpg", - attributes: {}, - }) expect(updateUserMock).not.toHaveBeenCalled() expect(createSessionMock).toHaveBeenCalledWith({ id: expect.any(String), diff --git a/packages/elysia/test/stateful/index.test.ts b/packages/elysia/test/stateful/index.test.ts index dad2e7a5..367a31e1 100644 --- a/packages/elysia/test/stateful/index.test.ts +++ b/packages/elysia/test/stateful/index.test.ts @@ -360,6 +360,13 @@ describe("POST /api/auth/signIn/credentials", () => { test("returns 200 and a session cookie when valid credentials are provided", async () => { const csrfToken = await createCSRF(auth.jose) + await adapter.createUser({ + id: "credentials:valid", + name: "John Doe", + email: "johndoe@example.com", + image: "https://johndoe.example.com/avatar.png", + }) + const response = await app.handle( new Request("http://localhost/api/auth/signIn/credentials", { method: "POST", @@ -379,6 +386,25 @@ describe("POST /api/auth/signIn/credentials", () => { redirectURL: null, }) expect(response.headers.get("set-cookie")).toBeDefined() + const sessionToken = response.headers.getSetCookie()?.find((cookie) => cookie.startsWith("aura-auth.session_token=")) + const parsed = parseSetCookie(sessionToken!).value + const session = await app.handle( + new Request("http://localhost/api/auth/session", { + headers: { Cookie: `aura-auth.session_token=${parsed}` }, + }) + ) + expect(await session.json()).toEqual({ + success: true, + session: { + user: { + sub: "credentials:valid", + name: "John Doe", + email: "johndoe@example.com", + image: "https://johndoe.example.com/avatar.png", + }, + expires: expect.any(String), + }, + }) }) }) @@ -725,7 +751,7 @@ describe("GET /api/auth/getProviderTokens", () => { }) ) - expect(response.status).toBe(401) + expect(response.status).toBe(400) const body = await response.json() expect(body).toMatchObject({ success: false,