Sketching com.atproto.server.createActorAuth

From the call yesterday, here’s a sketch of an actor authentication credential. It’s adjacent to space credentials and service auth.

Summary

com.atproto.server.createActorAuth is an XRPC procedure that lets an authorized identity obtain a short-lived token to perform a single XRPC request as another identity. The canonical case is a community moderator writing a record into the community account’s repository without ever holding the community’s credentials.

The method is the delegated counterpart to com.atproto.server.getServiceAuth. Where getServiceAuth mints inter-service auth tokens for the caller’s own identity, createActorAuth mints them for an identity the caller is authorized to act for, binds them to the exact request being made, and records the true actor in the token. Use getServiceAuth to call a service as yourself; use createActorAuth to make one specific request as an identity you are authorized to act for.

Motivation

Shared identities exist today as community accounts, organization accounts, bots operated by a team, etc. Acting as one currently means sharing credentials, usually an app password passed among moderators. That approach has no attribution, no scoping, and revoking one person means rotating everyone.

Group and community work needs members to perform actions as the group identity under policy. The requirements this proposal targets:

  • Attributed: the true actor is recorded in the credential.
  • Narrow: one method, one request, a validity window measured in seconds.
  • Policy-checked at issuance, with the full request content visible to the policy engine.
  • Verifiable by receiving services with no new infrastructure beyond what inter-service auth already requires.

Design overview

Three parties participate:

  • The actor: the identity requesting to act (the moderator).
  • The authorizing service: the service that controls the represented identity’s #atproto signing key. This is typically the identity’s PDS, or a dedicated community or group service. It serves createActorAuth.
  • The target service: the service that will receive the eventual request, named in aud.

The flow has two steps. First, the actor sends the fully formed target request to createActorAuth on the authorizing service, authenticated as themselves, naming the represented identity, the target service and the target method in headers. The authorizing service authenticates the actor, evaluates policy against the actual request content, and returns a signed JWT bound to that request. Second, the actor sends the target request, byte-identical, directly to the target service, presenting the token inside a proof of its own identity.

The token is signed with the represented identity’s #atproto signing key and carries the represented identity as iss. Verification at the target service is therefore ordinary service-auth verification (resolve iss, verify the signature against the #atproto verification method, check aud, exp, and lxm) plus the binding and presentation checks defined below. The target service does not need to understand the delegation relationship at all.

Authorization policy is out of scope. How membership, roles, and permitted actions are expressed belongs to the authorizing service and to the broader Atmosphere Groups work. This proposal defines only the minting and verification mechanics.

The request

POST /xrpc/com.atproto.server.createActorAuth

