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}