Commit 622d365
Fix OAuth service-proxying audience for combined
* feat(did): add AtprotoDidRefAbsolute, DidRefRelative, AtprotoScopeAud, AtprotoTokenAud
Renames AtprotoAudience to AtprotoDidRefAbsolute (kept as deprecated alias),
adds DidRefRelative + isDidRefRelative for #fragment-only references, and
introduces use-site unions: AtprotoScopeAud (always combined, for OAuth
scopes) and AtprotoTokenAud (bare DID or combined, for service-auth JWT
audience claims). Lays the groundwork for Phase 1 of proposal 0014.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(did): cover new DID-reference predicates
Tightens isAtprotoDidRefAbsolute to reject empty fragments
(`did:plc:abc#`), aligning it with isDidRefRelative which already
rejects bare `#`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(oauth-scopes): use AtprotoScopeAud / isAtprotoDidRefAbsolute
Switches from the legacy AtprotoAudience names to the renamed canonical
forms. Behavior unchanged; the deprecated alias from @atproto/did keeps
external consumers compiling.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(lexicons): loosen getServiceAuth.aud to accept did or did#serviceId
The 'format: did' constraint excluded the combined did#serviceId syntax
that OAuth-scoped credentials grant via 'rpc:lxm?aud=did#service'.
Loosens to a plain string with a maxLength cap; handler-side validation
will enforce that the value is either a bare atproto DID or an
AtprotoDidRefAbsolute.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(lexicons): tighten getServiceAuth.aud description and maxLength
Drops the internal "Phase 1" reference from the user-facing description
and bumps maxLength from 1024 to 2048 to accommodate the spec maximum
DID length plus a service-id fragment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(pds): accept did#serviceId aud in getServiceAuth handler
Validates that the caller-supplied aud is a valid atproto DID or
AtprotoDidRefAbsolute, then passes the original string straight through
to permissions.assertRpc and createServiceJwt. Combined-aud OAuth scopes
now match (resolves part of bluesky-social#4850); password-session callers continue to
use bare DID.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(pds): pass combined did#serviceId aud to pipethrough scope check
parseProxyInfo and parseProxyHeader now return the serviceId alongside
the did and url. The proxyHandler uses the combined did#serviceId form
when calling permissions.assertRpc, so OAuth credentials with
'rpc:lxm?aud=did#service' scopes succeed (resolves the proxy half of
bluesky-social#4850). The outbound service-auth JWT keeps bare-DID aud — Slice D adds
the operator-visible flag to flip that.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(pds): rename scopeAud/tokenAud to clarify Phase 1 asymmetry
Names the Phase 1 audience asymmetry structurally rather than relying
on a comment to document it. The scope check uses the combined
did#serviceId form; the outbound JWT keeps bare-DID aud.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(pds): assert parseProxyHeader returns serviceId
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(oauth-scopes): cover combined-aud assertRpc match (issue bluesky-social#4850)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(pds): validate getServiceAuth aud for all caller types
Moves the aud predicate check from the OAuth-only authorize callback into
the shared handler body, so app-password and session callers also get
syntactic validation — restoring the coverage that the lexicon's
format: did constraint provided before Slice A loosened it to a plain
string with maxLength.
Also updates app-passwords tests to use valid atproto DIDs instead of
did:example:test, which now correctly fails the predicate check.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Address review feedback for service-auth oauth fix
- Annotate non-self-documenting checks in isAtprotoDidRefAbsolute and
isDidRefRelative with terse case-name comments.
- Spell out "Phase 1 of service auth updates" in the proxyHandler
asymmetry comment.
- Drop the issue-number reference from the assertRpc combined-aud
describe block.
- Mirror PR bluesky-social#4602's createReport pattern: use scopeAud/tokenAud naming
to make the bare-DID JWT audience structurally clear.
- Add a proxyHandler regression test that fails when scope-check aud
reverts to bare DID.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(pds): use bare-DID aud in get/putPreferences proxy fallback
When getPreferences/putPreferences proxy to a non-default appview, they
were sending the combined did#serviceId form as the JWT audience. This
contradicts the Phase 1 convention used elsewhere (proxyHandler,
pipethrough(), createReport) where the scope check sees the combined
form but the outbound service-auth JWT keeps bare-DID aud.
Adds a small bareDidFromProxyTo() helper in pipethrough.ts and uses it
at both call sites.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(pds): use real AppContext + override only authorization
Replaces the proxy-oauth-aud test's stub-AppContext with a real
TestNetworkNoAppView. Only AuthVerifier.authorization is overridden to
drive the OAuth path off an x-test-scope header — every other ctx field
stays real and real-typed. The override's signature is constrained by
the real method type, so future shape changes (renames, output type
restructures) break the test at compile time rather than silently
diverging.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(pds): tighten proxy-oauth-aud cases
Drops the lastAud capture (a 200 already implies combined-form aud
reached the scope check) and replaces the bare-DID-aud negative case
with a different-service-id negative case. Bare-DID aud isn't a valid
rpc scope — isAudParam rejects it at parse time — so the prior negative
case wasn't actually exercising audience matching. The new case
(`upstream.did#atproto_other`) parses valid and proves the runtime
aud's service id is being compared.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: add changesets for service-auth oauth fix
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(pds): cover getServiceAuth aud handling
Adds five integration cases: bare-DID and combined did#serviceId aud
both produce a token whose aud claim equals the input; malformed,
non-atproto, and empty-fragment aud values are rejected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: pnpm style:fix
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(pds): revert scopeAud/tokenAud rename in handlers without both
The naming pair was meant to disambiguate when scope-check and JWT
audiences appear in the same scope. In get/putPreferences and
createReport only one of the two is in scope at a time, so the rename
adds noise without clarifying anything. proxyHandler keeps both names
since they sit side-by-side there.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(did): drop OAuth-flavored types; remove deprecated AtprotoAudience
Removes AtprotoScopeAud, AtprotoTokenAud, AtprotoAudience, and
isAtprotoAudience from @atproto/did. The only consumer (oauth-scopes)
now uses AtprotoDidRefAbsolute / isAtprotoDidRefAbsolute directly via
its own AudParam alias. Keeps @atproto/did focused on DID-level
primitives without OAuth or service-auth concepts. The
AtprotoAudience/isAtprotoAudience deprecated aliases are dropped
outright (breaking) — migration is a single import swap.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(did): split did-ref.ts into atproto-agnostic primitives
did-ref.ts now hosts generic DidRefAbsolute / DidRefRelative
primitives (and their predicates) parameterized on a DID method.
The atproto-flavored AtprotoDidRefAbsolute moves to atproto.ts
alongside the other Atproto* helpers, defined as
DidRefAbsolute<AtprotoIdentityDidMethods>. Public API surface is
unchanged (both still flow through the package barrel).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(did): use String.includes for double-hash check
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: changeset for oauth-scopes breaking re-export change
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(oauth-scopes): tighten scopeNeededFor casts back to concrete types
The casts `as AudParam`/`as LxmParam` widened to include `'*'`, which
is wrong inside scopeNeededFor — it's constructing the specific scope
for a single call, never a wildcard. Restore the original
AtprotoDidRefAbsolute / Nsid casts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: lead BREAKING-change changesets with **BREAKING:** marker
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>did#serviceId scopes (bluesky-social#4992)1 parent 92ed125 commit 622d365
22 files changed
Lines changed: 526 additions & 46 deletions
File tree
- .changeset
- lexicons/com/atproto/server
- packages
- did
- src
- tests
- oauth/oauth-scopes/src
- scopes
- pds
- src
- api
- app/bsky/actor
- com/atproto/server
- tests
- proxied
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
15 | | - | |
| 14 | + | |
| 15 | + | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | | - | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
115 | 116 | | |
116 | 117 | | |
117 | 118 | | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
125 | | - | |
126 | | - | |
127 | | - | |
128 | | - | |
129 | 119 | | |
130 | 120 | | |
131 | 121 | | |
| |||
231 | 221 | | |
232 | 222 | | |
233 | 223 | | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
Lines changed: 36 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
264 | 264 | | |
265 | 265 | | |
266 | 266 | | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
267 | 303 | | |
0 commit comments