createActorAuth defines no query parameters or request body of its own. The query string and body of the call are those of the target request, passed through verbatim. This gives the authorizing service the exact bytes it needs for both validation and hashing; a JSON envelope would force re-serialization and break byte-exactness. The body may be any encoding (lexicon input encoding */*). Servers MUST accept arbitrary query parameters on this endpoint; they are validated against the target method, not this one.

Method-specific inputs are carried in headers:

  • Authorization (required): inter-service auth proving the actor’s own identity, with lxm naming com.atproto.server.createActorAuth (not the target method).
  • atproto-identity (required): DID or handle of the identity to act as. Becomes the token’s iss.
  • atproto-lxm (required): NSID of the target method.
  • atproto-aud (required): DID of the target service. For a repository operation this is the represented identity’s PDS; for a labeler, chat service or other target it is that service.

Both parties are named rather than inferred because not every delegable method states its subject in its content. For example, com.atproto.repo.uploadBlob declares no parameters and its body is opaque bytes. Naming it also removes a circularity in the previous default for atproto-aud, which was “the represented identity’s PDS” and therefore could not be computed before the represented identity was known.

Where the content does name a subject, the two MUST agree. An authorizing service MUST refuse a request whose atproto-identity names one identity and whose body names another, since the body is replayed verbatim and the resulting token could never be spent. Content remains what policy is evaluated against.

Lexicon schemas cannot model headers, so these are protocol-level headers documented in spec text, following the precedent of atproto-proxy and atproto-accept-labelers.

On success the response is:

{ "token": "<jwt>" }

Defined errors: ActorNotAuthorized (the actor may not perform this request as iss), InvalidRequest (the request fails validation against the target lexicon or issuance policy), and MethodNotSupported (the named lxm is not eligible for delegation through this service).

Token format

The token is a JWT signed with the represented identity’s #atproto signing key, using ES256 or ES256K according to the key’s curve. The header typ MUST be actor-request+jwt. Explicit typing (RFC 8725) keeps these tokens from being confused with ordinary service-auth tokens, and means services that have not implemented this spec fail closed by rejecting the unknown type.

Claim Required Description
iss yes DID of the identity the request is performed as
act yes DID of the actor that requested the token
aud yes DID of the target service
lxm yes NSID of the method being invoked
iat yes Issued-at, UNIX timestamp, seconds precision
exp yes Expiration, UNIX timestamp, seconds precision
jti yes Unique random string nonce
qry when the request has a query string unpadded base64url SHA-256 of the query string bytes (excluding the leading ?)
bdy when the request has a body unpadded base64url SHA-256 of the request body bytes
mty when the request has a body the request’s normalized Content-Type

Hashes are computed over the exact bytes as transmitted. No canonicalization is defined; the actor replays the identical query string and body, in the style of AWS SigV4 content hashing. qry and bdy use unpadded base64url (RFC 7515 §2), as every other binary value a JWT carries does (see jkt in RFC 9449, at_hash and c_hash in OIDC, x5t#S256 in RFC 7515, etc.).

mty is the exception, and is compared after normalization rather than byte-for-byte: byte-exactness is right for a body and wrong for a header, because intermediaries and client libraries legitimately re-case media types and re-space parameters. Normalize by lowercasing the type and subtype, lowercasing parameter keys while preserving parameter values (boundary is case-sensitive), sorting parameters by key, and joining with ; and no whitespace. It is present whenever a body is, so a verifier never needs lexicon knowledge to know whether the claim was required. Without it, a token binding a blob’s bytes leaves its declared type free for the life of the window, which for an open-encoding method is most of what the request means.

The claim name is mty rather than cty deliberately because RFC 7519 §5.2 already registers cty as a JWT header parameter meaning the content type of the payload itself, and reusing the spelling across the dot for a different thing invites a confident, wrong implementation.

The act claim follows the delegation semantics of RFC 8693, carried here as a plain DID string.

Presentation

An actor-request token is never presented on its own. The actor presents it through a proof of its own identity: inter-service auth from the actor, carrying the actor-request token in an arq claim.

{
  "iss": "<actor DID>",
  "aud": "<target service DID>",
  "iat": 1787273230,
  "exp": 1787273290,
  "jti": "...",
  "lxm": "com.atproto.repo.putRecord",
  "arq": "<the actor-request token>"
}

The outer token is ordinary service auth, typed JWT and signed with the actor’s own #atproto key. The target service verifies it as such, verifies the embedded token under the verification rules below, requires the outer iss to equal the embedded act, and performs the request as the embedded iss.

This is what makes a stolen actor-request token worth nothing rather than worth one already-approved request: presenting one requires a fresh signature from the actor’s own key, which an attacker holding only the token does not have. It follows attestation-based client authentication, and layers on top of the token without changing its claims. It also means an actor must control a signing key which is the same requirement the Authorization header on createActorAuth already imposes.

Issuance rules

The authorizing service, before minting a token:

  1. Authenticates the actor from the Authorization header.
  2. Reads the represented identity from atproto-identity, and refuses if the target request’s content names a different one.
  3. Evaluates policy with the full request in hand. Content-level policy is the point of the passthrough design: a service can express rules like “moderators may only put records that validate against these lexicons into these collections,” which is not possible when binding to an opaque client-supplied hash.
  4. Computes qry, bdy and mty over the exact received bytes and headers, and MUST include them whenever the prepared request has a query string or body.
  5. Sets a short validity window. Lifetimes SHOULD be 60 seconds or less.

Issuance is stateless: no per-token state is required at the authorizing service. The service SHOULD log (act, iss, lxm, jti, qry, bdy) per issuance, which yields a complete delegation audit trail as a side effect.

Verification rules

The actor presents the outer token as Authorization: Bearer <jwt> on a request sent directly to the target service. It cannot be routed through the actor’s own PDS proxying, which would substitute its own service auth.

The target service verifies the outer token as ordinary inter-service auth against the actor’s key, then extracts arq. For that embedded token it performs standard service-auth verification: resolve iss, verify the signature against the #atproto verification method, confirm aud names this service, check iat and exp, and confirm lxm matches the endpoint being invoked. In addition it:

  1. MUST require the outer iss to equal the embedded act.
  2. MUST recompute the query string and body hashes from the received request and compare them to qry and bdy, and compare the received Content-Type, normalized, to mty.
  3. MUST reject a request that carries a query string, body or content type not covered by a matching claim. Without this rule, a token silently degrades into an lxm-scoped bearer token for its validity window.
  4. MUST enforce one-time use of the embedded jti, tracked by the claim value, within the validity window. This matters most for non-idempotent methods such as createRecord without a caller-supplied rkey. Only the embedded jti need be tracked: it is the one bound to this request, so burning it defeats replay of the pair.

The act claim does not participate in authorization; the request is authorized as iss under existing rules (for repository writes, repo equals iss, so no receiver-side changes are needed there). Verifiers SHOULD record act for audit.

Worked example

@ngerakines.me (did:plc:cbkjy5n7bk3ax2wplmtjofq2) is an admin of the community @dayton-pokemon.club (did:plc:rkh7hrrprgtarfraofpbh4vt) and wants to publish a post in the community’s repository, hosted at pds.example.com.

The community identity’s DID document contains the following #atproto verification method:

{
  "id": "did:plc:rkh7hrrprgtarfraofpbh4vt#atproto",
  "type": "Multikey",
  "controller": "did:plc:rkh7hrrprgtarfraofpbh4vt",
  "publicKeyMultibase": "zDnaefD6Yud7RRJfhURCaQaJD94w1ZPShJZqdjVXmUEJY6q6w"
}

The actor’s #atproto key is a K-256 key:

{
  "id": "did:plc:cbkjy5n7bk3ax2wplmtjofq2#atproto",
  "type": "Multikey",
  "controller": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
  "publicKeyMultibase": "zQ3shS9QEN53kkT9VuhSM4tYiiUuWACZ3P3Nd8U3NDNbXyKss"
}

For reproducing this example, the corresponding private keys are the P-256 multikey z42twjghVXySWgXmJedhoTvVeBRxR7HuCmbp12N73CoVw9Zw and the K-256 multikey z3vLddkAPHRMiip76Jsb3hufp6QQNmu5pLMJPToK5VWmQYsG. They exist only for this document; never reuse them.

The actor requests the token:

POST /xrpc/com.atproto.server.createActorAuth
Authorization: Bearer <inter-service auth for did:plc:cbkjy5n7bk3ax2wplmtjofq2>
atproto-identity: did:plc:rkh7hrrprgtarfraofpbh4vt
atproto-aud: did:web:pds.example.com
atproto-lxm: com.atproto.repo.putRecord
Content-Type: application/json
{
  "repo": "did:plc:rkh7hrrprgtarfraofpbh4vt",
  "collection": "app.bsky.feed.post",
  "rkey": "3mtkjhgfvkc2m",
  "record": {
    "$type": "app.bsky.feed.post",
    "text": "Hello, community!",
    "createdAt": "2026-08-20T23:00:00.000Z"
  }
}

The bdy hash below covers these exact body bytes: two-space indentation, LF line endings, no trailing newline. The authorizing service verifies the actor’s credential, reads the represented identity from atproto-identity, confirms it matches the repo field, confirms the actor may put this record as the community, validates the body, and returns a token with the header and claims:

{
  "typ": "actor-request+jwt",
  "kid": "#atproto",
  "alg": "ES256"
}
{
  "iss": "did:plc:rkh7hrrprgtarfraofpbh4vt",
  "aud": "did:web:pds.example.com",
  "act": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
  "iat": 1787273224,
  "exp": 1787273284,
  "jti": "f685a6fbdf2b69ad667a3efd6e7f5293465dafcf11617691",
  "lxm": "com.atproto.repo.putRecord",
  "bdy": "_9xuLwdyNovZs55aMS3aNLIsMJRsU4Z0G9TSPpfFWEI",
  "mty": "application/json"
}

The compact serialization, signed over the whitespace-free header and claims in the field order above (line breaks at the dots only):

eyJ0eXAiOiJhY3Rvci1yZXF1ZXN0K2p3dCIsImtpZCI6IiNhdHByb3RvIiwiYWxnIjoiRVMyNTYifQ
.
eyJpc3MiOiJkaWQ6cGxjOnJraDdocnJwcmd0YXJmcmFvZnBiaDR2dCIsImF1ZCI6ImRpZDp3ZWI6cGRzLmV4YW1wbGUuY29tIiwiYWN0IjoiZGlkOnBsYzpjYmtqeTVuN2JrM2F4MndwbG10am9mcTIiLCJpYXQiOjE3ODcyNzMyMjQsImV4cCI6MTc4NzI3MzI4NCwianRpIjoiZjY4NWE2ZmJkZjJiNjlhZDY2N2EzZWZkNmU3ZjUyOTM0NjVkYWZjZjExNjE3NjkxIiwibHhtIjoiY29tLmF0cHJvdG8ucmVwby5wdXRSZWNvcmQiLCJiZHkiOiJfOXh1THdkeU5vdlpzNTVhTVMzYU5MSXNNSlJzVTRaMEc5VFNQcGZGV0VJIiwibXR5IjoiYXBwbGljYXRpb24vanNvbiJ9
.
eqT-287A1w1v54Fhgs6-e2pBntfS7nWoAufEiJOUPwoO_joHDi__GcNiaNpVwJ7hAjlc8KDKkh-u80w9ghYYCQ

The actor then presents it, wrapped in inter-service auth of its own:

{
  "typ": "JWT",
  "kid": "#atproto",
  "alg": "ES256K"
}
{
  "iss": "did:plc:cbkjy5n7bk3ax2wplmtjofq2",
  "aud": "did:web:pds.example.com",
  "iat": 1787273230,
  "exp": 1787273290,
  "jti": "fefaee01ba113b86f90d7420d284bccfdeac745ca583aee0",
  "lxm": "com.atproto.repo.putRecord",
  "arq": "eyJ0eXAiOiJhY3Rvci1yZXF1ZXN0K2p3...w9ghYYCQ"
}

The arq value is the complete actor-request token from above, elided here. The compact serialization (line breaks at the dots only):

eyJ0eXAiOiJKV1QiLCJraWQiOiIjYXRwcm90byIsImFsZyI6IkVTMjU2SyJ9
.
eyJpc3MiOiJkaWQ6cGxjOmNia2p5NW43YmszYXgyd3BsbXRqb2ZxMiIsImF1ZCI6ImRpZDp3ZWI6cGRzLmV4YW1wbGUuY29tIiwiaWF0IjoxNzg3MjczMjMwLCJleHAiOjE3ODcyNzMyOTAsImp0aSI6ImZlZmFlZTAxYmExMTNiODZmOTBkNzQyMGQyODRiY2NmZGVhYzc0NWNhNTgzYWVlMCIsImx4bSI6ImNvbS5hdHByb3RvLnJlcG8ucHV0UmVjb3JkIiwiYXJxIjoiZXlKMGVYQWlPaUpoWTNSdmNpMXlaWEYxWlhOMEsycDNkQ0lzSW10cFpDSTZJaU5oZEhCeWIzUnZJaXdpWVd4bklqb2lSVk15TlRZaWZRLmV5SnBjM01pT2lKa2FXUTZjR3hqT25KcmFEZG9jbkp3Y21kMFlYSm1jbUZ2Wm5CaWFEUjJkQ0lzSW1GMVpDSTZJbVJwWkRwM1pXSTZjR1J6TG1WNFlXMXdiR1V1WTI5dElpd2lZV04wSWpvaVpHbGtPbkJzWXpwalltdHFlVFZ1TjJKck0yRjRNbmR3YkcxMGFtOW1jVElpTENKcFlYUWlPakUzT0RjeU56TXlNalFzSW1WNGNDSTZNVGM0TnpJM016STROQ3dpYW5ScElqb2laalk0TldFMlptSmtaakppTmpsaFpEWTJOMkV6Wldaa05tVTNaalV5T1RNME5qVmtZV1pqWmpFeE5qRTNOamt4SWl3aWJIaHRJam9pWTI5dExtRjBjSEp2ZEc4dWNtVndieTV3ZFhSU1pXTnZjbVFpTENKaVpIa2lPaUpmT1hoMVRIZGtlVTV2ZGxwek5UVmhUVk16WVU1TVNYTk5TbEp6VlRSYU1FYzVWRk5RY0daR1YwVkpJaXdpYlhSNUlqb2lZWEJ3YkdsallYUnBiMjR2YW5OdmJpSjkuZXFULTI4N0ExdzF2NTRGaGdzNi1lMnBCbnRmUzduV29BdWZFaUpPVVB3b09fam9IRGlfX0djTmlhTnBWd0o3aEFqbGM4S0RLa2gtdTgwdzlnaFlZQ1EifQ
.
BFhTBxD6lVlvdxnJ_hQz9JRLjQhReWzw4s6g2ZsLTBEKS7io8vm-lEv8X4UYg0Kpr-nq4DfwtMAGEzKhTow0kg

The request itself:

POST /xrpc/com.atproto.repo.putRecord
Authorization: Bearer <outer token above>
Content-Type: application/json

<identical body bytes>

The PDS verifies the outer token against the actor’s key, verifies the embedded token as described, recomputes the body hash, confirms lxm, aud, mty and that the outer iss equals the embedded act, and writes the record into the community’s repository under existing authorization rules, since repo equals iss.

Lexicon

{
  "lexicon": 1,
  "id": "com.atproto.server.createActorAuth",
  "defs": {
    "main": {
      "type": "procedure",
      "description": "Create a short-lived, request-bound token to perform a single XRPC request as another identity. The represented identity, target service and target method are carried in the required atproto-identity, atproto-aud and atproto-lxm headers. The query string and body of this call are those of the target request, verbatim.",
      "parameters": {
        "type": "params",
        "description": "The query string of the target request, passed through verbatim. This method defines no parameters of its own.",
        "properties": {}
      },
      "input": {
        "encoding": "*/*"
      },
      "output": {
        "encoding": "application/json",
        "schema": {
          "type": "object",
          "required": ["token"],
          "properties": {
            "token": { "type": "string" }
          }
        }
      },
      "errors": [
        { "name": "ActorNotAuthorized" },
        { "name": "InvalidRequest" },
        { "name": "MethodNotSupported" }
      ]
    }
  }
}

