Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Atproto Oauth Bundle Laravel Package

wecoprod/atproto-oauth-bundle

Laravel package that adds AT Protocol (Bluesky) OAuth sign-in to your app. It handles the OAuth flow, DIDs, and session management so users can log in with their Atmosphere account, with drop-in integration into Laravel's existing auth system.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package, so "getting started" in a Laravel app means deciding which layer you actually want:

  1. Read the source first, install second. Before adding a dependency whose framework wiring won't register in Laravel, clone the repo and read three things in order: the atproto-oauth:generate-key command, the client-metadata/JWKS controller, and SessionManager. Those three define the whole contract (key handling, public client identity, token lifecycle) and are also the parts you'd re-implement.
  2. Install the low-level pieces only. composer require the package and immediately check whether the classes you need (AtprotoClient, the identity resolver, the DPoP signer) have container or bundle dependencies. If they're plain services taking PSR-18/PSR-17 clients, you can use them standalone and skip the bundle entirely. If not, use the package as a reference implementation and port the flow — it's a few hundred lines of OAuth ceremony you don't want to invent.
  3. First use case: post-to-Bluesky on behalf of a user — "sign in with atproto" (a Laravel Socialite-shaped flow) plus a queued job that publishes an announcement. That exercises identity resolution, PAR/PKCE, DPoP, session persistence, and the XRPC POST path in one pass.

Non-negotiable prerequisites: PHP 8.2+, HTTPS APP_URL (AT Protocol authorization servers require client_id to be an https:// URL whose origin matches client_uri; http://localhost is the only dev exception), and a persistent cache + lock store.

Implementation Patterns

Wrap it in a service provider, don't fight the bundle. Register the pieces you reuse as singletons in AppServiceProvider, bind config from config/services.php (atproto.client_id, atproto.redirect_uri, atproto.key_path), and expose a small AtprotoManager facade. Keep the vendor namespace at arm's length so a future swap is a one-file change.

Port the command, keep the behaviour. atproto-oauth:generate-key → php artisan atproto:generate-key. Store the ES256 private key via Laravel's encrypted cast or a secrets manager, never in .env or a committed JSON file. Cache the derived public JWK so the metadata endpoint can serve it without touching the key.

Own the HTTP surface. Serve the two public endpoints as routes: GET /.well-known/oauth-client-metadata (JSON) and GET /.well-known/jwks.json. The package's client_uri-vs-client_id-origin check belongs here — implement it as a startup assertion or a test, because a mismatch produces a confusing rejection from the authorization server rather than a clear local error.

Session model. Persist atproto sessions in their own table (atproto_sessions: did, handle, pds, auth_server, access_token, refresh_token, dpop_jti/nonce, expires_at), keyed to users.id, rather than reusing Sanctum tokens or the sessions table. Two token lifecycles that look similar will eventually collide.

Refresh with a Cache::lock. This is the package's symfony/lock serialization, translated. Before any XRPC call, if expires_at is within the skew window, take Cache::lock("atproto:refresh:{$did}", 10), re-read the row, refresh once, release. Without this, a queue worker and a web request will both burn the same refresh token and one will get revoked.

Queue the writes. AtprotoClient::post/uploadBlob behind jobs with $backoff and $tries = 3. XRPC is rate-limited per-DID and per-IP; a job that retries immediately is how you get a 429 storm.

Identity resolution as a standalone resolver. Handle→DID→PDS→authorization server, with the bidirectional handle check, is genuinely useful outside OAuth — enriching a stored DID, validating a handle a user typed, detecting a handle change. Wrap it in a ResolveAtprotoIdentity action and cache results for minutes, not hours (PDS records change).

Use it as a Socialite driver. If sign-in is the main need, model your provider on Socialite's AbstractProvider contract so Socialite::driver('atproto') reads familiar. The two-hop dance (auth server discovery, then PAR) doesn't fit Socialite's shape exactly, but the ergonomics are worth imitating.

Gotchas and Tips

Symfony version constraint is the highest-risk item. Requiring Symfony 7.2+ components can conflict with laravel/framework's pinned symfony/* versions depending on your Laravel minor. Run composer require and inspect the resolution before writing code; you may need -W and a Laravel upgrade, or to only require the non-conflicting sub-packages (symfony/lock, symfony/http-client).

DPoP is stateful and easy to get subtly wrong. The server returns DPoP-Nonce; you must retry the request once with that nonce, and cache the nonce per authorization server. If you skip the retry you'll see intermittent 400s that vanish on a second attempt — the classic symptom of an unimplemented nonce loop. Store nonces in Redis, not the file cache, so all workers see them.

client_id is an identifier, not config. Its origin must equal client_uri's origin. Changing your app domain invalidates every existing session. Treat the pair as immutable and version it deliberately.

Handles are not identities. Store the did as the primary key and the handle as a mutable label. The bidirectional check exists precisely because a handle can be reassigned; a user renaming their handle should not orphan their account.

uploadBlob limits. There are hard size limits (on the order of a few MB) and MIME restrictions; validate and downscale before upload rather than catching a rejection after a long job. Store the returned blob ref, not the bytes.

Queue workers need shared cache and lock stores. File-based cache/locks silently break refresh serialization the moment you run more than one worker. Set CACHE_STORE=redis (or database) and cache.locks accordingly.

Token expiry is not auth expiry. Don't tie atproto refresh to your Laravel session lifetime. Revoking on logout should call the revocation endpoint and delete the row; a stale refresh token will otherwise resurrect the session.

Dev against bsky.social with a throwaway account. The release notes confirm production testing against bsky.social, which is the right target for verification, but the atproto sandbox story is thin — budget for a real test identity and log the full OAuth exchange (redacting tokens) while integrating.

Debugging checklist for a failed handshake: client_id origin match → key/JWKS served and reachable → PKCE verifier preserved across the redirect → nonce retry implemented → exact redirect_uri match (one trailing slash difference is fatal).

Extension point to watch: if AtprotoClient accepts an injected PSR-18 client, bind Laravel's Http client with retry and logging middleware — that gives you observability over every XRPC call for free. If it doesn't, that injection seam is the first thing worth proposing upstream.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core