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 k256::ecdsa::{Signature, SigningKey, signature::Signer};
7use uuid;
8
9pub const TOKEN_TYPE_ACCESS: &str = "at+jwt";
10pub const TOKEN_TYPE_REFRESH: &str = "refresh+jwt";
11pub const TOKEN_TYPE_SERVICE: &str = "jwt";
12
13pub const SCOPE_ACCESS: &str = "com.atproto.access";
14pub const SCOPE_REFRESH: &str = "com.atproto.refresh";
15pub const SCOPE_APP_PASS: &str = "com.atproto.appPass";
16pub const SCOPE_APP_PASS_PRIVILEGED: &str = "com.atproto.appPassPrivileged";
17
18pub struct TokenWithMetadata {
19 pub token: String,
20 pub jti: String,
21 pub expires_at: DateTime<Utc>,
22}
23
24pub fn create_access_token(did: &str, key_bytes: &[u8]) -> Result<String> {
25 Ok(create_access_token_with_metadata(did, key_bytes)?.token)
26}
27
28pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String> {
29 Ok(create_refresh_token_with_metadata(did, key_bytes)?.token)
30}
31
32pub fn create_access_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> {
33 create_signed_token_with_metadata(did, SCOPE_ACCESS, TOKEN_TYPE_ACCESS, key_bytes, Duration::minutes(120))
34}
35
36pub fn create_refresh_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> {
37 create_signed_token_with_metadata(did, SCOPE_REFRESH, TOKEN_TYPE_REFRESH, key_bytes, Duration::days(90))
38}
39
40pub fn create_service_token(did: &str, aud: &str, lxm: &str, key_bytes: &[u8]) -> Result<String> {
41 let signing_key = SigningKey::from_slice(key_bytes)?;
42
43 let expiration = Utc::now()
44 .checked_add_signed(Duration::seconds(60))
45 .expect("valid timestamp")
46 .timestamp();
47
48 let claims = Claims {
49 iss: did.to_owned(),
50 sub: did.to_owned(),
51 aud: aud.to_owned(),
52 exp: expiration as usize,
53 iat: Utc::now().timestamp() as usize,
54 scope: None,
55 lxm: Some(lxm.to_string()),
56 jti: uuid::Uuid::new_v4().to_string(),
57 };
58
59 sign_claims(claims, &signing_key)
60}
61
62fn create_signed_token_with_metadata(
63 did: &str,
64 scope: &str,
65 typ: &str,
66 key_bytes: &[u8],
67 duration: Duration,
68) -> Result<TokenWithMetadata> {
69 let signing_key = SigningKey::from_slice(key_bytes)?;
70
71 let expires_at = Utc::now()
72 .checked_add_signed(duration)
73 .expect("valid timestamp");
74 let expiration = expires_at.timestamp();
75 let jti = uuid::Uuid::new_v4().to_string();
76
77 let claims = Claims {
78 iss: did.to_owned(),
79 sub: did.to_owned(),
80 aud: format!(
81 "did:web:{}",
82 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string())
83 ),
84 exp: expiration as usize,
85 iat: Utc::now().timestamp() as usize,
86 scope: Some(scope.to_string()),
87 lxm: None,
88 jti: jti.clone(),
89 };
90
91 let token = sign_claims_with_type(claims, &signing_key, typ)?;
92 Ok(TokenWithMetadata {
93 token,
94 jti,
95 expires_at,
96 })
97}
98
99fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String> {
100 sign_claims_with_type(claims, key, TOKEN_TYPE_SERVICE)
101}
102
103fn sign_claims_with_type(claims: Claims, key: &SigningKey, typ: &str) -> Result<String> {
104 let header = Header {
105 alg: "ES256K".to_string(),
106 typ: typ.to_string(),
107 };
108
109 let header_json = serde_json::to_string(&header)?;
110 let claims_json = serde_json::to_string(&claims)?;
111
112 let header_b64 = URL_SAFE_NO_PAD.encode(header_json);
113 let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
114
115 let message = format!("{}.{}", header_b64, claims_b64);
116 let signature: Signature = key.sign(message.as_bytes());
117 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
118
119 Ok(format!("{}.{}", message, signature_b64))
120}