Relationship to existing mechanisms

The token deliberately reuses the inter-service auth vocabulary (iss, aud, exp, iat, lxm, jti) and the same signing key and verification path, so most of the verifier already exists in any service that accepts service auth. com.atproto.server.getServiceAuth covers the self-issued case; this method adds delegation and request binding. The act claim reuses the delegation semantics of RFC 8693 token exchange. Request binding by content hash follows AWS SigV4; type separation to prevent token confusion follows the DPoP pattern; the presentation binding follows attestation-based client authentication.

Security considerations

A stolen actor-request token authorizes nothing on its own. Presenting one requires a fresh signature from the actor’s own #atproto key, so an attacker holding only the token cannot spend it. Should the presentation requirement be relaxed, the fallback property is that lxm, aud, exp, qry, bdy and mty together reduce a stolen token to a single already-approved request within about a minute — provided verifiers enforce the binding rules. That enforcement requirement is why the distinct typ exists; a service that treats these as ordinary service auth would accept a different body against the same method within the window.

Issued tokens cannot be revoked. If a moderator is demoted one second after issuance, their outstanding token still works until exp. Short lifetimes are the mitigation, and the reason exp SHOULD stay at or under 60 seconds. Implementations should resist the temptation to add a revocation list: a revocation that works only when the target service happens to be the same server that minted the token fails silently everywhere else, which is worse than not offering one.

