@@ -426,6 +426,35 @@ func (l ServerAuthError) Error() string {
426
426
return "[" + strings .Join (errs , ", " ) + "]"
427
427
}
428
428
429
+ // ServerAuthCallbacks defines server-side authentication callbacks.
430
+ type ServerAuthCallbacks struct {
431
+ // PasswordCallback behaves like [ServerConfig.PasswordCallback].
432
+ PasswordCallback func (conn ConnMetadata , password []byte ) (* Permissions , error )
433
+
434
+ // PublicKeyCallback behaves like [ServerConfig.PublicKeyCallback].
435
+ PublicKeyCallback func (conn ConnMetadata , key PublicKey ) (* Permissions , error )
436
+
437
+ // KeyboardInteractiveCallback behaves like [ServerConfig.KeyboardInteractiveCallback].
438
+ KeyboardInteractiveCallback func (conn ConnMetadata , client KeyboardInteractiveChallenge ) (* Permissions , error )
439
+
440
+ // GSSAPIWithMICConfig behaves like [ServerConfig.GSSAPIWithMICConfig].
441
+ GSSAPIWithMICConfig * GSSAPIWithMICConfig
442
+ }
443
+
444
+ // PartialSuccessError can be returned by any of the [ServerConfig]
445
+ // authentication callbacks to indicate to the client that authentication has
446
+ // partially succeeded, but further steps are required.
447
+ type PartialSuccessError struct {
448
+ // Next defines the authentication callbacks to apply to further steps. The
449
+ // available methods communicated to the client are based on the non-nil
450
+ // ServerAuthCallbacks fields.
451
+ Next ServerAuthCallbacks
452
+ }
453
+
454
+ func (p * PartialSuccessError ) Error () string {
455
+ return "ssh: authenticated with partial success"
456
+ }
457
+
429
458
// ErrNoAuth is the error value returned if no
430
459
// authentication method has been passed yet. This happens as a normal
431
460
// part of the authentication loop, since the client first tries
@@ -441,6 +470,15 @@ func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, err
441
470
authFailures := 0
442
471
var authErrs []error
443
472
var displayedBanner bool
473
+ partialSuccessReturned := false
474
+ // Set the initial authentication callbacks from the config. They can be
475
+ // changed if a PartialSuccessError is returned.
476
+ authConfig := ServerAuthCallbacks {
477
+ PasswordCallback : config .PasswordCallback ,
478
+ PublicKeyCallback : config .PublicKeyCallback ,
479
+ KeyboardInteractiveCallback : config .KeyboardInteractiveCallback ,
480
+ GSSAPIWithMICConfig : config .GSSAPIWithMICConfig ,
481
+ }
444
482
445
483
userAuthLoop:
446
484
for {
@@ -471,6 +509,11 @@ userAuthLoop:
471
509
return nil , errors .New ("ssh: client attempted to negotiate for unknown service: " + userAuthReq .Service )
472
510
}
473
511
512
+ if s .user != userAuthReq .User && partialSuccessReturned {
513
+ return nil , fmt .Errorf ("ssh: client changed the user after a partial success authentication, previous user %q, current user %q" ,
514
+ s .user , userAuthReq .User )
515
+ }
516
+
474
517
s .user = userAuthReq .User
475
518
476
519
if ! displayedBanner && config .BannerCallback != nil {
@@ -491,20 +534,17 @@ userAuthLoop:
491
534
492
535
switch userAuthReq .Method {
493
536
case "none" :
494
- if config .NoClientAuth {
537
+ // We don't allow none authentication after a partial success
538
+ // response.
539
+ if config .NoClientAuth && ! partialSuccessReturned {
495
540
if config .NoClientAuthCallback != nil {
496
541
perms , authErr = config .NoClientAuthCallback (s )
497
542
} else {
498
543
authErr = nil
499
544
}
500
545
}
501
-
502
- // allow initial attempt of 'none' without penalty
503
- if authFailures == 0 {
504
- authFailures --
505
- }
506
546
case "password" :
507
- if config .PasswordCallback == nil {
547
+ if authConfig .PasswordCallback == nil {
508
548
authErr = errors .New ("ssh: password auth not configured" )
509
549
break
510
550
}
@@ -518,17 +558,17 @@ userAuthLoop:
518
558
return nil , parseError (msgUserAuthRequest )
519
559
}
520
560
521
- perms , authErr = config .PasswordCallback (s , password )
561
+ perms , authErr = authConfig .PasswordCallback (s , password )
522
562
case "keyboard-interactive" :
523
- if config .KeyboardInteractiveCallback == nil {
563
+ if authConfig .KeyboardInteractiveCallback == nil {
524
564
authErr = errors .New ("ssh: keyboard-interactive auth not configured" )
525
565
break
526
566
}
527
567
528
568
prompter := & sshClientKeyboardInteractive {s }
529
- perms , authErr = config .KeyboardInteractiveCallback (s , prompter .Challenge )
569
+ perms , authErr = authConfig .KeyboardInteractiveCallback (s , prompter .Challenge )
530
570
case "publickey" :
531
- if config .PublicKeyCallback == nil {
571
+ if authConfig .PublicKeyCallback == nil {
532
572
authErr = errors .New ("ssh: publickey auth not configured" )
533
573
break
534
574
}
@@ -562,11 +602,18 @@ userAuthLoop:
562
602
if ! ok {
563
603
candidate .user = s .user
564
604
candidate .pubKeyData = pubKeyData
565
- candidate .perms , candidate .result = config .PublicKeyCallback (s , pubKey )
566
- if candidate .result == nil && candidate .perms != nil && candidate .perms .CriticalOptions != nil && candidate .perms .CriticalOptions [sourceAddressCriticalOption ] != "" {
567
- candidate .result = checkSourceAddress (
605
+ candidate .perms , candidate .result = authConfig .PublicKeyCallback (s , pubKey )
606
+ _ , isPartialSuccessError := candidate .result .(* PartialSuccessError )
607
+
608
+ if (candidate .result == nil || isPartialSuccessError ) &&
609
+ candidate .perms != nil &&
610
+ candidate .perms .CriticalOptions != nil &&
611
+ candidate .perms .CriticalOptions [sourceAddressCriticalOption ] != "" {
612
+ if err := checkSourceAddress (
568
613
s .RemoteAddr (),
569
- candidate .perms .CriticalOptions [sourceAddressCriticalOption ])
614
+ candidate .perms .CriticalOptions [sourceAddressCriticalOption ]); err != nil {
615
+ candidate .result = err
616
+ }
570
617
}
571
618
cache .add (candidate )
572
619
}
@@ -578,8 +625,8 @@ userAuthLoop:
578
625
if len (payload ) > 0 {
579
626
return nil , parseError (msgUserAuthRequest )
580
627
}
581
-
582
- if candidate .result == nil {
628
+ _ , isPartialSuccessError := candidate . result .( * PartialSuccessError )
629
+ if candidate .result == nil || isPartialSuccessError {
583
630
okMsg := userAuthPubKeyOkMsg {
584
631
Algo : algo ,
585
632
PubKey : pubKeyData ,
@@ -629,11 +676,11 @@ userAuthLoop:
629
676
perms = candidate .perms
630
677
}
631
678
case "gssapi-with-mic" :
632
- if config .GSSAPIWithMICConfig == nil {
679
+ if authConfig .GSSAPIWithMICConfig == nil {
633
680
authErr = errors .New ("ssh: gssapi-with-mic auth not configured" )
634
681
break
635
682
}
636
- gssapiConfig := config .GSSAPIWithMICConfig
683
+ gssapiConfig := authConfig .GSSAPIWithMICConfig
637
684
userAuthRequestGSSAPI , err := parseGSSAPIPayload (userAuthReq .Payload )
638
685
if err != nil {
639
686
return nil , parseError (msgUserAuthRequest )
@@ -689,49 +736,70 @@ userAuthLoop:
689
736
break userAuthLoop
690
737
}
691
738
692
- authFailures ++
693
- if config .MaxAuthTries > 0 && authFailures >= config .MaxAuthTries {
694
- // If we have hit the max attempts, don't bother sending the
695
- // final SSH_MSG_USERAUTH_FAILURE message, since there are
696
- // no more authentication methods which can be attempted,
697
- // and this message may cause the client to re-attempt
698
- // authentication while we send the disconnect message.
699
- // Continue, and trigger the disconnect at the start of
700
- // the loop.
701
- //
702
- // The SSH specification is somewhat confusing about this,
703
- // RFC 4252 Section 5.1 requires each authentication failure
704
- // be responded to with a respective SSH_MSG_USERAUTH_FAILURE
705
- // message, but Section 4 says the server should disconnect
706
- // after some number of attempts, but it isn't explicit which
707
- // message should take precedence (i.e. should there be a failure
708
- // message than a disconnect message, or if we are going to
709
- // disconnect, should we only send that message.)
710
- //
711
- // Either way, OpenSSH disconnects immediately after the last
712
- // failed authnetication attempt, and given they are typically
713
- // considered the golden implementation it seems reasonable
714
- // to match that behavior.
715
- continue
739
+ var failureMsg userAuthFailureMsg
740
+
741
+ if partialSuccess , ok := authErr .(* PartialSuccessError ); ok {
742
+ // After a partial success error we don't allow changing the user
743
+ // name and execute the NoClientAuthCallback.
744
+ partialSuccessReturned = true
745
+
746
+ // In case a partial success is returned, the server may send
747
+ // a new set of authentication methods.
748
+ authConfig = partialSuccess .Next
749
+
750
+ // Reset pubkey cache, as the new PublicKeyCallback might
751
+ // accept a different set of public keys.
752
+ cache = pubKeyCache {}
753
+
754
+ // Send back a partial success message to the user.
755
+ failureMsg .PartialSuccess = true
756
+ } else {
757
+ // Allow initial attempt of 'none' without penalty.
758
+ if authFailures > 0 || userAuthReq .Method != "none" {
759
+ authFailures ++
760
+ }
761
+ if config .MaxAuthTries > 0 && authFailures >= config .MaxAuthTries {
762
+ // If we have hit the max attempts, don't bother sending the
763
+ // final SSH_MSG_USERAUTH_FAILURE message, since there are
764
+ // no more authentication methods which can be attempted,
765
+ // and this message may cause the client to re-attempt
766
+ // authentication while we send the disconnect message.
767
+ // Continue, and trigger the disconnect at the start of
768
+ // the loop.
769
+ //
770
+ // The SSH specification is somewhat confusing about this,
771
+ // RFC 4252 Section 5.1 requires each authentication failure
772
+ // be responded to with a respective SSH_MSG_USERAUTH_FAILURE
773
+ // message, but Section 4 says the server should disconnect
774
+ // after some number of attempts, but it isn't explicit which
775
+ // message should take precedence (i.e. should there be a failure
776
+ // message than a disconnect message, or if we are going to
777
+ // disconnect, should we only send that message.)
778
+ //
779
+ // Either way, OpenSSH disconnects immediately after the last
780
+ // failed authnetication attempt, and given they are typically
781
+ // considered the golden implementation it seems reasonable
782
+ // to match that behavior.
783
+ continue
784
+ }
716
785
}
717
786
718
- var failureMsg userAuthFailureMsg
719
- if config .PasswordCallback != nil {
787
+ if authConfig .PasswordCallback != nil {
720
788
failureMsg .Methods = append (failureMsg .Methods , "password" )
721
789
}
722
- if config .PublicKeyCallback != nil {
790
+ if authConfig .PublicKeyCallback != nil {
723
791
failureMsg .Methods = append (failureMsg .Methods , "publickey" )
724
792
}
725
- if config .KeyboardInteractiveCallback != nil {
793
+ if authConfig .KeyboardInteractiveCallback != nil {
726
794
failureMsg .Methods = append (failureMsg .Methods , "keyboard-interactive" )
727
795
}
728
- if config .GSSAPIWithMICConfig != nil && config .GSSAPIWithMICConfig .Server != nil &&
729
- config .GSSAPIWithMICConfig .AllowLogin != nil {
796
+ if authConfig .GSSAPIWithMICConfig != nil && authConfig .GSSAPIWithMICConfig .Server != nil &&
797
+ authConfig .GSSAPIWithMICConfig .AllowLogin != nil {
730
798
failureMsg .Methods = append (failureMsg .Methods , "gssapi-with-mic" )
731
799
}
732
800
733
801
if len (failureMsg .Methods ) == 0 {
734
- return nil , errors .New ("ssh: no authentication methods configured but NoClientAuth is also false " )
802
+ return nil , errors .New ("ssh: no authentication methods available " )
735
803
}
736
804
737
805
if err := s .transport .writePacket (Marshal (& failureMsg )); err != nil {
0 commit comments