feat(api): support OIDC workload token via HTTP Basic auth

Agent-Logs-Url: https://github.com/project-zot/zot/sessions/0c0a0243-d702-44d5-a93f-457595fe485d

Co-authored-by: rchincha <45800463+rchincha@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-18 22:37:34 +00:00
committed by GitHub
parent eda8739fb7
commit 3b040cc6d7
3 changed files with 101 additions and 7 deletions
+39
View File
@@ -1239,6 +1239,45 @@ func TestBearerOIDCWorkloadIdentity(t *testing.T) {
So(resp.StatusCode, ShouldEqual, http.StatusOK)
})
Convey("OIDC authentication success with token in basic auth password", func() {
conf := config.New()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf.HTTP.Port = port
conf.HTTP.Auth = &config.AuthConfig{
Bearer: &config.BearerConfig{
OIDC: []config.BearerOIDCConfig{{
Issuer: issuer,
Audiences: []string{audience},
}},
},
}
conf.Storage.RootDirectory = t.TempDir()
ctlr := api.NewController(conf)
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
token, err := createWorkloadOIDCToken(privKey, issuer, audience, nil)
So(err, ShouldBeNil)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, baseURL+"/v2/_catalog", nil)
So(err, ShouldBeNil)
basicAuth := base64.StdEncoding.EncodeToString([]byte("workload:" + token))
req.Header.Set("Authorization", "Basic "+basicAuth)
client := &http.Client{}
resp, err := client.Do(req)
So(err, ShouldBeNil)
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusOK)
})
Convey("OIDC authentication success with groups", func() {
conf := config.New()
port := test.GetFreePort()
+46 -7
View File
@@ -4,11 +4,12 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
@@ -25,8 +26,6 @@ import (
// a refresh roughly once per minute, but this is best-effort and not a strict upper bound.
const oidcProviderRefreshInterval = 1 * time.Minute
var bearerOIDCTokenMatch = regexp.MustCompile("(?i)bearer (.*)")
// OIDCBearerAuthorizer validates OIDC ID tokens for workload identity authentication.
type OIDCBearerAuthorizer struct {
providers []*oidcProvider
@@ -169,10 +168,10 @@ func (a *oidcProvider) authenticate(ctx context.Context, header string) (*cel.Cl
return nil, zerr.ErrNoBearerToken
}
// Extract token from Authorization header
tokenString := bearerOIDCTokenMatch.ReplaceAllString(header, "$1")
if tokenString == "" || tokenString == header {
return nil, zerr.ErrInvalidBearerToken
// Extract token from Authorization header.
tokenString, err := getOIDCTokenFromAuthorizationHeader(header)
if err != nil {
return nil, err
}
// Get verifier.
@@ -210,6 +209,46 @@ func (a *oidcProvider) authenticate(ctx context.Context, header string) (*cel.Cl
return res, nil
}
func getOIDCTokenFromAuthorizationHeader(header string) (string, error) {
splitStr := strings.SplitN(header, " ", 2) //nolint:mnd
if len(splitStr) != 2 {
return "", zerr.ErrInvalidBearerToken
}
switch strings.ToLower(splitStr[0]) {
case "bearer":
tokenString := strings.TrimSpace(splitStr[1])
if tokenString == "" {
return "", zerr.ErrInvalidBearerToken
}
return tokenString, nil
case "basic":
decodedStr, err := base64.StdEncoding.DecodeString(splitStr[1])
if err != nil {
return "", zerr.ErrInvalidBearerToken
}
pair := strings.SplitN(string(decodedStr), ":", 2) //nolint:mnd
if len(pair) != 2 { //nolint:mnd
return "", zerr.ErrInvalidBearerToken
}
tokenString := pair[1]
if tokenString == "" {
tokenString = pair[0]
}
if strings.TrimSpace(tokenString) == "" {
return "", zerr.ErrInvalidBearerToken
}
return tokenString, nil
default:
return "", zerr.ErrInvalidBearerToken
}
}
// getVerifier retrieves or refreshes the oidc.IDTokenVerifier as needed.
func (o *oidcProvider) getVerifier(ctx context.Context) (*oidc.IDTokenVerifier, error) {
// If the verifier is still fresh, return it.
+16
View File
@@ -6,6 +6,7 @@ import (
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"maps"
@@ -181,6 +182,21 @@ func TestOIDCBearerAuthorizer(t *testing.T) {
So(result.Groups, ShouldBeEmpty)
})
Convey("Valid token in basic auth password", func() {
subject := "test-user" //nolint:goconst
token, err := createTestOIDCToken(privKey, issuer, audience, subject, nil)
So(err, ShouldBeNil)
basicCredentials := base64.StdEncoding.EncodeToString([]byte("workload:" + token))
authHeader := "Basic " + basicCredentials
result, err := authorizer.Authenticate(ctx, authHeader)
So(err, ShouldBeNil)
So(result, ShouldNotBeNil)
So(result.Username, ShouldEqual, issuer+"/"+subject)
So(result.Groups, ShouldBeEmpty)
})
Convey("Valid token with groups", func() {
subject := "test-user"
testGroups := []string{"group1", "group2"}