Policy runs at issuance, not at verification. The target service learns nothing about why the request was authorized, only that the represented identity’s key signed off on it. This keeps policy private and verifiers simple, at the cost of a round trip per action and a dependency on the authorizing service’s availability.

The represented identity now arrives in a caller-chosen header, which makes account existence cheap to probe: the difference between “no such identity here” and “not authorized” answers whether a server hosts a given DID. Authorizing services SHOULD collapse the two into a single ActorNotAuthorized.

Content-level scrutiny is not uniform. A record write can be validated against its lexicon; a blob cannot, and for uploadBlob the policy engine sees only a size and a declared type.

Attribution is service-level, not user-visible. The act claim gives the authorizing and target services an audit trail, but the resulting repository record is indistinguishable from one the community wrote itself. Surfacing “posted by X via Y” to users is a lexicon-level concern (for example, a via field on the record) and out of scope here.


Changes. The request: atproto-identity added and atproto-aud made required; content-based determination of the represented identity dropped. Token format: mty binds Content-Type; qry/bdy are now unpadded base64url. Presentation: the former postscript is now normative — tokens are not presented bare. Worked example re-signed.

5 Likes

Wow, cool! I will read through this properly later. :sparkling_heart:

1 Like

I read through the whole thing and I really like the way it nestles in nicely with the existing service auth concepts and requires minimal extra effort to support in XRPC servers.

Small nit: I wonder if it would be good to be explicit here and make the audience required?

It’s reminding me a little bit of the atproto-proxy header and how the reference PDS doesn’t require it in as many situations as is defined in the ATProto spec ( as far as I’ve heard ) and so Tranqui, which follows the spec closer often breaks apps because they work with the more lenient assumptions of the reference PDS.

Not sure if it’d be better just to make it required for now and if it seems to make sense to make a default later we could still do that.

I’m pretty sure we should be explicit about the represented identity.

When making the arbiter I initially thought that I could just infer the identify from the request, since many of the repo requests had the DID in the repo argument anyway, but there are XRPC endpoints that do not have any way to guess the DID, such as com.atproto.identity.updateHandle.

And there’s no guarantee that other XRPC endpoints offered by 3rd party services, which you might want to call as the represented identity, have any way to detect the DID to represent.

Ooh, neat trick! Since the actor already has to present the token itself it doesn’t need any extra network messages either, it looks like.


So I have a couple of bigger questions.

  1. How can we easily handle delegating com.atproto.server.createActorAuth to a service external to the PDS?
  2. Is proxying a useful alternative to having the client get a actorAuth token and then sending the request again to the target service?

Delegating createActorAuth to a Service

With the arbiter I’ve been working on, its job is basically to act as this “authorizing service”, but it doesn’t want to perform the job of a complete PDS. It only handles policy execution.

If the PDS is the service that has custody of the #atproto verification method private key, then my arbiter service is unable to directly produce the actor auth JWT signatures.

The arbiter would ideally have some kind of AppPassword / OAuth credential with a scope that grants it permission to sign arbitrary payloads with the PDS’s verification key.[1] That is obviously a very powerful scope, but I think it’s appropriate for these kind of trusted setups where you are kind of splitting the responsibilities of the PDS across multiple services.

In the case of ATProto community DIDs, instead of a “normal” PDS, we’d have a space host that could maybe be expected to have whatever kind of “arbiter” it needs built-in, but at the same time I think there’s still a use-case for an external arbiter for the same reason as the PDS: the arbiter doesn’t have to be a full-blown space host, it can just act as an XRPC authorizer.

Proxying Instead of Signing JWTs

The arbiter as I have it right now acts as an XRPC proxy: instead of signing a JWT it uses an AppPassword to act on behalf of the managed account when an actor makes a request.

This was done mostly because it doesn’t require changes to any PDS or XRPC endpoint.

The question is whether there’s any advantage to having the authorizing service authorize and then proxy the XRPC request to the target destination instead of signing a JWT.

It seems like there might be half of a round trip difference in latency because we only need one response to the client instead of two, but I’m not sure how impactful that is, and I do like that having the authorizing service just do authorization is simpler and seems less dangerous from a perspective of trying to manipulate the authorizing service for spam purposes or something like that.


  1. Didn’t you actually make a proposal for this once? I thought I remembered something like that. ↩︎

2 Likes

Yeah, I agree.

Agreed, adding atproto-identity header as a penciled-in solution.

After creating a plan for this, I think that this should be required.

1 Like

I was thinking about this a little. The spec is starting to become more flexible with verification method use, like having auth keys default to “#atproto” but not say it’s the only key reference.

Ideally, a PDS could have a configuration element to accounts to indicate which additional keys could be used for signing this way. The PDS just needs to be informed that that the verification method “#acting” in the DID document of the identity of the account will be used by an authoritative service that has the private key.

1 Like

That would be much nicer than having multiple services needing to sign under the same private key, and would also save on network round trips.

1 Like

In atproto-pds my UX is starting to take shape. For the delegation configuration bit, it’s per identity and uses oauth scopes.

There’s also an activity log, but this is probably going to need heavy constraint.

1 Like

I’m not sure this resolves the issue we were discussing in the working group meeting. Though please let me know if I’m misunderstanding the proposal.

This proposal seems most useful when the authorizing service and the target service are different services. In the Atmospheric Groups standard, they’re the same service! The “group host” hosts both the spaces and the repos for the group and maintains the role & access logic.

This is why the null hypothesis for the “writing as a group DID” problem is just an OAuth credential from the group host which it can issue based on it’s internal access logic & then verify itself on write.

I’d also be interested in how com.atproto.server.createActorAuth shows up as an OAuth scope. I think this would need to be a new resource type alongside rpc? Would we be interested in keeping it fully generic, or would it be better to just enable repo/space CRUD with it. Would it be parameterized by both authorizer & target? Or does parameterizing with just authorizer suffice.

1 Like

There was a pretty lengthy discussion about how to go about allowing identities to “act on behalf of” a community identity. It covered a lot of ground and towards the end the lot of us were talking about impersonation, delegation, and actors.

