ATProto OAuth: client, discovery, and session management
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

OCaml 93.1%
JavaScript 2.5%
Dune 0.8%
Shell 0.5%
Other 3.1%
16 1 0

Clone this repository

https://tangled.org/gazagnaire.org/ocaml-atproto-oauth https://tangled.org/did:plc:jhift2vwcxhou52p3sewcrpx/ocaml-atproto-oauth
git@git.recoil.org:gazagnaire.org/ocaml-atproto-oauth git@git.recoil.org:did:plc:jhift2vwcxhou52p3sewcrpx/ocaml-atproto-oauth

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

atproto-oauth#

ATProto OAuth client for OCaml.

atproto-oauth implements the ATProto-specific OAuth 2.0 profile on top of oauth: PAR (RFC 9126) + PKCE (RFC 7636) + DPoP (RFC 9449) with mandatory dpop_bound_access_tokens, client metadata built to the ATProto defaults, and full discovery from handle to PDS to authorization server per the ATProto OAuth clients spec.

Packages#

  • atproto-oauth — profile rules, client metadata builders, session type with refresh.
  • atproto-oauth.discovery — handle → DID → PDS → resource → AS chain with profile validation at every step.
  • atproto-oauth.login — full public-client CLI login flow (loopback listener + PAR + PKCE + DPoP).

Installation#

Install with opam:

$ opam install atproto-oauth

If opam cannot find the package, it may not yet be released in the public opam-repository. Add the overlay repository, then install it:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install atproto-oauth

Usage#

Scopes and client metadata#

The profile mandates the atproto scope; ATProto clients typically combine it with transition:generic for legacy API access:

# Atproto_oauth.Profile.default_scope
- : string = "atproto transition:generic"

Build a public loopback-client metadata document for a CLI tool:

# let meta =
    Atproto_oauth.Client_metadata.public_loopback
      ~client_id:"https://example.com/client-metadata.json"
      ~redirect_uris:[ "http://127.0.0.1/callback" ]
      () in
  Oauth.Client.(meta.scope, meta.dpop_bound_access_tokens)
- : string option * bool = (Some "atproto transition:generic", true)

Full login flow (public loopback client)#

Atproto_oauth_login.login walks handle resolution, discovery, profile validation, PAR, browser consent, and the DPoP-bound token exchange. The caller hands it an Eio switch, clock, net, and a Requests.t, plus the client id of a hosted metadata document:

let login env ~http handle =
  Eio.Switch.run @@ fun sw ->
  match
    Atproto_oauth_login.login
      ~sw
      ~clock:(Eio.Stdenv.clock env)
      ~net:(Eio.Stdenv.net env)
      ~http
      ~client_id:"https://example.com/client-metadata.json"
      handle
  with
  | Ok session -> Fmt.pr "logged in: %a@." Atproto_oauth.Session.pp session
  | Error e -> Fmt.pr "login failed: %a@." Atproto_oauth_login.pp_error e

Discovery on its own#

If you already hold a DID or handle and want the PDS + AS metadata without running the full flow:

let discover env ~http handle =
  Eio.Switch.run @@ fun sw ->
  Atproto_oauth_discovery.of_handle
    ~sw
    ~clock:(Eio.Stdenv.clock env)
    ~net:(Eio.Stdenv.net env)
    ~http handle

The result bundles the DID, the PDS URL, the RFC 9728 resource metadata and the RFC 8414 server metadata — all already validated against the ATProto profile.

Session refresh and persistence#

Atproto_oauth.Session.refresh rotates the access token using the stored refresh token and DPoP key, handling DPoP-Nonce retries transparently. Sessions serialise to JSON via Atproto_oauth.Session.save / load (write at 0600; the file contains private key material and the refresh token).

Licence#

ISC