Musing: UCANs, Groups, Communities, & ATProto

This isn’t going to be perfect, and won’t take everything into account, but here’s an attempt to think through a scenario with UCANs.

I’m going to probably iterate on this a lot, and this is very draft quality thinking out loud, but might be worth sharing.

This is just to get the thoughts out. I will probably do a proper writeup later and think about more scenarios after I refine my thoughts a bit.

Groups

So starting with groups.

  • Let’s imagine that groups are made out of “normal” ATProto accounts with extra functionality layered on top.
  • That means that they have a DID, an ATProto repo, and a signing key.
  • Groups also have an internal member list.
    • I could imagine this being public or private. If it’s public, maybe it’s saved in in the ATProto repo.
  • Access to manage the group is controlled with UCANs.
  • When the group is intiially created, a UCAN is signed by the group’s private key granting full access to the group for the account that created it.
  • That UCAN can be used to invoke XRPC endpoints for adding or removing members from the group.
  • The admin can also delegate subsets of XRPC endpoints, potentially with restricted parameters, specified by the UCAN policy.
    • For example, if Bob creates a group, he could delegate permission to add members to the group to Alice, without allowing her to remove members.
  • Note: Since the group also has normal ATProto repo, you could possibly delegate permissions to create Bluesky posts under the group’s account or things like that.

Communities

Communities could be things like Discord guilds, public forums, etc.

  • Let’s say the community is also a “normal” ATProto account.
  • It works basically similar to a group where it delegates admin access to the creator when it is created.
  • We need to think about read access, but to scope the conversation lets think about write access.
  • The idea is that we want users to be able to create certain kinds of content in the community repo, possibly with certain restrictions.
  • Lets say that we hypothetically want an admin group that can create, delete, and edit all posts, and a user group that can create posts and edit their own posts.
  • We can create those two groups, and then delegate the permissions that the community grants each group to the group account.
  • Now we can add whatever members we want to those admin and user groups.
  • But becuase of the way UCAN works, that doesn’t automatically grant the members of the group any capability to take action inside the community.

Group Community Delegation

  • So if Charlie is in the user group, and he wants to post in the community, he has to make an XRPC request to the group, asking for a UCAN that delgates the permission the group to the community, to Charlie.
    • The group will check it’s members list, make sure that charlie is in there, and if it he is, then it will create and sign a new UCAN for charlie delegating the access it has to the community space.
  • If the group is granted any new permissions in the space, then Charlie will have to request a new UCAN that includes the newly granted permissions.
3 Likes

I think what UCANs add is making groups a flexible concept that can be somewhat transparent to the community.

i.e. when a community issues a UCAN to an identity to grant it access to the community, it automatically allows that identity to delegate access to other identities.

Therefore issuing a UCAN to a group account, automatically allows that group to delegate the access you give it to members.

And that can even happen in multiple nested layers if you wish, such as a user identity delegating access to one of their devices, or one group delegating access to another group.

I think it’s also potentially compelling that UCANs are quite expressive, and allow policies that can filter on the specific arguments to a “command” that they are allowed to execute.

This could possibly map quite cleanly with commands to to XRPC endpoints and policies to restrictions on the XRPC arguments.

2 Likes

<I HAVE BEEN SUMMONED> :smiling_face_with_horns:

I’m biased but this aligns with how I think about these kinds of things :+1:

But becuase of the way UCAN works, that doesn’t automatically grant the members of the group any capability to take action inside the community.

You can (ha!) set it up to grant anything, but indeed POLA is the way to go. The alternate designs that I have seen are pretty coarse and have lots of edge cases that are hard to think about / confused deputies from e.g. PDSes acting on behalf of users with their own permissions.

transparent to the community

Yes, I think that this stuff should be as hidden as possible except for people that want to do custom stuff. There’s an old article (that shows its age a bit) called “Not One Click for Security” — TLDR actions in capability systems should come directly from user action/intention and not feel like an extra layer in their face. Uploading a file opens a window that has EVERY FILE on your computer, but when you submit the browser only gets access to that one (granular). We didn’t go through some special auth flow — it just comes from user intention.

3 Likes

A motivating example for me is the feature in Discord where you can start a private thread in a public channel. You can add other users you like and mods always see it. How would this look in UCAN?

Without writing out the actual certificates, the over flow of authority would be something like:

   [Discord]
       |
{id:0, what: Discord, can: *} ......................
       |                                           .
       V                                           .
    [Mods] --{ what: *, ------> [AliceTheMod]      .
       |       can: *,                             .
       |       proof: [id: 0]                      .
       |     }                                     .
       |                                           .
{ id: 1, ...........................................
  what: Discord,                                   .
  can: [read, post, make_threads],                 .
  proof: [id0]                                     .
}                                                  .
       |                                           .
       V                                           .
  [BobTheUser] <--{ id: 2, ---------- [BobThread]  .
                   what: BobThread,                .
                   can: *,                         .
                   proof: [id1, id0]................
                  }

