a tiny oauth browser client for atproto using a service worker
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```