auth.rs

  1use super::{
  2    db::{self, UserId},
  3    errors::TideResultExt,
  4};
  5use crate::{github, Request, RequestExt as _};
  6use anyhow::{anyhow, Context};
  7use async_trait::async_trait;
  8pub use oauth2::basic::BasicClient as Client;
  9use rand::thread_rng;
 10use rpc::auth as zed_auth;
 11use scrypt::{
 12    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
 13    Scrypt,
 14};
 15use serde::Serialize;
 16use std::convert::TryFrom;
 17use surf::StatusCode;
 18use tide::Error;
 19
 20static CURRENT_GITHUB_USER: &'static str = "current_github_user";
 21
 22#[derive(Serialize)]
 23pub struct User {
 24    pub github_login: String,
 25    pub avatar_url: String,
 26    pub is_insider: bool,
 27    pub is_admin: bool,
 28}
 29
 30pub async fn process_auth_header(request: &Request) -> tide::Result<UserId> {
 31    let mut auth_header = request
 32        .header("Authorization")
 33        .ok_or_else(|| {
 34            Error::new(
 35                StatusCode::BadRequest,
 36                anyhow!("missing authorization header"),
 37            )
 38        })?
 39        .last()
 40        .as_str()
 41        .split_whitespace();
 42    let user_id = UserId(auth_header.next().unwrap_or("").parse().map_err(|_| {
 43        Error::new(
 44            StatusCode::BadRequest,
 45            anyhow!("missing user id in authorization header"),
 46        )
 47    })?);
 48    let access_token = auth_header.next().ok_or_else(|| {
 49        Error::new(
 50            StatusCode::BadRequest,
 51            anyhow!("missing access token in authorization header"),
 52        )
 53    })?;
 54
 55    let state = request.state().clone();
 56    let mut credentials_valid = false;
 57    for password_hash in state.db.get_access_token_hashes(user_id).await? {
 58        if verify_access_token(&access_token, &password_hash)? {
 59            credentials_valid = true;
 60            break;
 61        }
 62    }
 63
 64    if !credentials_valid {
 65        Err(Error::new(
 66            StatusCode::Unauthorized,
 67            anyhow!("invalid credentials"),
 68        ))?;
 69    }
 70
 71    Ok(user_id)
 72}
 73
 74#[async_trait]
 75pub trait RequestExt {
 76    async fn current_user(&self) -> tide::Result<Option<User>>;
 77}
 78
 79#[async_trait]
 80impl RequestExt for Request {
 81    async fn current_user(&self) -> tide::Result<Option<User>> {
 82        if let Some(details) = self.session().get::<github::User>(CURRENT_GITHUB_USER) {
 83            let user = self.db().get_user_by_github_login(&details.login).await?;
 84            Ok(Some(User {
 85                github_login: details.login,
 86                avatar_url: details.avatar_url,
 87                is_insider: user.is_some(),
 88                is_admin: user.map_or(false, |user| user.admin),
 89            }))
 90        } else {
 91            Ok(None)
 92        }
 93    }
 94}
 95
 96const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
 97
 98pub async fn create_access_token(db: &dyn db::Db, user_id: UserId) -> tide::Result<String> {
 99    let access_token = zed_auth::random_token();
100    let access_token_hash =
101        hash_access_token(&access_token).context("failed to hash access token")?;
102    db.create_access_token_hash(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE)
103        .await?;
104    Ok(access_token)
105}
106
107fn hash_access_token(token: &str) -> tide::Result<String> {
108    // Avoid slow hashing in debug mode.
109    let params = if cfg!(debug_assertions) {
110        scrypt::Params::new(1, 1, 1).unwrap()
111    } else {
112        scrypt::Params::recommended()
113    };
114
115    Ok(Scrypt
116        .hash_password(
117            token.as_bytes(),
118            None,
119            params,
120            &SaltString::generate(thread_rng()),
121        )?
122        .to_string())
123}
124
125pub fn encrypt_access_token(access_token: &str, public_key: String) -> tide::Result<String> {
126    let native_app_public_key =
127        zed_auth::PublicKey::try_from(public_key).context("failed to parse app public key")?;
128    let encrypted_access_token = native_app_public_key
129        .encrypt_string(&access_token)
130        .context("failed to encrypt access token with public key")?;
131    Ok(encrypted_access_token)
132}
133
134pub fn verify_access_token(token: &str, hash: &str) -> tide::Result<bool> {
135    let hash = PasswordHash::new(hash)?;
136    Ok(Scrypt.verify_password(token.as_bytes(), &hash).is_ok())
137}