@@ -510,6 +510,106 @@ func TestDeviceLoginRetriesOnTransientTokenError(t *testing.T) {
510510 }
511511}
512512
513+ // TestDeviceLoginPending401 verifies polling survives providers (e.g. dex)
514+ // that return authorization_pending with HTTP 401 instead of the
515+ // RFC 8628-mandated 400: the OAuth error code in the body wins over the
516+ // HTTP status code.
517+ func TestDeviceLoginPending401 (t * testing.T ) {
518+ mux := http .NewServeMux ()
519+ mux .HandleFunc ("/device" , func (w http.ResponseWriter , r * http.Request ) {
520+ w .Header ().Set ("Content-Type" , "application/json" )
521+ _ = json .NewEncoder (w ).Encode (map [string ]interface {}{
522+ "device_code" : "dev_code_1" ,
523+ "user_code" : "CCCC-DDDD" ,
524+ "verification_uri" : "https://example.com/device" ,
525+ "expires_in" : 60 ,
526+ "interval" : 1 ,
527+ })
528+ })
529+
530+ var pollCount int32
531+ mux .HandleFunc ("/token" , func (w http.ResponseWriter , r * http.Request ) {
532+ w .Header ().Set ("Content-Type" , "application/json" )
533+ if atomic .AddInt32 (& pollCount , 1 ) == 1 {
534+ w .WriteHeader (http .StatusUnauthorized )
535+ _ = json .NewEncoder (w ).Encode (map [string ]string {"error" : "authorization_pending" })
536+ return
537+ }
538+ _ = json .NewEncoder (w ).Encode (map [string ]string {"id_token" : "token_after_pending_401" })
539+ })
540+
541+ server := httptest .NewServer (mux )
542+ defer server .Close ()
543+
544+ node := & SamNode {}
545+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
546+ defer cancel ()
547+
548+ token , err := node .DeviceLogin (ctx , server .URL + "/device" , server .URL + "/token" , "client_id_test" , "sam-e2e" , false )
549+ if err != nil {
550+ t .Fatalf ("DeviceLogin failed: %v" , err )
551+ }
552+ if token != "token_after_pending_401" {
553+ t .Fatalf ("Expected token_after_pending_401, got %q" , token )
554+ }
555+ if got := atomic .LoadInt32 (& pollCount ); got < 2 {
556+ t .Fatalf ("expected at least 2 poll attempts, got %d" , got )
557+ }
558+ }
559+
560+ // TestDeviceLoginFatalErrors verifies polling aborts on terminal responses:
561+ // a non-OAuth error body (regardless of status) and an OAuth error code
562+ // that is not a pending/slow_down signal.
563+ func TestDeviceLoginFatalErrors (t * testing.T ) {
564+ cases := []struct {
565+ name string
566+ status int
567+ body string
568+ wantErr string
569+ }{
570+ {"non-oauth 401" , http .StatusUnauthorized , `{"message":"nope"}` , "token request failed with status" },
571+ {"invalid_client 401" , http .StatusUnauthorized , `{"error":"invalid_client","error_description":"unknown client"}` , "invalid_client" },
572+ {"access_denied 400" , http .StatusBadRequest , `{"error":"access_denied"}` , "denied" },
573+ {"oauth-shaped 500 is not a protocol error" , http .StatusInternalServerError , `{"error":"server_error"}` , "server_error" },
574+ {"html 502 surfaces the body" , http .StatusBadGateway , `<html>bad gateway</html>` , "bad gateway" },
575+ }
576+ for _ , tc := range cases {
577+ t .Run (tc .name , func (t * testing.T ) {
578+ mux := http .NewServeMux ()
579+ mux .HandleFunc ("/device" , func (w http.ResponseWriter , r * http.Request ) {
580+ w .Header ().Set ("Content-Type" , "application/json" )
581+ _ = json .NewEncoder (w ).Encode (map [string ]interface {}{
582+ "device_code" : "dev_code_1" ,
583+ "user_code" : "EEEE-FFFF" ,
584+ "verification_uri" : "https://example.com/device" ,
585+ "expires_in" : 60 ,
586+ "interval" : 1 ,
587+ })
588+ })
589+ mux .HandleFunc ("/token" , func (w http.ResponseWriter , r * http.Request ) {
590+ w .Header ().Set ("Content-Type" , "application/json" )
591+ w .WriteHeader (tc .status )
592+ _ , _ = w .Write ([]byte (tc .body ))
593+ })
594+
595+ server := httptest .NewServer (mux )
596+ defer server .Close ()
597+
598+ node := & SamNode {}
599+ ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
600+ defer cancel ()
601+
602+ _ , err := node .DeviceLogin (ctx , server .URL + "/device" , server .URL + "/token" , "client_id_test" , "sam-e2e" , false )
603+ if err == nil {
604+ t .Fatal ("expected DeviceLogin to fail" )
605+ }
606+ if ! strings .Contains (err .Error (), tc .wantErr ) {
607+ t .Fatalf ("expected error containing %q, got %v" , tc .wantErr , err )
608+ }
609+ })
610+ }
611+ }
612+
513613func TestParseAuthMode (t * testing.T ) {
514614 cases := []struct {
515615 in string
0 commit comments