this repo has no description
1use crate::config::AuthConfig;
2use crate::oauth::OAuthError;
3use base64::Engine;
4use base64::engine::general_purpose::URL_SAFE_NO_PAD;
5use chrono::Utc;
6use hmac::Mac;
7use sha2::{Digest, Sha256};
8use subtle::ConstantTimeEq;
9
10const ACCESS_TOKEN_EXPIRY_SECONDS: i64 = 300;
11
12pub struct TokenClaims {
13 pub jti: String,
14 pub sid: String,
15 pub exp: i64,
16 pub iat: i64,
17}
18
19pub fn verify_pkce(code_challenge: &str, code_verifier: &str) -> Result<(), OAuthError> {
20 let mut hasher = Sha256::new();
21 hasher.update(code_verifier.as_bytes());
22 let hash = hasher.finalize();
23 let computed_challenge = URL_SAFE_NO_PAD.encode(hash);
24 if !bool::from(
25 computed_challenge
26 .as_bytes()
27 .ct_eq(code_challenge.as_bytes()),
28 ) {
29 return Err(OAuthError::InvalidGrant(
30 "PKCE verification failed".to_string(),
31 ));
32 }
33 Ok(())
34}
35
36pub fn create_access_token(
37 session_id: &str,
38 sub: &str,
39 dpop_jkt: Option<&str>,
40 scope: Option<&str>,
41) -> Result<String, OAuthError> {
42 create_access_token_with_delegation(session_id, sub, dpop_jkt, scope, None)
43}
44
45pub fn create_access_token_with_delegation(
46 session_id: &str,
47 sub: &str,
48 dpop_jkt: Option<&str>,
49 scope: Option<&str>,
50 controller_did: Option<&str>,
51) -> Result<String, OAuthError> {
52 use serde_json::json;
53 let jti = uuid::Uuid::new_v4().to_string();
54 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
55 let issuer = format!("https://{}", pds_hostname);
56 let now = Utc::now().timestamp();
57 let exp = now + ACCESS_TOKEN_EXPIRY_SECONDS;
58 let actual_scope = scope.unwrap_or("atproto");
59 let mut payload = json!({
60 "iss": issuer,
61 "sub": sub,
62 "aud": issuer,
63 "iat": now,
64 "exp": exp,
65 "jti": jti,
66 "sid": session_id,
67 "scope": actual_scope
68 });
69 if let Some(jkt) = dpop_jkt {
70 payload["cnf"] = json!({ "jkt": jkt });
71 }
72 if let Some(controller) = controller_did {
73 payload["act"] = json!({ "sub": controller });
74 }
75 let header = json!({
76 "alg": "HS256",
77 "typ": "at+jwt"
78 });
79 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
80 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
81 let signing_input = format!("{}.{}", header_b64, payload_b64);
82 let config = AuthConfig::get();
83 type HmacSha256 = hmac::Hmac<Sha256>;
84 let mut mac = HmacSha256::new_from_slice(config.jwt_secret().as_bytes())
85 .map_err(|_| OAuthError::ServerError("HMAC key error".to_string()))?;
86 mac.update(signing_input.as_bytes());
87 let signature = mac.finalize().into_bytes();
88 let signature_b64 = URL_SAFE_NO_PAD.encode(signature);
89 Ok(format!("{}.{}", signing_input, signature_b64))
90}
91
92pub fn extract_token_claims(token: &str) -> Result<TokenClaims, OAuthError> {
93 let parts: Vec<&str> = token.split('.').collect();
94 if parts.len() != 3 {
95 return Err(OAuthError::InvalidToken("Invalid token format".to_string()));
96 }
97 let header_bytes = URL_SAFE_NO_PAD
98 .decode(parts[0])
99 .map_err(|_| OAuthError::InvalidToken("Invalid token encoding".to_string()))?;
100 let header: serde_json::Value = serde_json::from_slice(&header_bytes)
101 .map_err(|_| OAuthError::InvalidToken("Invalid token header".to_string()))?;
102 if header.get("typ").and_then(|t| t.as_str()) != Some("at+jwt") {
103 return Err(OAuthError::InvalidToken(
104 "Not an OAuth access token".to_string(),
105 ));
106 }
107 if header.get("alg").and_then(|a| a.as_str()) != Some("HS256") {
108 return Err(OAuthError::InvalidToken(
109 "Unsupported algorithm".to_string(),
110 ));
111 }
112 let config = AuthConfig::get();
113 let secret = config.jwt_secret();
114 let signing_input = format!("{}.{}", parts[0], parts[1]);
115 let provided_sig = URL_SAFE_NO_PAD
116 .decode(parts[2])
117 .map_err(|_| OAuthError::InvalidToken("Invalid signature encoding".to_string()))?;
118 type HmacSha256 = hmac::Hmac<Sha256>;
119 let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
120 .map_err(|_| OAuthError::ServerError("HMAC initialization failed".to_string()))?;
121 mac.update(signing_input.as_bytes());
122 let expected_sig = mac.finalize().into_bytes();
123 if !bool::from(expected_sig.ct_eq(&provided_sig)) {
124 return Err(OAuthError::InvalidToken(
125 "Invalid token signature".to_string(),
126 ));
127 }
128 let payload_bytes = URL_SAFE_NO_PAD
129 .decode(parts[1])
130 .map_err(|_| OAuthError::InvalidToken("Invalid payload encoding".to_string()))?;
131 let payload: serde_json::Value = serde_json::from_slice(&payload_bytes)
132 .map_err(|_| OAuthError::InvalidToken("Invalid token payload".to_string()))?;
133 let jti = payload
134 .get("jti")
135 .and_then(|j| j.as_str())
136 .ok_or_else(|| OAuthError::InvalidToken("Missing jti claim".to_string()))?
137 .to_string();
138 let sid = payload
139 .get("sid")
140 .and_then(|s| s.as_str())
141 .ok_or_else(|| OAuthError::InvalidToken("Missing sid claim".to_string()))?
142 .to_string();
143 let exp = payload
144 .get("exp")
145 .and_then(|e| e.as_i64())
146 .ok_or_else(|| OAuthError::InvalidToken("Missing exp claim".to_string()))?;
147 let iat = payload
148 .get("iat")
149 .and_then(|i| i.as_i64())
150 .ok_or_else(|| OAuthError::InvalidToken("Missing iat claim".to_string()))?;
151 Ok(TokenClaims { jti, sid, exp, iat })
152}