-6
lexicons/io/atcr/authFullApp.json
-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
+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