atproto_core/oauth/flow

The authorization flow as Effects, from identifier to verified session.

start resolves the account to a DID, resolves that DID natively to its PDS (the resolver’s answer is only a hint), discovers and verifies the PDS’s authorization server, and pushes the request (PAR) with PKCE and DPoP. It yields the authorization URL and a Pending flow that carries the DID, the verified authorization server, state, and the PKCE verifier.

callback is the only way to exchange a code: it takes the pending flow and the full callback query, requires state to match and iss to be the verified issuer (RFC 9207), surfaces error responses, exchanges the code, and returns a VerifiedSession only when the token sub is the flow’s DID and that DID resolves back to the same issuer.

To survive a redirect (a browser app navigating away), to_json stores the pending flow’s plain parts and resume rebuilds it, re-fetching and re-verifying the authorization server metadata rather than trusting stored endpoints. The DPoP key is not part of it; keep that separately.

A Pending must be used at most once. The kernel cannot enforce that for a value the caller stores, so the caller’s store must remove the flow when it looks it up by state (a take, not a read).

Types

pub type CallbackError {
  MissingParameter(name: String)
  DuplicateParameter(name: String)
  StateMismatch
  IssuerMismatch(expected: String, actual: String)
  AuthorizationFailed(
    error: String,
    description: option.Option(String),
  )
  ExchangeFailed(error: session.TokenError)
}

Constructors

  • MissingParameter(name: String)
  • DuplicateParameter(name: String)
  • StateMismatch
  • IssuerMismatch(expected: String, actual: String)
  • AuthorizationFailed(
      error: String,
      description: option.Option(String),
    )

    The authorization server reported an error (e.g. access_denied).

  • ExchangeFailed(error: session.TokenError)

Pending

opaque

An authorization request waiting for its callback.

pub opaque type Pending
pub type ResumeError {
  StoredFlowInvalid(reason: String)
  ResumeDiscoveryFailed(error: metadata.MetadataError)
}

Constructors

pub type StartError {
  IdentifierResolveFailed(
    identifier: String,
    error: xrpc.XrpcError,
  )
  InvalidIdentity(error: did.DidError)
  HandleNotClaimed(handle: String, did: String)
  DiscoverFailed(error: metadata.MetadataError)
  ParFailed(error: transport.PostError)
  ParRejected(status: Int, error: option.Option(String))
  ParResponseInvalid(reason: String)
}

Constructors

  • IdentifierResolveFailed(
      identifier: String,
      error: xrpc.XrpcError,
    )

    The resolver could not map a handle to a DID.

  • InvalidIdentity(error: did.DidError)
  • HandleNotClaimed(handle: String, did: String)

    The handle’s DID document does not list the handle in alsoKnownAs.

  • DiscoverFailed(error: metadata.MetadataError)
  • ParFailed(error: transport.PostError)
  • ParRejected(status: Int, error: option.Option(String))
  • ParResponseInvalid(reason: String)

Values

pub fn callback(
  pending: Pending,
  query: List(#(String, String)),
  client_auth: transport.ClientAuth,
) -> effect.Effect(Result(session.VerifiedSession, CallbackError))

Complete a flow from the redirect’s query parameters (all of them, as received). Consumes pending; see the module docs on single use.

pub fn callback_error_to_string(error: CallbackError) -> String
pub fn did(pending: Pending) -> did.Did
pub fn issuer(pending: Pending) -> remote.Origin
pub fn pds(pending: Pending) -> remote.Origin
pub fn resume(
  stored: String,
) -> effect.Effect(Result(Pending, ResumeError))

Rebuild a pending flow from to_json output, re-verifying the issuer’s metadata.

pub fn resume_error_to_string(error: ResumeError) -> String
pub fn start(
  resolver resolver: String,
  identifier identifier: String,
  client_id client_id: String,
  redirect_uri redirect_uri: String,
  scope scope: String,
  pkce_verifier pkce_verifier: String,
  pkce_challenge pkce_challenge: String,
  state state: String,
  client_auth client_auth: transport.ClientAuth,
) -> effect.Effect(Result(#(String, Pending), StartError))

Start a flow for identifier (a handle or a DID). resolver is the Slingshot-compatible service used to map a handle to a DID; it is not consulted for a DID, and its PDS answer is never used. Returns the URL to send the user to and the pending flow to keep until the callback.

pub fn start_error_to_string(error: StartError) -> String
pub fn state(pending: Pending) -> String
pub fn to_json(pending: Pending) -> String

The pending flow as a JSON string for short-lived client-side storage (e.g. sessionStorage keyed by state). It holds the PKCE verifier; keep it no longer than the login takes.

✨ Search Document