mirror of
https://github.com/project-zot/zot.git
synced 2026-06-18 13:37:57 +08:00
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:
committed by
GitHub
parent
af99f64534
commit
4bc4261e84
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user