Change google_auth_return url to /

This commit is contained in:
Chris Jean-Marie 2024-10-04 20:15:23 +00:00
parent 06a6811972
commit 042f6b17aa
7 changed files with 109 additions and 93 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
backend/target
backend/db
backend/id_rsa
backend/id_rsa.pub

View File

@ -61,7 +61,6 @@ pub async fn login(
State(db_pool): State<SqlitePool>,
Host(hostname): Host,
) -> Result<Redirect, AppError> {
if user_data.is_some() {
// check if already authenticated
return Ok(Redirect::to("/"));
@ -69,10 +68,9 @@ pub async fn login(
let return_url = params
.remove("return_url")
.unwrap_or_else(|| "/dashboard".to_string());
.unwrap_or_else(|| "/".to_string());
// TODO: check if return_url is valid
println!("Return URL: {}", return_url);
let client = get_client(hostname)?;
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
@ -103,10 +101,15 @@ pub async fn login(
pub async fn google_auth_return(
Query(mut params): Query<HashMap<String, String>>,
State(db_pool): State<SqlitePool>,
cookie: Option<TypedHeader<Cookie>>,
Host(hostname): Host,
) -> Result<impl IntoResponse, AppError> {
let state = CsrfToken::new(params.remove("state").ok_or("OAuth: without state")?);
let code = AuthorizationCode::new(params.remove("code").ok_or("OAuth: without code")?);
let mut headers = axum::response::AppendHeaders([(
axum::http::header::SET_COOKIE,
"session_token=".to_owned() + "; path=/; httponly; secure; samesite=strict",
)]);
let query: (String, String) = sqlx::query_as(
r#"DELETE FROM oauth2_state_storage WHERE csrf_state = ? RETURNING pkce_code_verifier,return_url"#,
@ -115,20 +118,8 @@ pub async fn google_auth_return(
.fetch_one(&db_pool)
.await?;
// Alternative:
// let query: (String, String) = sqlx::query_as(
// r#"SELECT pkce_code_verifier,return_url FROM oauth2_state_storage WHERE csrf_state = ?"#,
// )
// .bind(state.secret())
// .fetch_one(&db_pool)
// .await?;
// let _ = sqlx::query("DELETE FROM oauth2_state_storage WHERE csrf_state = ?")
// .bind(state.secret())
// .execute(&db_pool)
// .await;
let pkce_code_verifier = query.0;
let return_url = query.1;
let _return_url = query.1;
let pkce_code_verifier = PkceCodeVerifier::new(pkce_code_verifier);
// Exchange the code with a token.
@ -190,46 +181,61 @@ pub async fn google_auth_return(
.bind(email.as_str())
.fetch_one(&db_pool)
.await;
let user_id = if let Ok(query) = query {
let user_id = if let Ok(query) = query
{
query.0
} else {
let query: (i64,) = sqlx::query_as("INSERT INTO users (email, name, family_name, given_name) VALUES (?, ?, ?, ?) RETURNING id")
.bind(email)
.bind(name)
.bind(family_name)
.bind(given_name)
.bind(email.clone())
.bind(name.clone())
.bind(family_name.clone())
.bind(given_name.clone())
.fetch_one(&db_pool)
.await?;
// Add public role
sqlx::query("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)")
.bind(query.0)
.bind("1")
.execute(&db_pool)
.await?;
query.0
};
// Create a session for the user
let session_token_p1 = Uuid::new_v4().to_string();
let session_token_p2 = Uuid::new_v4().to_string();
let session_token = [session_token_p1.as_str(), "_", session_token_p2.as_str()].concat();
let headers = axum::response::AppendHeaders([(
axum::http::header::SET_COOKIE,
"session_token=".to_owned()
+ &*session_token
+ "; path=/; httponly; secure; samesite=strict",
)]);
let now = Utc::now().timestamp();
// Update session with user id or create new session
if let Some(cookie) = cookie {
if let Some(_session_token) = cookie.get("session_token") {
} else {
println!("google_auth_return : No session token");
// Create a session for the user
let session_token_p1 = Uuid::new_v4().to_string();
let session_token_p2 = Uuid::new_v4().to_string();
let session_token =
[session_token_p1.as_str(), "_", session_token_p2.as_str()].concat();
headers = axum::response::AppendHeaders([(
axum::http::header::SET_COOKIE,
"session_token=".to_owned()
+ &*session_token
+ "; path=/; httponly; secure; samesite=strict",
)]);
let now = Utc::now().timestamp();
sqlx::query(
"INSERT INTO user_sessions
sqlx::query(
"INSERT INTO user_sessions
(session_token_p1, session_token_p2, user_id, created_at, expires_at)
VALUES (?, ?, ?, ?, ?);",
)
.bind(session_token_p1)
.bind(session_token_p2)
.bind(user_id)
.bind(now)
.bind(now + 60 * 60 * 24)
.execute(&db_pool)
.await?;
println!("Returning to: {}", return_url);
Ok((headers, Redirect::to(return_url.as_str())))
)
.bind(session_token_p1)
.bind(session_token_p2)
.bind(user_id) // Set user to anonymous
.bind(now)
.bind(now + 60 * 60 * 24)
.execute(&db_pool)
.await?;
}
}
Ok((headers, Redirect::to("/")))
}
pub async fn logout(

View File

@ -15,11 +15,11 @@ mod routes;
use error_handling::AppError;
use middlewares::{check_auth, inject_user_data};
use google_oauth::{login, logout, google_auth_return};
use routes::{dashboard, index, profile, user_profile, useradmin};
use routes::{dashboard, index, about, profile, user_profile, useradmin};
#[derive(Clone)]
pub struct AppState {
pub db_pool: SqlitePool,
pub db_pool: SqlitePool
}
#[derive(Default, Clone, Debug, FromRow, Serialize, Deserialize)]
@ -42,7 +42,7 @@ async fn main() {
.connect("sqlite://db/db.sqlite3")
.await;
let app_state = AppState {db_pool: db_pool.expect("Failed to get db_pool") };
let app_state = AppState {db_pool:db_pool.expect("Failed to get db_pool")};
static MIGRATOR: Migrator = sqlx::migrate!();
@ -55,21 +55,18 @@ async fn main() {
// build our application with some routes
let app = Router::new()
//Routes that require authentication
.route("/dashboard", get(dashboard))
.route("/profile", get(profile))
.route("/useradmin", get(useradmin))
.route("/users/:user_id", get(user_profile))
.route_layer(middleware::from_fn_with_state(app_state.db_pool.clone(), check_auth))
//Routes that don't require authentication
.nest_service("/assets", ServeDir::new("templates/assets")
.fallback(get_service(ServeDir::new("templates/assets"))))
.route("/", get(index))
.route("/about", get(about))
.route("/login", get(login))
.route("/logout", get(logout))
//.route("/google_auth", get(google_auth))
.route("/google_auth_return", get(google_auth_return))
.route_layer(middleware::from_fn_with_state(app_state.db_pool.clone(), check_auth))
.route_layer(middleware::from_fn_with_state(app_state.db_pool.clone(), inject_user_data))
.with_state(app_state.db_pool)
.layer(Extension(user_data))

View File

@ -19,11 +19,11 @@ pub async fn inject_user_data(
mut request: Request<Body>,
next: Next,
) -> Result<impl IntoResponse, AppError> {
println!("Injecting user data");
println!("inject_user_data : Injecting user data");
if let Some(cookie) = cookie {
println!("{:#?}", cookie.get("session_token"));
println!("inject_user_data : {:#?}", cookie.get("session_token"));
if let Some(session_token) = cookie.get("session_token") {
println!("Found session token: {}", session_token);
println!("inject_user_data : Found session token: {}", session_token);
let session_token: Vec<&str> = session_token.split('_').collect();
let query: Result<(i64, i64, String), _> = sqlx::query_as(
r#"SELECT user_id,expires_at,session_token_p2 FROM user_sessions WHERE session_token_p1=?"#,
@ -46,7 +46,7 @@ pub async fn inject_user_data(
session_token_p2_db,
) {
let id = query.0;
println!("Found user: {}", id);
println!("inject_user_data : Found user: {}", id);
let expires_at = query.1;
if expires_at > Utc::now().timestamp() {
let row = sqlx::query_as!(
@ -65,7 +65,7 @@ pub async fn inject_user_data(
given_name: row.given_name,
}));
} else {
println!("Session expired");
println!("inject_user_data : Session expired");
}
}
}
@ -82,8 +82,8 @@ pub async fn check_auth(
request: Request<Body>,
next: Next,
) -> Result<impl IntoResponse, AppError> {
println!("check_auth");
println!("{:#?}", request);
println!("check_auth : Starting");
//println!("{:#?}", request);
if request
.extensions()
.get::<Option<UserData>>()
@ -91,9 +91,8 @@ pub async fn check_auth(
.is_some()
{
let path = &*request.uri().to_string();
println!("{}", path);
println!(
"{}",
"check_auth : {}",
&*request
.extensions()
.get::<Option<UserData>>()
@ -123,13 +122,14 @@ pub async fn check_auth(
// loop through path to find a permission
let mut remaining_path = Path::new(path);
loop {
let query: Result<(String,), _> = sqlx::query_as(r#"select r.item from role_permissions r join user_roles ur on ur.role_id = r.role_id join users u on u.id = ur.user_id where item = ? and email =?"#)
let query: Result<(String,), _> = sqlx::query_as(r#"select r.item from role_permissions r join user_roles ur on ur.role_id = r.role_id left join users u on u.id = ur.user_id where item = ? and (email = ? or r.role_id = 1)"#)
.bind(remaining_path.to_str().unwrap())
.bind(request.extensions().get::<Option<UserData>>().unwrap().as_ref().unwrap().email.as_str())
.fetch_one(&app_state)
.await;
if let Ok(query) = query {
if query.0 != "" {
println!("check_auth : found auth for {}", query.0);
break;
}
}
@ -137,10 +137,8 @@ pub async fn check_auth(
break;
}
remaining_path = remaining_path.parent().unwrap();
println!("{}", remaining_path.to_str().unwrap());
}
sqlx::query_as(r#"select u.id from role_permissions r join user_roles ur on ur.role_id = r.role_id join users u on u.id = ur.user_id where item = ? and email =?"#)
sqlx::query_as(r#"select u.id from role_permissions r join user_roles ur on ur.role_id = r.role_id join users u on u.id = ur.user_id where item = ? and (email = ? or r.role_id = 1)"#)
.bind(remaining_path.to_str().unwrap())
.bind(request.extensions().get::<Option<UserData>>().unwrap().as_ref().unwrap().email.as_str())
.fetch_one(&app_state)
@ -158,7 +156,7 @@ pub async fn check_auth(
0
};
println!("{}", user_id);
println!("check_auth : Authenticated user {}", user_id);
if user_id == 0 {
// user does not have the proper role
// display banner and return to home page
@ -169,17 +167,11 @@ pub async fn check_auth(
} else {
// user is not logged in
// redirect to login page with return_url
let login_url = "/login?return_url=".to_owned() + &*request.uri().to_string();
Ok(Redirect::to(login_url.as_str()).into_response())
//let login_url = "/login?return_url=".to_owned() + &*request.uri().to_string();
//let login_url = "/login".to_owned();
//Ok(Redirect::to(login_url.as_str()).into_response())
//println!("check_auth : {:#?}", request.headers().get("referer").unwrap());
println!("check_auth : No user data in the request");
Ok(next.run(request).await)
}
}
fn access_auth (_uri: &str, _userid: i64) {
// Check uri and userid for access and build the menu for links to the allowed resources
let _user_auth = true;
let _menu: Vec<String> = vec![];
}

View File

@ -1,6 +1,6 @@
use askama_axum::{Response, Template};
use axum::{extract::{Path, State}, response::{Html, IntoResponse}, Extension};
use http::{Request, StatusCode};
use http::StatusCode;
use sqlx::SqlitePool;
use crate::UserData;
@ -45,23 +45,21 @@ struct DashboardTemplate {
name: String,
}
pub async fn index<T>(
pub async fn index(
Extension(user_data): Extension<Option<UserData>>,
_request: Request<T>,
) -> impl IntoResponse {
let user_name = user_data.map(|s| s.name);
let logged_in = user_name.is_some();
let name = user_name.unwrap_or_default();
println!("logged_in: {}, name: {}", logged_in, name);
println!("index : logged_in = {}, name = {}", logged_in, name);
let template = IndexTemplate { logged_in, name };
HtmlTemplate(template)
}
pub async fn dashboard<T>(
pub async fn dashboard(
Extension(user_data): Extension<Option<UserData>>,
_request: Request<T>,
) -> impl IntoResponse {
let user_name = user_data.map(|s| s.name);
let logged_in = user_name.is_some();
@ -72,9 +70,8 @@ pub async fn dashboard<T>(
}
/// Handles the profile page.
pub async fn profile<T>(
pub async fn profile(
Extension(user_data): Extension<Option<UserData>>,
_request: Request<T>,
) -> impl IntoResponse {
// Extract the user's name from the user data.
let user_name = user_data.as_ref().map(|s| s.name.clone());
@ -89,11 +86,10 @@ pub async fn profile<T>(
return HtmlTemplate(template)
}
pub async fn user_profile<T>(
pub async fn user_profile(
Path(user_id): Path<i64>,
State(db_pool): State<SqlitePool>,
Extension(user_data): Extension<Option<UserData>>,
_request: Request<T>,
) -> impl IntoResponse {
// Extract the user's name from the user data.
let user_name = user_data.as_ref().map(|s| s.name.clone());
@ -123,10 +119,9 @@ struct UserAdminTemplate {
users: Vec<UserData>
}
pub async fn useradmin<T>(
pub async fn useradmin(
Extension(user_data): Extension<Option<UserData>>,
State(db_pool): State<SqlitePool>,
_request: Request<T>,
) -> impl IntoResponse {
let user_email = user_data.map(|s| s.email);
@ -141,3 +136,22 @@ pub async fn useradmin<T>(
let template = UserAdminTemplate { logged_in, name, users };
HtmlTemplate(template)
}
#[derive(Template)]
#[template(path = "about.html")]
struct AboutTemplate {
logged_in: bool,
name: String,
}
pub async fn about(
Extension(user_data): Extension<Option<UserData>>,
) -> impl IntoResponse {
let user_name = user_data.map(|s| s.name);
let logged_in = user_name.is_some();
let name = user_name.unwrap_or_default();
let template = AboutTemplate { logged_in, name };
HtmlTemplate(template)
}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}About{% endblock %}
{% block content %}
This is a demo OAuth website.
This is will give more information about the website
{% endblock %}

5
backend/toprod.sh Executable file
View File

@ -0,0 +1,5 @@
#ssh www@192.168.59.11 'pkill jean-marie'
scp -i id_rsa target/release/jean-marie www@192.168.59.11:/opt/jean-marie
scp -i id_rsa .env www@192.168.59.11:/opt/jean-marie
scp -i id_rsa -r templates www@192.168.59.11:/opt/jean-marie
#ssh www@192.168.59.11 'cd /opt/jean-marie && ./jean-marie&'