11/**
22 * @vitest -environment node
33 */
4+
5+ import { execFileSync } from 'node:child_process'
6+ import { mkdtempSync , readFileSync , rmSync } from 'node:fs'
7+ import { tmpdir } from 'node:os'
8+ import path from 'node:path'
49import {
510 createMockRequest ,
611 dbChainMock ,
@@ -13,6 +18,7 @@ import {
1318 setEnv ,
1419 setEnvFlags ,
1520} from '@sim/testing'
21+ import { loggerMock } from '@sim/testing/mocks/logger.mock'
1622import { afterAll , beforeEach , describe , expect , it , vi } from 'vitest'
1723
1824const {
@@ -91,6 +97,17 @@ vi.mock('@/lib/core/security/input-validation.server', () => ({
9197
9298import { POST } from '@/app/api/auth/sso/register/route'
9399
100+ type MockLogger = { info : { mock : { calls : unknown [ ] [ ] } } }
101+
102+ /** The logger the route built at import time, so its calls can be inspected. */
103+ const routeLogger = loggerMock . createLogger . mock . calls . reduce < MockLogger | null > (
104+ ( found , call , index ) =>
105+ call [ 0 ] === 'SSORegisterRoute'
106+ ? ( loggerMock . createLogger . mock . results [ index ] . value as MockLogger )
107+ : found ,
108+ null
109+ )
110+
94111const OIDC_BODY = {
95112 providerType : 'oidc' as const ,
96113 providerId : 'acme-oidc' ,
@@ -364,8 +381,40 @@ describe('POST /api/auth/sso/register', () => {
364381 } )
365382
366383 describe ( 'SAML encrypted assertions' , ( ) => {
367- const SP_CERT = `-----BEGIN CERTIFICATE-----\nQUJD\n-----END CERTIFICATE-----`
368- const SP_KEY = `-----BEGIN PRIVATE KEY-----\nREVG\n-----END PRIVATE KEY-----`
384+ /**
385+ * Real key material, because the route parses both halves and checks they
386+ * belong together. Generated per run rather than committed: a private key
387+ * in the repository is exactly what this PR is about not doing.
388+ */
389+ const keyPair = ( subject : string ) => {
390+ const dir = mkdtempSync ( path . join ( tmpdir ( ) , 'sim-sso-keys-' ) )
391+ execFileSync ( 'openssl' , [
392+ 'req' ,
393+ '-x509' ,
394+ '-newkey' ,
395+ 'rsa:2048' ,
396+ '-keyout' ,
397+ path . join ( dir , 'key.pem' ) ,
398+ '-out' ,
399+ path . join ( dir , 'cert.pem' ) ,
400+ '-days' ,
401+ '2' ,
402+ '-nodes' ,
403+ '-subj' ,
404+ `/CN=${ subject } ` ,
405+ ] )
406+ const pair = {
407+ cert : readFileSync ( path . join ( dir , 'cert.pem' ) , 'utf8' ) ,
408+ key : readFileSync ( path . join ( dir , 'key.pem' ) , 'utf8' ) ,
409+ }
410+ rmSync ( dir , { recursive : true , force : true } )
411+ return pair
412+ }
413+
414+ const SP = keyPair ( 'sim-test-sp' )
415+ const OTHER = keyPair ( 'sim-test-other' )
416+ const SP_CERT = SP . cert
417+ const SP_KEY = SP . key
369418 const samlBody = ( overrides : Record < string , unknown > = { } ) => ( {
370419 providerType : 'saml' as const ,
371420 providerId : 'acme-saml' ,
@@ -396,9 +445,29 @@ describe('POST /api/auth/sso/register', () => {
396445 } )
397446 /** The certificate travels in the metadata document, stripped of its PEM armor. */
398447 expect ( samlConfig . spMetadata . metadata ) . toContain ( 'use="encryption"' )
399- expect ( samlConfig . spMetadata . metadata ) . toContain ( 'QUJD' )
448+ expect ( samlConfig . spMetadata . metadata ) . toContain (
449+ SP_CERT . replace ( / - - - - - ( B E G I N | E N D ) C E R T I F I C A T E - - - - - / g, '' ) . replace ( / \s + / g, '' )
450+ )
400451 expect ( samlConfig . spMetadata . metadata ) . not . toContain ( 'BEGIN CERTIFICATE' )
401- expect ( samlConfig . spMetadata . metadata ) . not . toContain ( 'REVG' )
452+ expect ( samlConfig . spMetadata . metadata ) . not . toContain ( 'PRIVATE KEY' )
453+ } )
454+
455+ it ( 'never writes the private key to a log line' , async ( ) => {
456+ queueMembers ( [ { organizationId : 'org1' , role : 'owner' } ] )
457+ queueProviders ( [ ] )
458+
459+ await POST (
460+ request (
461+ samlBody ( { encryptAssertions : true , spEncryptionCert : SP_CERT , spDecryptionKey : SP_KEY } )
462+ )
463+ )
464+
465+ /** The route logs its resolved provider config; the key must be redacted there. */
466+ const logged = ( routeLogger ?. info . mock . calls ?? [ ] )
467+ . map ( ( call ) => JSON . stringify ( call ) )
468+ . join ( '\n' )
469+ expect ( logged ) . not . toContain ( 'PRIVATE KEY' )
470+ expect ( logged ) . toContain ( '[REDACTED]' )
402471 } )
403472
404473 it ( 'leaves the metadata and key material alone when encryption is off' , async ( ) => {
@@ -441,7 +510,9 @@ describe('POST /api/auth/sso/register', () => {
441510 const res = await POST ( request ( samlBody ( { encryptAssertions : true , ...overrides } ) ) )
442511
443512 expect ( res . status ) . toBe ( 400 )
444- await expect ( res . json ( ) ) . resolves . toMatchObject ( { error : expect . stringContaining ( 'PEM' ) } )
513+ await expect ( res . json ( ) ) . resolves . toMatchObject ( {
514+ error : expect . stringMatching ( / P E M | m a t c h i n g p a i r / ) ,
515+ } )
445516 expect ( mockRegisterSSOProvider ) . not . toHaveBeenCalled ( )
446517 } )
447518
0 commit comments