Skip to content

⚠️ WIP ⚠️ Enable forwarding auth challenges to client#48847

Draft
laurazard wants to merge 1 commit intomoby:masterfrom
laurazard:client-stream-auth-requests
Draft

⚠️ WIP ⚠️ Enable forwarding auth challenges to client#48847
laurazard wants to merge 1 commit intomoby:masterfrom
laurazard:client-stream-auth-requests

Conversation

@laurazard
Copy link
Copy Markdown
Member

@laurazard laurazard commented Nov 11, 2024

- What I did

Implement client auth protocol – as discussed in #38591.

With this change, a client can opt-in to the new client auth flow by including a clientAuth URL parameter set to a non-falsy value ("0", "false", "[empty]").

If a client opts in, any 401 response with a WWW-Authenticate header returned by the registry causes the pull to fail and a 401 response with the unaltered WWW-Authenticate header to be returned to the client.

The client can then handle this challenge any way it sees fit, and initiate another request with the relevant auth configuration.


Consider an OAuth authentication scenario: previously, a client could make use of the IdentityToken

IdentityToken string `json:"identitytoken,omitempty"`
and RegistryToken
RegistryToken string `json:"registrytoken,omitempty"`
fields in the AuthConfig struct to send along a bearer and refresh token to be used for the registry.

When using the IdentityToken field, on any 401 response from the registry with an WWW-Authenticate header, the engine would attempt to use the refresh token passed here against the realm from the challenge to fetch an access token to use for authentication. After fetching the new credentials, the engine does not send them back to the client, which means the client cannot store the new, refreshed credentials.

On top of that, OAuth tenants frequently implement refresh token rotation – with this practice, each time that a client refreshes it's credentials with a refresh token flow, it receives a new access token AND a new refresh token, which it's expected to store and use for future refresh flows. When this is done, each individual refresh token has a shorter lifetime, after which it cannot be used. This is fine if a client stores the new refresh token every time it does a refresh flow, since the new refresh token has a fresh lifetime. However, given that the engine does not forward the new access + refresh tokens to the client, this flow is incompatible with the current engine auth implementations.

With the new client auth protocol implemented in this patch, instead of the client sending along a refresh token with the pull request, the client instead initiates the pull request, receives a 401 back from the engine with the auth challenge, refreshes it's credentials (and can store the new – rotated – refresh token), and then restarts the pull request with the relevant credentials for authentication.

- How I did it

Add a clientAuth flag to the /images/create endpoint, which toggles the client auth handling flow.

Also added a ChallengeHandlerFunc field to the API PullOptions struct, which is used to pass along "challenge handling closure" to be used by the API client.

If the client auth flow is used, then a new clientChallengeAuthorizer is used when initiating the request, which returns the new error type ErrAuthenticationChallenge if any challenge is received. This new ErrAuthenticationChallenge is bubbled up to the API handler, which handles it by responding with a 401 with the returned WWW-Authenticate header.

- How to verify it

Check the CLI PR implementing this – docker/cli#5606.

Note

While working at this, I first implemented something more akin to master...simonferquel:auth-streaming – i.e. negotiate credentials by streaming the auth requests while the pull is ongoing instead of responding to the /images/create request with a 401 and having the client handle the challenge and start the pull again. I can push this if it makes sense, but I prefer this more conventional/less complex approach.

- Description for the changelog

Add support for forwarding auth challenges during registry operations to the client initiating the registry operation.

- A picture of a cute animal (not mandatory but encouraged)

@thaJeztah
Copy link
Copy Markdown
Member

Looks like it needs a rebase 🙈

@laurazard
Copy link
Copy Markdown
Member Author

It doesn't really matter, it has vendored code that won't compile (yet), it's just for discussion 😅

@laurazard laurazard force-pushed the client-stream-auth-requests branch 2 times, most recently from 393293d to 78f8133 Compare November 12, 2024 10:28
@laurazard
Copy link
Copy Markdown
Member Author

No conflicts/it builds now :') Also significantly simplified the implementation.

@laurazard laurazard force-pushed the client-stream-auth-requests branch 4 times, most recently from 3bf75ff to 01ea7c2 Compare November 18, 2024 12:05
@laurazard laurazard added area/api API kind/feature Functionality or other elements that the project doesn't currently have. Features are new and shiny impact/api impact/changelog area/authentication labels Nov 18, 2024
@laurazard laurazard self-assigned this Nov 18, 2024
@laurazard laurazard force-pushed the client-stream-auth-requests branch from 01ea7c2 to e2d31a1 Compare November 18, 2024 15:33
Currently, for registry operations requiring auth (such as pull/push),
the client is expected to preemptively send credentials it believes will
be appropriate/required alonside with the image pull/push request to the
daemon, which the daemon will then use to negotiate any challenges (in
the form of `WWW-Authenticate` headers) it encounters while
communicating with the registry.

This means that, as it stands, the daemon has no avenue of communication
to request credentials as-needed, during a registry operation – all the
credentials must be sent up-front by the client (who without foresight
must somehow know credentials will actually be necessary – which poses
challenges when paired with mirrors for private registries, etc. since
in cases like these the client won't necessarily know where the engine
will actually be pulling images from, and we also don't want to just
send client credentials to any host).

This patch introduces a new way of handling auth during push/pull –
following what has been discussed in
moby#38591, instead of negotiating auth
on behalf of the client (using the provided credentials), the daemon
will instead send a `401` back to the client alonside with the received
`WWW-Authenticate` header when a challenge is received. The client can
then handle the challenge however it sees fit, pick the credentials it
wants to use (which addresses the issue with private registry mirrors)
and then initiate another pull with the relevant credentials.

This new auth handling flow is also better suited for situations where
oauth is being used to authenticate against the registry – in case it's
necessary to do a refresh flow to fetch a new access token, the client
can handle the refresh flow, store the new refresh+access tokens, and
start the pull with the new fresh credentials.

Signed-off-by: Laura Brehm <[email protected]>
Signed-off-by: Sebastiaan van Stijn <[email protected]>
@thaJeztah thaJeztah force-pushed the client-stream-auth-requests branch from e2d31a1 to a31f030 Compare November 4, 2025 09:25
@github-actions github-actions Bot added area/images Image Service area/daemon Core Engine containerd-integration Issues and PRs related to containerd integration module/client labels Nov 4, 2025
@thaJeztah
Copy link
Copy Markdown
Member

Did a rebase, as I think this is still something to explore /cc @Benehiko

(some changes got easier, as we now have some option-structs for the backend, so no need to change signatures for those)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api API area/authentication area/daemon Core Engine area/images Image Service containerd-integration Issues and PRs related to containerd integration impact/api impact/changelog kind/feature Functionality or other elements that the project doesn't currently have. Features are new and shiny module/client

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants