# atsw.js A tiny OAuth browser client for atproto using a service worker. Make authenticated requests to your PDS using plain `‌fetch` calls. Check out a [live demo](https://jake.tngl.io/atsw/). **This is very experimental** — you should probably use [atcute](https://codeberg.org/mary-ext/atcute/src/branch/trunk/packages/oauth/browser-client) if you're building something real. ## Installation Copy and paste atsw.js into your project! It's less than 750 lines of readable vanilla JavaScript. ## Usage ### Client metadata Create a [`‌client-metadata.json`](https://github.com/bluesky-social/atproto/blob/main/packages/api/OAUTH.md#step-1-create-your-client-metadata): ```json { "client_id": "https://example.com/client-metadata.json", "client_uri": "https://example.com", "redirect_uris": ["https://example.com"], "application_type": "native", "client_name": "Your App", "dpop_bound_access_tokens": true, "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "scope": "atproto repo?collection=com.atproto.server.getSession", "token_endpoint_auth_method": "none" } ``` ### Setup Register `‌atsw.js` as a service worker: ```js await navigator.serviceWorker.register("./atsw.js", { type: "module" }); await navigator.serviceWorker.ready; ``` ### Logging in Pass the configuration and handle to `‌logIn`. It'll automatically redirect the user to their PDS: ```js import { logIn } from "./atsw.js"; const config = { clientId: "https://example.com/client-metadata.json", redirectUri: "https://example.com", scope: "atproto repo?collection=com.atproto.server.getSession", }; await logIn(config, "you.bsky.social"); ``` The configuration must match the corresponding fields from `client-metadata.json`. If you don't know the user's handle, `‌logIn` also accepts the `https://` URL of a PDS or entryway: ```js await logIn(config, "https://bsky.social"); ``` ### Resuming sessions When the user is redirected back to your application, they'll be authenticated! You can get the PDS host from the session: ```js import { getSession, listSessions } from "./atsw.js"; const session = await getSession("did:plc:users-did-goes-here"); const allSessions = await listSessions(); ``` ### Making requests Make authenticated requests using plain `‌fetch` calls: ```js const res = await fetch(`${session.pds}/xrpc/com.atproto.server.getSession`); const data = await res.json(); ``` Any `/xrpc/` requests to the authenticated user's PDS will automatically include an auth token and handle DPoP retries and token refreshes. If a user has multiple authenticated sessions with the same PDS, use the `x-atsw-did` header to indicate which user's authentication token to use: ```js const res = await fetch(`${session.pds}/xrpc/com.atproto.server.getSession`, { headers: { "x-atsw-did": session.did }, }); const data = await res.json(); ``` ### Logging out ```js import { logOut } from "./atsw.js"; await logOut(session.did); ``` This also revokes the session's tokens with the auth server, if it supports revocation. ### Handling session expiry If a refresh token is rejected by the auth server, the session is deleted and the service worker notifies every client. Use `‌onSessionEnded` to listen for it: ```js import { onSessionEnded } from "./atsw.js"; const unsubscribe = onSessionEnded(({ did }) => { // the session for this did has ended }); ``` ## Custom service worker If you want to create your own service worker rather than simply registering `atsw.js`, you can import atsw.js's helpers: ```js import { authedFetch, handleCallback } from "./atsw.js"; self.onfetch = (e) => e.respondWith(handleFetch(e.request)); async function handleFetch(req) { const res = await handleCallback(req); if (res) return res; if (req.mode === "navigate") return fetch(req); return authedFetch(req); } ``` ## How does it work? `‌atsw.js` uses a [service worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) as an "auth proxy" that intercepts outgoing and incoming requests. - When the PDS redirects the user back to your callback URL, the service worker reads the URL and finishes setting up the session. - When you make a request to the authenticated user's PDS, the service worker adds an `authorization` header with the user's auth token. If the server rejects the DPoP nonce, the service worker will automatically retry with a new one. - If the auth token has expired, the service worker will refresh it before making a request. ## Security Sessions, including access and refresh tokens, are stored in IndexedDB, which is readable by any script running on the same origin — atsw.js doesn't protect against a malicious or compromised dependency. DPoP private keys are generated non-extractable, so a script with full IndexedDB access can't exfiltrate them for later use elsewhere — it can only sign with them while running on the page. The service worker only attaches tokens to requests going to the session's own PDS, so a compromised script can't use `‌fetch` to send a user's tokens somewhere else. ## Tests ```sh node --test ```