AT Protocol Oauth TLDR
Two weekends after thinking I’ll quickly try the bookmarks lexicon over the weekend in Tauri, I might finally have a decent idea of some of the oauth process. Bluesky PBC has published a set of libraries that are much easier to use from javascript, but if you are not in such a fortunate postition, or just want to know how the process works this might be useful.
If you want to read it all or follow along here are the documents published by Bluesky PBC:
This all looked like a jumbled mess to me before seeing the chart in the proposal:
┌───────┐ ┌────────┐ ┌─────┐ ┌──────────────────────┐
│ User │ │ Client │ │ PDS │ │ Authorization Server │
└───┬───┘ └───┬────┘ └──┬──┘ └────────────┬─────────┘
│ User enters @handle (1) │ │ │
├──────────────────────────►│ │ │
│ ├──┐ │ │
│ │ │ Client resolves PDS URL (2)│ │
│ │◄─┘ │ │
│ │ │ │
│ (3)│ GET /.well-known │ │
│ │ /oauth-protected-resource │ │
│ ├──────────────────────────────►│ │
│ │ GET /.well-known │ │
│ │ /oauth-authorization-server │ │
│ ├───────────────────────────────┼────────────────►│
│ │ │ │
│ ├──┐ │ │
│ │ │ Validate issuer (4) │ │
│ │◄─┘ │ │
│ │ │ │
│ │ Pushed Authorization Request (5) │
│ ├───────────────────────────────┬────────────────►│
│ │ │ │
│ │ Client metadata discovery (6) │ │
│ │◄──────────────────────────────┼─────────────────┤
│ User redirected to │ │ │
│ authorize URL (7) │ │ │
│◄──────────────────────────┤ │ │
│ │ │ │
│ redirected to authorize URL (8) │ │
├───────────────────────────┬───────────────────────────────┼────────────────►│
│ │ │ ├──┐
│ │ │ │ │ Verification
│ User authenticates themself on AS and approves the request (10) │◄─┘ (9)
│◄──────────────────────────┬───────────────────────────────┬────────────────►│
│ │ │ │
│ User redirected to redirect_uri (11) │ │
├──────────────────────────►┐ │ │
│ │ │ │
│ │ Token retrieval (12) │ │
│ ├───────────────────────────────┼────────────────►│
│ │ │ ├──┐
│ │ │ │ │ Session
│ │ Tokens are issued and returned to the client(14)│◄─┘ created (13)
│ │◄──────────────────────────────┬─────────────────┤
│ │ │ │
│ │ Client makes API requests (15)│ │
│ ├──────────────────────────────►│ │
│ │ │ │
If you were going to use javascript you probably wouldn’t be reading this, so let’s start in the shell with curl and jq.
- dig or nslookup will work if you have them, otherwise you can use a web service to resolve the name. also check
same.supply/.well-known
… I haven’t found a handle to test this with yet.
curl -s "https://dns.google/resolve?name=_atproto.same.supply&type=TXT"
Response:
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": false,
"CD": false,
"Question": [
{
"name": "_atproto.same.supply.",
"type": 16
}
],
"Answer": [
{
"name": "_atproto.same.supply.",
"type": 16,
"TTL": 300,
"data": "did=did:plc:ukgwapa3bceculh4cobcopg3" // <-- this is what we want
}
]
}
- take the did from the response and send it to plc.directory. to get the pds server url (service.serviceEndpoint)
curl -s "https://plc.directory/did:plc:ukgwapa3bceculh4cobcopg3" | jq
Response:
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1"
],
"id": "did:plc:ukgwapa3bceculh4cobcopg3",
"alsoKnownAs": [
"at://same.supply"
],
"verificationMethod": [
{
"id": "did:plc:ukgwapa3bceculh4cobcopg3#atproto",
"type": "Multikey",
"controller": "did:plc:ukgwapa3bceculh4cobcopg3",
"publicKeyMultibase": "zQ3shokSyNnHdGaBFi3hBEfBNH4EMHa3PSa3MKqPjqPt1L3vi"
}
],
"service": [
{
"id": "#atproto_pds",
"type": "AtprotoPersonalDataServer",
"serviceEndpoint": "https://shiitake.us-east.host.bsky.network"
}
]
}
- pds server url /.well-known/oauth-protected-resource gives the authorization_servers
curl -s "https://shiitake.us-east.host.bsky.network/.well-known/oauth-protected-resource" | jq
Response:
{
"resource": "https://shiitake.us-east.host.bsky.network",
"authorization_servers": [
"https://bsky.social"
],
"scopes_supported": [],
"bearer_methods_supported": [
"header"
],
"resource_documentation": "https://atproto.com"
}
- finally authorization server /.well-known/oauth-authorization-server gives you a large amount of information on what settings the server supports. scopes, subject_types, and more.
curl -s "https://bsky.social/.well-known/oauth-authorization-server" | jq
Response:
{
"issuer": "https://bsky.social",
"scopes_supported": [
"atproto",
"transition:generic",
"transition:chat.bsky"
],
"subject_types_supported": [
"public"
],
"response_types_supported": [
"code"
],
"response_modes_supported": [
"query",
"fragment",
"form_post"
],
"grant_types_supported": [
"authorization_code",
"refresh_token"
],
"code_challenge_methods_supported": [
"S256"
],
"ui_locales_supported": [
"en-US"
],
"display_values_supported": [
"page",
"popup",
"touch"
],
"authorization_response_iss_parameter_supported": true,
"request_object_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES256K",
"ES384",
"ES512",
"none"
],
"request_object_encryption_alg_values_supported": [],
"request_object_encryption_enc_values_supported": [],
"request_parameter_supported": true,
"request_uri_parameter_supported": true,
"require_request_uri_registration": true,
"jwks_uri": "https://bsky.social/oauth/jwks",
"authorization_endpoint": "https://bsky.social/oauth/authorize",
"token_endpoint": "https://bsky.social/oauth/token",
"token_endpoint_auth_methods_supported": [
"none",
"private_key_jwt"
],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES256K",
"ES384",
"ES512"
],
"revocation_endpoint": "https://bsky.social/oauth/revoke",
"introspection_endpoint": "https://bsky.social/oauth/introspect",
"pushed_authorization_request_endpoint": "https://bsky.social/oauth/par",
"require_pushed_authorization_requests": true,
"dpop_signing_alg_values_supported": [
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES256K",
"ES384",
"ES512"
],
"client_id_metadata_document_supported": true
}
You might not need to do this in every case. If you already know the user’s pds or authorization server’s url it’s fine to skip the steps before. On the other hand you absolutely, positively must check the schema returned by the authoritative server matches the spec and reject it otherwise. So go give the response a thorough skim for anything that looks off.
Now what exactly is a ‘Pushed Authorization Request’? It’s a message sent as json to the authorization server’s pushed_authorization_request_endpoint
.
There is a special exception for the localhost development workflow to use http://127.0.0.1 or http://[::1] URLs, with matching rules described in the “Localhost Client Development” section. These clients use web URLs, but have application_type set to native in the generated client metadata.
For native clients, the redirect_uri may use a custom URI scheme to have the operating system redirect the user back to the app, instead of a web browser. Native clients are also allowed to use an HTTPS URL. Any custom scheme must match the client_id hostname in reverse-domain order. The URI scheme must be followed by a single colon (:) then a single forward slash (/) and then a URI path component. For example, an app with client_id https://app.example.com/client-metadata.json could have a redirect_uri of com.example.app:/callback.
curl -X POST https://bsky.social/oauth/par \
-H "Content-Type: application/json" \
-d '{
"client_id": "http://localhost",
"state": "random_state_token",
"code_challenge": "derived_code_challenge",
"code_challenge_method": "S256",
"scope": "atproto",
"redirect_uri": "http://[::1]/",
"response_type": "code"
}'
Response:
{
"request_uri":"urn:ietf:params:oauth:request_uri:req-2f722e81216c7ee8b1477e02d552e507",
"expires_in":299
}
This isn’t a regular auth request in a couple ways. We’re using the localhost client path because it’s the fastest way to get to the end of this article (and might be your best option for some kinds of native apps). We’ll circle back before the end to cover client metadata. state
and code_challenge
should also be a properly generated random token and PKCE challenge string. Servers are expected to reject repeated ‘state’ so no one can ever use “random_state_token” on bsky.social ever again.
For this auth flow the hard part is mostly done. The next step is to redirect the user to their authorization server, where they decide to allow your app. The url for that would be https://{authorization_server}/{authorization_endpoint}?request_uri={request_uri}&client_id={client_id}
in this case: https://bsky.social/oauth/authorize?request_uri=urn:ietf:params:oauth:request_uri:req-2f722e81216c7ee8b1477e02d552e507&client_id=http://localhost
. After the user signs in and approves your app they will be redirected to the aptly named redirect_uri
. They will also get sent here if they don’t approve the app. You will be able to tell from the response or token received.