AT Protocol Identity for Dart
atproto_identity resolves AT Protocol identities and verifies inbound
service-auth JWTs. It is a small, dependency-light building block: no code
generation, and it depends only on http and
did_plc.
Use it to:
- Resolve a handle or DID to its DID, PDS origin, and
#atprotosigning key. - Verify an inbound AppView service-auth JWT (e.g. in a custom feed generator or other AppView) and recover the viewer's DID.
Installation
dependencies:
atproto_identity: ^0.1.0 # Replace with the actual version
Resolving an identity
HttpIdentityResolver accepts a handle (alice.example, optionally prefixed
with @ or at://) or a DID (did:plc: / did:web:):
import 'package:atproto_identity/atproto_identity.dart';
Future<void> main() async {
final resolver = HttpIdentityResolver();
final identity = await resolver.resolve('shinyakato.dev');
print(identity.did); // did:plc:...
print(identity.pds); // https://... (origin, no trailing slash)
print(identity.handle); // shinyakato.dev
print(identity.signingKey); // #atproto publicKeyMultibase, or null
}
When resolution starts from a handle, the resolver verifies the DID document
claims that handle back through alsoKnownAs (bidirectional handle
verification). On any failure it throws an IdentityException.
HttpIdentityResolver is configurable and injectable:
final resolver = HttpIdentityResolver(
handleResolver: 'https://public.api.bsky.app',
plcDirectory: 'https://plc.directory',
httpClient: myHttpClient, // optional package:http Client
);
IdentityResolver is an interface, so you can supply your own (cached, offline,
test-double) implementation anywhere this package expects one.
Verifying an inbound service-auth JWT
verifyServiceAuth verifies an inbound AppView service-auth JWT taken from an
Authorization: Bearer <jwt> header and returns the issuer (viewer) DID:
import 'package:atproto_identity/atproto_identity.dart';
Future<String> authenticate(String authorizationHeader) async {
return verifyServiceAuth(
authorizationHeader,
serviceDid: 'did:web:feed.example.com', // this service's DID; must equal `aud`
resolver: HttpIdentityResolver(),
expectedLxm: 'app.bsky.feed.getFeedSkeleton', // optional; must equal `lxm`
);
}
It validates the JOSE header, the token's aud, exp, iat, nbf, optional
lxm, and iss claims, resolves the issuer's #atproto signing key, and checks
the signature (ES256K / P-256) via did_plc. It fails closed:
- the
algheader must beES256KorES256, rejectingnone, HMAC, and RSA (the signing curve is still pinned from the DID document — this is defense in depth), andtyp, when present, must be a JWT media type; - the signature must be a 64-byte compact ECDSA signature;
- an out-of-range
expis rejected up front instead of overflowing; - the token lifetime is bounded by
maxTokenLifetime(default 60 minutes; passnullto opt out), andiat(not in the future) /nbf(not-before) are enforced when present, with a 30-second clock-skew allowance.
Any failure — malformed header/JWT, an untrusted alg, wrong audience,
expired/not-yet-valid token, an exp beyond maxTokenLifetime, lxm mismatch,
unresolvable issuer, missing signing key, or a signature that does not verify —
throws an IdentityException.
Contribution 🏆
If you would like to contribute to atproto_identity, please create an
issue or create a PR.
Support ❤️
The simplest way to show us your support is by giving the project a star at GitHub and pub.dev.
License 🔍
All resources of atproto_identity are provided under the BSD-3 license.