Skip to content

Commit 9291203

Browse files
imphilchrmarti
authored andcommitted
Re-authenticate against OCI registry after 403 error
Writing to an OCI registry, as done with `devcontainer features publish` requires authentication against the registry with the `push` OAuth scope. Currently, devcontainer CLI only authenticates or re-authenticates if the registry returns a 401 error (invalid_token). But some registries, notably the IBM Container Registry (icr.io) may also return a 403 error (insufficient_scope) in case of a request being authenticated, but without sufficient scopes (i.e., the token was only valid for the `pull` scope, but `push,pull` was required for write access). Update the code to attempt to re-authenticate in case of a 403 error, just like it's done for a 401 error. The server does supply the correct scope in its `WWW-Authenticate` header and subsequent requests will then work as expected. See also [RFC 6750, Section 3.1 (Error Codes)](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1) for a standards reference. This improvement makes `devcontainer features publish` work with IBM Cloud Container Registry. --- Test: ``` $ devcontainer.js features publish --registry icr.io -n my-ns/features ~/my-feature` ``` HTTP trace (abbreviated) *before* this change: ``` -> POST https://icr.io/v2/my-ns/features/my-feature/tags/list -> 401 Unauthorized www-authenticate: Bearer realm="https://icr.io/oauth/token",service="registry",scope="repository:my-ns/features/my-feature:pull" -> POST https://icr.io/oauth/token client_id=devcontainer&grant_type=refresh_token&service=registry&scope=repository%3Amy-ns%2Ffeatures%2Fmy-feature%3Apull&refresh_token=... <- 200 OK -> POST https://icr.io/v2/my-ns/features/my-feature/blobs/uploads/ authorization: Bearer <- 403 Forbidden: www-authenticate: Bearer realm="https://icr.io/oauth/token",service="registry",scope="repository:my-ns/features/my-feature:pull,push",error="insufficient_scope" ``` HTTP trace (abbreviated) before *after* change: ``` -> POST https://icr.io/v2/my-ns/features/my-feature/tags/list -> 401 Unauthorized www-authenticate: Bearer realm="https://icr.io/oauth/token",service="registry",scope="repository:my-ns/features/my-feature:pull" -> POST https://icr.io/oauth/token client_id=devcontainer&grant_type=refresh_token&service=registry&scope=repository%3Amy-ns%2Ffeatures%2Fmy-feature%3Apull&refresh_token=... <- 200 OK -> POST https://icr.io/v2/my-ns/features/my-feature/blobs/uploads/ authorization: Bearer <- 403 Forbidden: www-authenticate: Bearer realm="https://icr.io/oauth/token",service="registry",scope="repository:my-ns/features/my-feature:pull,push",error="insufficient_scope" -> POST https://icr.io/oauth/token client_id=devcontainer&grant_type=refresh_token&service=registry&scope=repository%3Amy-ns%2Ffeatures%2Fmy-feature%3Apull%2Cpush&refresh_token=... <- 200 OK ``` Note the second auth request after the 403 response.
1 parent e0598db commit 9291203

File tree

1 file changed

+5
-5
lines changed

1 file changed

+5
-5
lines changed

src/spec-configuration/httpOCIRegistry.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const scopeRegex = /scope="([^"]+)"/;
3737

3838
// https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate
3939
export async function requestEnsureAuthenticated(params: CommonParams, httpOptions: { type: string; url: string; headers: HEADERS; data?: Buffer }, ociRef: OCIRef | OCICollectionRef) {
40-
// If needed, Initialize the Authorization header cache.
40+
// If needed, Initialize the Authorization header cache.
4141
if (!params.cachedAuthHeader) {
4242
params.cachedAuthHeader = {};
4343
}
@@ -54,14 +54,14 @@ export async function requestEnsureAuthenticated(params: CommonParams, httpOptio
5454

5555
const initialAttemptRes = await requestResolveHeaders(httpOptions, output);
5656

57-
// For anything except a 401 response
58-
// Simply return the original response to the caller.
59-
if (initialAttemptRes.statusCode !== 401) {
57+
// For anything except a 401 (invalid/no token) or 403 (insufficient scope)
58+
// response simply return the original response to the caller.
59+
if (initialAttemptRes.statusCode !== 401 && initialAttemptRes.statusCode !== 403) {
6060
output.write(`[httpOci] ${initialAttemptRes.statusCode} (${maybeCachedAuthHeader ? 'Cached' : 'NoAuth'}): ${httpOptions.url}`, LogLevel.Trace);
6161
return initialAttemptRes;
6262
}
6363

64-
// -- 'responseAttempt' status code was 401 at this point.
64+
// -- 'responseAttempt' status code was 401 or 403 at this point.
6565

6666
// Attempt to authenticate via WWW-Authenticate Header.
6767
const wwwAuthenticate = initialAttemptRes.resHeaders['WWW-Authenticate'] || initialAttemptRes.resHeaders['www-authenticate'];

0 commit comments

Comments
 (0)