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