Skip to content

Commit

Permalink
GitLab registry header auth
Browse files Browse the repository at this point in the history
* add auth to oci_downloader for gitlab registries
* authentication has same workflow as public auth but with authenticated token fetch

Signed-off-by: Florian Schrag <f@schr.ag>
  • Loading branch information
gitu authored and ashutosh-narkar committed Jul 21, 2023
1 parent 768dcd9 commit fe50e18
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 7 deletions.
29 changes: 25 additions & 4 deletions download/oci_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,7 @@ func dockerResolver(plugin rest.HTTPAuthPlugin, config *rest.Config, logger logg

authorizer := pluginAuthorizer{
plugin: plugin,
authorizer: docker.NewDockerAuthorizer(
docker.WithAuthClient(client),
),
client: client,
logger: logger,
}

Expand All @@ -356,7 +354,11 @@ func dockerResolver(plugin rest.HTTPAuthPlugin, config *rest.Config, logger logg
}

type pluginAuthorizer struct {
plugin rest.HTTPAuthPlugin
plugin rest.HTTPAuthPlugin
client *http.Client

// authorizer will be populated by the first call to pluginAuthorizer.Prepare
// since it requires a first pass through the plugin.Prepare method.
authorizer docker.Authorizer

logger logging.Logger
Expand All @@ -380,6 +382,25 @@ func (a *pluginAuthorizer) Authorize(ctx context.Context, req *http.Request) err
return err
}

if a.authorizer == nil {
// Some registry authentication implementations require a token fetch from
// a separate authenticated token server. This flow is described in the
// docker token auth spec:
// https://docs.docker.com/registry/spec/auth/token/#requesting-a-token
//
// Unfortunately, the containerd implementation does not use the Prepare
// mechanism to authenticate these token requests and we need to add
// auth information in form of a static docker.WithAuthHeader.
//
// Since rest.HTTPAuthPlugins will set the auth header on the request
// passed to HTTPAuthPlugin.Prepare, we can use it afterwards to build
// our docker.Authorizer.
a.authorizer = docker.NewDockerAuthorizer(
docker.WithAuthHeader(req.Header),
docker.WithAuthClient(a.client),
)
}

return a.authorizer.Authorize(ctx, req)
}

Expand Down
39 changes: 39 additions & 0 deletions download/oci_download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,45 @@ func TestOCIPublicRegistryAuth(t *testing.T) {
}
}

// TestOCITokenAuth tests the registry `token` auth that is used for some registries (f.e. gitlab).
// After the initial fetch the token has to be added to the request that fetches the temporary token.
// This test verifies that the token is added to the second token request.
func TestOCITokenAuth(t *testing.T) {
ctx := context.Background()
fixture := newTestFixture(t, withAuthenticatedTokenAuth())
plainToken := "secret"
token := base64.StdEncoding.EncodeToString([]byte(plainToken)) // token should be base64 encoded
fixture.server.expAuth = fmt.Sprintf("Bearer %s", token) // test on private repository
fixture.server.expEtag = "sha256:c5834dbce332cabe6ae68a364de171a50bf5b08024c27d7c08cc72878b4df7ff"

restConf := fmt.Sprintf(`{
"url": %q,
"type": "oci",
"credentials": {
"bearer": {
"token": %q
}
}
}`, fixture.server.server.URL, plainToken)

client, err := rest.New([]byte(restConf), map[string]*keys.Config{})
if err != nil {
t.Fatalf("failed to create rest client: %s", err)
}
fixture.setClient(client)

config := Config{}
if err := config.ValidateAndInjectDefaults(); err != nil {
t.Fatal(err)
}

d := NewOCI(Config{}, fixture.client, "ghcr.io/org/repo:latest", t.TempDir())

if err := d.oneShot(ctx); err != nil {
t.Fatalf("Unexpected error: %s", err)
}
}

func TestOCICustomAuthPlugin(t *testing.T) {
fixture := newTestFixture(t)
defer fixture.server.stop()
Expand Down
86 changes: 83 additions & 3 deletions download/testharness.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,70 @@ func withPublicRegistryAuth() fixtureOpt {
}
}

// withAuthenticatedTokenAuth sets up a token auth flow according to
// the spec https://docs.docker.com/registry/spec/auth/token/.
//
// The flow is the same as for public registries but additionally
// the request for fetching the token also has to be authenticated.
// Used for example with gitlab registries.
//
// The token issuing and validation differs between providers,
// and we only use a minimal version for testing.
func withAuthenticatedTokenAuth() fixtureOpt {
const token = "some-test-token"
tokenServer := httptest.NewServer(tokenHandlerAuth("c2VjcmV0", token))

const wwwAuthenticateFmt = "Bearer realm=%q service=%q scope=%q"
tokenServiceURL := tokenServer.URL + "/token"
wwwAuthenticate := fmt.Sprintf(wwwAuthenticateFmt,
tokenServiceURL,
"testRegistry.io",
"[pull]")

return func(tf *testFixture) error {
tf.server.customAuth = func(w http.ResponseWriter, r *http.Request) error {

authHeader := r.Header.Get("Authorization")

if authHeader == "" {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("no authorization header: %w", errUnauthorized)
}

if !strings.HasPrefix(authHeader, "Bearer ") {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("expects bearer scheme: %w", errUnauthorized)
}

bearerToken := strings.TrimPrefix(authHeader, "Bearer ")
if bearerToken != token {
w.Header().Set("WWW-Authenticate", wwwAuthenticate)
return fmt.Errorf("token %q doesn't match %q: %w", bearerToken, token, errUnauthorized)
}

return nil
}

return nil
}
}

// tokenHandler returns an http.Handler that responds with the
// specified token to GET /token requests.
func tokenHandler(token string) http.HandlerFunc {
func tokenHandler(issuedToken string) http.HandlerFunc {
return tokenHandlerAuth("", issuedToken)
}

// tokenHandlerAuth returns an http.Handler that responds with the
// specified token to GET /token requests.
//
// If expectedToken is not empty, the handler will check that the
// Authorization header matches the expected token.
func tokenHandlerAuth(expectedToken, issuedToken string) http.HandlerFunc {
tokenResponse := struct {
Token string `json:"token"`
}{
Token: token,
Token: issuedToken,
}

responseBody, err := json.Marshal(tokenResponse)
Expand All @@ -137,7 +194,30 @@ func tokenHandler(token string) http.HandlerFunc {
return
}

w.Write(responseBody)
// If no expected token is set, we don't check the Authorization header.
if expectedToken == "" {
_, _ = w.Write(responseBody)
return
}

authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}

if !strings.HasPrefix(authHeader, "Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
return
}

bearerToken := strings.TrimPrefix(authHeader, "Bearer ")
if bearerToken != expectedToken {
w.WriteHeader(http.StatusUnauthorized)
return
}

_, _ = w.Write(responseBody)
}
}

Expand Down

0 comments on commit fe50e18

Please sign in to comment.