Skip to content

Should auth cache refresh expired bearer tokens when registries return invalid_token as 400? #1227

Description

@akashsinghal

Summary

oras-go's shared auth cache stores bearer tokens without tracking token expiry and refreshes only when a registry returns 401 Unauthorized. Some registries, notably public ECR, return 400 Bad Request with an invalid-token error when a bearer token is expired. In that case oras-go can keep reusing the cached expired token and surface repeated 400s instead of refreshing.

This appears to be the same class of issue we found in oras-dotnet. Related oras-dotnet scope issue: oras-project/oras-dotnet#395.

Now the real question is whether we want to support this. My interpretation of the spec is that 401 is the only valid auth signal to refresh off of. But I am curious to hear what the maintainers here believe.

Why this matters

Public ECR tokens expire after 1 hour. The token is opaque to a normal registry client, but the returned token body can be decoded as an outer JSON wrapper with an expiration epoch. After the token expires, public ECR responds to manifest requests using that token with:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{"errors":[{"code":"DENIED","message":"Your Authorization Token is invalid."}]}

Per RFC 6750, expired/invalid bearer tokens should generally be returned as 401 Unauthorized with WWW-Authenticate: Bearer ... error="invalid_token", allowing clients to obtain a new token. Since public ECR returns 400 instead, clients that refresh only on 401 do not recover.

oras-go behavior

From the current auth client/cache implementation:

  • cached bearer token is applied before making the request
  • the client only enters the challenge/token refresh path when resp.StatusCode == http.StatusUnauthorized
  • the default shared cache stores token strings by registry/scheme/scope key and does not track TTL/expiry

So an expired cached public ECR token can produce a persistent 400 until the cache is dropped or the process restarts.

Repro outline

Using public ECR and any digest that exists, for example a Redis manifest digest:

  1. Fetch a token:
    GET https://public.ecr.aws/token/?service=public.ecr.aws&scope=repository:docker/library/redis:pull
    
  2. Use it immediately against:
    HEAD/GET https://public.ecr.aws/v2/docker/library/redis/manifests/<digest>
    
    Result: 200.
  3. Wait until after the token's embedded expiration (~1 hour).
  4. Reuse the same token for the same HEAD/GET.
    Result: 400 with DENIED: Your Authorization Token is invalid.

Expected behavior

oras-go should be able to recover from this narrow invalid-token response, either by:

  1. honoring token expiry when available / when supplied by the token response, or
  2. treating a narrow registry invalid-token response as auth-refreshable, evicting the cached token for the current registry/scope and retrying once.

I do not think all 400s should be treated as auth-refreshable. The special handling should be limited to an invalid-token signature such as registry error code DENIED with message Your Authorization Token is invalid, ideally only when the request included a bearer token.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions