this repo has no description
1use super::types::{ActClaim, Claims, Header, TokenWithMetadata};
2use anyhow::Result;
3use base64::Engine as _;
4use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use chrono::{Duration, Utc};
6use hmac::{Hmac, Mac};
7use k256::ecdsa::{Signature, SigningKey, signature::Signer};
8use sha2::Sha256;
9
10type HmacSha256 = Hmac<Sha256>;
11
12pub const TOKEN_TYPE_ACCESS: &str = "at+jwt";
13pub const TOKEN_TYPE_REFRESH: &str = "refresh+jwt";
14pub const TOKEN_TYPE_SERVICE: &str = "jwt";
15pub const SCOPE_ACCESS: &str = "com.atproto.access";
16pub const SCOPE_REFRESH: &str = "com.atproto.refresh";
17pub const SCOPE_APP_PASS: &str = "com.atproto.appPass";
18pub const SCOPE_APP_PASS_PRIVILEGED: &str = "com.atproto.appPassPrivileged";
19
20pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String> {
21 Ok(create_access_token_with_metadata(did, key_bytes)?.token)
22}
23
24pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String> {
25 Ok(create_refresh_token_with_metadata(did, key_bytes)?.token)
26}
27
28pub fn create_access_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> {
29 create_access_token_with_scope_metadata(did, key_bytes, None, None)
30}
31
32pub fn create_access_token_with_scope_metadata(
33 did: &str,
34 key_bytes: &[u8],
35 scopes: Option<&str>,
36 hostname: Option<&str>,
37) -> Result<TokenWithMetadata> {
38 let scope = scopes.unwrap_or(SCOPE_ACCESS);
39 create_signed_token_with_metadata(
40 did,
41 scope,
42 TOKEN_TYPE_ACCESS,
43 key_bytes,
44 Duration::minutes(15),
45 hostname,
46 )
47}
48
49pub fn create_access_token_with_delegation(
50 did: &str,
51 key_bytes: &[u8],
52 scopes: Option<&str>,
53 controller_did: Option<&str>,
54 hostname: Option<&str>,
55) -> Result<TokenWithMetadata> {
56 let scope = scopes.unwrap_or(SCOPE_ACCESS);
57 let act = controller_did.map(|c| ActClaim { sub: c.to_string() });
58 create_signed_token_with_act(
59 did,
60 scope,
61 TOKEN_TYPE_ACCESS,
62 key_bytes,
63 Duration::minutes(15),
64 act,
65 hostname,
66 )
67}
68
69pub fn create_refresh_token_with_metadata(
70 did: &str,
71 key_bytes: &[u8],
72) -> Result<TokenWithMetadata> {
73 create_signed_token_with_metadata(
74 did,
75 SCOPE_REFRESH,
76 TOKEN_TYPE_REFRESH,
77 key_bytes,
78 Duration::days(14),
79 None,
80 )
81}
82
83pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String> {
84 let signing_key = SigningKey::from_slice(key_bytes)?;
85
86 let expiration = Utc::now()
87 .checked_add_signed(Duration::seconds(60))
88 .expect("valid timestamp")
89 .timestamp();
90
91 let claims = Claims {
92 iss: did.to_owned(),
93 sub: did.to_owned(),
94 aud: aud.to_owned(),
95 exp: expiration as usize,
96 iat: Utc::now().timestamp() as usize,
97 scope: None,
98 lxm: Some(lxm.to_string()),
99 jti: uuid::Uuid::new_v4().to_string(),
100 act: None,
101 };
102
103 sign_claims(claims, &signing_key)
104}
105
106fn create_signed_token_with_metadata(
107 did: &str,
108 scope: &str,
109 typ: &str,
110 key_bytes: &[u8],
111 duration: Duration,
112 hostname: Option<&str>,
113) -> Result<TokenWithMetadata> {
114 create_signed_token_with_act(did, scope, typ, key_bytes, duration, None, hostname)
115}
116
117fn create_signed_token_with_act(
118 did: &str,
119 scope: &str,
120 typ: &str,
121 key_bytes: &[u8],
122 duration: Duration,
123 act: Option<ActClaim>,
124 hostname: Option<&str>,
125) -> Result<TokenWithMetadata> {
126 let signing_key = SigningKey::from_slice(key_bytes)?;
127
128 let expires_at = Utc::now()
129 .checked_add_signed(duration)
130 .expect("valid timestamp");
131
132 let expiration = expires_at.timestamp();
133 let jti = uuid::Uuid::new_v4().to_string();
134
135 let aud_hostname = hostname.map(|h| h.to_string()).unwrap_or_else(|| {
136 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
137 });
138
139 let claims = Claims {
140 iss: did.to_owned(),
141 sub: did.to_owned(),
142 aud: format!("did:web:{}", aud_hostname),
143 exp: expiration as usize,
144 iat: Utc::now().timestamp() as usize,
145 scope: Some(scope.to_string()),
146 lxm: None,
147 jti: jti.clone(),
148 act,
149 };
150
151 let token = sign_claims_with_type(claims, &signing_key, typ)?;
152
153 Ok(TokenWithMetadata {
154 token,
155 jti,
156 expires_at,
157 })
158}
159
160fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String> {
161 sign_claims_with_type(claims, key, TOKEN_TYPE_SERVICE)
162}
163
164fn sign_claims_with_type(claims: Claims, key: &SigningKey, typ: &str) -> Result<String> {
165 let header = Header {
166 alg: "ES256K".to_string(),
167 typ: typ.to_string(),
168 };
169
170 let header_json = serde_json::to_string(&header)?;
171 let claims_json = serde_json::to_string(&claims)?;
172
173 let header_b64 = URL_SAFE_NO_PAD.encode(header_json);
174 let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
175
176 let message = format!("{}.{}", header_b64, claims_b64);
177 let signature: Signature = key.sign(message.as_bytes());
178 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
179
180 Ok(format!("{}.{}", message, signature_b64))
181}
182
183pub fn create_access_token_hs256(did: &str, secret: &[u8]) -> Result<String> {
184 Ok(create_access_token_hs256_with_metadata(did, secret)?.token)
185}
186
187pub fn create_refresh_token_hs256(did: &str, secret: &[u8]) -> Result<String> {
188 Ok(create_refresh_token_hs256_with_metadata(did, secret)?.token)
189}
190
191pub fn create_access_token_hs256_with_metadata(
192 did: &str,
193 secret: &[u8],
194) -> Result<TokenWithMetadata> {
195 create_hs256_token_with_metadata(
196 did,
197 SCOPE_ACCESS,
198 TOKEN_TYPE_ACCESS,
199 secret,
200 Duration::minutes(15),
201 )
202}
203
204pub fn create_refresh_token_hs256_with_metadata(
205 did: &str,
206 secret: &[u8],
207) -> Result<TokenWithMetadata> {
208 create_hs256_token_with_metadata(
209 did,
210 SCOPE_REFRESH,
211 TOKEN_TYPE_REFRESH,
212 secret,
213 Duration::days(14),
214 )
215}
216
217pub fn create_service_token_hs256(
218 did: &str,
219 aud: &str,
220 lxm: &str,
221 secret: &[u8],
222) -> Result<String> {
223 let expiration = Utc::now()
224 .checked_add_signed(Duration::seconds(60))
225 .expect("valid timestamp")
226 .timestamp();
227
228 let claims = Claims {
229 iss: did.to_owned(),
230 sub: did.to_owned(),
231 aud: aud.to_owned(),
232 exp: expiration as usize,
233 iat: Utc::now().timestamp() as usize,
234 scope: None,
235 lxm: Some(lxm.to_string()),
236 jti: uuid::Uuid::new_v4().to_string(),
237 act: None,
238 };
239
240 sign_claims_hs256(claims, TOKEN_TYPE_SERVICE, secret)
241}
242
243fn create_hs256_token_with_metadata(
244 did: &str,
245 scope: &str,
246 typ: &str,
247 secret: &[u8],
248 duration: Duration,
249) -> Result<TokenWithMetadata> {
250 let expires_at = Utc::now()
251 .checked_add_signed(duration)
252 .expect("valid timestamp");
253
254 let expiration = expires_at.timestamp();
255 let jti = uuid::Uuid::new_v4().to_string();
256
257 let claims = Claims {
258 iss: did.to_owned(),
259 sub: did.to_owned(),
260 aud: format!(
261 "did:web:{}",
262 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
263 ),
264 exp: expiration as usize,
265 iat: Utc::now().timestamp() as usize,
266 scope: Some(scope.to_string()),
267 lxm: None,
268 jti: jti.clone(),
269 act: None,
270 };
271
272 let token = sign_claims_hs256(claims, typ, secret)?;
273
274 Ok(TokenWithMetadata {
275 token,
276 jti,
277 expires_at,
278 })
279}
280
281fn sign_claims_hs256(claims: Claims, typ: &str, secret: &[u8]) -> Result<String> {
282 let header = Header {
283 alg: "HS256".to_string(),
284 typ: typ.to_string(),
285 };
286
287 let header_json = serde_json::to_string(&header)?;
288 let claims_json = serde_json::to_string(&claims)?;
289
290 let header_b64 = URL_SAFE_NO_PAD.encode(header_json);
291 let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
292
293 let message = format!("{}.{}", header_b64, claims_b64);
294
295 let mut mac = HmacSha256::new_from_slice(secret)
296 .map_err(|e| anyhow::anyhow!("Invalid secret length: {}", e))?;
297 mac.update(message.as_bytes());
298
299 let signature = mac.finalize().into_bytes();
300 let signature_b64 = URL_SAFE_NO_PAD.encode(signature);
301
302 Ok(format!("{}.{}", message, signature_b64))
303}