Ctrl K
ComunidadGuías…

server/http/auth-session.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 }
}