A build your own ATProto adventure, OAuth already figured out for you.
demo.atpoke.xyz
1// Loads all your OAuth settings from here and then uses the client everywhere else. metadata endpoint, jwks, etc
2
3import { atprotoLoopbackClientMetadata, Keyset, NodeOAuthClient } from '@atproto/oauth-client-node';
4import { JoseKey } from '@atproto/jwk-jose';
5import { db } from '$lib/server/db';
6import { SessionStore, StateStore } from '$lib/server/atproto/storage';
7import { env } from '$env/dynamic/private';
8import type { OAuthClientMetadataInput } from '@atproto/oauth-types';
9
10//You will need to change these if you are using another collection, can also change by setting the env OAUTH_SCOPES
11//For permission to all you can uncomment below
12// const DEFAULT_SCOPES = 'atproto transition:generic';
13const DEFAULT_SCOPES = 'atproto repo:app.bsky.feed.post?action=create repo:xyz.atpoke.graph.poke';
14const loadJwk = async () => {
15 const raw = env.OAUTH_JWK;
16 if (!raw) return undefined;
17 const json = JSON.parse(raw);
18 if (!json) return undefined;
19 const keys = await Promise.all(
20 json.map((jwk: string | Record<string, unknown>) => JoseKey.fromJWK(jwk)),
21 );
22 return new Keyset(keys);
23};
24
25
26let client: Promise<NodeOAuthClient> | null = null;
27
28export const atpOAuthClient = async () => {
29 if (!client) {
30 client = (async () => {
31 const rootDomain = env.OAUTH_DOMAIN ?? '127.0.0.1:5173';
32 const dev = env.DEV !== undefined;
33 const isConfidential = env.OAUTH_JWK !== undefined;
34
35 if(!dev && env.OAUTH_DOMAIN === undefined){
36 throw new Error('OAUTH_DOMAIN must be set in production');
37 }
38
39 const keyset = env.OAUTH_JWK && env.OAUTH_DOMAIN
40 ? await loadJwk()
41 : undefined;
42 // @ts-expect-error I have no idea why it doesn't like use
43 const pk = keyset?.findPrivateKey({ use: 'sig' });
44 const rootUrl = `https://${rootDomain}`;
45 const clientMetadata: OAuthClientMetadataInput = dev
46 ? atprotoLoopbackClientMetadata(
47 `http://localhost?${new URLSearchParams([
48 ['redirect_uri', `http://${rootDomain}/oauth/callback`],
49 ['scope', env.OAUTH_SCOPES ?? DEFAULT_SCOPES],
50 ])}`,
51 ) :
52 {
53 client_name: env.OAUTH_CLIENT_NAME,
54 logo_uri: env.OAUTH_LOGO_URI,
55 client_id: `${rootUrl}/oauth-client-metadata.json`,
56 client_uri: rootUrl,
57 redirect_uris: [`${rootUrl}/oauth/callback`],
58 scope: env.OAUTH_SCOPES ?? DEFAULT_SCOPES,
59 grant_types: ['authorization_code', 'refresh_token'],
60 application_type: 'web',
61 token_endpoint_auth_method: isConfidential ? 'private_key_jwt' : 'none',
62 dpop_bound_access_tokens: true,
63 jwks_uri: isConfidential ? `${rootUrl}/.well-known/jwks.json` : undefined,
64 token_endpoint_auth_signing_alg: isConfidential ? pk?.alg : undefined,
65 tos_uri: env.OAUTH_TOS_URI,
66 policy_uri: env.OAUTH_POLICY_URI,
67 };
68
69 return new NodeOAuthClient({
70 stateStore: new StateStore(db),
71 sessionStore: new SessionStore(db),
72 keyset,
73 clientMetadata,
74 // Not needed since this all runs locally to one machine I believe. But if you do run multiple instances and change out the DB from sqlite may need this
75 // https://github.com/bluesky-social/atproto/tree/main/packages/oauth/oauth-client-node#requestlock
76 requestLock: undefined,
77 });
78 })();
79 }
80 return client;
81};