Those IDs are actually hashes and I’m skipping some UCAN fields, but this gives the basic idea. Here Alice is one of many mods (mods have dynamic membership), everything is hashed and signed, and anyone with a valid certificate chain can access BobThread

1 Like

Without writing out the actual certificates, the over flow of authority would be something like:

┌─────────┐
│ Discord │
└────┬────┘
     │
╔════╧═════════════════════════════╗
║ id: 0                            ║
║ what: Discord                    ║·····················╮
║ can: *                           ║                     ·
╚════╤═════════════════════════════╝                     ·
     │                                                   ·
     ▼                                                   ·
┌─────────┐                                              ·
│  Mods   │──╔═══════════════╗        ┌──────────────┐   ·
└────┬────┘  ║ what: *       ║───────▶│ AliceTheMod  │   ·
     │       ║ can: *        ║        └──────────────┘   ·
     │       ║ proof: [id:0] ║                           ·
     │       ╚═══════════════╝                           ·
     │                                                   ·
╔════╧═════════════════════════════╗                     ·
║ id: 1                            ║                     ·
║ what: Discord                    ║                     ·
║ can: [read, post, make_threads]  ║······················╮
║ proof: [id:0]                    ║                      ·
╚════╤═════════════════════════════╝                      ·
     │                                                    ·
     ▼                                                    ·
┌────────────┐  ╔════════════════════╗     ┌──────────┐   ·
│ BobTheUser │◀─║ id: 2              ║─────│ BobThread│   ·
└────────────┘  ║ what: BobThread    ║     └──────────┘   ·
                ║ can: *             ║                    ·
                ║ proof: [id:1, id:0]║····················╯
                ╚════════════════════╝

Those IDs are actually hashes and I’m skipping some UCAN fields, but this gives the basic idea. Here Alice is one of many mods (mods have dynamic membership), everything is hashed and signed, and anyone with a valid certificate chain can access BobThread

Sorry that the diagram is slightly wonky — it’s better than my hand-drawn ascii but now with more LLM

2 Likes

So when Bob makes a thread, he gets a UCAN from Discord saying that he can read / post / invite in the thread.

Then Bob could delegate his read / post access in that thread to Charlie and create a new UCAN to give to charlie when inviting him.

Depending on how this interacts with the thread listing functionality, Bob might want to send to Discord the UCAN that he delegated to Charlie, so that Discord knows charlie has read access to the thread and can show the thread in the threads list without charlie having to explicitly join or accept an invite to the thread.

Or maybe Bob has /invite permissions against the thread just sends Discord the invite command to have Discord give Charlie a UCAN so that Charlie can access the thread now.

That way if we revoke Bob’s access it doesn’t revoke Charlies access, too. ( Which it would if Charlie’s access was delegated by Bob. )

1 Like

Just to “yes and” this — you don’t have to register it with Discord per se; that’s just a convenient channel to get the UCAN to Charlie in this context

1 Like

Does someone need a new UCAN every time their permissions update? What are the setups where this is needed or not? Another motivating use-case, only people I follow can comment? As the reader on a blog platform, do I need a new UCAN when someone follows me and I gain this access to their content? There is a NxM relation mapping, yea, or is there a way to encode this concept once? What is the UX like here?

Can UCANs be broken down and contextualized? eg. One channel has a lot of permission churn, does that impact my tokens for all channels? Is this something an app has to keep in mind while designing the permission schemas? What’s the DX like here?

The diagrams help me understand more of the what, thanks! I’m still unclear on the how, especially as there are more actors involved.

Generally, yes. But at the same time, UCANs don’t necessarily need to be long-lived things that you try to keep around.

For instance, as long as the authority for a resource can check what permissions you should have, you can always request a new UCAN from them.

In this case you could have a VerdvermFollows service that tracks who follows you by watching the Jetsream or something like that.

When you create a post that you want to restrict comments on, you could delegate the comment permissions on that post to the VerdvermFollows service.

Now when a user wants to comment, they have to go and ask the VerdvermFollows service to give them a UCAN that allows commenting. It’s remotely similar to an OAuth login / scope request.

The commenting user would use a service-auth JWT that they got from their PDS and the VerdvermFollows service will use that JWT to validate their DID, then it will check that you follow that DID, before finally giving the user a UCAN that can be used to comment on your post for, lets say, 24 hours.

This pattern lets you use arbitrary logic around how you get a UCAN for some particular command like comment on a particular resource.

So the powerline pattern could be used to reduce churn, by having any members automatically be able to take any action that the group is able to. ( At the risk of some security and principle of least privilege. )

