Mojo-ATProto-OAuth
view release on metacpan or search on metacpan
### scopes
Default scopes requested by ["start\_auth\_flow"](#start_auth_flow) when no per-call `scopes` opt is given. Defaults to `['atproto']`.
Accepts either an arrayref of individual scope strings (`['atproto', 'account:email']`) or a single space-separated string (`'atproto account:email'`) - either form is normalized to the arrayref-of-tokens form internally, and always read back as one....
### private\_key
A [Crypt::PK::ECC](https://metacpan.org/pod/Crypt%3A%3APK%3A%3AECC) private key, for a confidential client. `undef` (the default) for a public client. Must be set together with ["key\_id"](#key_id) - see ["is\_confidential"](#is_confidential).
### key\_id
The key ID matching ["private\_key"](#private_key). See ["is\_confidential"](#is_confidential).
### loopback
Boolean, true for clients constructed via ["new\_localhost"](#new_localhost). Governs whether ["client\_metadata"](#client_metadata) may be called (it dies for a loopback client - there is no document to serve).
### identity
A [Mojo::ATProto::OAuth::Identity](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3AIdentity) instance, used to resolve handles and DIDs. Defaults to a fresh instance.
### resolver
A [Mojo::ATProto::OAuth::Resolver](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3AResolver) instance, used for auth-server discovery and metadata validation. Defaults to a fresh instance.
### store
A session/auth-request persistence backend - required for ["start\_auth\_flow"](#start_auth_flow), ["process\_callback"](#process_callback), ["start\_scope\_upgrade"](#start_scope_upgrade), and ["refresh\_tokens"](#refresh_tokens) (each dies immediat...
INTERFACE"](#the-store-interface) below. `undef` by default.
May be set to either a store instance, or a short class-name string that resolves to one of the built-in session storage drivers, or the full class name of a driver you want to use. A driver \*must\* implement the methods listed in [Mojo::ATProto::OA...
### ua
A [Mojo::UserAgent](https://metacpan.org/pod/Mojo%3A%3AUserAgent) instance used for every HTTP request this module makes. Defaults to a fresh instance with a 10-second request timeout.
### log
A [Mojo::Log](https://metacpan.org/pod/Mojo%3A%3ALog) instance for debug logging (see ["DEBUG LOGGING"](#debug-logging)). Defaults to a fresh instance at the level named by `MOJO_LOG_LEVEL` (`info` if unset).
### client
A [Mojo::ATProto::OAuth::ResourceClient](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3AResourceClient) instance, for making authenticated XRPC requests against a session's own PDS - see ["AUTHENTICATED RESOURCE-SERVER REQUESTS"](#authent...
## CONSTRUCTORS
### new\_localhost
my $oauth = Mojo::ATProto::OAuth->new_localhost(
callback_url => $callback_url, # required
scopes => \@scopes, # optional, default ['atproto']
ua => $ua, # optional
user_agent_header => $header, # optional
store => $store, # optional
);
Builds a client using ATProto OAuth's "loopback client" allowance for local-dev testing (ported from indigo's `NewLocalhostConfig`): rather than a real `https://` `client_id` URL serving a fetched metadata document, `client_id` is the fixed sentinel ...
The `client_id` host is the literal string `localhost` - a fixed spec sentinel, not a real address to resolve. That is a separate thing from `callback_url`, which must actually point at `127.0.0.1` (not `localhost`) for a plain-`http` redirect URI to...
### new
my $oauth = Mojo::ATProto::OAuth->new(
client_id => $client_id, # required, in the form of https://your-site.com/client-metadata.json or something appropriate
callback_url => $callback_url, # required
scopes => \@scopes, # optional, default ['atproto']
ua => $ua, # optional
user_agent_header => $header, # optional
store => $store, # optional
);
## METHODS
### is\_confidential
my $bool = $oauth->is_confidential;
True if both ["private\_key"](#private_key) and ["key\_id"](#key_id) are set.
### client\_metadata
my $doc = $oauth->client_metadata;
Returns the client ID metadata document (see [Mojo::ATProto::OAuth::ClientMetadata](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3AClientMetadata)) this client's `client_id` URL must serve byte for byte. Dies if called on a loopback clien...
### start\_auth\_flow
my $redirect_url = $oauth->start_auth_flow(%opts);
High-level helper for starting a new login; resolves an identity to auth-server metadata, sends the PAR request, persists the auth request via ["store"](#store), and returns the URL the user's browser should be redirected to for approval. Requires ["...
`%opts` - exactly one of:
- `identifier` - an ATProto handle/DID, or an `https://` auth-server URL directly (this second form skips identity resolution entirely until the callback - the returned session's `account_did`/ `handle` stay unresolved until then).
- `did` + `handle` + `host_url` (all three together) - when the caller has already resolved identity itself (e.g. to make a pre-auth decision, such as reading a public repo record to pick a scope set) and there's no reason to pay for a second identit...
Plus, independent of the above:
- `scopes` (optional, arrayref or space-separated string - see ["scopes"](#scopes)) - overrides ["scopes"](#scopes) for just this call; falls back to the client's configured default when omitted.
- `client_state` (optional hashref) - opaque, never inspected by this module; persisted on the auth request and handed back untouched inside ["process\_callback"](#process_callback)'s result. Intended for things like a post-login redirect target that...
- `extra` (optional hashref) - the same opaque pass-through treatment as `client_state`, but conventionally used by callers for data that belongs in their own session metadata once login completes (e.g. a pre-auth decision worth remembering), rather ...
### start\_auth\_flow\_p
Non-blocking counterpart of ["start\_auth\_flow"](#start_auth_flow).
### process\_callback
my $session_data = $oauth->process_callback($params);
High-level helper for completing the auth flow. `$params` is a plain hashref of the callback request's query parameters (e.g. `$c->req->params->to_hash` in a Mojolicious route handler). Verifies the callback params against the persisted auth reque...
A hashref will be returned as follows:
{
account_did => 'did:plc:...',
handle => 'alice.bsky.social', # or undef
session_id => $state, # the PAR 'state' value
host_url => 'https://pds.example.com',
auth_server_url => 'https://auth.example.com',
auth_server_token_endpoint => '...',
auth_server_revocation_endpoint => '...', # or undef
scopes => [ 'atproto', ... ],
access_token => '...',
refresh_token => '...',
dpop_authserver_nonce => '...',
dpop_host_nonce => '...',
dpop_private_key_pem => '...', # PEM, see Mojo::ATProto::OAuth::DPoP
client_state => $opts_client_state, # from start_auth_flow(_p), or undef
extra => $opts_extra, # from start_auth_flow(_p), or undef
}
If this auth request came from ["start\_scope\_upgrade"](#start_scope_upgrade), `session_id` here is the _existing_ session's id (not a new one) and `scopes` is the union of the existing session's scopes and the newly-granted ones - see ["start\_scop...
On success, the now-consumed auth-request row is deleted from ["store"](#store); a failure to delete it is logged and otherwise ignored (the session itself is already safely persisted at that point - a leftover auth-request row is inert, not a correc...
### process\_callback\_p
Non-blocking counterpart of ["process\_callback"](#process_callback). Rejects (rather than dying) on failure, with the same messages.
### start\_scope\_upgrade
my $redirect_url = $oauth->start_scope_upgrade($session, \@additional_scopes);
Starts a scope-upgrade authorization for an already-known, already- verified session; seamlessly request a broader scope set without a full re-login, merging the result back into the _existing_ session (rather than replacing it) once the callback com...
The PAR request actually asks for the union of `$session`'s current scopes and `$additional_scopes`, so a repeated upgrade request for the same additional scope is idempotent rather than narrowing what gets asked for. Returns the redirect URL, same a...
### start\_scope\_upgrade\_p
Non-blocking counterpart of ["start\_scope\_upgrade"](#start_scope_upgrade).
### refresh\_tokens
my $refreshed_session = $oauth->refresh_tokens($session);
Uses the session's stored refresh token to mint a new access token, without involving the user. Reuses the session's own DPoP key - RFC 9449 requires the same key for every proof across one authorization's lifetime, so refreshing never generates a ne...
### refresh\_tokens\_p
Non-blocking counterpart of ["refresh\_tokens"](#refresh_tokens).
## LOWER-LEVEL METHODS
These are used internally by the high-level methods above, and are also exposed for callers that need finer-grained control (e.g. a caller already holding a persisted auth-request row and only needing the token exchange step). Ordinary use of this mo...
### send\_auth\_request / send\_auth\_request\_p
my $info = $oauth->send_auth_request($auth_meta, %opts);
Sends the PAR request that kicks off an authorization flow, given already-validated auth-server metadata (as returned by ["resolve\_auth\_server\_metadata" in Mojo::ATProto::OAuth::Resolver](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3A...
### send\_initial\_token\_request / send\_initial\_token\_request\_p
my $token_resp = $oauth->send_initial_token_request($auth_code, $info);
Exchanges an authorization code for tokens. `$info` is the `AuthRequestData`- equivalent hashref from ["send\_auth\_request"](#send_auth_request) or a store lookup - reuses its DPoP keypair (RFC 9449 requires the same key for every proof tied to one ...
## THE STORE INTERFACE
["store"](#store) is semi-duck-typed, a base class exists in [Mojo::ATProto::OAuth::SessionStore](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3ASessionStore) that will complain loudly if you subclass it without implementing the proper me...
- `get_auth_request($state)` / `get_auth_request_p($state)`
- `save_auth_request($info)` / `save_auth_request_p($info)`
- `delete_auth_request($state)` / `delete_auth_request_p($state)`
- `get_session($account_did, $session_id)` / `get_session_p($account_did, $session_id)`
- `save_session($session_data)` / `save_session_p($session_data)`
- `delete_session($account_did, $session_id)` / `delete_session_p($account_did, $session_id)`
A store only needs to implement whichever half a given caller actually uses - the synchronous methods if the caller only ever calls this module's synchronous methods (["start\_auth\_flow"](#start_auth_flow), ["process\_callback"](#process_callback), ...
This distribution ships three session store drivers:
- [Mojo::ATProto::OAuth::SessionStore::Memory](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3ASessionStore%3A%3AMemory) - a plain in-process hashref store - sessions and auth requests are lost on process exit; fine for a single-process sc...
- [Mojo::ATProto::OAuth::SessionStore::SQLite](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3ASessionStore%3A%3ASQLite) - an SQLite backed session store, requires [Mojo::SQLite](https://metacpan.org/pod/Mojo%3A%3ASQLite) to be installed.
- [Mojo::ATProto::OAuth::SessionStore::Pg](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3ASessionStore%3A%3APg) - a Postgres backed session store, requires [Mojo::Pg](https://metacpan.org/pod/Mojo%3A%3APg) to be installed.
The Memory store takes no arguments, whereas the SQLite and Pg stores do (connection strings), these can be passed during construction of the OAuth object:
my $pg_backed = Mojo::ATProto::OAuth->new(
client_id => $client_id,
callback_url => $callback_url,
scopes => 'atproto account:email',
store => [ 'Pg' => 'postgresql://user:pass@host:port/dbname' ]
);
my $sqlite_backed = Mojo::ATProto::OAuth->new(
client_id => $client_id,
callback_url => $callback_url,
scopes => 'atproto account:email',
store => [ 'SQLite' => 'file:/tmp/test.db?wal_mode=1' ]
);
## AUTHENTICATED RESOURCE-SERVER REQUESTS
Everything above gets you a persisted session; it doesn't make any calls against the user's own PDS on your behalf. That's what ["client"](#client) (a [Mojo::ATProto::OAuth::ResourceClient](https://metacpan.org/pod/Mojo%3A%3AATProto%3A%3AOAuth%3A%3AR...
# an authenticated GET
my $profile = $oauth->client->request($account_did, $session_id, 'get', '/xrpc/app.bsky.actor.getProfile?actor=' . $account_did);
# an authenticated POST with optimistic-concurrency conflict handling
my $result = eval {
$oauth->client->request($account_did, $session_id, 'post', '/xrpc/com.atproto.repo.putRecord', {
repo => $account_did, collection => 'app.bsky.feed.post', rkey => $rkey, record => $record, swapRecord => $prior_cid,
});
};
if (my $err = $@) {
die $err unless $err =~ /xrpc_error=InvalidSwap/;
# ... re-read the record, retry with a fresh $prior_cid ...
}
( run in 0.398 second using v1.01-cache-2.11-cpan-ad19def0cd9 )