use crate::{ AppState, Error, Result, db::{AccessTokenId, Database, UserId}, rpc::Principal, }; use anyhow::Context as _; use axum::{ http::{self, Request, StatusCode}, middleware::Next, response::IntoResponse, }; use base64::prelude::*; use prometheus::{Histogram, exponential_buckets, register_histogram}; pub use rpc::auth::random_token; use scrypt::{ Scrypt, password_hash::{PasswordHash, PasswordVerifier}, }; use serde::{Deserialize, Serialize}; use sha2::Digest; use std::sync::OnceLock; use std::{sync::Arc, time::Instant}; use subtle::ConstantTimeEq; /// Validates the authorization header and adds an Extension to the request. /// Authorization: /// can be an access_token attached to that user, or an access token of an admin /// or (in development) the string ADMIN:. /// Authorization: "dev-server-token" pub async fn validate_header(mut req: Request, next: Next) -> impl IntoResponse { let mut auth_header = req .headers() .get(http::header::AUTHORIZATION) .and_then(|header| header.to_str().ok()) .ok_or_else(|| { Error::http( StatusCode::UNAUTHORIZED, "missing authorization header".to_string(), ) })? .split_whitespace(); let state = req.extensions().get::>().unwrap(); let first = auth_header.next().unwrap_or(""); if first == "dev-server-token" { Err(Error::http( StatusCode::UNAUTHORIZED, "Dev servers were removed in Zed 0.157 please upgrade to SSH remoting".to_string(), ))?; } let user_id = UserId(first.parse().map_err(|_| { Error::http( StatusCode::BAD_REQUEST, "missing user id in authorization header".to_string(), ) })?); let access_token = auth_header.next().ok_or_else(|| { Error::http( StatusCode::BAD_REQUEST, "missing access token in authorization header".to_string(), ) })?; let validate_result = verify_access_token(access_token, user_id, &state.db).await; if let Ok(validate_result) = validate_result && validate_result.is_valid { let user = state .db .get_user_by_id(user_id) .await? .with_context(|| format!("user {user_id} not found"))?; req.extensions_mut().insert(Principal::User(user)); return Ok::<_, Error>(next.run(req).await); } Err(Error::http( StatusCode::UNAUTHORIZED, "invalid credentials".to_string(), )) } #[derive(Serialize, Deserialize)] pub struct AccessTokenJson { pub version: usize, pub id: AccessTokenId, pub token: String, } /// Hashing prevents anyone with access to the database being able to login. /// As the token is randomly generated, we don't need to worry about scrypt-style /// protection. pub fn hash_access_token(token: &str) -> String { let digest = sha2::Sha256::digest(token); format!("$sha256${}", BASE64_URL_SAFE.encode(digest)) } pub struct VerifyAccessTokenResult { pub is_valid: bool, } /// Checks that the given access token is valid for the given user. pub async fn verify_access_token( token: &str, user_id: UserId, db: &Arc, ) -> Result { static METRIC_ACCESS_TOKEN_HASHING_TIME: OnceLock = OnceLock::new(); let metric_access_token_hashing_time = METRIC_ACCESS_TOKEN_HASHING_TIME.get_or_init(|| { register_histogram!( "access_token_hashing_time", "time spent hashing access tokens", exponential_buckets(10.0, 2.0, 10).unwrap(), ) .unwrap() }); let token: AccessTokenJson = serde_json::from_str(token)?; let db_token = db.get_access_token(token.id).await?; if db_token.user_id != user_id { return Err(anyhow::anyhow!("no such access token"))?; } let t0 = Instant::now(); let is_valid = if db_token.hash.starts_with("$scrypt$") { let db_hash = PasswordHash::new(&db_token.hash).map_err(anyhow::Error::new)?; Scrypt .verify_password(token.token.as_bytes(), &db_hash) .is_ok() } else { let token_hash = hash_access_token(&token.token); db_token.hash.as_bytes().ct_eq(token_hash.as_ref()).into() }; let duration = t0.elapsed(); log::info!("hashed access token in {:?}", duration); metric_access_token_hashing_time.observe(duration.as_millis() as f64); if is_valid && db_token.hash.starts_with("$scrypt$") { let new_hash = hash_access_token(&token.token); db.update_access_token_hash(db_token.id, &new_hash).await?; } Ok(VerifyAccessTokenResult { is_valid }) }