feat(api): gate OIDC basic token auth behind config flag

Agent-Logs-Url: https://github.com/project-zot/zot/sessions/2e5ae107-9578-43c4-b5f8-6e84e19fba6e

Co-authored-by: rchincha <45800463+rchincha@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-19 00:33:04 +00:00
committed by GitHub
parent af99f64534
commit 4bc4261e84
6 changed files with 83 additions and 4 deletions
+7 -1
View File
@@ -31,7 +31,8 @@ Add OIDC workload identity configuration to your bearer authentication settings.
"oidc": [
{
"issuer": "https://kubernetes.default.svc.cluster.local",
"audiences": ["zot"]
"audiences": ["zot"],
"allowBasicAuth": true
}
]
}
@@ -55,6 +56,10 @@ Add OIDC workload identity configuration to your bearer authentication settings.
- **`username`**: CEL expression to extract the username. Default: `"claims.iss + '/' + claims.sub"`
- **`groups`**: CEL expression to extract groups. Default: none (no groups extracted)
- **`allowBasicAuth`** (optional): Allow OIDC token extraction from HTTP Basic credentials (`username:token`).
- Default: `false`
- Use this only when clients cannot send Bearer tokens.
- **`certificateAuthority`** (optional): PEM-encoded CA certificate to validate the OIDC provider's TLS certificate. Useful when the OIDC issuer uses a private CA (e.g., Kubernetes API server with a self-signed certificate). Mutually exclusive with `certificateAuthorityFile`.
- **`certificateAuthorityFile`** (optional): Path to a PEM-encoded CA certificate file to validate the OIDC provider's TLS certificate. Mutually exclusive with `certificateAuthority`.
@@ -105,6 +110,7 @@ is specified (so the whole `claimMapping` section could be omitted in this examp
{
"issuer": "https://kubernetes.default.svc.cluster.local",
"audiences": ["zot", "https://zot.example.com"],
"allowBasicAuth": true,
"claimMapping": {
"username": "claims.iss + '/' + claims.sub"
}
@@ -14,6 +14,7 @@
{
"issuer": "https://kubernetes.default.svc.cluster.local",
"audiences": ["zot", "https://zot.example.com"],
"allowBasicAuth": true,
"claimMapping": {
"username": "claims.sub"
}
+41 -1
View File
@@ -1244,6 +1244,46 @@ func TestBearerOIDCWorkloadIdentity(t *testing.T) {
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},
AllowBasicAuth: true,
}},
},
}
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 with token in basic auth password is disabled by default", func() {
conf := config.New()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf.HTTP.Port = port
conf.HTTP.Auth = &config.AuthConfig{
Bearer: &config.BearerConfig{
@@ -1275,7 +1315,7 @@ func TestBearerOIDCWorkloadIdentity(t *testing.T) {
So(err, ShouldBeNil)
defer resp.Body.Close()
So(resp.StatusCode, ShouldEqual, http.StatusOK)
So(resp.StatusCode, ShouldEqual, http.StatusUnauthorized)
})
Convey("OIDC authentication success with groups", func() {
+8 -2
View File
@@ -37,6 +37,7 @@ type oidcProvider struct {
issuer string
audiences []string
claimProcessor *cel.ClaimProcessor
allowBasicAuth bool
skipIssuerCheck bool
httpClient *http.Client
log log.Logger
@@ -157,6 +158,7 @@ func newOIDCProvider(oidcConfig *config.BearerOIDCConfig, log log.Logger) (*oidc
issuer: oidcConfig.Issuer,
audiences: oidcConfig.Audiences,
claimProcessor: claimProcessor,
allowBasicAuth: oidcConfig.AllowBasicAuth,
skipIssuerCheck: oidcConfig.SkipIssuerVerification,
httpClient: httpClient,
log: log,
@@ -169,7 +171,7 @@ func (a *oidcProvider) authenticate(ctx context.Context, header string) (*cel.Cl
}
// Extract token from Authorization header.
tokenString, err := getOIDCTokenFromAuthorizationHeader(header)
tokenString, err := getOIDCTokenFromAuthorizationHeader(header, a.allowBasicAuth)
if err != nil {
return nil, err
}
@@ -209,7 +211,7 @@ func (a *oidcProvider) authenticate(ctx context.Context, header string) (*cel.Cl
return res, nil
}
func getOIDCTokenFromAuthorizationHeader(header string) (string, error) {
func getOIDCTokenFromAuthorizationHeader(header string, allowBasicAuth bool) (string, error) {
splitStr := strings.SplitN(header, " ", 2) //nolint:mnd
if len(splitStr) != 2 {
return "", zerr.ErrInvalidBearerToken
@@ -224,6 +226,10 @@ func getOIDCTokenFromAuthorizationHeader(header string) (string, error) {
return tokenString, nil
case "basic":
if !allowBasicAuth {
return "", zerr.ErrInvalidBearerToken
}
decodedStr, err := base64.StdEncoding.DecodeString(splitStr[1])
if err != nil {
return "", fmt.Errorf("%w: %w", zerr.ErrInvalidBearerToken, err)
+21
View File
@@ -184,6 +184,14 @@ func TestOIDCBearerAuthorizer(t *testing.T) {
Convey("Valid token in basic auth password", func() {
subject := "test-user" //nolint:goconst
cfg := []config.BearerOIDCConfig{{
Issuer: issuer,
Audiences: []string{audience},
AllowBasicAuth: true,
}}
authorizer, err := api.NewOIDCBearerAuthorizer(cfg, logger)
So(err, ShouldBeNil)
token, err := createTestOIDCToken(privKey, issuer, audience, subject, nil)
So(err, ShouldBeNil)
@@ -197,6 +205,19 @@ func TestOIDCBearerAuthorizer(t *testing.T) {
So(result.Groups, ShouldBeEmpty)
})
Convey("Basic auth token is rejected by default", 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, ShouldNotBeNil)
So(result, ShouldBeNil)
})
Convey("Valid token with groups", func() {
subject := "test-user"
testGroups := []string{"group1", "group2"}
+5
View File
@@ -250,6 +250,11 @@ type BearerOIDCConfig struct {
// Default: {"username":"claims.iss + '/' + claims.sub"}
ClaimMapping *CELClaimValidationAndMapping `json:"claimMapping,omitempty" mapstructure:"claimMapping,omitempty"`
// AllowBasicAuth enables extracting OIDC tokens from HTTP Basic credentials
// (using the password field, or username if password is empty).
// Default: false
AllowBasicAuth bool `json:"allowBasicAuth,omitempty" mapstructure:"allowBasicAuth,omitempty"`
// CertificateAuthority is a PEM-encoded optional CA certificate to validate the OIDC provider's TLS certificate.
// Mutually exclusive with CertificateAuthorityFile.
CertificateAuthority string `json:"certificateAuthority,omitempty" mapstructure:"certificateAuthority,omitempty"`