diff --git a/lexicons/io/atcr/authFullApp.json b/lexicons/io/atcr/authFullApp.json index a757246..8074f8e 100644 --- a/lexicons/io/atcr/authFullApp.json +++ b/lexicons/io/atcr/authFullApp.json @@ -14,12 +14,6 @@ "resource": "repo", "action": ["create", "update", "delete"], "collection": ["io.atcr.manifest", "io.atcr.tag", "io.atcr.sailor.star", "io.atcr.sailor.profile", "io.atcr.repo.page"] - }, - { - "type": "permission", - "resource": "rpc", - "lxm": ["com.atproto.repo.getRecord"], - "aud": "*" } ] } diff --git a/pkg/auth/oauth/client.go b/pkg/auth/oauth/client.go index 105c2cc..0c061f2 100644 --- a/pkg/auth/oauth/client.go +++ b/pkg/auth/oauth/client.go @@ -82,13 +82,8 @@ func GetDefaultScopes(did string) []string { // See lexicons/io/atcr/authFullApp.json for definition // Uses "include:" prefix per ATProto permission spec "include:io.atcr.authFullApp", - // Individual repo/rpc scopes (for current PDS compatibility) - // fmt.Sprintf("repo:%s", atproto.ManifestCollection), - // fmt.Sprintf("repo:%s", atproto.TagCollection), - // fmt.Sprintf("repo:%s", atproto.StarCollection), - // fmt.Sprintf("repo:%s", atproto.SailorProfileCollection), - // fmt.Sprintf("repo:%s", atproto.RepoPageCollection), - // "rpc:com.atproto.repo.getRecord?aud=*", + // com.atproto scopes must be separate (permission-sets are namespace-limited) + "rpc:com.atproto.repo.getRecord?aud=*", // Blob scopes (not supported in Lexicon permission-sets) // Image manifest types (single-arch) "blob:application/vnd.oci.image.manifest.v1+json", @@ -228,6 +223,18 @@ func (r *Refresher) DoWithSession(ctx context.Context, did string, fn func(sessi // The session's PersistSessionCallback will save nonce updates to DB err = fn(session) + // If request failed with auth error, delete session to force re-auth + if err != nil && isAuthError(err) { + slog.Warn("Auth error detected, deleting session to force re-auth", + "component", "oauth/refresher", + "did", did, + "error", err) + // Don't hold the lock while deleting - release first + mutex.Unlock() + _ = r.DeleteSession(ctx, did) + mutex.Lock() // Re-acquire for the deferred unlock + } + slog.Debug("Released session lock for DoWithSession", "component", "oauth/refresher", "did", did, @@ -236,6 +243,19 @@ func (r *Refresher) DoWithSession(ctx context.Context, did string, fn func(sessi return err } +// isAuthError checks if an error looks like an OAuth/auth failure +func isAuthError(err error) bool { + if err == nil { + return false + } + errStr := strings.ToLower(err.Error()) + return strings.Contains(errStr, "unauthorized") || + strings.Contains(errStr, "invalid_token") || + strings.Contains(errStr, "insufficient_scope") || + strings.Contains(errStr, "token expired") || + strings.Contains(errStr, "401") +} + // resumeSession loads a session from storage func (r *Refresher) resumeSession(ctx context.Context, did string) (*oauth.ClientSession, error) { // Parse DID @@ -260,28 +280,15 @@ func (r *Refresher) resumeSession(ctx context.Context, did string) (*oauth.Clien return nil, fmt.Errorf("no session found for DID: %s", did) } - // Validate that session scopes match current desired scopes + // Log scope differences for debugging, but don't delete session + // The PDS will reject requests if scopes are insufficient + // (Permission-sets get expanded by PDS, so exact matching doesn't work) desiredScopes := r.clientApp.Config.Scopes if !ScopesMatch(sessionData.Scopes, desiredScopes) { - slog.Debug("Scope mismatch, deleting session", + slog.Debug("Session scopes differ from desired (may be permission-set expansion)", "did", did, "storedScopes", sessionData.Scopes, "desiredScopes", desiredScopes) - - // Delete the session from database since scopes have changed - if err := r.clientApp.Store.DeleteSession(ctx, accountDID, sessionID); err != nil { - slog.Warn("Failed to delete session with mismatched scopes", "error", err, "did", did) - } - - // Also invalidate UI sessions since OAuth is now invalid - if r.uiSessionStore != nil { - r.uiSessionStore.DeleteByDID(did) - slog.Info("Invalidated UI sessions due to scope mismatch", - "component", "oauth/refresher", - "did", did) - } - - return nil, fmt.Errorf("OAuth scopes changed, re-authentication required") } // Resume session