Then when delegating permissions to specific channels you can just delegate to groups instead of individual users for the most part.

For example, you could create an announcements channel and delegate write access to the moderators group and read access to the users group.

If you later wanted to let the “announcers” group to also write to that channel, then you can just make a new delegation. ( Another UCAN )

If you want to remove the moderators group from being able to write to the channel, then you just revoke the UCAN that you used to delegate permission to them in the first place.


I think that there’s potentially two ways of thinking about integrating this with any system.

  1. Creating longer-lived UCANs where their very existence is meant to represent the “current state” of what is authorized or not in the system.
  2. Having other services responsible for determining what access is allowed and issuing short-lived UCANs on demand after validating that the requested permissions are granted.

I think these patterns can be mixed together a bit too.

For example, the VerdvermFollows service is an example of pattern 2. We have a service that uses it’s own logic to determine whether or access should be granted and it issues a short-lived UCAN token as proof of that access. This can then be invoked at the AppView / PDS to prove your right to take some action.

You could alternatively do something more similar to pattern 1:

You could create a group like CommentIfIFollowThem. Whenever you want to allow people that you follow to comment on a post, you just create a UCAN that delegates the /comment command to CommentIfIFollowThem and you attach that UCAN to your post.

Then, you go and create a powerline delegation from CommentIfIFollowThem to every user user that you follow, and you store those UCANs publicly or otherwise in a way that the people you follow will be able to read the UCAN that you issued for them.

Now any person you follow can simply grab their delegation UCAN from CommentIfIFollowThem and combine that with the UCAN that you attached to your post, to create an invocation that allows them to comment on your post.

To keep the system up-to-date, you need to revoke the delegations from CommentIfIFollowThem to any users that you unfollow, and create new delegations for any new users that you follow.

Note: the revocation list needs to be maintained and would have unbounded growth if you don’t set an expiry on your CommentIfIFollowThem delegations. If you do set an expiry ( probably best ), then you will have to renew the UCANs before they expire to make sure there is a valid one available as long as you still follow them.


I’m not 100% sure about what the best way to go about everything is yet, but what I like about UCANs is that they are quite flexible as to how they are applied, and they make authorization very simple at the point of invocation. You simply check that the identity has a valid UCAN for the action they are trying to take. How they obtained that UCAN is not the concern of the resource server at that point, and it’s quite flexible the options you have for obtaining a token.

Only when they get new permissions; UCANs compose, so you don’t need to revoke the old one (only if you want to remove some permission)

This way around works really well under UCAN (“people that follow me” would arguably be more involved, but still possible).

There are patterns for multiplexing like this (big parts of the design are there to handle this concern). You can think of it as having a node that represents that mapping, ridding you of that NxM any time you can group people/services/whatever together. For example, maybe you want to grant everyone in a chat room (N) access to a collection of services (M)

serviceA    service B  serviceC <=== "M"s
        \      |      /
         \     |     /
          v    v    v
       "multiplex/proxy"
            /      \
           v        v    
        Alice       Bob    <=== "N"s

This is the “powerline” pattern that @zicklag.dev referenced earlier. You can grant transitive access through that multiplexer.

Broken down: yes, this is arguably the defining feature (attenuation) of this kind of auth. Anyone in the delegation chain can attenuate (arbitrarily narrow) the authority.

Contextualized: can you expand on what you mean / give a concrete example?

Is this something an app has to keep in mind while designing the permission schemas?

The format is pretty open; you design your own schemas. It operates syntactically on your API calls: UCAN runs some predicates against the syntax of the call. UCAN knows nothing about the semantics of your service; if you want permissions for a service to work a different way, expose a different API. Because it lives at this layer, it’s generally easy to reason about, though I suppose you could write some complex set of predicates (but you can always check if you call will pass locally).

When to Not Use UCAN

I think that it’s important to talk about when things are a poor fit for any technology. Only discussing virtues makes it difficult to evaluate. UCAN is very expressive and flexible, but that comes with tradeoffs.

UCAN can be bridged to OAuth (or email+password login etc) services, but it works pretty differently under the hood. You can build a “log in with OAuth, get a session UCAN” service but it doesn’t come out of the box.

UCAN ensures partition tolerance, but that means that all invocations must be self-certifying (signed etc). This scales really nicely since services don’t need to store delegations, but you do need to store revocations (until the UCAN being revoked expires, of course). In related systems like object-capabilities (like Spritely’s Goblins), you don’t need to track revocations at all, so that system is very light on the wire, but requires that each intermediate server in a call chain is live & connected.

Being certificate based and partition tolerant, you will not have a complete list of every delegation that exists; anyone can make subdelegations locally without registering them centrally. This has benefits but also has a UX tradeoff: you can get a view of who can do what, but others could exist.

2 Likes