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