a tiny oauth browser client for atproto using a service worker
0

Configure Feed

Select the types of activity you want to include in your feed.

atsw / README.md
5.1 kB 151 lines
1# atsw.js 2 3A 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/). 4 5**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. 6 7## Installation 8 9Copy and paste atsw.js into your project! It's less than 750 lines of readable vanilla JavaScript. 10 11## Usage 12 13### Client metadata 14 15Create a [`‌client-metadata.json`](https://github.com/bluesky-social/atproto/blob/main/packages/api/OAUTH.md#step-1-create-your-client-metadata): 16 17```json 18{ 19 "client_id": "https://example.com/client-metadata.json", 20 "client_uri": "https://example.com", 21 "redirect_uris": ["https://example.com"], 22 "application_type": "native", 23 "client_name": "Your App", 24 "dpop_bound_access_tokens": true, 25 "grant_types": ["authorization_code", "refresh_token"], 26 "response_types": ["code"], 27 "scope": "atproto repo?collection=com.atproto.server.getSession", 28 "token_endpoint_auth_method": "none" 29} 30``` 31 32### Setup 33 34Register `‌atsw.js` as a service worker: 35 36```js 37await navigator.serviceWorker.register("./atsw.js", { type: "module" }); 38await navigator.serviceWorker.ready; 39``` 40 41### Logging in 42 43Pass the configuration and handle to `‌logIn`. It'll automatically redirect the user to their PDS: 44 45```js 46import { logIn } from "./atsw.js"; 47 48const config = { 49 clientId: "https://example.com/client-metadata.json", 50 redirectUri: "https://example.com", 51 scope: "atproto repo?collection=com.atproto.server.getSession", 52}; 53await logIn(config, "you.bsky.social"); 54``` 55 56The configuration must match the corresponding fields from `client-metadata.json`. 57 58If you don't know the user's handle, `‌logIn` also accepts the `https://` URL of a PDS or entryway: 59 60```js 61await logIn(config, "https://bsky.social"); 62``` 63 64### Resuming sessions 65 66When the user is redirected back to your application, they'll be authenticated! You can get the PDS host from the session: 67 68```js 69import { getSession, listSessions } from "./atsw.js"; 70 71const session = await getSession("did:plc:users-did-goes-here"); 72const allSessions = await listSessions(); 73``` 74 75### Making requests 76 77Make authenticated requests using plain `‌fetch` calls: 78 79```js 80const res = await fetch(`${session.pds}/xrpc/com.atproto.server.getSession`); 81const data = await res.json(); 82``` 83 84Any `/xrpc/` requests to the authenticated user's PDS will automatically include an auth token and handle DPoP retries and token refreshes. 85 86If 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: 87 88```js 89const res = await fetch(`${session.pds}/xrpc/com.atproto.server.getSession`, { 90 headers: { "x-atsw-did": session.did }, 91}); 92const data = await res.json(); 93``` 94 95### Logging out 96 97```js 98import { logOut } from "./atsw.js"; 99 100await logOut(session.did); 101``` 102 103This also revokes the session's tokens with the auth server, if it supports revocation. 104 105### Handling session expiry 106 107If 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: 108 109```js 110import { onSessionEnded } from "./atsw.js"; 111 112const unsubscribe = onSessionEnded(({ did }) => { 113 // the session for this did has ended 114}); 115``` 116 117## Custom service worker 118 119If you want to create your own service worker rather than simply registering `atsw.js`, you can import atsw.js's helpers: 120 121```js 122import { authedFetch, handleCallback } from "./atsw.js"; 123 124self.onfetch = (e) => e.respondWith(handleFetch(e.request)); 125 126async function handleFetch(req) { 127 const res = await handleCallback(req); 128 if (res) return res; 129 130 if (req.mode === "navigate") return fetch(req); 131 return authedFetch(req); 132} 133``` 134 135## How does it work? 136 137`‌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. 138 139- When the PDS redirects the user back to your callback URL, the service worker reads the URL and finishes setting up the session. 140- 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. 141- If the auth token has expired, the service worker will refresh it before making a request. 142 143## Security 144 145Sessions, 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. 146 147## Tests 148 149```sh 150node --test 151```