mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-02 00:06:58 +00:00
auth: surface read-only app passwords as 403, not 503
A read-only app password authenticates fine via createSession but cannot call com.atproto.server.getServiceAuth, which is privileged — the PDS answers 403 InsufficientScope. That fell through to the generic non-200 path and became a 503, which invites the client to retry a request that can never succeed, with no indication of what is actually wrong. Classify it with a sentinel error and map it to a 403 at /auth/token, carrying text that names the fix: use a full-access app password. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e6959e6dc6
commit
9d4ad84a3e
@@ -48,6 +48,22 @@ func isStaleBearerToken(name string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// ErrAppPasswordInsufficientScope indicates the app-password session lacks the
|
||||
// scope to mint a hold service-auth token via com.atproto.server.getServiceAuth.
|
||||
// Read-only app passwords trip this: the PDS rejects getServiceAuth with 403
|
||||
// InsufficientScope. It is a permanent authorization failure — retrying won't
|
||||
// help — so callers surface it as a 403 with a clear "use a full-access app
|
||||
// password" message rather than a 503.
|
||||
var ErrAppPasswordInsufficientScope = errors.New("app-password lacks scope to mint hold service token (read-only app password?)")
|
||||
|
||||
// isInsufficientScopeError reports whether a PDS error body signals that the
|
||||
// presented app-password session is scoped too narrowly to call getServiceAuth.
|
||||
// The reference PDS and tranquil both return the atproto error name
|
||||
// "InsufficientScope" (paired with 403) for read-only app passwords.
|
||||
func isInsufficientScopeError(body []byte) bool {
|
||||
return atprotoErrorName(body) == "InsufficientScope"
|
||||
}
|
||||
|
||||
// getErrorHint provides context-specific troubleshooting hints based on API error type
|
||||
func getErrorHint(apiErr *atclient.APIError) string {
|
||||
switch apiErr.Name {
|
||||
@@ -414,6 +430,21 @@ func GetOrFetchServiceTokenWithAppPassword(
|
||||
return "", fmt.Errorf("app-password token stale (%s): re-authentication required", name)
|
||||
}
|
||||
|
||||
// The app-password session is valid but scoped too narrowly to mint a
|
||||
// hold service-auth token (getServiceAuth is a privileged endpoint that
|
||||
// read-only app passwords can't call). This is a permanent authorization
|
||||
// failure — no cached token to evict, and retrying won't help — so flag it
|
||||
// with a sentinel the caller maps to a 403 with a clear message.
|
||||
if resp.StatusCode == http.StatusForbidden && isInsufficientScopeError(bodyBytes) {
|
||||
slog.Warn("App-password lacks scope for getServiceAuth (read-only app password?)",
|
||||
"component", "token/servicetoken",
|
||||
"did", did,
|
||||
"holdDID", holdDID,
|
||||
"pdsEndpoint", pdsEndpoint,
|
||||
"hint", "User must use a full-access app password, not a read-only one")
|
||||
return "", fmt.Errorf("%w: %s", ErrAppPasswordInsufficientScope, string(bodyBytes))
|
||||
}
|
||||
|
||||
slog.Error("Service token request returned non-200 status (app-password)",
|
||||
"component", "token/servicetoken",
|
||||
"did", did,
|
||||
|
||||
@@ -506,6 +506,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
"duration", fetchDur.Round(time.Millisecond))
|
||||
}
|
||||
if res.err != nil {
|
||||
// A read-only app password can authenticate (createSession) but can't
|
||||
// mint the hold service-auth token that pulls and pushes require. That
|
||||
// is a permanent authorization failure, not a transient outage, so
|
||||
// return a 403 with actionable guidance instead of a retry-inviting 503.
|
||||
if errors.Is(res.err, auth.ErrAppPasswordInsufficientScope) {
|
||||
slog.Info("service-auth pre-mint denied: app-password lacks scope", "did", did, "error", res.err)
|
||||
_ = errcode.ServeJSON(w, errcode.ErrorCodeDenied.WithMessage(
|
||||
"your app password lacks the permissions ATCR needs. A read-only app password cannot mint the service token used to authenticate with your storage hold. Use a standard (full-access) app password."))
|
||||
return
|
||||
}
|
||||
slog.Warn("service-auth pre-mint failed", "did", did, "error", res.err)
|
||||
_ = errcode.ServeJSON(w, errcode.ErrorCodeUnavailable.WithMessage(fmt.Sprintf("service-auth fetch failed: %v", res.err)))
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user