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