@brittanyellich.com summarized a good chunk under “Proxy Permissions for Community DIDs” and @zicklag.dev wrote about it as well.

This proposal allows an identity to be granted an “actor auth” token to invoke an XRPC method on behalf of the grantor.

2 Likes

Right yeah no I remember the convo!

From Brittany’s doc:

Core question: how to express ‘act on behalf of community DID’ in an OAuth scope a user can consent to

Two approaches weighed: spec a new third-party repo-access resource, vs. app holds its own OAuth credential to the community

My impression was the convo was mostly around how to & who presents this in an OAuth screen. And we were mostly weighing two options:

  • Communicate through an OAuth screen hosted by the group
    • Could have a group admin go through that OAuth flow per-app and then the application could juggle OAuth credentials and allow non-admin users to use that group credential if they’re authorized to do so.
    • This is a pretty traditional/familiar mechanism! Nothing really atproto-unique in it (besides of course the atproto OAuth profile)
    • I lean towards this one & if anything I think lean a bit stronger in this direction since our meeting
  • Communicate through an OAuth screen hosted by the user’s PDS & then make the request to the group through service auth or a service-auth-like thing
    • This would involve a protocol change: extending existing or speccing new OAuth resources
    • For example, adding a did parameter on the repo scope. This would allow the OAuth screen to say “allow this app to write to @atmosphere.community’s repo.” What that scope would grant is the ability to send the write request. Then the target repo needs to apply it’s on access policy to determine if it actually wants to process the write.
    • I just think this is a lot of complexity to push into the OAuth screen (which is already about at its complexity budget), which is why I lean toward the former.

A clarifying point, which is maybe the root of confusion: I assumed that the group host is also a repo host! Like the thing managing group permissions also just hosts & signs all the repos for the group DID. That’s what I mean by the authorizing & target services being the same service.

1 Like

Yeah, that’s the direction of what I’m proposing here and follows this train of thought:

(1) In order for a community (@dayton-pokemon.club) to exist with an app, it needs to grant some members the ability to bootstrap content and also perform periodic updates on its behalf.

Much like with spaces, consent is kind of one-sided. A community can arrange for an identity to have these acts-on-behalf-of permissions, the actor isn’t really able to “opt out” of them, and notifying the actor is out of scope.

This isn’t a temporary session that is heavily scoped, but rather individual XRPC calls that are allowed per the rules and constraints set by the community.

(2) The best “shape” for that is still probably OAuth scopes, but the internals could be implementation details that don’t need to be defined as part of the scope.

