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