Latch (latch v0.1.0)
Copy MarkdownA 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
authorize/2resolves the handle, pushes the authorization request, and returns the URL to redirect the browser to.- The user authorizes, their authorization server redirects back to your
redirect_uri. callback/2validates the callback params, exchanges the code, and stores the session for you using yourLatch.Storeimplementation. 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
Functions
@spec authorize(name(), String.t()) :: {:ok, String.t()} | {:error, Latch.Error.HandleNotFound.t() | Latch.Error.IdentityMismatch.t() | Latch.Error.Discovery.t() | Latch.Error.InvalidResponse.t() | Latch.Error.MissingDPoPNonce.t() | Latch.Error.OAuth.t() | Latch.Error.Store.t() | Latch.Error.Transport.t()}
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}}
@spec callback(name(), map()) :: {:ok, %{did: String.t(), handle: String.t()}} | {:error, Latch.Error.InvalidResponse.t() | Latch.Error.MissingDPoPNonce.t() | Latch.Error.OAuth.t() | Latch.Error.SecurityViolation.t() | Latch.Error.Store.t() | Latch.Error.Transport.t()}
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.
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}
@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)).
@spec delete_session(name(), String.t()) :: :ok | {:error, Latch.Error.Store.t()}
@spec procedure(name(), String.t(), String.t(), map()) :: {:ok, map()} | {:error, Latch.Error.InvalidResponse.t() | Latch.Error.MissingDPoPNonce.t() | Latch.Error.NoSession.t() | Latch.Error.RefreshFailed.t() | Latch.Error.Store.t() | Latch.Error.Transport.t() | Latch.Error.XRPC.t()}
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!"}
})
@spec query(name(), String.t(), String.t(), Keyword.t()) :: {:ok, map()} | {:error, Latch.Error.InvalidResponse.t() | Latch.Error.MissingDPoPNonce.t() | Latch.Error.NoSession.t() | Latch.Error.RefreshFailed.t() | Latch.Error.Store.t() | Latch.Error.Transport.t() | Latch.Error.XRPC.t()}
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...")
Starts a Latch supervisor.
See the module documentation for the supported options.
@spec upload_blob(name(), String.t(), binary(), String.t()) :: {:ok, map()} | {:error, Latch.Error.InvalidResponse.t() | Latch.Error.MissingDPoPNonce.t() | Latch.Error.NoSession.t() | Latch.Error.RefreshFailed.t() | Latch.Error.Store.t() | Latch.Error.Transport.t() | Latch.Error.XRPC.t()}
Upload a blob to the user's PDS using their DID's session.
content_type is the blob's MIME type, eg "image/png".