ATProto in Practice #1: Identity – mackuba.eu
I’m starting a new blog post series that I’ve named “ATProto in Practice”. I want to go through some practical tasks that you’re likely to need when working with the protocol, using Ruby and my Ruby gems for the examples. These will (hopefully) be a bit shorter than my standard book-length blog posts here 🫠
If you’re new to the AT Protocol and you haven’t read my long Introduction to AT Protocol post that goes through the whole architecture and defines the various pieces of it, I recommend you read that one first, because I won’t be explaining everything from scratch again here.
Let’s start with something pretty fundamental: user identity.
DIDs & handles
Every account has an immutable identifier called a DID (Decentralized Identifier) , e.g. did:plc:z72i7hdynmk6r22z27h6tvur. This ID serves a similar role as an UUID might in an SQL database – it’s used everywhere behind the scenes in the protocol for any references between accounts and records, in URIs and so on.
But generally every account also has a human-readable handle assigned to it, which can be changed at any time, and this handle is normally what you see in the UI, often in user-facing URLs, what you use to log in with, etc. The handle is a domain name, e.g. @python.org, and any existing domain name you own and use for your website can be used as your handle.
As you start building on ATProto, one of the first things you might need to do is convert between these two identifiers. For example, a user logs in to your app using a handle and you need to first “resolve” it to a DID, or a URI or other reference in a record points to a DID, but you want to show the corresponding user’s handle in the UI, and the handle is not included in the data you got.
A useful UI tool for quickly looking this up is internect.info. If you look up e.g. “firefox.com” there, you will get a page showing details of Firefox’s ATProto account: the DID (did:plc:m424cqoxwhxgjutbta7jmrur), when it was created, assigned PDS hostname, and so on.
Now, how to do the same thing in code?
Every DID has a so called DID Document , a JSON file storing some basic info about that identity, and among other things, that document includes the alsoKnownAs field, which lists the assigned handles, in the form of at:// URIs with only the first segment. Almost every account will have exactly one handle assigned, but it might happen that one will have zero or more than one. It might also happen that the array will contain some invalid strings which aren’t proper handles – e.g. don’t include the at:// prefix, or only contain one word and no periods. You need to filter only the valid ones.
The assignment is bi-directional – a DID has one (usually) handle assigned in its document, and a handle resolves to a DID. You should ideally check the assignment in both directions, because it could happen that the other sign of the assignment is no longer valid, or worse – has never been real. That is, when you resolve a handle to a DID, check if that DID has that handle configured, and if you check a DID’s assigned handles, check if those handles are verified (if they resolve to the same DID).
Resolving handles
When starting from the handle’s side, there are two ways that a handle can be verified (assigned to a DID), and you need to check for both, because the user might have used either method depending on what’s more convenient to them:
1) Verification by DNS
The first way to verify a handle is to add a DNS TXT record in your domain’s DNS config, with _atproto (with an underscore) as the subdomain name and did=... as the text. This method is generally more common for custom handles, when someone manually assigns some domain which they’ve used before for their site or other things (or that they bought specifically for this purpose) to their Bluesky/ATProto account.
In Ruby, you can read these DNS records this way:
require 'resolv'
records = Resolv::DNS.open { |dns|<br>dns.getresources('_atproto.eff.org', Resolv::DNS::Resource::IN::TXT)<br># => [#]
records.first.data<br># => "did=did:plc:lr36xv2l64jwtnyoaqem6z2z"
So to go from a handle to a DID you’ll need to get these TXT records, check if one exists, if the format looks as expected, and dig out the part after did=:
def resolve_handle_by_dns(domain)<br>dns_records = Resolv::DNS.open { |dns|<br>dns.getresources("_atproto.#{domain}", Resolv::DNS::Resource::IN::TXT)
if record = dns_records.first<br>if record.data =~ /\Adid\=(did\:\w+\:.+)\z/<br>return $1<br>end<br>end
return nil<br>end
2) Verification by HTTP .well-known
The second way is to put a text file with the DID inside in a special .well-known location on the given domain’s website, at .well-known/atproto-did (just the DID itself, without the did= prefix this time). This method is more common for default handles given...