server/http/auth-session.test.ts
Rama main · réplica de lectura del repositorio original.
import { createAuth } from '@server/auth'
import type {
AgentAuthPluginOptions,
EmailOtpPluginOptions,
OrganizationPluginOptions,
} from '@server/auth-test-plugin-types'
import type { Database } from '@server/db/client'
import { createApp } from '@server/http/app'
import { createIdentifierGeneratorFake } from '@server/usecases/identifier-generator.fake'
import type { ManagementSignInSettingsResponse } from '@shared/api/management'
import { jwtVerify, SignJWT } from 'jose'
import { describe, expect, it, vi } from 'vitest'
import { createTestDeps } from './test-deps'
describe('auth.test 1', () => {
it('publishes RFC 9728 metadata for Bearer Applications and DPoP-capable Agent grants', async () => {
const auth = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com',
['https://auth.example.com'],
createEmailSenderMock(),
createSecurityPolicy(),
)
const response = await createApp(auth, createTestDeps()).request(
'https://auth.example.com/.well-known/oauth-protected-resource/api',
)
expect(response.status).toBe(200)
await expect(response.json()).resolves.toMatchObject({
resource: 'https://auth.example.com/api',
authorization_servers: ['https://auth.example.com/api/auth'],
dpop_signing_alg_values_supported: ['ES256', 'EdDSA'],
bearer_methods_supported: ['header'],
dpop_bound_access_tokens_required: false,
scopes_supported: expect.arrayContaining(['agent:read', 'users:read', 'resource-servers:write']),
})
})
it('serves OIDC discovery from the mounted Better Auth issuer', async () => {
const auth = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com',
['https://auth.example.com'],
createEmailSenderMock(),
createSecurityPolicy(),
)
const response = await createApp(auth, createTestDeps()).request('/api/auth/.well-known/openid-configuration')
expect(response.status).toBe(200)
const metadata = (await response.json()) as {
token_endpoint_auth_methods_supported: string[]
grant_types_supported: string[]
id_token_signing_alg_values_supported: string[]
}
expect(metadata).toMatchObject({
issuer: 'https://auth.example.com/api/auth',
authorization_endpoint: 'https://auth.example.com/api/auth/oauth2/authorize',
device_authorization_endpoint: 'https://auth.example.com/api/auth/device/code',
token_endpoint: 'https://auth.example.com/api/auth/oauth2/token',
jwks_uri: 'https://auth.example.com/api/auth/jwks',
grant_types_supported: [
'authorization_code',
'client_credentials',
'refresh_token',
'urn:ietf:params:oauth:grant-type:device_code',
'urn:ietf:params:oauth:grant-type:jwt-bearer',
],
code_challenge_methods_supported: ['S256'],
scopes_supported: ['openid', 'profile', 'email', 'groups', 'offline_access'],
token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post', 'private_key_jwt'],
id_token_signing_alg_values_supported: ['RS256'],
})
expect(metadata.token_endpoint_auth_methods_supported).not.toContain('none')
})
it('validates device-code grants at the OAuth Provider token endpoint', async () => {
const auth = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com',
['https://auth.example.com'],
createEmailSenderMock(),
createSecurityPolicy(),
)
const response = await createApp(auth, createTestDeps()).request('/api/auth/oauth2/token', {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
}),
})
expect(response.status).toBe(400)
await expect(response.json()).resolves.toMatchObject({
error: 'invalid_request',
error_description: 'device_code is required',
})
})
it('accepts RFC 8628 form encoding at the mounted device authorization boundary [spec: management-api/management-native-device-approval]', async () => {
const auth = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com',
['https://auth.example.com'],
createEmailSenderMock(),
createSecurityPolicy(),
)
const response = await createApp(auth, createTestDeps()).request('/api/auth/device/code', {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: new URLSearchParams({ scope: 'openid' }),
})
expect(response.status).toBe(400)
await expect(response.json()).resolves.toMatchObject({
error: 'invalid_request',
error_description: 'client_id is required',
})
})
it('passes mounted authorize query parameters to Better Auth', async () => {
const auth = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com',
['https://auth.example.com'],
createEmailSenderMock(),
createSecurityPolicy(),
)
const query = new URLSearchParams({
client_id: 'client-1',
redirect_uri: 'https://app.example.com/callback',
response_type: 'code',
scope: 'openid profile email',
state: 'state-1',
code_challenge: 'challenge-1',
code_challenge_method: 'S256',
})
const response = await createApp(auth, createTestDeps()).request(`/api/auth/oauth2/authorize?${query}`)
expect(response.headers.get('location') ?? (await response.text())).not.toContain('client_id is required')
})
it('wires supported Better Auth email flows to the transactional email sender', async () => {
const emailSender = createEmailSenderMock()
const auth = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com',
['https://auth.example.com'],
emailSender,
createSecurityPolicy(),
)
await auth.options.emailVerification?.sendVerificationEmail?.({
user: createUser(),
url: 'https://auth.example.com/verify',
token: await new SignJWT({ email: 'user@example.com' })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('1h')
.sign(new TextEncoder().encode('01234567890123456789012345678901')),
})
await auth.options.emailAndPassword?.sendResetPassword?.({
user: createUser(),
url: 'https://auth.example.com/api/auth/reset-password/reset-token?callbackURL=callback',
token: 'reset-token',
})
await auth.options.emailAndPassword?.onPasswordReset?.({
user: createUser(),
})
await findPlugin<EmailOtpPluginOptions>(auth, 'email-otp').options.sendVerificationOTP({
email: 'user@example.com',
otp: '123456',
type: 'sign-in',
})
await findPlugin<EmailOtpPluginOptions>(auth, 'email-otp').options.sendVerificationOTP({
email: 'user@example.com',
otp: '654321',
type: 'forget-password',
})
await findPlugin<OrganizationPluginOptions>(auth, 'organization').options.sendInvitationEmail({
email: 'user@example.com',
id: 'invitation-1',
inviter: {
user: createUser(),
},
})
const verificationUrl = emailSender.send.mock.calls[0]?.[0].template.url as string
const { payload } = await jwtVerify(
new URL(verificationUrl).searchParams.get('token')!,
new TextEncoder().encode('01234567890123456789012345678901'),
)
expect(payload).toMatchObject({ sub: createUser().id, email: 'user@example.com' })
expect(emailSender.send).toHaveBeenCalledWith({
to: 'user@example.com',
template: {
type: 'verification',
url: expect.stringMatching(/^https:\/\/auth\.example\.com\/verify\?token=/),
},
})
expect(emailSender.send).toHaveBeenCalledWith({
to: 'user@example.com',
template: {
type: 'security-notification',
title: 'Your password was changed',
body: 'Your Realmroot password was changed. If this was not you, reset your password immediately.',
},
})
expect(emailSender.send).toHaveBeenCalledWith({
to: 'user@example.com',
template: {
type: 'otp',
otp: '123456',
},
})
expect(emailSender.send).toHaveBeenCalledWith({
to: 'user@example.com',
template: {
type: 'otp',
otp: '654321',
},
})
expect(emailSender.send).toHaveBeenCalledWith({
to: 'user@example.com',
template: {
type: 'password-reset',
url: 'https://auth.example.com/api/auth/reset-password/reset-token?callbackURL=callback',
},
})
expect(emailSender.send).toHaveBeenCalledWith({
to: 'user@example.com',
template: {
type: 'invitation',
inviterName: 'User',
url: 'https://auth.example.com/account/organizations?invitation=invitation-1',
},
})
})
it('[spec: hosted-auth/normal-signup-signin-account] requires email verification when delivery is ready', () => {
const auth = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com',
['https://auth.example.com'],
createEmailSenderMock(),
createSecurityPolicy(),
undefined,
{ emailDeliveryReady: true },
)
expect(auth.options.emailVerification?.sendOnSignUp).toBe(true)
expect(auth.options.emailVerification?.sendOnSignIn).toBe(true)
expect(auth.options.emailAndPassword?.requireEmailVerification).toBe(true)
const withoutDelivery = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com',
['https://auth.example.com'],
createEmailSenderMock(),
createSecurityPolicy(),
)
expect(withoutDelivery.options.emailVerification?.sendOnSignUp).toBe(false)
expect(withoutDelivery.options.emailVerification?.sendOnSignIn).toBe(false)
expect(withoutDelivery.options.emailAndPassword?.requireEmailVerification).toBe(false)
})
it('exposes security, email OTP, username, and organization invitation APIs', () => {
const auth = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com',
['https://auth.example.com'],
createEmailSenderMock(),
createSecurityPolicy(),
)
expect(Object.keys(auth.api)).toEqual(
expect.arrayContaining([
'enableTwoFactor',
'verifyTOTP',
'listPasskeys',
'generatePasskeyRegistrationOptions',
'sendVerificationOTP',
'requestPasswordReset',
'requestPasswordResetEmailOTP',
'signInUsername',
'isUsernameAvailable',
'createInvitation',
'getAgentConfiguration',
'register',
'requestCapability',
'approveCapability',
'executeCapability',
]),
)
expect(Object.keys(auth.api)).toEqual(
expect.arrayContaining(['createTeam', 'listOrganizationTeams', 'setActiveTeam', 'addTeamMember']),
)
})
it('configures AgentAuth only for delegated identity enrollment', async () => {
const auth = createAuth(
{} as Database,
createIdentifierGeneratorFake(),
'01234567890123456789012345678901',
'https://auth.example.com/api/auth',
['https://auth.example.com'],
createEmailSenderMock(),
createSecurityPolicy(),
)
const agentAuthPlugin = findPlugin<AgentAuthPluginOptions>(auth, 'agent-auth')
expect(agentAuthPlugin.options).toMatchObject({
providerName: 'Realmroot',
modes: ['delegated'],
approvalMethods: ['device_authorization'],
deviceAuthorizationPage: '/agent/enrollment',
allowDynamicHostRegistration: true,
defaultHostCapabilities: [],
requireAuthForCapabilities: false,
})
expect(agentAuthPlugin.options.capabilities).toEqual([])
expect(agentAuthPlugin.options.validateCapabilities([])).toBe(true)
expect(agentAuthPlugin.options.validateCapabilities(['applications:read'])).toBe(false)
expect(agentAuthPlugin.options.validateCapabilities(['tenant:read'])).toBe(false)
expect(agentAuthPlugin.options.validateCapabilities(['management.users.delete'])).toBe(false)
expect(agentAuthPlugin.options.resolveAutonomousUser).toBeUndefined()
})
})
function createEmailSenderMock() {
return {
send: vi.fn().mockResolvedValue({ messageId: 'email-1' }),
}
}
function createUser() {
return {
id: 'user-1',
name: 'User',
email: 'user@example.com',
emailVerified: false,
image: null,
createdAt: new Date(),
updatedAt: new Date(),
}
}
function createSecurityPolicy(overrides: Partial<SecurityPolicyInput> = {}) {
return {
mfa: {
mode: 'optional',
authenticatorAppEnabled: true,
emailOtpEnabled: false,
backupCodesEnabled: true,
...overrides.mfa,
},
passkeys: {
enabled: true,
rpId: 'auth.example.com',
rpName: 'Realmroot',
origins: ['https://auth.example.com'],
...overrides.passkeys,
},
sessions: {
expiresInSeconds: 60 * 60 * 24 * 7,
updateAgeSeconds: 60 * 60 * 24,
freshAgeSeconds: 60 * 60 * 24,
cookieCacheSeconds: 60 * 5,
...overrides.sessions,
},
password: {
minLength: 8,
requiredCharacterTypes: 1,
customWords: [],
rejectUserInfo: true,
rejectSequential: true,
rejectCustomWords: false,
...overrides.password,
},
captcha: {
enabled: false,
provider: 'turnstile',
siteKey: '',
projectId: null,
secretKey: '',
...overrides.captcha,
},
blocklist: {
blockSubaddressing: false,
entries: [],
...overrides.blocklist,
},
} satisfies SecurityPolicyInput
}
function _createBuiltInProviders(
phoneOverrides: Partial<ManagementSignInSettingsResponse['builtInProviders']['phone']> = {},
): ManagementSignInSettingsResponse['builtInProviders'] {
return {
email: {
enabled: true,
otpLength: 8,
expiresInSeconds: 900,
},
phone: {
enabled: true,
otpLength: 6,
expiresInSeconds: 300,
signUpOnVerification: false,
requireVerification: true,
smsProvider: 'twilio',
twilioAccountSid: 'twilio-sid',
twilioAuthToken: 'twilio-token',
twilioFromNumber: '+15555550100',
vonageApiKey: '',
vonageApiSecret: '',
vonageFrom: '',
messageBirdAccessKey: '',
messageBirdOriginator: '',
...phoneOverrides,
},
web3Wallet: {
enabled: true,
chains: [1],
domain: 'wallet.example.com',
emailDomainName: 'wallet.example.com',
ensLookupEnabled: false,
allowSignUp: true,
},
passkey: {
allowSignUp: true,
},
oneTap: {
enabled: true,
clientId: 'google-client-id',
autoSelect: false,
cancelOnTapOutside: true,
uxMode: 'popup',
context: 'signin',
promptBaseDelayMs: 1000,
promptMaxAttempts: 5,
disableSignUp: false,
},
}
}
interface SecurityPolicyInput {
mfa: {
mode: 'optional' | 'required'
authenticatorAppEnabled?: boolean
emailOtpEnabled?: boolean
backupCodesEnabled?: boolean
}
passkeys: {
enabled: boolean
rpId: string
rpName: string
origins: string[]
}
sessions: {
expiresInSeconds: number
updateAgeSeconds: number
freshAgeSeconds: number
cookieCacheSeconds: number
}
password: {
minLength: number
requiredCharacterTypes: number
customWords: string[]
rejectUserInfo: boolean
rejectSequential: boolean
rejectCustomWords: boolean
}
captcha: {
enabled: boolean
provider: 'turnstile'
siteKey: string
projectId: string | null
secretKey: string
}
blocklist: {
blockSubaddressing: boolean
entries: string[]
}
}
function _jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
})
}
function findPlugin<TOptions extends object>(auth: ReturnType<typeof createAuth>, id: string) {
const plugin = auth.options.plugins?.find((candidate) => candidate.id === id)
if (!plugin) {
throw new Error(`Plugin not found: ${id}`)
}
return plugin as unknown as { options: TOptions }
}