this repo has no description
1use super::{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_signed_token_with_metadata(
37 did,
38 SCOPE_ACCESS,
39 TOKEN_TYPE_ACCESS,
40 key_bytes,
41 Duration::minutes(15),
42 )
43}
44
45pub fn create_refresh_token_with_metadata(
46 did: &str,
47 key_bytes: &[u8],
48) -> Result<TokenWithMetadata> {
49 create_signed_token_with_metadata(
50 did,
51 SCOPE_REFRESH,
52 TOKEN_TYPE_REFRESH,
53 key_bytes,
54 Duration::days(14),
55 )
56}
57
58pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String> {
59 let signing_key = SigningKey::from_slice(key_bytes)?;
60
61 let expiration = Utc::now()
62 .checked_add_signed(Duration::seconds(60))
63 .expect("valid timestamp")
64 .timestamp();
65
66 let claims = Claims {
67 iss: did.to_owned(),
68 sub: did.to_owned(),
69 aud: aud.to_owned(),
70 exp: expiration as usize,
71 iat: Utc::now().timestamp() as usize,
72 scope: None,
73 lxm: Some(lxm.to_string()),
74 jti: uuid::Uuid::new_v4().to_string(),
75 };
76
77 sign_claims(claims, &signing_key)
78}
79
80fn create_signed_token_with_metadata(
81 did: &str,
82 scope: &str,
83 typ: &str,
84 key_bytes: &[u8],
85 duration: Duration,
86) -> Result<TokenWithMetadata> {
87 let signing_key = SigningKey::from_slice(key_bytes)?;
88
89 let expires_at = Utc::now()
90 .checked_add_signed(duration)
91 .expect("valid timestamp");
92
93 let expiration = expires_at.timestamp();
94 let jti = uuid::Uuid::new_v4().to_string();
95
96 let claims = Claims {
97 iss: did.to_owned(),
98 sub: did.to_owned(),
99 aud: format!(
100 "did:web:{}",
101 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
102 ),
103 exp: expiration as usize,
104 iat: Utc::now().timestamp() as usize,
105 scope: Some(scope.to_string()),
106 lxm: None,
107 jti: jti.clone(),
108 };
109
110 let token = sign_claims_with_type(claims, &signing_key, typ)?;
111
112 Ok(TokenWithMetadata {
113 token,
114 jti,
115 expires_at,
116 })
117}
118
119fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String> {
120 sign_claims_with_type(claims, key, TOKEN_TYPE_SERVICE)
121}
122
123fn sign_claims_with_type(claims: Claims, key: &SigningKey, typ: &str) -> Result<String> {
124 let header = Header {
125 alg: "ES256K".to_string(),
126 typ: typ.to_string(),
127 };
128
129 let header_json = serde_json::to_string(&header)?;
130 let claims_json = serde_json::to_string(&claims)?;
131
132 let header_b64 = URL_SAFE_NO_PAD.encode(header_json);
133 let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
134
135 let message = format!("{}.{}", header_b64, claims_b64);
136 let signature: Signature = key.sign(message.as_bytes());
137 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
138
139 Ok(format!("{}.{}", message, signature_b64))
140}
141
142pub fn create_access_token_hs256(did: &str, secret: &[u8]) -> Result<String> {
143 Ok(create_access_token_hs256_with_metadata(did, secret)?.token)
144}
145
146pub fn create_refresh_token_hs256(did: &str, secret: &[u8]) -> Result<String> {
147 Ok(create_refresh_token_hs256_with_metadata(did, secret)?.token)
148}
149
150pub fn create_access_token_hs256_with_metadata(
151 did: &str,
152 secret: &[u8],
153) -> Result<TokenWithMetadata> {
154 create_hs256_token_with_metadata(
155 did,
156 SCOPE_ACCESS,
157 TOKEN_TYPE_ACCESS,
158 secret,
159 Duration::minutes(15),
160 )
161}
162
163pub fn create_refresh_token_hs256_with_metadata(
164 did: &str,
165 secret: &[u8],
166) -> Result<TokenWithMetadata> {
167 create_hs256_token_with_metadata(
168 did,
169 SCOPE_REFRESH,
170 TOKEN_TYPE_REFRESH,
171 secret,
172 Duration::days(14),
173 )
174}
175
176pub fn create_service_token_hs256(
177 did: &str,
178 aud: &str,
179 lxm: &str,
180 secret: &[u8],
181) -> Result<String> {
182 let expiration = Utc::now()
183 .checked_add_signed(Duration::seconds(60))
184 .expect("valid timestamp")
185 .timestamp();
186
187 let claims = Claims {
188 iss: did.to_owned(),
189 sub: did.to_owned(),
190 aud: aud.to_owned(),
191 exp: expiration as usize,
192 iat: Utc::now().timestamp() as usize,
193 scope: None,
194 lxm: Some(lxm.to_string()),
195 jti: uuid::Uuid::new_v4().to_string(),
196 };
197
198 sign_claims_hs256(claims, TOKEN_TYPE_SERVICE, secret)
199}
200
201fn create_hs256_token_with_metadata(
202 did: &str,
203 scope: &str,
204 typ: &str,
205 secret: &[u8],
206 duration: Duration,
207) -> Result<TokenWithMetadata> {
208 let expires_at = Utc::now()
209 .checked_add_signed(duration)
210 .expect("valid timestamp");
211
212 let expiration = expires_at.timestamp();
213 let jti = uuid::Uuid::new_v4().to_string();
214
215 let claims = Claims {
216 iss: did.to_owned(),
217 sub: did.to_owned(),
218 aud: format!(
219 "did:web:{}",
220 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
221 ),
222 exp: expiration as usize,
223 iat: Utc::now().timestamp() as usize,
224 scope: Some(scope.to_string()),
225 lxm: None,
226 jti: jti.clone(),
227 };
228
229 let token = sign_claims_hs256(claims, typ, secret)?;
230
231 Ok(TokenWithMetadata {
232 token,
233 jti,
234 expires_at,
235 })
236}
237
238fn sign_claims_hs256(claims: Claims, typ: &str, secret: &[u8]) -> Result<String> {
239 let header = Header {
240 alg: "HS256".to_string(),
241 typ: typ.to_string(),
242 };
243
244 let header_json = serde_json::to_string(&header)?;
245 let claims_json = serde_json::to_string(&claims)?;
246
247 let header_b64 = URL_SAFE_NO_PAD.encode(header_json);
248 let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
249
250 let message = format!("{}.{}", header_b64, claims_b64);
251
252 let mut mac = HmacSha256::new_from_slice(secret)
253 .map_err(|e| anyhow::anyhow!("Invalid secret length: {}", e))?;
254 mac.update(message.as_bytes());
255
256 let signature = mac.finalize().into_bytes();
257 let signature_b64 = URL_SAFE_NO_PAD.encode(signature);
258
259 Ok(format!("{}.{}", message, signature_b64))
260}