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