diff --git a/Cargo.lock b/Cargo.lock index 937072cb3..4b9aa7c51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,7 @@ dependencies = [ "reqwest", "serde", "serde_derive", + "serde_json", "urlencoding", ] diff --git a/core/src/auth/google.rs b/core/src/auth/google.rs index de577c630..5cf3be047 100644 --- a/core/src/auth/google.rs +++ b/core/src/auth/google.rs @@ -1,26 +1,26 @@ use std::sync::Arc; use anyhow::{anyhow, Context}; -use axum::{Router, Extension, routing::get, response::Redirect, extract::Query}; +use axum::{extract::Query, response::Redirect, routing::get, Extension, Router}; use axum_oauth2::google::{GoogleOauthClient, GoogleOauthExtension}; use helpers::handle_anyhow_error; -use mungos::{Deserialize, doc}; -use types::{CoreConfig, monitor_timestamp, User}; +use mungos::{doc, Deserialize}; +use types::{monitor_timestamp, CoreConfig, User}; -use crate::{state::StateExtension, response}; +use crate::{response, state::StateExtension}; use super::JwtExtension; pub fn router(config: &CoreConfig) -> Router { - let client = GoogleOauthClient::new( + let client = GoogleOauthClient::new( config.google_oauth.id.clone(), config.google_oauth.secret.clone(), format!("{}/auth/google/callback", config.host), - &[], + &["https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"], "monitor".to_string(), ); - Router::new() - .route( + Router::new() + .route( "/login", get(|Extension(client): GoogleOauthExtension| async move { Redirect::to(&client.get_login_redirect_url()) @@ -35,13 +35,14 @@ pub fn router(config: &CoreConfig) -> Router { response!(redirect) }), ) - .layer(Extension(Arc::new(client))) + .layer(Extension(Arc::new(client))) } #[derive(Deserialize)] struct CallbackQuery { - state: String, - code: String, + state: Option, + code: Option, + error: Option, } async fn callback( @@ -50,11 +51,24 @@ async fn callback( Extension(state): StateExtension, Query(query): Query, ) -> anyhow::Result { - if !client.check_state(&query.state) { + if let Some(error) = query.error { + return Err(anyhow!("auth error from google: {error}")); + } + if !client.check_state( + &query + .state + .ok_or(anyhow!("callback query does not contain state"))?, + ) { return Err(anyhow!("state mismatch")); } - let token = client.get_access_token(&query.code).await?; - let google_user = client.get_google_user(&token.access_token)?; + let token = client + .get_access_token( + &query + .code + .ok_or(anyhow!("callback query does not contain code"))?, + ) + .await?; + let google_user = client.get_google_user(&token.id_token)?; let google_id = google_user.id.to_string(); let user = state .db @@ -69,7 +83,13 @@ async fn callback( None => { let ts = monitor_timestamp(); let user = User { - username: google_user.email.split("@").collect::>().get(0).unwrap().to_string(), + username: google_user + .email + .split("@") + .collect::>() + .get(0) + .unwrap() + .to_string(), avatar: google_user.picture.into(), google_id: google_id.into(), created_at: ts.clone(), @@ -92,4 +112,4 @@ async fn callback( "{}?token={exchange_token}", state.config.host ))) -} \ No newline at end of file +} diff --git a/frontend/public/icons/google.svg b/frontend/public/icons/google.svg new file mode 100644 index 000000000..9b1891a8e --- /dev/null +++ b/frontend/public/icons/google.svg @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/components/login/Login.tsx b/frontend/src/components/login/Login.tsx index fe55fa48f..dfa1da5f4 100644 --- a/frontend/src/components/login/Login.tsx +++ b/frontend/src/components/login/Login.tsx @@ -83,6 +83,12 @@ const Login: Component<{}> = (p) => { > log in with github + ); diff --git a/frontend/src/components/shared/Icon.tsx b/frontend/src/components/shared/Icon.tsx index 683e7f9db..748a5ade1 100644 --- a/frontend/src/components/shared/Icon.tsx +++ b/frontend/src/components/shared/Icon.tsx @@ -35,6 +35,7 @@ export type IconType = | "cut" | "fullscreen" | "github" + | "google" | "edit" | "clipboard" | "check" diff --git a/lib/axum_oauth2/Cargo.toml b/lib/axum_oauth2/Cargo.toml index 04b9694dd..140976185 100644 --- a/lib/axum_oauth2/Cargo.toml +++ b/lib/axum_oauth2/Cargo.toml @@ -11,6 +11,7 @@ reqwest = { version = "0.11", features = ["json"] } anyhow = "1.0" serde = "1.0" serde_derive = "1.0" +serde_json = "1.0" urlencoding = "2.1" rand = "0.8" jwt = "0.16" \ No newline at end of file diff --git a/lib/axum_oauth2/src/google.rs b/lib/axum_oauth2/src/google.rs index 7b0470ace..e672e17bf 100644 --- a/lib/axum_oauth2/src/google.rs +++ b/lib/axum_oauth2/src/google.rs @@ -1,10 +1,11 @@ -use std::sync::{Mutex, Arc}; +use std::sync::{Arc, Mutex}; use anyhow::{anyhow, Context}; use axum::Extension; -use jwt::{Header, Token}; +use jwt::Token; use reqwest::StatusCode; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; use crate::random_string; @@ -77,35 +78,29 @@ impl GoogleOauthClient { ("grant_type", "authorization_code"), ], None, - None, ) .await .context("failed to get google access token using code") } - pub fn get_google_user(&self, token: &str) -> anyhow::Result { - let token: Token = Token::parse_unverified(token)?; - Ok(token.claims().to_owned()) + pub fn get_google_user(&self, id_token: &str) -> anyhow::Result { + let t: Token = Token::parse_unverified(id_token).context("failed to parse id_token")?; + Ok(t.claims().to_owned()) } async fn post( &self, endpoint: &str, - query: &[(&str, &str)], - body: Option<&B>, + body: &[(&str, &str)], bearer_token: Option<&str>, ) -> anyhow::Result { let mut req = self .http .post(endpoint) - .query(query) + .form(body) .header("Accept", "application/json") .header("User-Agent", &self.user_agent); - if let Some(body) = body { - req = req.json(body); - } - if let Some(bearer_token) = bearer_token { req = req.header("Authorization", format!("Bearer {bearer_token}")); } @@ -132,6 +127,7 @@ impl GoogleOauthClient { #[derive(Deserialize)] pub struct AccessTokenResponse { pub access_token: String, + pub id_token: String, pub scope: String, pub token_type: String, }