Ctrl K
ComunidadGuías…

server/http/auth-plugins.test.ts

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

Archivo original ↗Preguntar a KiBot
import * as applicationRepositories from '@server/adapters/repos/applications'
import * as authorizationRepositories from '@server/adapters/repos/authorization'
import { buildOAuthAccessTokenClaims, createAuth } from '@server/auth'
import type { OAuthProviderPluginOptions } from '@server/auth-test-plugin-types'
import type { Database } from '@server/db/client'
import { createApp } from '@server/http/app'
import * as authorizationUsecase from '@server/usecases/authorization'
import type { Deps } from '@server/usecases/deps'
import { createIdentifierGeneratorFake } from '@server/usecases/identifier-generator.fake'
import type { ManagementSignInSettingsResponse } from '@shared/api/management'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createTestDeps } from './test-deps'

describe('auth.test 2', () => {
  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('publishes Agent identity enrollment without legacy capability execution [spec: admin-console/agent-discovery]', async () => {
    const auth = createAuth(
      {} as Database,
      createIdentifierGeneratorFake(),
      '01234567890123456789012345678901',
      'https://auth.example.com',
      ['https://auth.example.com'],
      createEmailSenderMock(),
      createSecurityPolicy(),
    )
    const app = createApp(auth, createTestDeps())

    const discovery = await app.request('https://auth.example.com/.well-known/agent-configuration')
    expect(discovery.status).toBe(200)
    const metadata = await discovery.json()
    expect(metadata).toMatchObject({
      issuer: 'https://auth.example.com/api/auth',
      agent_identity_issuer: 'https://auth.example.com/api/auth',
      agent_endpoint: 'https://auth.example.com/api/agent',
      agent_enrollment_endpoint: 'https://auth.example.com/api/agent/enrollments',
      agent_profile_uri_template: 'https://auth.example.com/api/public/agents/{subject}',
      agent_token_endpoint: 'https://auth.example.com/api/auth/oauth2/token',
      agent_bootstrap_scopes_supported: [
        'agent:read',
        'resource-servers:read',
        'authorization-details:read',
        'access-requests:read',
        'access-requests:write',
      ],
      agent_jwks_uri: 'https://auth.example.com/api/auth/jwks',
      modes: ['delegated'],
      approval_methods: ['device_authorization'],
      endpoints: {
        register: 'https://auth.example.com/api/auth/agent/register',
      },
    })
    const capabilitiesResponse = await app.request('https://auth.example.com/api/auth/capability/list')
    expect(capabilitiesResponse.status).toBe(404)
  })

  it('configures organization access control with explicit Teams and no default Team', () => {
    const auth = createAuth(
      {} as Database,
      createIdentifierGeneratorFake(),
      '01234567890123456789012345678901',
      'https://auth.example.com',
      ['https://auth.example.com'],
      createEmailSenderMock(),
      createSecurityPolicy(),
    )

    const organizationPlugin = auth.options.plugins?.find((plugin) => plugin.id === 'organization')

    expect(organizationPlugin?.options).toMatchObject({
      teams: {
        enabled: true,
        defaultTeam: { enabled: false },
      },
    })
  })

  it('keeps Realmroot resource capabilities out of OAuth client scopes and userinfo', async () => {
    const auth = createAuth(
      {} as Database,
      createIdentifierGeneratorFake(),
      '01234567890123456789012345678901',
      'https://auth.example.com',
      ['https://auth.example.com'],
      createEmailSenderMock(),
      createSecurityPolicy(),
    )
    const oauth = findPlugin<OAuthProviderPluginOptions>(auth, 'oauth-provider').options
    expect(oauth.clientRegistrationAllowedScopes).toEqual(['openid', 'profile', 'email', 'groups', 'offline_access'])
    expect(oauth.customUserInfoClaims).toBeUndefined()
  })

  it('[spec: hosted-auth/oauth-authorization-context-selection] redirects post-login to Context selection and revalidates membership', async () => {
    const findOrganization = vi.fn().mockResolvedValue({ id: 'org-1', disabled: false })
    const findMemberByOrganizationUser = vi.fn().mockResolvedValue({ id: 'member-1' })
    const listUserMemberships = vi.fn().mockResolvedValue([{ organizationId: 'org-1' }])
    vi.spyOn(authorizationRepositories, 'createDrizzleAuthorizationRepository').mockReturnValue({
      findOrganization,
      findMemberByOrganizationUser,
      findResource: vi.fn().mockResolvedValue({
        id: 'resource-1',
        enabled: true,
        visibility: 'public',
      }),
      findResourceByResourceUrl: vi.fn().mockResolvedValue({ id: 'resource-1' }),
      listUserMemberships,
    } as never)
    vi.spyOn(applicationRepositories, 'createDrizzleApplicationRepository').mockReturnValue({
      findByClientId: vi.fn().mockResolvedValue({ visibility: 'public' }),
    } as never)
    const auth = createAuth(
      {} as Database,
      createIdentifierGeneratorFake(),
      '01234567890123456789012345678901',
      'https://auth.example.com',
      ['https://auth.example.com'],
      createEmailSenderMock(),
      createSecurityPolicy(),
    )
    const postLogin = findPlugin<OAuthProviderPluginOptions>(auth, 'oauth-provider').options.postLogin

    expect(postLogin.page).toBe('/auth/context')
    await expect(postLogin.shouldRedirect({ headers: new Headers(), user: { id: 'user-1' } })).resolves.toBe(false)
    await expect(
      postLogin.shouldRedirect({
        headers: new Headers({
          'x-realmroot-oauth-client-id': 'client-1',
          'x-realmroot-oauth-resources': JSON.stringify(['https://api.example.com/']),
        }),
        user: { id: 'user-1' },
      }),
    ).resolves.toBe(true)
    listUserMemberships.mockResolvedValueOnce([])
    await expect(
      postLogin.shouldRedirect({
        headers: new Headers({
          'x-realmroot-oauth-client-id': 'client-1',
          'x-realmroot-oauth-resources': JSON.stringify(['https://api.example.com/']),
        }),
        user: { id: 'user-1' },
      }),
    ).resolves.toBe('user:user-1')
    await expect(
      postLogin.consentReferenceId({
        session: { activeOrganizationId: 'org-1' },
        user: { id: 'user-1' },
      }),
    ).resolves.toBeUndefined()
    await expect(
      postLogin.consentReferenceId({
        session: { activeOrganizationId: null },
        user: { id: 'user-1' },
        referenceId: 'user:user-1',
      }),
    ).resolves.toBe('user:user-1')
    await expect(
      postLogin.consentReferenceId({
        session: { activeOrganizationId: null },
        user: { id: 'user-1' },
        referenceId: 'organization:org-1',
      }),
    ).resolves.toBe('organization:org-1')
    expect(findMemberByOrganizationUser).toHaveBeenCalledWith('org-1', 'user-1')

    findMemberByOrganizationUser.mockResolvedValueOnce(null)
    await expect(
      postLogin.consentReferenceId({
        session: { activeOrganizationId: null },
        user: { id: 'user-1' },
        referenceId: 'organization:org-1',
      }),
    ).rejects.toMatchObject({
      status: 'FORBIDDEN',
      body: { error: 'access_denied' },
    })
    findOrganization.mockResolvedValueOnce({ id: 'org-1', disabled: true })
    await expect(
      postLogin.consentReferenceId({
        session: { activeOrganizationId: null },
        user: { id: 'user-1' },
        referenceId: 'organization:org-1',
      }),
    ).rejects.toMatchObject({
      status: 'FORBIDDEN',
      body: { error: 'access_denied' },
    })
  })

  it('maps OAuth provider context into authorization token claims', async () => {
    const deps = {
      applications: {
        findById: vi.fn().mockResolvedValue({ visibility: 'public' }),
      },
      authorization: {
        findOrganization: vi.fn().mockResolvedValue({ id: 'org-1', disabled: false }),
        findMemberByOrganizationUser: vi.fn().mockResolvedValue({ id: 'member-1' }),
      },
    } as unknown as Deps
    const buildTokenClaims = vi.spyOn(authorizationUsecase, 'buildTokenClaims').mockResolvedValue({
      authorization: { roles: ['contacts-reader'] },
      roles: ['contacts-reader'],
      groups: ['org-1'],
    })

    await expect(
      buildOAuthAccessTokenClaims(deps, {
        user: { id: 'user-1' },
        scopes: new Set(['openid', 'contacts:read']),
        resource: 'https://api.example.com/contacts',
        referenceId: 'organization:org-1',
        metadata: {
          applicationId: 'app-1',
          ignored: 'value',
        },
      }),
    ).resolves.toEqual({
      roles: ['contacts-reader'],
      groups: ['org-1'],
      'urn:realmroot:params:oauth:org': 'org-1',
    })

    expect(buildTokenClaims).toHaveBeenCalledWith(deps, {
      userId: 'user-1',
      applicationId: 'app-1',
      organizationId: 'org-1',
      resource: 'https://api.example.com/contacts',
      scopes: ['openid', 'contacts:read'],
      authorizedScopes: ['contacts:read'],
    })
  })

  it('rejects a stale public Application Organization Context after membership removal', async () => {
    const deps = {
      applications: {
        findById: vi.fn().mockResolvedValue({ visibility: 'public' }),
      },
      authorization: {
        findOrganization: vi.fn().mockResolvedValue({ id: 'org-1', disabled: false }),
        findMemberByOrganizationUser: vi.fn().mockResolvedValue(null),
      },
    } as unknown as Deps
    const buildTokenClaims = vi.spyOn(authorizationUsecase, 'buildTokenClaims')

    await expect(
      buildOAuthAccessTokenClaims(deps, {
        user: { id: 'user-1' },
        scopes: new Set(['openid']),
        referenceId: 'organization:org-1',
        metadata: { applicationId: 'app-1' },
      }),
    ).rejects.toMatchObject({ body: { error: 'access_denied' } })
    expect(buildTokenClaims).not.toHaveBeenCalled()
  })
})

function createEmailSenderMock() {
  return {
    send: vi.fn().mockResolvedValue({ messageId: 'email-1' }),
  }
}

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 }
}