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.

JavaScript 96.3%
HTML 3.5%
Makefile 0.2%
54 1 0

Clone this repository

https://git.vm.fail/jakelazaroff.com/atsw https://git.vm.fail/did:plc:76vhbpqzldxb6adje543l2rf
ssh://git@knot1.tangled.sh:2222/jakelazaroff.com/atsw ssh://git@knot1.tangled.sh:2222/did:plc:76vhbpqzldxb6adje543l2rf

For self-hosted knots, clone URLs may differ based on your setup.


README.md

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.

This is very experimental — you should probably use atcute 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:

{
	"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:

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:

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:

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:

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:

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:

const res = await fetch(`${session.pds}/xrpc/com.atproto.server.getSession`, {
	headers: { "x-atsw-did": session.did },
});
const data = await res.json();

Logging out#

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:

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:

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 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#

node --test