6666 errFailedExecuteChallengeRequest = errors .New ("failed to execute challenge request" )
6767 // errFailedCreateBearerRequest indicates a failure to construct the HTTP request for a bearer token.
6868 errFailedCreateBearerRequest = errors .New ("failed to create bearer token request" )
69+ // errFailedConstructBearerAuthURL indicates a failure to construct the bearer authentication URL.
70+ errFailedConstructBearerAuthURL = errors .New ("failed to construct bearer auth url" )
6971 // errFailedExecuteBearerRequest indicates a failure to send or receive a response for the bearer token request.
7072 errFailedExecuteBearerRequest = errors .New ("failed to execute bearer token request" )
7173 // errFailedUnmarshalBearerResponse indicates a failure to parse the bearer token response JSON.
@@ -265,44 +267,31 @@ func handleBearerAuth(
265267) (string , string , bool , string , error ) {
266268 logrus .WithFields (fields ).Debug ("Entering Bearer auth path" )
267269
268- var challengeHost string
269-
270- // Parse the WWW-Authenticate header.
271- scope , realm , service , err := ProcessChallenge (wwwAuthHeader , container .ImageName ())
272- logrus .WithFields (fields ).
273- WithField ("realm" , realm ).
274- WithField ("service" , service ).
275- WithField ("scope" , scope ).
276- WithField ("err" , err ).
277- Debug ("Processed challenge header" )
278-
279- switch {
280- case err != nil :
281- logrus .WithError (err ).WithFields (fields ).Debug ("Failed to process challenge header" )
282- // Proceed with token retrieval, as challengeHost is optional.
283- case realm != "" :
284- challengeHost = extractChallengeHost (realm , fields )
285- if challengeHost != "" {
286- logrus .WithFields (fields ).
287- WithField ("challenge_host" , challengeHost ).
288- Debug ("Extracted challenge host" )
289- }
290- default :
291- logrus .WithFields (fields ).Debug ("Empty realm in challenge header" )
292- }
293-
294- // Fetch the bearer token.
295270 normalizedRef , err := reference .ParseNormalizedNamed (container .ImageName ())
296271 if err != nil {
297272 logrus .WithError (err ).WithFields (fields ).Debug ("Failed to parse image name" )
298273
299274 return "" , "" , redirected , redirectHost , fmt .Errorf ("%w: %w" , errFailedParseImageName , err )
300275 }
301276
302- token , err := GetBearerHeader (
277+ authURL , err := GetAuthURL (strings .ToLower (wwwAuthHeader ), normalizedRef )
278+ if err != nil {
279+ logrus .WithError (err ).WithFields (fields ).Debug ("Failed to construct bearer auth URL" )
280+
281+ return "" , "" , redirected , redirectHost , fmt .Errorf ("%w: %w" , errFailedConstructBearerAuthURL , err )
282+ }
283+
284+ challengeHost := authURL .Host
285+ if challengeHost != "" {
286+ logrus .WithFields (fields ).
287+ WithField ("challenge_host" , challengeHost ).
288+ Debug ("Extracted challenge host" )
289+ }
290+
291+ token , err := getBearerHeader (
303292 ctx ,
304- strings . ToLower ( wwwAuthHeader ) ,
305- normalizedRef ,
293+ authURL ,
294+ container . ImageName () ,
306295 registryAuth ,
307296 client ,
308297 )
@@ -489,6 +478,7 @@ func GetToken(
489478// ProcessChallenge parses the WWW-Authenticate header to extract authentication details.
490479//
491480// It supports Bearer authentication, extracting the realm, service, and optional scope for token requests.
481+ // If a registry omits service, the service is derived from the realm host.
492482//
493483// Parameters:
494484// - wwwAuthHeader: The WWW-Authenticate header value (e.g., 'Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:linuxserver/nginx:pull"').
@@ -498,7 +488,7 @@ func GetToken(
498488// - string: The scope for the token request (e.g., "repository:linuxserver/nginx:pull"), or empty if not provided.
499489// - string: The realm URL for the token request (e.g., "https://ghcr.io/token").
500490// - string: The service identifier (e.g., "ghcr.io").
501- // - error: Non-nil if parsing fails critically (missing realm or service), nil otherwise.
491+ // - error: Non-nil if parsing fails critically (missing realm or derivable service), nil otherwise.
502492func ProcessChallenge (wwwAuthHeader , image string ) (string , string , string , error ) {
503493 fields := logrus.Fields {
504494 "image" : image ,
@@ -524,11 +514,20 @@ func ProcessChallenge(wwwAuthHeader, image string) (string, string, string, erro
524514 }
525515 }
526516
527- realm , realmOK := values ["realm" ]
528- service , serviceOK := values ["service" ]
517+ realm := values ["realm" ]
518+ service := values ["service" ]
529519 scope := values ["scope" ] // Scope is optional
530520
531- if ! realmOK || ! serviceOK {
521+ if service == "" && realm != "" {
522+ service = extractChallengeHost (realm , fields )
523+ if service != "" {
524+ logrus .WithFields (fields ).
525+ WithField ("service" , service ).
526+ Debug ("Derived challenge service from realm" )
527+ }
528+ }
529+
530+ if realm == "" || service == "" {
532531 logrus .WithFields (fields ).Warn ("Missing required challenge header values: realm or service" )
533532
534533 return "" , "" , "" , fmt .Errorf (
@@ -610,70 +609,97 @@ func GetBearerHeader(
610609 return "" , err
611610 }
612611
613- // Build the token request with context.
612+ return getBearerHeader (ctx , authURL , imageRef .Name (), registryAuth , client )
613+ }
614+
615+ func getBearerHeader (
616+ ctx context.Context ,
617+ authURL * url.URL ,
618+ imageName string ,
619+ registryAuth string ,
620+ client Client ,
621+ ) (string , error ) {
622+ r , err := newBearerRequest (ctx , authURL , imageName )
623+ if err != nil {
624+ return "" , err
625+ }
626+
627+ addBasicAuth (r , imageName , registryAuth )
628+ logrus .WithField ("url" , r .URL .String ()).Debug ("Sending bearer token request" )
629+
630+ authResponse , err := client .Do (r )
631+ if err != nil {
632+ logrus .WithError (err ).WithFields (logrus.Fields {
633+ "image" : imageName ,
634+ "url" : authURL .String (),
635+ }).Debug ("Failed to execute bearer token request" )
636+
637+ return "" , fmt .Errorf ("%w: %w" , errFailedExecuteBearerRequest , err )
638+ }
639+
640+ defer authResponse .Body .Close ()
641+
642+ token , err := readBearerToken (authResponse .Body , imageName )
643+ if err != nil {
644+ return "" , err
645+ }
646+
647+ logrus .WithFields (logrus.Fields {
648+ "image" : imageName ,
649+ }).Debug ("Retrieved bearer token" )
650+
651+ return "Bearer " + token , nil
652+ }
653+
654+ func newBearerRequest (ctx context.Context , authURL * url.URL , imageName string ) (* http.Request , error ) {
614655 r , err := http .NewRequestWithContext (ctx , http .MethodGet , authURL .String (), nil )
615656 if err != nil {
616657 logrus .WithError (err ).WithFields (logrus.Fields {
617- "image" : imageRef . Name () ,
658+ "image" : imageName ,
618659 "url" : authURL .String (),
619660 }).Debug ("Failed to create bearer token request" )
620661
621- return "" , fmt .Errorf ("%w: %w" , errFailedCreateBearerRequest , err )
662+ return nil , fmt .Errorf ("%w: %w" , errFailedCreateBearerRequest , err )
622663 }
623664
624- // Add Basic auth header if credentials are provided.
665+ return r , nil
666+ }
667+
668+ func addBasicAuth (r * http.Request , imageName , registryAuth string ) {
625669 if registryAuth != "" {
626670 logrus .WithFields (logrus.Fields {
627- "image" : imageRef . Name () ,
671+ "image" : imageName ,
628672 }).Debug ("Found credentials" )
629673
630674 if logrus .GetLevel () == logrus .TraceLevel {
631675 logrus .WithFields (logrus.Fields {
632- "image" : imageRef . Name () ,
676+ "image" : imageName ,
633677 "registryAuth" : registryAuth ,
634678 }).Trace ("Using credentials" )
635679 }
636680
637681 r .Header .Add ("Authorization" , "Basic " + registryAuth )
638682 } else {
639683 logrus .WithFields (logrus.Fields {
640- "image" : imageRef . Name () ,
684+ "image" : imageName ,
641685 }).Debug ("No credentials found" )
642686 }
687+ }
643688
644- // Execute the token request.
645- logrus .WithField ("url" , r .URL .String ()).Debug ("Sending bearer token request" )
646-
647- authResponse , err := client .Do (r )
648- if err != nil {
649- logrus .WithError (err ).WithFields (logrus.Fields {
650- "image" : imageRef .Name (),
651- "url" : authURL .String (),
652- }).Debug ("Failed to execute bearer token request" )
653-
654- return "" , fmt .Errorf ("%w: %w" , errFailedExecuteBearerRequest , err )
655- }
656-
657- defer authResponse .Body .Close ()
658-
659- // Read and parse the response body into a token structure.
660- body , _ := io .ReadAll (authResponse .Body )
689+ func readBearerToken (body io.Reader , imageName string ) (string , error ) {
690+ b , _ := io .ReadAll (body )
661691 tokenResponse := & types.TokenResponse {}
662692
663- err = json .Unmarshal (body , tokenResponse )
693+ err : = json .Unmarshal (b , tokenResponse )
664694 if err != nil {
665695 logrus .WithError (err ).
666- WithField ("image" , imageRef . Name () ).
696+ WithField ("image" , imageName ).
667697 Debug ("Failed to unmarshal bearer token response" )
668698
669699 return "" , fmt .Errorf ("%w: %w" , errFailedUnmarshalBearerResponse , err )
670700 }
671701
672- logrus .WithFields (logrus.Fields {
673- "image" : imageRef .Name (),
674- }).Debug ("Retrieved bearer token" )
675-
676- return "Bearer " + tokenResponse .Token , nil
702+ return tokenResponse .Token , nil
677703}
678704
679705// GetAuthURL constructs an authentication URL from challenge instructions.
@@ -726,6 +752,19 @@ func GetAuthURL(challenge string, imageRef reference.Named) (*url.URL, error) {
726752 values ["realm" ] = "https://ghcr.io/token"
727753 }
728754
755+ if values ["service" ] == "" && values ["realm" ] != "" {
756+ values ["service" ] = extractChallengeHost (values ["realm" ], logrus.Fields {
757+ "image" : imageRef .Name (),
758+ "challenge" : challenge ,
759+ })
760+ if values ["service" ] != "" {
761+ logrus .WithFields (logrus.Fields {
762+ "image" : imageRef .Name (),
763+ "service" : values ["service" ],
764+ }).Debug ("Derived challenge service from realm" )
765+ }
766+ }
767+
729768 logrus .WithFields (logrus.Fields {
730769 "image" : imageRef .Name (),
731770 "realm" : values ["realm" ],
0 commit comments