A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go

fix scope mismatch?

evan.jarrett.net c1f2ae0f 012a14c4

verified
Changed files
+31 -30
lexicons
pkg
auth
oauth
-6
lexicons/io/atcr/authFullApp.json
··· 14 14 "resource": "repo", 15 15 "action": ["create", "update", "delete"], 16 16 "collection": ["io.atcr.manifest", "io.atcr.tag", "io.atcr.sailor.star", "io.atcr.sailor.profile", "io.atcr.repo.page"] 17 - }, 18 - { 19 - "type": "permission", 20 - "resource": "rpc", 21 - "lxm": ["com.atproto.repo.getRecord"], 22 - "aud": "*" 23 17 } 24 18 ] 25 19 }
+31 -24
pkg/auth/oauth/client.go
··· 82 82 // See lexicons/io/atcr/authFullApp.json for definition 83 83 // Uses "include:" prefix per ATProto permission spec 84 84 "include:io.atcr.authFullApp", 85 - // Individual repo/rpc scopes (for current PDS compatibility) 86 - // fmt.Sprintf("repo:%s", atproto.ManifestCollection), 87 - // fmt.Sprintf("repo:%s", atproto.TagCollection), 88 - // fmt.Sprintf("repo:%s", atproto.StarCollection), 89 - // fmt.Sprintf("repo:%s", atproto.SailorProfileCollection), 90 - // fmt.Sprintf("repo:%s", atproto.RepoPageCollection), 91 - // "rpc:com.atproto.repo.getRecord?aud=*", 85 + // com.atproto scopes must be separate (permission-sets are namespace-limited) 86 + "rpc:com.atproto.repo.getRecord?aud=*", 92 87 // Blob scopes (not supported in Lexicon permission-sets) 93 88 // Image manifest types (single-arch) 94 89 "blob:application/vnd.oci.image.manifest.v1+json", ··· 228 223 // The session's PersistSessionCallback will save nonce updates to DB 229 224 err = fn(session) 230 225 226 + // If request failed with auth error, delete session to force re-auth 227 + if err != nil && isAuthError(err) { 228 + slog.Warn("Auth error detected, deleting session to force re-auth", 229 + "component", "oauth/refresher", 230 + "did", did, 231 + "error", err) 232 + // Don't hold the lock while deleting - release first 233 + mutex.Unlock() 234 + _ = r.DeleteSession(ctx, did) 235 + mutex.Lock() // Re-acquire for the deferred unlock 236 + } 237 + 231 238 slog.Debug("Released session lock for DoWithSession", 232 239 "component", "oauth/refresher", 233 240 "did", did, ··· 236 243 return err 237 244 } 238 245 246 + // isAuthError checks if an error looks like an OAuth/auth failure 247 + func isAuthError(err error) bool { 248 + if err == nil { 249 + return false 250 + } 251 + errStr := strings.ToLower(err.Error()) 252 + return strings.Contains(errStr, "unauthorized") || 253 + strings.Contains(errStr, "invalid_token") || 254 + strings.Contains(errStr, "insufficient_scope") || 255 + strings.Contains(errStr, "token expired") || 256 + strings.Contains(errStr, "401") 257 + } 258 + 239 259 // resumeSession loads a session from storage 240 260 func (r *Refresher) resumeSession(ctx context.Context, did string) (*oauth.ClientSession, error) { 241 261 // Parse DID ··· 260 280 return nil, fmt.Errorf("no session found for DID: %s", did) 261 281 } 262 282 263 - // Validate that session scopes match current desired scopes 283 + // Log scope differences for debugging, but don't delete session 284 + // The PDS will reject requests if scopes are insufficient 285 + // (Permission-sets get expanded by PDS, so exact matching doesn't work) 264 286 desiredScopes := r.clientApp.Config.Scopes 265 287 if !ScopesMatch(sessionData.Scopes, desiredScopes) { 266 - slog.Debug("Scope mismatch, deleting session", 288 + slog.Debug("Session scopes differ from desired (may be permission-set expansion)", 267 289 "did", did, 268 290 "storedScopes", sessionData.Scopes, 269 291 "desiredScopes", desiredScopes) 270 - 271 - // Delete the session from database since scopes have changed 272 - if err := r.clientApp.Store.DeleteSession(ctx, accountDID, sessionID); err != nil { 273 - slog.Warn("Failed to delete session with mismatched scopes", "error", err, "did", did) 274 - } 275 - 276 - // Also invalidate UI sessions since OAuth is now invalid 277 - if r.uiSessionStore != nil { 278 - r.uiSessionStore.DeleteByDID(did) 279 - slog.Info("Invalidated UI sessions due to scope mismatch", 280 - "component", "oauth/refresher", 281 - "did", did) 282 - } 283 - 284 - return nil, fmt.Errorf("OAuth scopes changed, re-authentication required") 285 292 } 286 293 287 294 // Resume session