Latch (latch v0.1.0)

Copy Markdown

A library for building atproto OAuth integrations with a low-level client runtime.

Latch implements atproto OAuth, including:

  • identity resolution
  • server discovery
  • pushed authorization requests (PAR)
  • proof key for code exchange (PKCE)
  • demonstrating proof of possession (DPoP) with server-issued nonces
  • token exchange
  • refresh
  • authenticated XRPC calls to a user's PDS

Get started

Add Latch to your supervision tree, giving it a unique name and a Latch.Store implementation:

children = [

{Latch,
  name: MyApp.Latch,
  store: MyApp.LatchStore,
  client_id: "https://myapp.example/oauth-client-metadata.json",
  redirect_uri: "https://myapp.example/auth/callback",
  scope: "atproto",
  signing_key: "key"}

]

Optional keys: :client_name, :client_uri and request_ttl.

Login flow

  1. authorize/2 resolves the handle, pushes the authorization request, and returns the URL to redirect the browser to.
  2. The user authorizes, their authorization server redirects back to your redirect_uri.
  3. callback/2 validates the callback params, exchanges the code, and stores the session for you using your Latch.Store implementation. Returns identity information for the user.

When a user logs out, call delete_session to clear their session.

Authenticated requests

query/4, procedure/4, and upload_blob/4 make XRPC calls to a user's PDS, where their data is stored, using DPoP under the hood. The session lives in your datastore, defined through your Latch.Store module. Latch uses that to store and rotate access tokens for the client requests.

Latch.query(MyApp.Latch, "did:plc:abc123", "com.atproto.repo.getRecord",

repo: "did:plc:abc123",
collection: "app.bsky.feed.post",
rkey: "3k2..."

)

Errors

Public functions return {:error, exception} tuples and will not normally raise on errors. See Latch.Error for more information.

Summary

Functions

Begins an authorization flow for handle.

Completes an authorization flow from the OAuth callback params.

Returns a child specification to start Latch under a supervisor.

Returns the client metadata map.

Performs a procedure against the user's PDS using their DID's session.

Query the user's PDS using their DID's session.

Starts a Latch supervisor.

Upload a blob to the user's PDS using their DID's session.

Types

name()

@type name() :: atom() | pid()

Functions

authorize(name, handle)

Begins an authorization flow for handle.

Resolves the handle to a DID and PDS, discovers the authorization server, pushes the authorization request (PAR), stores the in-flight request in the configured Latch.Store, and returns the URL to redirect the browser to.

The stored request is single-use. callback/2 consumes it.

Examples

iex> {:ok, _pid} = Latch.start_link(name: LatchAuthorizeExample, store: Latch.TestStore, client_id: "https://myapp.example/metadata.json", redirect_uri: "https://myapp.example/callback", scope: "atproto", signing_key: Jason.encode!(Latch.DPoP.generate_key()))
iex> Latch.authorize(LatchAuthorizeExample, "not a handle")
{:error, %Latch.Error.HandleNotFound{handle: "not a handle", reason: :invalid_handle}}

callback(name, params)

Completes an authorization flow from the OAuth callback params.

Consumes a stored request. Single use, so a replayed callback fails with %Latch.Error.SecurityViolation{}. Verifies the issuer, exchanges the code, stores the session using the Latch.Store implementation, and returns identity information.

child_spec(init_arg)

Returns a child specification to start Latch under a supervisor.

Examples

iex> Latch.child_spec(name: MyApp.Latch, store: MyApp.Store, client_id: "https://myapp.example/metadata.json", redirect_uri: "https://myapp.example/callback", scope: "atproto", signing_key: :test_key)
%{id: MyApp.Latch, start: {Latch, :start_link, [[name: MyApp.Latch, store: MyApp.Store, client_id: "https://myapp.example/metadata.json", redirect_uri: "https://myapp.example/callback", scope: "atproto", signing_key: :test_key]]}, type: :supervisor}

client_metadata(name)

@spec client_metadata(name()) :: Latch.ClientMetadata.t()

Returns the client metadata map.

Serve it as JSON at the URL configured as :client_id, e.g. from a controller: json(conn, Latch.client_metadata(MyApp.Latch)).

delete_session(name, did)

@spec delete_session(name(), String.t()) :: :ok | {:error, Latch.Error.Store.t()}

procedure(name, did, method, body)

Performs a procedure against the user's PDS using their DID's session.

Examples

Latch.procedure(MyApp.Latch, "did:plc:abc123", "com.atproto.repo.putRecord", %{
  repo: "did:plc:abc123",
  collection: "app.bsky.feed.post",
  rkey: "3k2...",
  record: %{"$type" => "app.bsky.feed.post", "text" => "Hello!"}
})

query(name, did, method, params \\ [])

Query the user's PDS using their DID's session.

method is the XRPC method NSID, eg "com.atproto.repo.getRecord". params is passed as the query string.

Assumes the session exists, that the user of that did is authenticated. If not, returns {:error, %NoSession{}}.

Examples

Latch.query(MyApp.Latch, "did:plc:abc123", "com.atproto.repo.getRecord", repo: "did:plc:abc123", collection: "app.bsky.feed.post", rkey: "3k2...")

start_link(opts)

Starts a Latch supervisor.

See the module documentation for the supported options.

upload_blob(name, did, bytes, content_type)

Upload a blob to the user's PDS using their DID's session.

content_type is the blob's MIME type, eg "image/png".