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