I think granting delegated scopes like “repo:app.bsky.feed.post blob:image/*” to an identity so they can post on behalf of the community feels natural, so the account-management feature of atproto-pds can include scopes to delegation for that purpose. Other implementations may want to represent it differently or leave it out of the PDS entirely as an entryway feature.

(3) The actual end-result is that an identity (@ngerakines.me) needs temporary access to an XRPC method (com.atproto.repo.putRecord) which is provided through a short-lived “actor auth token” that is similar to a inter-service authentication token, but can be consumed by the PDS that the community (@dayton-pokemon.club) resides on.


Initially I was thinking that this can follow the “log in as @foo” account delegation feature that atproto-pds currently provides, but I started to go down the path of what the use cases are:

  1. As a community moderator of @dayton-pokemon.club, I @ngerakines.me want to log into https://bulleted.app/ and do things as the community itself.

This scenario would benefit from the full “log in as the community” OAuth flow where I effectively am doing everything as the community itself.

Example: I’m writing a blog post as the community published to the community’s repository.

  1. As a community moderator of @dayton-pokemon.club, I want to do things as myself that may have side-effect actions that must be done as the community.

This scenario would benefit from individual calls that are in addition to the active session.

Example: I’m creating a forum post as my identity, but then pinning the post as the community and creating a feed post to circulate it as the community.

1 Like

Here’s a mock-up of the kind of design I’m thinking of. Any scope that can be requested for your own account can also be requested for accounts that you manage. Hypothetically this could also be optionally restricted further to only include a specific list of accounts that you manage.

When displaying the scopes, if there are any delegated scope requests, then we can show them almost identically to how we would show them normally, but under a heading that indicates that they are being requested not from your account, but from accounts that you have been delegated access to.[1]

This is not a dramatic increase in complexity. It is a relatively small change that adds nothing else other than an explanatory heading and description and it only shows up when an app requests access to delegated accounts.

I’m not totally sure what the delgated scopes would look like, but maybe they are just a delegated: prefix to an existing scope, for example: delegated:repo:app.bsky.feed.post?action=create.

I think this is a huge downgrade from the experience we have today in the Atmosphere for public data: anybody can login to any app and use their authority in that app just like they could in any other app, if they grant the required OAuth scopes at login time, once.

Yeah, I don’t think that this is excluded by this proposal!

But if you need to make an generic XRPC request under the managed group account, then it may not be hosted on the same server. For example, it might be an AppView. This mechanism allows us to generically represent “another user acting under the authority of the group” irregardless of whether the XRPC destination is hosted on the same server that is authorizing the request.

I think this is the main use-case we have for general “community” / “org” account features. We want to allow admins, moderators, etc. to create records on the community’s PDS, change group settings, make API calls to other apps’ AppViews under the community’s authority, all without having to log in as the community, or login to every community that they are an admin of.

This is especially important if you have have features impacting the community account that might be granted automatically based on trust levels for instance. It would be nontrivial UX impact if a user that was just promoted up a trust level had to now login to the specific space because they have access to new features.

They really just need to be able to get a token, similar to a space credential, from the DID host, that proves that they are allowed to make a certain XRPC call on behalf of the community’s DID. This is a permission that should be able to be granted directly from the OAuth consent screen on the PDS.

By having com.atproto.server.createActorAuth, we give the PDS a clean way to restrict what an app is allowed to request to be delegated, very similar to service auth. Without a delegated: scope you wouldn’t be allowed to call createActorAuth at all.

Otherwise we’ll just have to keep using our town.muni.arbiter.proxy endpoint which will be one big, dangerous OAuth scope that we must request, granting full access to any of the delegated accounts that you control with no restrictions.


In summary, without a protocol change we end up with 3 non-optimal options:

  1. Require users exercising delegated authority to login with a new OAuth consent screen for every community they manage, for every app they use to manage it and re-doing that if sessions expire.
  2. Require apps to take on the responsibility of authorization for the community, and white-list the apps that are allowed to act on behalf of the community.
  3. Request access to delegated accounts at login time, but without being able to restrict the scope of that delegation at all. This is like going back to the transition:generic days when it comes to delegated accounts.

Private data is already requiring protocol changes, and one of the very first things we need immediately after having it is the ability for people to cleanly act on behalf of the community’s account, so I think this should be under consideration.


  1. Note that this is completely compatible with permission sets. On the call we talked about not wanting to allow people to mislead on the OAuth screen with permissions sets, since in the current design they are properly checked based on NSID. This preserves that. ↩︎

2 Likes

I previously argued the necessity of a system like this in my blogpost on how to keep the ecosystem viably open for small “third-party” operators (i.e. my very beloved “fuck-ass Astro sites”), but making the point again:

To me, “each App goes and juggles OAuth credentials to allow non-admin users to do things” is a non-starter.

As was said, the alternative to Nick’s proposal is to have “a group admin go through that OAuth flow per-app”. As someone who moderates multiple communities, I’m not going to each individual community to ask administrators to allowlist every single app I may want to use, just so I can do operations I’m already authorized to do. No one I know likes asking administrators for permission to use software that improves their personal experience—and when it comes to moderation, people who manage one community usually manage multiple, which makes this particularly onerous. If we can’t find a better answer than “go to each administrator and ask”, people who manage communities (and I know, cause I talked with them) will be extremely pissed to see a supposedly-open ecosystem claim this as a reasonable solution.

We’re comparing a system where, to do something that I have permission to do, using the app of my own choice, I have to:

  1. Open the app
  2. Click “authorize” on a screen, which—this is true!—I may or may not understand the meaning of
  3. Do the thing, but a) not anything more than what I (wittingly or unwittingly) authorized, and b) not anything more than what the community allowed me to do anyway

To a system where, to do the same thing, I have to:

  1. Go to a community administrator (or multiple ones)
  2. Plead the case that I should be allowed to use a specific app to do something which, again, I am already authorized to do
  3. Ask (indirectly) that each administrator find the time and mental energy to figure out whether they should consider trusting this app—never mind, in many cases, needing to ensure all their co-admins agree to extend this trust
  4. Ask (indirectly) that each administrator evaluate which scopes and permissions are reasonable to give to this app—especially considering that other members may also benefit, or that someone may in the future request extended functionality (which would make the whole process start over)
  5. Go back to the app
  6. Still click “authorize” on an OAuth screen to prove my identity—a screen that, this time, doesn’t even mention what permissions I’m giving at all to an app that I cannot trust doesn’t have more permissions than I do (and that can now act as me)
  7. Do the thing and pray that the app developer didn’t accidentally (and I say this without judgement) vibecode the “remove emoji” operation into the “delete the community forever” operation

The world in which each administrator individually authorizes Apps may seem like a safer world, but it actually isn’t: it only looks safer because we shift the burden of trust onto each community’s administrator, the burden of implementation onto every single developer—and, in doing so, create a world where the answer to “am I allowed to safely use this app that no one but me loves and finds useful?” is by default “no”. Which, to me, is the exact opposite of what individual-user ATproto is all about.

Ultimately, for me, figuring out how to manage the complexity of an OAuth screen with delegated permissions is a reasonable ask when the result is that we get to live in a world where my permissions—just like my data—travel with me.

5 Likes

Couple framing things before I get into my thoughts/considerations between the two alternatives.

Delegated authorization as protocol mechanism

First authorization delegation is a new protocol mechanism. It’s not part of atproto spaces work and it’s not part of the “atmosphere groups” working group. The process for a protocol change of this magnitude generally involves months of back and forth, drafting specs, making a public proposal, requesting & integrating feedback, and implementing in multiple languages before deploying. Consider the work on auth scopes & permission sets for a rough frame of reference. We’re pretty strapped for resources to do that type of work as all of our time/energy is going into getting atproto spaces and this atmosphere groups standard as polished as possible & out as quickly as possible.

If we’re discussing authorization delegation as a general protocol mechanism, I think I prefer authorization token formats that natively capture the delegation chain (such as UCAN). This is actually the protocol mechanism I’m most interested in getting into after atproto spaces. Generally as a maturation of service auth but also ideally capturing things things like delegated write authz as this proposal discusses. Still, thinking on all of this is very vague and is probably at least 6 months out from being able to give it any real deep thought. I’m skeptical of sneaking in a stopgap thing like this in the meantime.

I don’t bring this up to shut down the discussion. But I do want to be clear about the reality of the work required for this option. It’s not a simple “this or that”.

Importance of OAuth

I know I harp on OAuth a lot. And I know it’s frustrating when representation in the OAuth screen is a sticking point for doing something. But I really do think it’s critical to the long-term success of the Atmosphere.

Things are different right now. We’re all devs, we know whats going on under the hood. We personally interact with the other devs that are building the apps we’re using. There hasn’t been a scandal yet from an app over-requesting OAuth scopes and then abusing authorization. In a world with 100ks of Atmosphere apps, users having permissioned/private data, and users that don’t understand atproto, this is not going to hold.

To give a sense of why it’s so important:

  • We’re suggesting that users have universal accounts containing their data from across the entire internet.
  • Anyone can make an app.
  • That app can request access to whatever it wants.
  • There is no app store doing application review.
  • The Authorization Server doesn’t understand the resources it’s giving access to and has no relationship with the app
  • And the only thing stopping them from accessing whatever it wants is the user having some understanding of what they hit “Accept” on

I really appreciate your mockup of the authorization screen @zicklag.dev. If we’re serious about this, that’s the type of work we need to be doing.


Alrighty now my thoughts on what’s been proposed & an alternative:

Open audience

In @zicklag.dev’s sketch, I actually quite like the simplicity of the authorization screen! However, I’m worried about leaving the audience open on all of these delegated scopes.

For example, I may be okay using an application to act on behalf of a small group of a few of my friends, but the calculus may be different for acting on behalf of a large 10k person community that I run.

I think delegated scopes, like rpc resources and permission sets, need to be able to be parameterized by audience. However, unlike rpc resources, this isn’t a value that the application can supply. That means that the authorization screen probably would need to have some sort of “picker” that let’s the user choose which audiences the delegated scopes apply to.

Fully expressive delegation

I think we’re biting off more than we need with fully expressive delegation. Really the thing we’re looking for for our current use cases is delegating writes. Basically space, repo, and blob scopes.

I think identity and account scopes are mostly too dangerous to delegate in this manner.

rpc scopes are generally harder to wrangle/communicate. So we simplify our problem quite a bit by not tangling with trying to delegate those. The matrix of scope parameterization gets increasingly complex when both rpc audience and delegated audience of variables.

“Allowing this app to authenticate with another service on behalf of an account you manage” is a pretty abstract thing to communicate.

In defense of just using OAuth

I really think having community accounts and managed accounts generally “just use OAuth”, keeps things clean & clear.

For one, this requires no new protocol work! Everything Just Works :trade_mark: as is!

This is also very straightforward & honest about what is actually happening, ie who is granting access to what. Authorization servers grant access to applications. Applications use those credentials to access or alter the resources they have access to. It avoids indirections like “My Authorization server granting me access to authenticate with another server which actually owns the resource and determines if I have authorization to access it”.

@essentialrandom.bsky.social, I appreciate your breakdown, but I think it’s a bit uncharitable to the OAuth proposal.

Specifically, it’s completely at the app’s discretion when to do the OAuth flow and what to do with the credential.

For the Bluesky app, we expect many mods/admins in the same communities will be using the app. Bluesky also has a reputation of not accidentally deleting entire repositories when a user “removes an emoji”, and I hope we intend to maintain that reputation! Therefore, we’ll likely opt for the lower-overall-friction option of having an admin authorize a community and then apply our own notion of user access to use that credential. This is exactly how most web applications with OAuth integrations work today.

However, for more bespoke applications or “individual-user atproto”, this same pattern does not need to apply. An application may instead opt to send each individual user that needs to act on behalf of a community through the OAuth flow. If a user has “moderator” access or simply “authorization to pin a post”, then the application sends them through the OAuth flow the first time they wish to do so, holds the credential, associates it with the user who went through the flow, and only uses it for them. At no point in this flow is an admin required.

As you can see, this offers the same amount of openness as the delegated authorization proposal. However, it requires no new protocol mechanisms and I believe is more legible to users around what specific resources they’re granting to each application.

1 Like

Unsurprisingly to those who know me, this is a pretty exciting discussion for me! as the person who came up with Tranquils current approach to this issue. This is something I’ve wanted to see addressed for a while and Tranquil has been the way I could “just do things” and the results have been pretty good in my opinion. Tranquils delegated accounts continue to be one of my favourites of things I have done/contributed to in the atproto space since it means that we have something for this use case that works today and that I and others use, actively, for organisation and community accounts. And @bmann.ca mentions running a community instance here which, in my opinion, should be a fine stop gap until we can agree on a final form for all of this.

I think it’s worth going over Tranquils approach here since it’s prior work in exactly this space, uses only existing protocol mechanisms (though could, and IMO likely should, be extended to allow for better UX), and keeps the “my permissions travel with me” property that @essentialrandom.bsky.social is mentioning here. That was actually a really major consideration for me when designing it! Tranquils delegation has it’s roots in my ideas on how Tangled would best support “org accounts” and me believing that Tangled taking on the responsibility of mediating access to org accounts would be messy and has uncomfortable security implications if you ask me.

As an overview of delegated/org/community accounts in the Tranquil model: In Tranquil “delegated accounts” are accounts where the OAuth login method isn’t a password or a passkey or the like but rather a nested OAuth session with another atproto account that is one of the defined “controllers”. It’s just another OAuth login method. This means that, currently, using your delegated access to a shared account means logging in to a client as that account, at which point the delegated accounts PDS will ask you for your personal handle and then pass you on to your own PDS to authenticate there, then back to the delegated account to finalise the OAuth flow of the delegated account. From there on you have a broadly normal OAuth session with the delegated accounts PDS within the limits of your defined controller status (this is handled as OAuth scopes assigned to each controller and they’re only allowed to authorise OAuth sessions with credentials that are a subset of their controller scopes). Taking actions as the delegated account is thus just normal XRPC requests to the PDS. The PDS knows that the session is a delegated session and can scope access accordingly, and will (we don’t do this in Tranquil yet but it’s coming as part of a clean up of delegated accounts) add a act claim to the inter-service auth tokens that consuming XRPC services can, if they support it, read for info about the acting controller identity. The PDS will also keep an audit log of all logins and actions performed using delegation.

Tranquils approach to me has a few upsides to the one described there:

  • It’s transparent to both client and XRPC services. Nothing other than the PDS the delegated account is hosted with has to change in any way for Tranquils scheme to work. This is how I can here and now use the tranquil.farm account with delegated access through my personal nel.pet account on every single existing and future atproto client.
  • It keeps the semantics of inter-service tokens broadly the same. An inter-service token signed by the signing key for tranquil.farm remains, always, a token that’s used to take actions for that account, same for all inter-service tokens signed by the nel.pet signing key.
  • Per my first point, it requires to support or allowance from anything other than the delegated account. I don’t need a client to allow me to use delegation for me to be able to use tranquil.farm with that client. I can just do that because tranquil.farm (or rather another controller of tranquil.farm with high enough permissions) has allowed me to do it.
  • PDS proxying still works entirely normally for delegated accounts.
  • The OAuth flow issues are naturally resolved by the fact that the scopes for the delegated session are just standard OAuth scopes. Your own account is only ever used by the delegation handling PDS for authentication of the controller, not for authorisation, that’s done with the existing OAuth infrastructure.
  • Delegated access is always revocable to the same degree that non-delegated access is. Inter-service tokens of course still can’t be revoked but a PDS could invalidate a users session at any time if the delegation relation changes.

As a catch-all point: The Tranquil approach is designed to Just Work with the existing way of doing things. This means your delegated access truly does move with you, no matter the client you use. While keeping the door open for a “progressive enhancement” like approach to improved client-side UX around these accounts.

Not without a few downsides either of course! but these mainly come from not wanting to force clients to do anything specific to support this and I believe these downsides can all be addressed with client and broader PDS support. Namely: Having to log in to a dedicated community account session has it’s upsides in that it makes the distinction of when you’re acting as yourself, vs as the community account extremely clear. However it can also have some downsides UX wise. Clients can be supported in knowing about the delegation relationship to help this. Additionally the delegated accounts PDS can allow clients to authorise calls as the delegated account using a personal inter-service token. Similar to the approach taken for createAccount during migration. This would allow clients to only deal with a session at the controllers PDS, allows for the kind of short lived access to the delegated account described here.

I’m confident Tranquils approach isn’t perfect but I do believe it to be a strong contender and at the very least a strong starting point and reference point for future protocol work (which I agree with @dholms.xyz will likely be a bigger undertaking if it is to change things at the protocol level). If nothing else Tranquil shows that this problem space does have solutions that work here and now! Even if it’s not perfect.

6 Likes

Yeah I really appreciate the framing.

Yeah, and I understand and appreciate the weight of the extra openness, too.

Relatedly I was just chatting with someone on Colibri who was realizing that it’s kind of concerning that users can bring their own PDS provider, and any space you invite that user to can be read by a malicious PDS host if they have one. It’s just an anecdote of how this universal profile and dramatic openness has it’s own unique issues to take into account.

What I fear is that by making apps register with a community to get a better UX we are going to be pushing things closer to the feudalistic scenario where we are no longer granting the user freedom of choice regarding their software by disadvantaging independent clients.

Yeah, if we want to make it more sophisticated we can do it like GitHub does, where I am able to uncheck one of my organizations when I connect a service to my GitHub account, or the way that you can uncheck your sharing your email address when an app asks for it.

I know this isn’t just a trivial win, though, and will take real design work.

Roomy does need at least identity:handle so that admins can change the handle of the space.

We also need to be able to send XRPC requests so that we can give admins access to the Bluesky chat of our @roomy.space account, which has been used by people to get in contact with us.

I don’t think it’s really much more abstract than “authenticating with another service on behalf of you”. It fits just as well under your account as it does “accounts you manage” IMO.

Yeah, we weren’t intending to suggest at all that Bluesky was going to mess it up.

The only thing that we have issue with is that you are able to “opt for the lower-overall-friction option” because you are well known and/or because you are the community host in the first place, and other side-apps don’t have a lower-friction option without getting community admin approval, which itself is a friction.

Yes, the same amount of openness, there’s just a UX penalty against independent clients. It motivates centralizing things for ease of use.


I really do understand the motivation and the appeal of not changing things and just using OAuth per-community.

I just don’t think the UX is acceptable.

I feel like if this were the situation, that Roomy would quite possibly just opt for the opaque “Perform any action on your Roomy communities” permission on the OAuth consent screen and continue to use XRPC “proxying” to allow users to take any action authorized for their account under the community.

We’ll have to discuss it more as a team, but I think that might be the best balance for us. It keeps auth centralized at the space host, allows the user to use any app, doesn’t give AppViews the ability to do anything the user can’t already do themselves, and doesn’t require custom apps to re-implement the space host’s auth system.

Self-hosted Roomy instances don’t need to grant the main Roomy instance full access to their communities, and there’s now UX downgrade for joining spaces hosted on separate instances.

The only disadvantage is that it’s all-or-nothing on the OAuth screen.

This is gonna take some more thought.


I understand the pressure here. I really think this is important, but I do also understand that we’re in a difficult place as far as what’s required to improve the situation on the protocol.


PS: Posting now but I haven’t actually read @nel.pet’s reply yet.

4 Likes

I’d like to weigh in with a few thoughts of my own in terms of complexity when it comes to this.

Colibri is an app that works, in a lot of ways, like the proposed Open Social communities. When you create a community, we provision a DID on a PDS we host. We create it using the admin API, generate a password, then store that password securely. Once the account is provisioned, we generate a session, write all the records that are needed for the community to get started (like initial categories and channels), then save that session’s token as well.

Whenever a user, like an admin or moderator, wants to write a record as that community (for example a new role), we have to check our session token, possibly refresh it, then write for that user. Why do I bring up this flow? It’s because it’s a lot to maintain. I am not sure just how much of our codebase is constant session juggling for accounts that might be needed at any point because any user could write to them.

A delegation token, issued by the community PDS itself, would remedy a whole chunk of this. Instead of, on every write request, needing to check if the user is authorized to do a thing, then getting the session, potentially refreshing a token, then writing the record for the user, we could simply tell the PDS that the user is allowed to write a certain type of record in a certain location, then be done with it. To compare, here are the two flows for changing a community’s profile on Colibri:

Right now:
1. Edit the form
2. Make a request with the data to the AppView
3. AppView checks permissions based on roles & ownership
4. AppView gets community DID session from DB
   (+ Potentially refresh, which is a couple more steps)
5. Write data using that session
6. Done
Maybe possible with createActorAuth (as I understand it):
1. Edit the form
2. Make a request with the data to the community PDS (or the authorizing service)
3. Done

The point I’d like to make is that, for a lot of people, implementing the writes the current way is a massive undertaking that requires a lot of understanding of how these things work. If createActorAuth works the way I understand it does, a lot of not all of this could be abstracted away into the PDS and the client itself, potentially into the @atproto libraries, meaning that someone just wanting to build things on the protocol suddenly has much less things to do for a service that maybe only they wish to use, with a static site and no server. It massively lowers the barrier of entry.

To be clear, I don’t wish to push this as something that needs to happen right now, with an implementation that should land yesterday. I’m fine if this is something we keep in mind and get started with in a couple of months, maybe more. My goal/idea/”what I’m hoping” for is an easier way for people to write apps and maintain them.

4 Likes

Hey y’all, chatted with with @ngerakines.me for a few minutes this morning. Sounds like my response came off more aggro than I intended it. I’m sorry, I didn’t mean to be aggressive or dismissive of the proposals here. My intention was just to give a sense of my considerations when weighing these options.

The biggest is that this is just at a different layer of the stack than the Atmosphere Groups lexicon standard that we’re working on in the working group. Which is not to say that it isn’t relevant to it. But it requires a new protocol mechanism, and that process is quite a bit more involved and moves more slowly. I’m worried about making that work a dependency for the Atmosphere Groups work.

I really appreciate the on-the-ground feedback on the need for this. And to be clear I’m not pushing back against the need for account delegation! As I mentioned, it’s probably the thing I’m most interested in working on from a protocol perspective after spaces are out!


Couple quick responses to posts since then:

Wow thanks for sharing! I actually didn’t realize that Tranquil had this feature. But this is basically exactly what I’m proposing we do for Atmosphere Groups! Does it work cross-PDS? As @nel.pet says, this has the big advantage of Just Working without any required extra machinery. I’d be interested in any other learnings you’ve had from this or rough edges that you’ve run into. I’ll definitely be taking a look at what Tranquil has done.

I need to know more about Roomy’s particular setup. But if these are Roomy XRPCs, then you can wrap them up in permission sets that define the different levels of access.

Another relevant thing that we want to do is add developer API tokens that you can create to control an account programatically. These would be more like standard API tokens that you’re used to using in SDKs, but they would encode OAuth scopes and would be long-lived and not need all that refresh logic. That’s actually a much much lower lift from a protocol perspective. If the main issue is wrangling session refreshes, this may be able to help in the shorter term

6 Likes

On that, I’m going to archive this topic! After some good back and forth with @dholms.xyz, we are both in agreement that what I was proposing is too far into the protocol, as opposed to something more lexicon and user-data. The timeline for working on this would be prohibitively costly to meet the needs of the community proposal and spec.

I really appreciate everyone’s comments and contributions to the discussion! There is a lot of really good context around use cases and need, and I’m confident that it’ll be useful when service authentication is revisited.

4 Likes