mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
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:
committed by
GitHub
parent
eda8739fb7
commit
3b040cc6d7
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user