atproto user agency toolkit for individuals and groups
1import { SignJWT, jwtVerify, errors, type JWTPayload } from "jose";
2import { compare } from "bcryptjs";
3
4export class TokenExpiredError extends Error {
5 constructor(message = "Token has expired") {
6 super(message);
7 this.name = "TokenExpiredError";
8 }
9}
10
11const ACCESS_TOKEN_LIFETIME = "120m";
12const REFRESH_TOKEN_LIFETIME = "90d";
13
14function createSecretKey(secret: string): Uint8Array {
15 return new TextEncoder().encode(secret);
16}
17
18export async function createAccessToken(
19 jwtSecret: string,
20 userDid: string,
21 serviceDid: string,
22): Promise<string> {
23 const secret = createSecretKey(jwtSecret);
24
25 return new SignJWT({ scope: "com.atproto.access" })
26 .setProtectedHeader({ alg: "HS256", typ: "at+jwt" })
27 .setIssuedAt()
28 .setAudience(serviceDid)
29 .setSubject(userDid)
30 .setExpirationTime(ACCESS_TOKEN_LIFETIME)
31 .sign(secret);
32}
33
34export async function createRefreshToken(
35 jwtSecret: string,
36 userDid: string,
37 serviceDid: string,
38): Promise<string> {
39 const secret = createSecretKey(jwtSecret);
40 const jti = crypto.randomUUID();
41
42 return new SignJWT({ scope: "com.atproto.refresh" })
43 .setProtectedHeader({ alg: "HS256", typ: "refresh+jwt" })
44 .setIssuedAt()
45 .setAudience(serviceDid)
46 .setSubject(userDid)
47 .setJti(jti)
48 .setExpirationTime(REFRESH_TOKEN_LIFETIME)
49 .sign(secret);
50}
51
52export async function verifyAccessToken(
53 token: string,
54 jwtSecret: string,
55 serviceDid: string,
56): Promise<JWTPayload> {
57 const secret = createSecretKey(jwtSecret);
58
59 let payload: JWTPayload;
60 let protectedHeader: { typ?: string };
61
62 try {
63 const result = await jwtVerify(token, secret, {
64 audience: serviceDid,
65 });
66 payload = result.payload;
67 protectedHeader = result.protectedHeader;
68 } catch (err) {
69 if (err instanceof errors.JWTExpired) {
70 throw new TokenExpiredError();
71 }
72 throw err;
73 }
74
75 if (protectedHeader.typ !== "at+jwt") {
76 throw new Error("Invalid token type");
77 }
78
79 if (payload.scope !== "com.atproto.access") {
80 throw new Error("Invalid scope");
81 }
82
83 return payload;
84}
85
86export async function verifyRefreshToken(
87 token: string,
88 jwtSecret: string,
89 serviceDid: string,
90): Promise<JWTPayload> {
91 const secret = createSecretKey(jwtSecret);
92
93 let payload: JWTPayload;
94 let protectedHeader: { typ?: string };
95
96 try {
97 const result = await jwtVerify(token, secret, {
98 audience: serviceDid,
99 });
100 payload = result.payload;
101 protectedHeader = result.protectedHeader;
102 } catch (err) {
103 if (err instanceof errors.JWTExpired) {
104 throw new TokenExpiredError();
105 }
106 throw err;
107 }
108
109 if (protectedHeader.typ !== "refresh+jwt") {
110 throw new Error("Invalid token type");
111 }
112
113 if (payload.scope !== "com.atproto.refresh") {
114 throw new Error("Invalid scope");
115 }
116
117 if (!payload.jti) {
118 throw new Error("Missing token ID");
119 }
120
121 return payload;
122}
123
124export { compare as verifyPassword };