@@ -782,3 +782,147 @@ func TestCANCELSendsBothResponses(t *testing.T) {
782782 // Verify we received the critical 487 response
783783 require .True (t , invite487Received , "Should have received 487 Request Terminated response to INVITE when CANCEL is sent" )
784784}
785+
786+ // TestSameCallIDForAuthFlow verifies that the same LiveKit call ID is assigned to both
787+ // the initial INVITE (without auth) and the subsequent INVITE (with auth)
788+ func TestSameCallIDForAuthFlow (t * testing.T ) {
789+ const (
790+ fromUser = "test@example.com"
791+ toUser = "agent@example.com"
792+ username = "testuser"
793+ password = "testpass"
794+ callID = "same-call-id@test.com"
795+ )
796+
797+ var capturedCallIDs []string
798+ var mu sync.Mutex
799+
800+ h := & TestHandler {
801+ GetAuthCredentialsFunc : func (ctx context.Context , call * rpc.SIPCall ) (AuthInfo , error ) {
802+ // Capture the LiveKit call ID from the first request
803+ mu .Lock ()
804+ capturedCallIDs = append (capturedCallIDs , call .LkCallId )
805+ mu .Unlock ()
806+
807+ return AuthInfo {
808+ Result : AuthPassword ,
809+ Username : username ,
810+ Password : password ,
811+ }, nil
812+ },
813+ DispatchCallFunc : func (ctx context.Context , info * CallInfo ) CallDispatch {
814+ return CallDispatch {
815+ Result : DispatchNoRuleReject ,
816+ // No room config needed for reject
817+ }
818+ },
819+ OnSessionEndFunc : func (ctx context.Context , callIdentifier * CallIdentifier , callInfo * livekit.SIPCallInfo , reason string ) {
820+ // No-op for tests to avoid async logging issues
821+ },
822+ }
823+
824+ // Create service with authentication enabled
825+ sipPort := rand .Intn (testPortSIPMax - testPortSIPMin ) + testPortSIPMin
826+ localIP , err := config .GetLocalIP ()
827+ require .NoError (t , err )
828+
829+ sipServerAddress := fmt .Sprintf ("%s:%d" , localIP , sipPort )
830+
831+ mon , err := stats .NewMonitor (& config.Config {MaxCpuUtilization : 0.9 })
832+ require .NoError (t , err )
833+
834+ // Use a no-op logger to avoid panics from async logging after test completion
835+ log := logger .LogRLogger (logr .Discard ())
836+ s , err := NewService ("" , & config.Config {
837+ HideInboundPort : false , // Enable authentication
838+ SIPPort : sipPort ,
839+ SIPPortListen : sipPort ,
840+ RTPPort : rtcconfig.PortRange {Start : testPortRTPMin , End : testPortRTPMax },
841+ }, mon , log , func (projectID string ) rpc.IOInfoClient { return nil })
842+ require .NoError (t , err )
843+ require .NotNil (t , s )
844+ t .Cleanup (s .Stop )
845+
846+ s .SetHandler (h )
847+ require .NoError (t , s .Start ())
848+
849+ sipUserAgent , err := sipgo .NewUA (
850+ sipgo .WithUserAgent (fromUser ),
851+ sipgo .WithUserAgentLogger (slog .New (logger .ToSlogHandler (s .log ))),
852+ )
853+ require .NoError (t , err )
854+
855+ sipClient , err := sipgo .NewClient (sipUserAgent )
856+ require .NoError (t , err )
857+
858+ offer , err := sdp .NewOffer (localIP , 0xB0B , sdp .EncryptionNone )
859+ require .NoError (t , err )
860+ offerData , err := offer .SDP .Marshal ()
861+ require .NoError (t , err )
862+
863+ // Create first INVITE request (without auth)
864+ inviteRecipient := sip.Uri {User : toUser , Host : sipServerAddress }
865+ inviteRequest1 := sip .NewRequest (sip .INVITE , inviteRecipient )
866+ inviteRequest1 .SetDestination (sipServerAddress )
867+ inviteRequest1 .SetBody (offerData )
868+ inviteRequest1 .AppendHeader (sip .NewHeader ("Content-Type" , "application/sdp" ))
869+ inviteRequest1 .AppendHeader (sip .NewHeader ("Call-ID" , callID ))
870+
871+ tx1 , err := sipClient .TransactionRequest (inviteRequest1 )
872+ require .NoError (t , err )
873+ t .Cleanup (tx1 .Terminate )
874+
875+ // Should receive 100 Trying first, then 407 Unauthorized
876+ res1 := getResponseOrFail (t , tx1 )
877+ require .Equal (t , sip .StatusCode (100 ), res1 .StatusCode , "First request should receive 100 Trying" )
878+ res1 = getResponseOrFail (t , tx1 )
879+ require .Equal (t , sip .StatusCode (407 ), res1 .StatusCode , "First request should receive 407 Unauthorized" )
880+
881+ // Get the challenge from first response
882+ authHeader1 := res1 .GetHeader ("Proxy-Authenticate" )
883+ require .NotNil (t , authHeader1 , "First response should have Proxy-Authenticate header" )
884+ challenge1 := authHeader1 .Value ()
885+
886+ // Parse the challenge to extract nonce and realm
887+ challenge , err := digest .ParseChallenge (challenge1 )
888+ require .NoError (t , err , "Should be able to parse challenge" )
889+
890+ // Compute the digest response using the challenge and credentials
891+ cred , err := digest .Digest (challenge , digest.Options {
892+ Method : "INVITE" ,
893+ URI : inviteRecipient .String (),
894+ Username : username ,
895+ Password : password ,
896+ })
897+ require .NoError (t , err , "Should be able to compute digest response" )
898+
899+ // Create second INVITE request (with auth) using the SAME Call-ID
900+ inviteRequest2 := sip .NewRequest (sip .INVITE , inviteRecipient )
901+ inviteRequest2 .SetDestination (sipServerAddress )
902+ inviteRequest2 .SetBody (offerData )
903+ inviteRequest2 .AppendHeader (sip .NewHeader ("Content-Type" , "application/sdp" ))
904+ inviteRequest2 .AppendHeader (sip .NewHeader ("Call-ID" , callID ))
905+ inviteRequest2 .AppendHeader (sip .NewHeader ("Proxy-Authorization" , cred .String ()))
906+
907+ tx2 , err := sipClient .TransactionRequest (inviteRequest2 )
908+ require .NoError (t , err )
909+ t .Cleanup (tx2 .Terminate )
910+
911+ // Should receive 100 Trying first, then proceed with authentication
912+ res2 := getResponseOrFail (t , tx2 )
913+ require .Equal (t , sip .StatusCode (100 ), res2 .StatusCode , "Second request should receive 100 Trying" )
914+
915+ // Wait a bit for the handler to be called
916+ time .Sleep (100 * time .Millisecond )
917+
918+ // Verify we captured exactly 2 call IDs
919+ mu .Lock ()
920+ require .Len (t , capturedCallIDs , 2 , "Should have captured 2 call IDs" )
921+ require .Equal (t , capturedCallIDs [0 ], capturedCallIDs [1 ], "Both requests should have the same LiveKit call ID" )
922+ require .NotEmpty (t , capturedCallIDs [0 ], "Call ID should not be empty" )
923+ require .Contains (t , capturedCallIDs [0 ], "SCL_" , "Call ID should have SCL_ prefix" )
924+ mu .Unlock ()
925+
926+ t .Logf ("First call ID: %s" , capturedCallIDs [0 ])
927+ t .Logf ("Second call ID: %s" , capturedCallIDs [1 ])
928+ }
0 commit comments