Skip to content

Commit

Permalink
Merge pull request #140 from nightly-labs/login-with-google
Browse files Browse the repository at this point in the history
Login with google
  • Loading branch information
Giems authored Mar 21, 2024
2 parents b38920c + a00d744 commit 1bbb668
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 4 deletions.
168 changes: 168 additions & 0 deletions server/src/http/cloud/login_with_google.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use crate::{
env::NONCE,
structs::cloud::api_cloud_errors::CloudApiErrors,
utils::{generate_tokens, validate_request},
};
use axum::{
extract::{ConnectInfo, State},
http::StatusCode,
Json,
};
use database::{
db::Db,
tables::{grafana_users::table_struct::GrafanaUser, utils::get_current_datetime},
};
use garde::Validate;
use log::error;
use pwhash::bcrypt;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::{Deserialize, Serialize};
use std::{net::SocketAddr, sync::Arc};
use ts_rs::TS;
use uuid7::uuid7;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct LoginWithGoogleResponse {
pub user_id: String,
pub auth_token: String,
pub refresh_token: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct HttpLoginWithGoogleRequest {
#[garde(ascii, length(min = 6, max = 300))]
oauth_token: String,
#[garde(email)]
email: String,
#[garde(skip)]
enforce_ip: bool,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct GoogleResponse {
id: String,
email: String,
verified_email: bool,
}

pub async fn login_with_google(
ConnectInfo(ip): ConnectInfo<SocketAddr>,
State(db): State<Option<Arc<Db>>>,
Json(request): Json<HttpLoginWithGoogleRequest>,
) -> Result<Json<LoginWithGoogleResponse>, (StatusCode, String)> {
// Db connection has already been checked in the middleware
let db = db.as_ref().ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
CloudApiErrors::CloudFeatureDisabled.to_string(),
))?;

// Validate request
validate_request(&request, &())?;

// Get data from google and validate the payload
let google_user_data = get_google_data(&request.oauth_token).await?;

// Check if email is the same
if google_user_data.email != request.email {
return Err((
StatusCode::BAD_REQUEST,
CloudApiErrors::AccessTokenFailure.to_string(),
));
};

// Check if user is already registered
match db.get_user_by_email(&request.email).await {
Ok(Some(user)) => {
let (auth_token, refresh_token) =
generate_tokens(request.enforce_ip, ip, &user.user_id)?;

return Ok(Json(LoginWithGoogleResponse {
user_id: user.user_id,
auth_token: auth_token,
refresh_token: refresh_token,
}));
}
Ok(None) => {
// Generate random password
let random_password: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect();
let hashed_password = bcrypt::hash(format!("{}_{}", NONCE(), random_password))
.map_err(|e| {
error!("Failed to hash password: {:?}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
CloudApiErrors::InternalServerError.to_string(),
)
})?;

// Register user
let user_id = uuid7().to_string();
let grafana_user = GrafanaUser {
user_id: user_id.clone(),
email: request.email.clone(),
password_hash: hashed_password,
creation_timestamp: get_current_datetime(),
};
if let Err(err) = db.add_new_user(&grafana_user).await {
error!("Failed to create user: {:?}", err);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
CloudApiErrors::DatabaseError.to_string(),
));
}

// Generate tokens
let (auth_token, refresh_token) = generate_tokens(request.enforce_ip, ip, &user_id)?;

return Ok(Json(LoginWithGoogleResponse {
user_id: user_id,
auth_token: auth_token,
refresh_token: refresh_token,
}));
}
Err(_) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
CloudApiErrors::DatabaseError.to_string(),
))
}
}
}

