@@ -623,3 +623,147 @@ func TestDigestAuthStandardFlow(t *testing.T) {
623623 t .Logf ("Second request got status: %d" , res2 .StatusCode )
624624 }
625625}
626+
627+ // TestSameCallIDForAuthFlow verifies that the same LiveKit call ID is assigned to both
628+ // the initial INVITE (without auth) and the subsequent INVITE (with auth)
629+ func TestSameCallIDForAuthFlow (t * testing.T ) {
630+ const (
631+ fromUser = "test@example.com"
632+ toUser = "agent@example.com"
633+ username = "testuser"
634+ password = "testpass"
635+ callID = "same-call-id@test.com"
636+ )
637+
638+ var capturedCallIDs []string
639+ var mu sync.Mutex
640+
641+ h := & TestHandler {
642+ GetAuthCredentialsFunc : func (ctx context.Context , call * rpc.SIPCall ) (AuthInfo , error ) {
643+ // Capture the LiveKit call ID from the first request
644+ mu .Lock ()
645+ capturedCallIDs = append (capturedCallIDs , call .LkCallId )
646+ mu .Unlock ()
647+
648+ return AuthInfo {
649+ Result : AuthPassword ,
650+ Username : username ,
651+ Password : password ,
652+ }, nil
653+ },
654+ DispatchCallFunc : func (ctx context.Context , info * CallInfo ) CallDispatch {
655+ return CallDispatch {
656+ Result : DispatchNoRuleReject ,
657+ // No room config needed for reject
658+ }
659+ },
660+ OnSessionEndFunc : func (ctx context.Context , callIdentifier * CallIdentifier , callInfo * livekit.SIPCallInfo , reason string ) {
661+ // No-op for tests to avoid async logging issues
662+ },
663+ }
664+
665+ // Create service with authentication enabled
666+ sipPort := rand .Intn (testPortSIPMax - testPortSIPMin ) + testPortSIPMin
667+ localIP , err := config .GetLocalIP ()
668+ require .NoError (t , err )
669+
670+ sipServerAddress := fmt .Sprintf ("%s:%d" , localIP , sipPort )
671+
672+ mon , err := stats .NewMonitor (& config.Config {MaxCpuUtilization : 0.9 })
673+ require .NoError (t , err )
674+
675+ // Use a no-op logger to avoid panics from async logging after test completion
676+ log := logger .LogRLogger (logr .Discard ())
677+ s , err := NewService ("" , & config.Config {
678+ HideInboundPort : false , // Enable authentication
679+ SIPPort : sipPort ,
680+ SIPPortListen : sipPort ,
681+ RTPPort : rtcconfig.PortRange {Start : testPortRTPMin , End : testPortRTPMax },
682+ }, mon , log , func (projectID string ) rpc.IOInfoClient { return nil })
683+ require .NoError (t , err )
684+ require .NotNil (t , s )
685+ t .Cleanup (s .Stop )
686+
687+ s .SetHandler (h )
688+ require .NoError (t , s .Start ())
689+
690+ sipUserAgent , err := sipgo .NewUA (
691+ sipgo .WithUserAgent (fromUser ),
692+ sipgo .WithUserAgentLogger (slog .New (logger .ToSlogHandler (s .log ))),
693+ )
694+ require .NoError (t , err )
695+
696+ sipClient , err := sipgo .NewClient (sipUserAgent )
697+ require .NoError (t , err )
698+
699+ offer , err := sdp .NewOffer (localIP , 0xB0B , sdp .EncryptionNone )
700+ require .NoError (t , err )
701+ offerData , err := offer .SDP .Marshal ()
702+ require .NoError (t , err )
703+
704+ // Create first INVITE request (without auth)
705+ inviteRecipient := sip.Uri {User : toUser , Host : sipServerAddress }
706+ inviteRequest1 := sip .NewRequest (sip .INVITE , inviteRecipient )
707+ inviteRequest1 .SetDestination (sipServerAddress )
708+ inviteRequest1 .SetBody (offerData )
709+ inviteRequest1 .AppendHeader (sip .NewHeader ("Content-Type" , "application/sdp" ))
710+ inviteRequest1 .AppendHeader (sip .NewHeader ("Call-ID" , callID ))
711+
712+ tx1 , err := sipClient .TransactionRequest (inviteRequest1 )
713+ require .NoError (t , err )
714+ t .Cleanup (tx1 .Terminate )
715+
716+ // Should receive 100 Trying first, then 407 Unauthorized
717+ res1 := getResponseOrFail (t , tx1 )
718+ require .Equal (t , sip .StatusCode (100 ), res1 .StatusCode , "First request should receive 100 Trying" )
719+ res1 = getResponseOrFail (t , tx1 )
720+ require .Equal (t , sip .StatusCode (407 ), res1 .StatusCode , "First request should receive 407 Unauthorized" )
721+
722+ // Get the challenge from first response
723+ authHeader1 := res1 .GetHeader ("Proxy-Authenticate" )
724+ require .NotNil (t , authHeader1 , "First response should have Proxy-Authenticate header" )
725+ challenge1 := authHeader1 .Value ()
726+
727+ // Parse the challenge to extract nonce and realm
728+ challenge , err := digest .ParseChallenge (challenge1 )
729+ require .NoError (t , err , "Should be able to parse challenge" )
730+
731+ // Compute the digest response using the challenge and credentials
732+ cred , err := digest .Digest (challenge , digest.Options {
733+ Method : "INVITE" ,
734+ URI : inviteRecipient .String (),
735+ Username : username ,
736+ Password : password ,
737+ })
738+ require .NoError (t , err , "Should be able to compute digest response" )
739+
740+ // Create second INVITE request (with auth) using the SAME Call-ID
741+ inviteRequest2 := sip .NewRequest (sip .INVITE , inviteRecipient )
742+ inviteRequest2 .SetDestination (sipServerAddress )
743+ inviteRequest2 .SetBody (offerData )
744+ inviteRequest2 .AppendHeader (sip .NewHeader ("Content-Type" , "application/sdp" ))
745+ inviteRequest2 .AppendHeader (sip .NewHeader ("Call-ID" , callID ))
746+ inviteRequest2 .AppendHeader (sip .NewHeader ("Proxy-Authorization" , cred .String ()))
747+
748+ tx2 , err := sipClient .TransactionRequest (inviteRequest2 )
749+ require .NoError (t , err )
750+ t .Cleanup (tx2 .Terminate )
751+
752+ // Should receive 100 Trying first, then proceed with authentication
753+ res2 := getResponseOrFail (t , tx2 )
754+ require .Equal (t , sip .StatusCode (100 ), res2 .StatusCode , "Second request should receive 100 Trying" )
755+
756+ // Wait a bit for the handler to be called
757+ time .Sleep (100 * time .Millisecond )
758+
759+ // Verify we captured exactly 2 call IDs
760+ mu .Lock ()
761+ require .Len (t , capturedCallIDs , 2 , "Should have captured 2 call IDs" )
762+ require .Equal (t , capturedCallIDs [0 ], capturedCallIDs [1 ], "Both requests should have the same LiveKit call ID" )
763+ require .NotEmpty (t , capturedCallIDs [0 ], "Call ID should not be empty" )
764+ require .Contains (t , capturedCallIDs [0 ], "SCL_" , "Call ID should have SCL_ prefix" )
765+ mu .Unlock ()
766+
767+ t .Logf ("First call ID: %s" , capturedCallIDs [0 ])
768+ t .Logf ("Second call ID: %s" , capturedCallIDs [1 ])
769+ }
0 commit comments