resolveFromIdentity static method
- OAuthClientMetadata metadata,
- String identity, {
- String handleResolver = 'https://public.api.bsky.app',
- String plcDirectory = 'https://plc.directory',
- Client? httpClient,
Resolves an atproto identity (handle or DID) down to its OAuth authorization server and returns a ready-to-use OAuthClient.
Implements the atproto identity-resolution flow:
- When
identityis a handle, it is resolved to a DID viacom.atproto.identity.resolveHandleagainsthandleResolver. Adid:...identity is used as-is. - The DID document is fetched — from
plcDirectoryfordid:plc, or from/.well-known/did.json(or the path form) fordid:web. - When the flow started from a handle, bidirectional verification is
performed: the DID document's
alsoKnownAsmust containat://<handle>. - The
#atproto_pdsservice endpoint is extracted and the PDS is resolved to its authorization server exactly like resolveFromPds.
The returned client has expectedSub set to the resolved DID, so the
sub of every token response is verified to match the identity this
flow was initiated for.
identity may be a handle (alice.example.com, optionally prefixed
with @ or at://) or a DID (did:plc:... / did:web:...).
handleResolver is the XRPC service used to resolve handles; it
defaults to the public Bluesky AppView. Self-hosters can point this at
their own PDS/entryway.
Throws an OAuthException when any resolution step fails, when
bidirectional handle verification fails, or when the DID method is not
supported (only did:plc and did:web are).
Implementation
static Future<OAuthClient> resolveFromIdentity(
final OAuthClientMetadata metadata,
final String identity, {
final String handleResolver = 'https://public.api.bsky.app',
final String plcDirectory = 'https://plc.directory',
final http.Client? httpClient,
}) async {
var normalized = identity.trim();
if (normalized.startsWith('at://')) {
normalized = normalized.substring('at://'.length);
}
if (normalized.startsWith('@')) {
normalized = normalized.substring(1);
}
if (normalized.isEmpty) {
throw ArgumentError.value(identity, 'identity', 'must not be empty');
}
final String did;
final String? handle;
if (normalized.startsWith('did:')) {
did = normalized;
handle = null;
} else {
// Handles are case-insensitive; normalize to lowercase.
handle = normalized.toLowerCase();
did = await _resolveHandle(handle, handleResolver, httpClient);
}
final didDocument = await _resolveDidDocument(
did,
plcDirectory,
httpClient,
);
if (handle != null) {
// Bidirectional verification: the DID document must claim the handle
// back via `alsoKnownAs`, otherwise anyone could point a handle at an
// arbitrary DID.
final alsoKnownAs = didDocument['alsoKnownAs'];
final claimsHandle =
alsoKnownAs is List &&
alsoKnownAs.whereType<String>().any(
(final aka) => aka.toLowerCase() == 'at://$handle',
);
if (!claimsHandle) {
throw OAuthException(
'Bidirectional handle verification failed: the DID document for '
'"$did" does not list "at://$handle" in "alsoKnownAs"',
);
}
}
final pdsOrigin = _extractPdsEndpoint(didDocument, did);
final authorizationServer = await _resolveAuthorizationServer(
pdsOrigin,
httpClient,
);
return OAuthClient(
metadata,
service: authorizationServer.authority,
pds: pdsOrigin,
expectedSub: did,
httpClient: httpClient,
);
}