async fn get_google_data(oauth_token: &str) -> Result<GoogleResponse, (StatusCode, String)> {
let google_user_data = reqwest::get(&format!(
"https://www.googleapis.com/oauth2/v1/userinfo?access_token={}",
oauth_token
))
.await
.map_err(|error| {
(
StatusCode::BAD_REQUEST,
format!(
"{} {}",
CloudApiErrors::AccessTokenFailure.to_string(),
error
),
)
})?
.json::<GoogleResponse>()
.await
.map_err(|error| {
(
StatusCode::BAD_REQUEST,
format!(
"{} {}",
CloudApiErrors::AccessTokenFailure.to_string(),
error
),
)
})?;
Ok(google_user_data)
}
1 change: 1 addition & 0 deletions server/src/http/cloud/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod add_user_to_team;
pub mod events;
pub mod get_user_joined_teams;
pub mod login_with_google;
pub mod login_with_password;
pub mod register_new_app;
pub mod register_new_team;
Expand Down
10 changes: 7 additions & 3 deletions server/src/routes/cloud_router.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::{
http::cloud::{
add_user_to_team::add_user_to_team, events::events,
get_user_joined_teams::get_user_joined_teams, login_with_password::login_with_password,
register_new_app::register_new_app, register_new_team::register_new_team,
register_with_password::register_with_password,
get_user_joined_teams::get_user_joined_teams, login_with_google::login_with_google,
login_with_password::login_with_password, register_new_app::register_new_app,
register_new_team::register_new_team, register_with_password::register_with_password,
remove_user_from_team::remove_user_from_team,
},
middlewares::auth_middleware::access_auth_middleware,
Expand Down Expand Up @@ -36,6 +36,10 @@ pub fn public_router(state: ServerState) -> Router<ServerState> {
&HttpCloudEndpoint::LoginWithPassword.to_string(),
post(login_with_password),
)
.route(
&HttpCloudEndpoint::LoginWithGoogle.to_string(),
post(login_with_google),
)
.route(
&HttpCloudEndpoint::RegisterWithPassword.to_string(),
post(register_with_password),
Expand Down
3 changes: 3 additions & 0 deletions server/src/structs/cloud/cloud_http_endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum HttpCloudEndpoint {
RegisterWithPassword,
#[serde(rename = "/login_with_password")]
LoginWithPassword,
#[serde(rename = "/login_with_google")]
LoginWithGoogle,
#[serde(rename = "/register_new_team")]
RegisterNewTeam,
#[serde(rename = "/add_user_to_team")]
Expand All @@ -28,6 +30,7 @@ impl HttpCloudEndpoint {
HttpCloudEndpoint::RegisterNewApp => "/register_new_app".to_string(),
HttpCloudEndpoint::RegisterWithPassword => "/register_with_password".to_string(),
HttpCloudEndpoint::LoginWithPassword => "/login_with_password".to_string(),
HttpCloudEndpoint::LoginWithGoogle => "/login_with_google".to_string(),
HttpCloudEndpoint::RegisterNewTeam => "/register_new_team".to_string(),
HttpCloudEndpoint::AddUserToTeam => "/add_user_to_team".to_string(),
HttpCloudEndpoint::RemoveUserFromTeam => "/remove_user_from_team".to_string(),
Expand Down
38 changes: 37 additions & 1 deletion server/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::{
auth::AuthToken,
env::JWT_SECRET,
ip_geolocation::GeolocationRequester,
statics::{NAME_REGEX, REGISTER_PASSWORD_VALIDATOR},
structs::{
Expand All @@ -11,7 +13,7 @@ use database::{
};
use database::{structs::geo_location::Geolocation, tables::utils::get_current_datetime};
use garde::Validate;
use log::warn;
use log::{error, warn};
use std::{
net::SocketAddr,
str::FromStr,
Expand Down Expand Up @@ -160,3 +162,37 @@ pub fn custom_validate_new_password(password: &String, _context: &()) -> garde::
}
Ok(())
}

pub fn generate_tokens(
enforce_ip: bool,
ip: SocketAddr,
user_id: &String,
// (Auth Token, Refresh Token)
) -> Result<(String, String), (StatusCode, String)> {
// Generate tokens
let ip = if enforce_ip { Some(ip) } else { None };
// Access token
let token = match AuthToken::new_access(&user_id, ip).encode(JWT_SECRET()) {
Ok(token) => token,
Err(err) => {
error!("Failed to create access token: {:?}", err);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
CloudApiErrors::AccessTokenFailure.to_string(),
));
}
};
// Refresh token
let refresh_token = match AuthToken::new_refresh(&user_id, ip).encode(JWT_SECRET()) {
Ok(token) => token,
Err(err) => {
error!("Failed to create refresh token: {:?}", err);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
CloudApiErrors::RefreshTokenFailure.to_string(),
));
}
};

Ok((token, refresh_token))
}

0 comments on commit 1bbb668

Please sign in to comment.