atproto user agency toolkit for individuals and groups
at main 124 lines 2.8 kB view raw
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 };