From c10c7e8d5c20427696504c6edb94b948f46ff07e Mon Sep 17 00:00:00 2001 From: Griffin Obeid Date: Wed, 8 Nov 2023 09:33:00 -0500 Subject: [PATCH 01/10] Some refactor for testing --- actix-api/src/lib.rs | 172 ++++++++++++++++++++++++++++++++++++++ actix-api/src/main.rs | 161 ++--------------------------------- actix-api/tests/server.rs | 19 +++++ docker/Dockerfile.actix | 2 +- docker/Dockerfile.yew | 4 +- 5 files changed, 199 insertions(+), 159 deletions(-) create mode 100644 actix-api/src/lib.rs create mode 100644 actix-api/tests/server.rs diff --git a/actix-api/src/lib.rs b/actix-api/src/lib.rs new file mode 100644 index 0000000..45c9dd6 --- /dev/null +++ b/actix-api/src/lib.rs @@ -0,0 +1,172 @@ +use actix_cors::Cors; +use actix_web::{ + cookie::{ + time::{Duration, OffsetDateTime}, + Cookie, SameSite, + }, + error, get, http, + web::{self, Json}, + App, Error, HttpResponse, HttpServer, dev::{ServiceFactory, ServiceRequest, ServiceResponse}, body::{EitherBody, BoxBody}, +}; +use r2d2::Pool; +use r2d2_postgres::{PostgresConnectionManager, postgres::NoTls}; + +use crate::auth::{ + fetch_oauth_request, generate_and_store_oauth_request, request_token, upsert_user, +}; +use crate::{ + auth::AuthRequest, + db::{get_pool, PostgresPool}, +}; +use reqwest::header::LOCATION; +use types::HelloResponse; + +const OAUTH_CLIENT_ID: &str = std::env!("OAUTH_CLIENT_ID"); +const OAUTH_AUTH_URL: &str = std::env!("OAUTH_AUTH_URL"); +const OAUTH_TOKEN_URL: &str = std::env!("OAUTH_TOKEN_URL"); +const OAUTH_SECRET: &str = std::env!("OAUTH_CLIENT_SECRET"); +const OAUTH_REDIRECT_URL: &str = std::env!("OAUTH_REDIRECT_URL"); +const SCOPE: &str = "email%20profile%20openid"; +pub const ACTIX_PORT: &str = std::env!("ACTIX_PORT"); +const UI_PORT: &str = std::env!("TRUNK_SERVE_PORT"); +const UI_HOST: &str = std::env!("TRUNK_SERVE_HOST"); +const AFTER_LOGIN_URL: &str = concat!("http://localhost:", std::env!("TRUNK_SERVE_PORT")); + +pub mod auth; +pub mod db; + +/** + * Function used by the Web Application to initiate OAuth. + * + * The server responds with the OAuth login URL. + * + * The server implements PKCE (Proof Key for Code Exchange) to protect itself and the users. + */ +#[get("/login")] +async fn login(pool: web::Data) -> Result { + // TODO: verify if user exists in the db by looking at the session cookie, (if the client provides one.) + let pool2 = pool.clone(); + + // 2. Generate and Store OAuth Request. + let (csrf_token, pkce_challenge) = { + let pool = pool2.clone(); + web::block(move || generate_and_store_oauth_request(pool)).await? + } + .map_err(|e| { + log::error!("{:?}", e); + error::ErrorInternalServerError(e) + })?; + + // 3. Craft OAuth Login URL + let oauth_login_url = format!("{oauth_url}?client_id={client_id}&redirect_uri={redirect_url}&response_type=code&scope={scope}&prompt=select_account&pkce_challenge={pkce_challenge}&state={state}&access_type=offline", + oauth_url=OAUTH_AUTH_URL, + redirect_url=OAUTH_REDIRECT_URL, + client_id=OAUTH_CLIENT_ID, + scope=SCOPE, + pkce_challenge=pkce_challenge.as_str(), + state=&csrf_token.secret() + ); + + // 4. Redirect the browser to the OAuth Login URL. + let mut response = HttpResponse::Found(); + response.append_header((LOCATION, oauth_login_url)); + Ok(response.finish()) +} + +/** + * Handle OAuth callback from Web App. + * + * This service is responsible for using the provided authentication code to fetch + * the OAuth access_token and refresh token. + * + * It upserts the user using their email and stores the access_token & refresh_code. + */ +#[get("/login/callback")] +async fn handle_google_oauth_callback( + pool: web::Data, + info: web::Query, +) -> Result { + let state = info.state.clone(); + + // 1. Fetch OAuth request, if this fails, probably a hacker is trying to p*wn us. + let oauth_request = { + let pool = pool.clone(); + web::block(move || fetch_oauth_request(pool, state)).await? + } + .map_err(|e| { + log::error!("{:?}", e); + error::ErrorBadRequest("couldn't find a request, are you a hacker?") + })?; + + // 2. Request token from OAuth provider. + let (oauth_response, claims) = request_token( + OAUTH_REDIRECT_URL, + OAUTH_CLIENT_ID, + OAUTH_SECRET, + &oauth_request.pkce_verifier, + OAUTH_TOKEN_URL, + &info.code, + ) + .await + .map_err(|err| { + log::error!("{:?}", err); + error::ErrorBadRequest("couldn't find a request, are you a hacker?") + })?; + + // 3. Store tokens and create user. + { + let claims = claims.clone(); + web::block(move || upsert_user(pool, &claims, &oauth_response)).await? + } + .map_err(|err| { + log::error!("{:?}", err); + error::ErrorInternalServerError(err) + })?; + + // 4. Create session cookie with email. + let cookie = Cookie::build("email", claims.email) + .path("/") + .same_site(SameSite::Lax) + // Session lasts only 360 secs to test cookie expiration. + .expires(OffsetDateTime::now_utc().checked_add(Duration::seconds(360))) + .finish(); + + // 5. Send cookie and redirect browser to AFTER_LOGIN_URL + let mut response = HttpResponse::Found(); + response.append_header((LOCATION, AFTER_LOGIN_URL)); + response.cookie(cookie); + Ok(response.finish()) +} + +/** + * Sample service + */ +#[get("/hello/{name}")] +async fn greet(name: web::Path) -> Json { + Json(HelloResponse { + name: name.to_string(), + }) +} + +pub fn get_app() -> App>, Error = actix_web::Error, InitError = ()>> { + // TODO: Deal with https, maybe we should just expose this as an env var? + let allowed_origin = if UI_PORT != "80" { + format!("http://{}:{}", UI_HOST, UI_PORT) + } else { + format!("http://{}", UI_HOST) + }; + let cors = Cors::default() + .allowed_origin(allowed_origin.as_str()) + .allowed_methods(vec!["GET", "POST"]) + .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) + .allowed_header(http::header::CONTENT_TYPE) + .max_age(3600); + + let pool = get_pool(); + App::new() + .app_data(web::Data::new(pool)) + .wrap(cors) + .service(greet) + .service(handle_google_oauth_callback) + .service(login) +} diff --git a/actix-api/src/main.rs b/actix-api/src/main.rs index 4cbe9b7..2e77f67 100644 --- a/actix-api/src/main.rs +++ b/actix-api/src/main.rs @@ -1,3 +1,4 @@ +use actix_api::{get_app, ACTIX_PORT}; use actix_cors::Cors; use actix_web::{ cookie::{ @@ -9,172 +10,20 @@ use actix_web::{ App, Error, HttpResponse, HttpServer, }; -use crate::auth::{ +use actix_api::auth::{ fetch_oauth_request, generate_and_store_oauth_request, request_token, upsert_user, }; -use crate::{ +use actix_api::{ auth::AuthRequest, db::{get_pool, PostgresPool}, }; use reqwest::header::LOCATION; use types::HelloResponse; -const OAUTH_CLIENT_ID: &str = std::env!("OAUTH_CLIENT_ID"); -const OAUTH_AUTH_URL: &str = std::env!("OAUTH_AUTH_URL"); -const OAUTH_TOKEN_URL: &str = std::env!("OAUTH_TOKEN_URL"); -const OAUTH_SECRET: &str = std::env!("OAUTH_CLIENT_SECRET"); -const OAUTH_REDIRECT_URL: &str = std::env!("OAUTH_REDIRECT_URL"); -const SCOPE: &str = "email%20profile%20openid"; -const ACTIX_PORT: &str = std::env!("ACTIX_PORT"); -const UI_PORT: &str = std::env!("TRUNK_SERVE_PORT"); -const UI_HOST: &str = std::env!("TRUNK_SERVE_HOST"); -const AFTER_LOGIN_URL: &str = concat!("http://localhost:", std::env!("TRUNK_SERVE_PORT")); - -pub mod auth; -pub mod db; - -/** - * Function used by the Web Application to initiate OAuth. - * - * The server responds with the OAuth login URL. - * - * The server implements PKCE (Proof Key for Code Exchange) to protect itself and the users. - */ -#[get("/login")] -async fn login(pool: web::Data) -> Result { - // TODO: verify if user exists in the db by looking at the session cookie, (if the client provides one.) - let pool2 = pool.clone(); - - // 2. Generate and Store OAuth Request. - let (csrf_token, pkce_challenge) = { - let pool = pool2.clone(); - web::block(move || generate_and_store_oauth_request(pool)).await? - } - .map_err(|e| { - log::error!("{:?}", e); - error::ErrorInternalServerError(e) - })?; - - // 3. Craft OAuth Login URL - let oauth_login_url = format!("{oauth_url}?client_id={client_id}&redirect_uri={redirect_url}&response_type=code&scope={scope}&prompt=select_account&pkce_challenge={pkce_challenge}&state={state}&access_type=offline", - oauth_url=OAUTH_AUTH_URL, - redirect_url=OAUTH_REDIRECT_URL, - client_id=OAUTH_CLIENT_ID, - scope=SCOPE, - pkce_challenge=pkce_challenge.as_str(), - state=&csrf_token.secret() - ); - - // 4. Redirect the browser to the OAuth Login URL. - let mut response = HttpResponse::Found(); - response.append_header((LOCATION, oauth_login_url)); - Ok(response.finish()) -} - -/** - * Handle OAuth callback from Web App. - * - * This service is responsible for using the provided authentication code to fetch - * the OAuth access_token and refresh token. - * - * It upserts the user using their email and stores the access_token & refresh_code. - */ -#[get("/login/callback")] -async fn handle_google_oauth_callback( - pool: web::Data, - info: web::Query, -) -> Result { - let state = info.state.clone(); - - // 1. Fetch OAuth request, if this fails, probably a hacker is trying to p*wn us. - let oauth_request = { - let pool = pool.clone(); - web::block(move || fetch_oauth_request(pool, state)).await? - } - .map_err(|e| { - log::error!("{:?}", e); - error::ErrorBadRequest("couldn't find a request, are you a hacker?") - })?; - - // 2. Request token from OAuth provider. - let (oauth_response, claims) = request_token( - OAUTH_REDIRECT_URL, - OAUTH_CLIENT_ID, - OAUTH_SECRET, - &oauth_request.pkce_verifier, - OAUTH_TOKEN_URL, - &info.code, - ) - .await - .map_err(|err| { - log::error!("{:?}", err); - error::ErrorBadRequest("couldn't find a request, are you a hacker?") - })?; - - // 3. Store tokens and create user. - { - let claims = claims.clone(); - web::block(move || upsert_user(pool, &claims, &oauth_response)).await? - } - .map_err(|err| { - log::error!("{:?}", err); - error::ErrorInternalServerError(err) - })?; - - // 4. Create session cookie with email. - let cookie = Cookie::build("email", claims.email) - .path("/") - .same_site(SameSite::Lax) - // Session lasts only 360 secs to test cookie expiration. - .expires(OffsetDateTime::now_utc().checked_add(Duration::seconds(360))) - .finish(); - - // 5. Send cookie and redirect browser to AFTER_LOGIN_URL - let mut response = HttpResponse::Found(); - response.append_header((LOCATION, AFTER_LOGIN_URL)); - response.cookie(cookie); - Ok(response.finish()) -} - -/** - * Sample service - */ -#[get("/hello/{name}")] -async fn greet(name: web::Path) -> Json { - Json(HelloResponse { - name: name.to_string(), - }) -} - #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init(); - - // TODO: Deal with https, maybe we should just expose this as an env var? - let allowed_origin = if UI_PORT != "80" { - format!("http://{}:{}", UI_HOST, UI_PORT) - } else { - format!("http://{}", UI_HOST) - }; - HttpServer::new(move || { - let cors = Cors::default() - .allowed_origin(allowed_origin.as_str()) - .allowed_methods(vec!["GET", "POST"]) - .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) - .allowed_header(http::header::CONTENT_TYPE) - .max_age(3600); - - let pool = get_pool(); - - App::new() - .app_data(web::Data::new(pool)) - .wrap(cors) - .service(greet) - .service(handle_google_oauth_callback) - .service(login) - }) - .bind(("0.0.0.0", ACTIX_PORT.parse::().unwrap()))? - .run() - .await + get_app() + }).bind(("0.0.0.0", ACTIX_PORT.parse::().unwrap()))?.run().await } diff --git a/actix-api/tests/server.rs b/actix-api/tests/server.rs new file mode 100644 index 0000000..baf8f87 --- /dev/null +++ b/actix-api/tests/server.rs @@ -0,0 +1,19 @@ +use actix_api::get_app; + +/// Test login inserts pkce_challenge, pkce_verifier, csrf_state +/// And returns a login url with the pkce_challenge +/// + +#[actix_rt::test] +async fn test_login() { + let app = get_app(); + let mut app = test::init_service(app).await; + let req = test::TestRequest::get().uri("/login").to_request(); + let resp = test::call_service(&mut app, req).await; + assert!(resp.status().is_success()); + let body = test::read_body(resp).await; + let body = String::from_utf8(body.to_vec()).unwrap(); + assert!(body.contains("https://accounts.google.com/o/oauth2/v2/auth")); + assert!(body.contains("code_challenge=")); + assert!(body.contains("state=")); +} \ No newline at end of file diff --git a/docker/Dockerfile.actix b/docker/Dockerfile.actix index 0334305..91ba032 100644 --- a/docker/Dockerfile.actix +++ b/docker/Dockerfile.actix @@ -1,4 +1,4 @@ -FROM rust:1.70-slim-bullseye +FROM rust:1.73-slim-buster RUN apt-get --yes update && apt-get --yes install curl git pkg-config libssl-dev RUN curl https://github.com/amacneil/dbmate/releases/download/v2.4.0/dbmate-linux-amd64 -L -o /usr/bin/dbmate && chmod +x /usr/bin/dbmate diff --git a/docker/Dockerfile.yew b/docker/Dockerfile.yew index 7aa3001..f95d21f 100644 --- a/docker/Dockerfile.yew +++ b/docker/Dockerfile.yew @@ -1,6 +1,6 @@ -FROM rust:1.64-slim-bullseye +FROM rust:1.73-slim-buster -RUN rustup default nightly-2022-10-21 +RUN rustup default nightly-2023-11-07 RUN apt-get --yes update && apt-get --yes install git pkg-config libssl-dev RUN cargo install wasm-bindgen-cli --version 0.2.78 From b8f03d1099f1efc65e0d28fefbda7bb0d124e8ba Mon Sep 17 00:00:00 2001 From: Griffin Obeid Date: Wed, 8 Nov 2023 11:03:04 -0500 Subject: [PATCH 02/10] Progress mofo --- actix-api/tests/server.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/actix-api/tests/server.rs b/actix-api/tests/server.rs index baf8f87..b262f59 100644 --- a/actix-api/tests/server.rs +++ b/actix-api/tests/server.rs @@ -1,15 +1,16 @@ use actix_api::get_app; +use actix_web::test; /// Test login inserts pkce_challenge, pkce_verifier, csrf_state /// And returns a login url with the pkce_challenge /// -#[actix_rt::test] +#[actix_web::test] async fn test_login() { - let app = get_app(); - let mut app = test::init_service(app).await; + let mut app = test::init_service(get_app()).await; let req = test::TestRequest::get().uri("/login").to_request(); let resp = test::call_service(&mut app, req).await; + drop(app); assert!(resp.status().is_success()); let body = test::read_body(resp).await; let body = String::from_utf8(body.to_vec()).unwrap(); From afbdd2aaffed7fb6be3207227ce1d238631603b7 Mon Sep 17 00:00:00 2001 From: Griffin Obeid Date: Thu, 9 Nov 2023 09:07:16 -0500 Subject: [PATCH 03/10] Save --- Makefile | 11 +++++++---- actix-api/src/lib.rs | 38 ++++++++++++++++++++++++-------------- actix-api/src/main.rs | 7 ++++--- actix-api/tests/common.rs | 34 ++++++++++++++++++++++++++++++++++ actix-api/tests/server.rs | 9 +++++++-- docker/docker-compose.yaml | 20 +++++++++++--------- 6 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 actix-api/tests/common.rs diff --git a/Makefile b/Makefile index b46b7d8..bbc97de 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ test: + make test-api + make test-ui +test-api: + docker compose -f docker/docker-compose.yaml run actix-api bash -c "cd app/actix-api && cargo test -- --nocapture" +test-ui: docker compose -f docker/docker-compose.yaml run yew-ui bash -c "cd app/yew-ui && cargo test" - docker compose -f docker/docker-compose.yaml run actix-api bash -c "cd app/actix-api && cargo test" - up: docker compose -f docker/docker-compose.yaml up down: @@ -21,7 +24,7 @@ clippy-fix: docker compose -f docker/docker-compose.yaml run yew-ui bash -c "cd app/yew-ui && cargo clippy --fix" docker compose -f docker/docker-compose.yaml run actix-api bash -c "cd app/actix-api && cargo clippy --fix && cd ../types && cargo clippy --fix" -check: +check: # The ui does not support clippy yet docker compose -f docker/docker-compose.yaml run yew-ui bash -c "cd app/yew-ui && cargo clippy --all -- --deny warnings && cargo fmt --check" - docker compose -f docker/docker-compose.yaml run actix-api bash -c "cd app/actix-api && cargo clippy --all -- --deny warnings && cargo fmt --check" \ No newline at end of file + docker compose -f docker/docker-compose.yaml run actix-api bash -c "cd app/actix-api && cargo clippy --all -- --deny warnings && cargo fmt --check" diff --git a/actix-api/src/lib.rs b/actix-api/src/lib.rs index 45c9dd6..2ac10e3 100644 --- a/actix-api/src/lib.rs +++ b/actix-api/src/lib.rs @@ -1,15 +1,17 @@ use actix_cors::Cors; use actix_web::{ + body::{BoxBody, EitherBody}, cookie::{ time::{Duration, OffsetDateTime}, Cookie, SameSite, }, + dev::{ServiceFactory, ServiceRequest, ServiceResponse}, error, get, http, web::{self, Json}, - App, Error, HttpResponse, HttpServer, dev::{ServiceFactory, ServiceRequest, ServiceResponse}, body::{EitherBody, BoxBody}, + App, Error, HttpResponse, HttpServer, }; use r2d2::Pool; -use r2d2_postgres::{PostgresConnectionManager, postgres::NoTls}; +use r2d2_postgres::{postgres::NoTls, PostgresConnectionManager}; use crate::auth::{ fetch_oauth_request, generate_and_store_oauth_request, request_token, upsert_user, @@ -148,7 +150,15 @@ async fn greet(name: web::Path) -> Json { }) } -pub fn get_app() -> App>, Error = actix_web::Error, InitError = ()>> { +pub fn get_app() -> App< + impl ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse>, + Error = actix_web::Error, + InitError = (), + >, +> { // TODO: Deal with https, maybe we should just expose this as an env var? let allowed_origin = if UI_PORT != "80" { format!("http://{}:{}", UI_HOST, UI_PORT) @@ -156,17 +166,17 @@ pub fn get_app() -> App std::io::Result<()> { env_logger::init(); - HttpServer::new(move || { - get_app() - }).bind(("0.0.0.0", ACTIX_PORT.parse::().unwrap()))?.run().await + HttpServer::new(move || get_app()) + .bind(("0.0.0.0", ACTIX_PORT.parse::().unwrap()))? + .run() + .await } diff --git a/actix-api/tests/common.rs b/actix-api/tests/common.rs new file mode 100644 index 0000000..0678e93 --- /dev/null +++ b/actix-api/tests/common.rs @@ -0,0 +1,34 @@ +use std::process::Command; + +pub fn dbmate_up(url: &str) { + log::info!("dbmate up DATABASE_URL: {}", url); + let do_steps = || -> bool { + Command::new("sh") + .arg("-c") + .arg("dbmate up") + .env("DATABASE_URL", url) + .status() + .expect("failed to execute process") + .success() + }; + if !do_steps() { + panic!("Failed to perform dbmate up operation"); + } +} + +pub fn dbmate_drop(url: &str) { + log::info!("dbmate drop DATABASE_URL: {}", url); + let do_steps = || -> bool { + Command::new("sh") + .arg("-c") + .arg("dbmate drop") + .env("DATABASE_URL", url) + .status() + .expect("failed to execute process") + .success() + }; + if !do_steps() { + log::error!("Failed to perform dbmate drop operation"); + } +} + diff --git a/actix-api/tests/server.rs b/actix-api/tests/server.rs index b262f59..e3a4180 100644 --- a/actix-api/tests/server.rs +++ b/actix-api/tests/server.rs @@ -1,20 +1,25 @@ +mod common; use actix_api::get_app; use actix_web::test; /// Test login inserts pkce_challenge, pkce_verifier, csrf_state /// And returns a login url with the pkce_challenge -/// +/// #[actix_web::test] async fn test_login() { + let db_url = std::env::var("PG_URL").unwrap(); + println!("DB_URL: {}", db_url); + common::dbmate_up(&db_url); let mut app = test::init_service(get_app()).await; let req = test::TestRequest::get().uri("/login").to_request(); let resp = test::call_service(&mut app, req).await; drop(app); + println!("{:?}", resp.status()); assert!(resp.status().is_success()); let body = test::read_body(resp).await; let body = String::from_utf8(body.to_vec()).unwrap(); assert!(body.contains("https://accounts.google.com/o/oauth2/v2/auth")); assert!(body.contains("code_challenge=")); assert!(body.contains("state=")); -} \ No newline at end of file +} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 0f304a5..3470809 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -6,9 +6,9 @@ services: - type: bind source: ../ target: /app - - /app/yew-ui/target + - yew-ui-target-cache:/app/yew-ui/target - yew-ui-cargo-registry-cache:/usr/local/cargo/registry - build: + build: dockerfile: ../docker/Dockerfile.yew command: bash -c "cd app/yew-ui && trunk serve --address 0.0.0.0 --port ${TRUNK_SERVE_PORT:-80}" environment: @@ -20,7 +20,7 @@ services: - "${TRUNK_SERVE_PORT:-80}:${TRUNK_SERVE_PORT:-80}" actix-api: - build: + build: dockerfile: ../docker/Dockerfile.actix command: bash -c "cd app/actix-api && cargo watch -x \"run\"" environment: @@ -40,8 +40,8 @@ services: - type: bind source: ../ target: /app - - /app/actix-api/target - - actix-web-cargo-registry-cache:/usr/local/cargo/registry + - actix-api-target-cache:/app/actix-api/target + - actix-api-cargo-registry-cache:/usr/local/cargo/registry depends_on: - postgres @@ -49,12 +49,12 @@ services: volumes: - ../dbmate/db:/app/db build: - context: ../dbmate + context: ../dbmate environment: DATABASE_URL: "postgres://postgres:docker@postgres:5432/actix-api-db?sslmode=disable" depends_on: - postgres - + postgres: image: postgres:12 environment: @@ -63,5 +63,7 @@ services: ports: - 5432 volumes: - actix-web-cargo-registry-cache: - yew-ui-cargo-registry-cache: \ No newline at end of file + actix-api-cargo-registry-cache: + actix-api-target-cache: + yew-ui-target-cache: + yew-ui-cargo-registry-cache: From c69ca410e9dff9ee7c3785b7448dfa43f1eedd51 Mon Sep 17 00:00:00 2001 From: Griffin Obeid Date: Thu, 9 Nov 2023 09:17:45 -0500 Subject: [PATCH 04/10] Passing basic test --- actix-api/tests/common.rs | 16 ---------------- actix-api/tests/server.rs | 8 +------- docker/docker-compose.yaml | 1 + 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/actix-api/tests/common.rs b/actix-api/tests/common.rs index 0678e93..f837cfe 100644 --- a/actix-api/tests/common.rs +++ b/actix-api/tests/common.rs @@ -16,19 +16,3 @@ pub fn dbmate_up(url: &str) { } } -pub fn dbmate_drop(url: &str) { - log::info!("dbmate drop DATABASE_URL: {}", url); - let do_steps = || -> bool { - Command::new("sh") - .arg("-c") - .arg("dbmate drop") - .env("DATABASE_URL", url) - .status() - .expect("failed to execute process") - .success() - }; - if !do_steps() { - log::error!("Failed to perform dbmate drop operation"); - } -} - diff --git a/actix-api/tests/server.rs b/actix-api/tests/server.rs index e3a4180..d901a55 100644 --- a/actix-api/tests/server.rs +++ b/actix-api/tests/server.rs @@ -15,11 +15,5 @@ async fn test_login() { let req = test::TestRequest::get().uri("/login").to_request(); let resp = test::call_service(&mut app, req).await; drop(app); - println!("{:?}", resp.status()); - assert!(resp.status().is_success()); - let body = test::read_body(resp).await; - let body = String::from_utf8(body.to_vec()).unwrap(); - assert!(body.contains("https://accounts.google.com/o/oauth2/v2/auth")); - assert!(body.contains("code_challenge=")); - assert!(body.contains("state=")); + assert!(resp.status() == 302); } diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 3470809..a3472f9 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -34,6 +34,7 @@ services: - OAUTH_REDIRECT_URL=http://localhost:${ACTIX_PORT:-8080}/login/callback - RUST_LOG=info - PG_URL=postgres://postgres:docker@postgres:5432/actix-api-db?sslmode=disable + - DBMATE_MIGRATIONS_DIR=/app/dbmate/db/migrations ports: - "${ACTIX_PORT:-8080}:${ACTIX_PORT:-8080}" volumes: From 81e332fd60d67293bde7c4e9aee52cd172aee9be Mon Sep 17 00:00:00 2001 From: Griffin Obeid Date: Thu, 9 Nov 2023 09:20:04 -0500 Subject: [PATCH 05/10] Clippy fixes --- actix-api/src/lib.rs | 6 +++--- actix-api/src/main.rs | 25 +++++++------------------ 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/actix-api/src/lib.rs b/actix-api/src/lib.rs index 2ac10e3..86ab74d 100644 --- a/actix-api/src/lib.rs +++ b/actix-api/src/lib.rs @@ -8,10 +8,10 @@ use actix_web::{ dev::{ServiceFactory, ServiceRequest, ServiceResponse}, error, get, http, web::{self, Json}, - App, Error, HttpResponse, HttpServer, + App, Error, HttpResponse, }; -use r2d2::Pool; -use r2d2_postgres::{postgres::NoTls, PostgresConnectionManager}; + + use crate::auth::{ fetch_oauth_request, generate_and_store_oauth_request, request_token, upsert_user, diff --git a/actix-api/src/main.rs b/actix-api/src/main.rs index b030c95..017df00 100644 --- a/actix-api/src/main.rs +++ b/actix-api/src/main.rs @@ -1,29 +1,18 @@ use actix_api::{get_app, ACTIX_PORT}; -use actix_cors::Cors; + use actix_web::{ - cookie::{ - time::{Duration, OffsetDateTime}, - Cookie, SameSite, - }, - error, get, http, - web::{self, Json}, - App, Error, HttpResponse, HttpServer, + HttpServer, }; -use actix_api::auth::{ - fetch_oauth_request, generate_and_store_oauth_request, request_token, upsert_user, -}; -use actix_api::{ - auth::AuthRequest, - db::{get_pool, PostgresPool}, -}; -use reqwest::header::LOCATION; -use types::HelloResponse; + + + + #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init(); - HttpServer::new(move || get_app()) + HttpServer::new(get_app) .bind(("0.0.0.0", ACTIX_PORT.parse::().unwrap()))? .run() .await From af67e230597896a964fc80e8e6e1354eece9d145 Mon Sep 17 00:00:00 2001 From: Griffin Obeid Date: Thu, 9 Nov 2023 09:20:40 -0500 Subject: [PATCH 06/10] fmt --- actix-api/src/lib.rs | 2 -- actix-api/src/main.rs | 9 +-------- actix-api/tests/common.rs | 1 - 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/actix-api/src/lib.rs b/actix-api/src/lib.rs index 86ab74d..faca112 100644 --- a/actix-api/src/lib.rs +++ b/actix-api/src/lib.rs @@ -11,8 +11,6 @@ use actix_web::{ App, Error, HttpResponse, }; - - use crate::auth::{ fetch_oauth_request, generate_and_store_oauth_request, request_token, upsert_user, }; diff --git a/actix-api/src/main.rs b/actix-api/src/main.rs index 017df00..eaf4fa4 100644 --- a/actix-api/src/main.rs +++ b/actix-api/src/main.rs @@ -1,13 +1,6 @@ use actix_api::{get_app, ACTIX_PORT}; -use actix_web::{ - HttpServer, -}; - - - - - +use actix_web::HttpServer; #[actix_web::main] async fn main() -> std::io::Result<()> { diff --git a/actix-api/tests/common.rs b/actix-api/tests/common.rs index f837cfe..bf4d305 100644 --- a/actix-api/tests/common.rs +++ b/actix-api/tests/common.rs @@ -15,4 +15,3 @@ pub fn dbmate_up(url: &str) { panic!("Failed to perform dbmate up operation"); } } - From b8288e5c54743afaba122a98002964258d2e5c00 Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Fri, 24 Nov 2023 22:31:47 -0500 Subject: [PATCH 07/10] move main to lib --- actix-api/src/lib.rs | 6 +- actix-api/src/main.rs | 137 ------------------------------------------ 2 files changed, 3 insertions(+), 140 deletions(-) diff --git a/actix-api/src/lib.rs b/actix-api/src/lib.rs index faca112..84ebb24 100644 --- a/actix-api/src/lib.rs +++ b/actix-api/src/lib.rs @@ -50,7 +50,7 @@ async fn login(pool: web::Data) -> Result { // 2. Generate and Store OAuth Request. let (csrf_token, pkce_challenge) = { let pool = pool2.clone(); - web::block(move || generate_and_store_oauth_request(pool)).await? + generate_and_store_oauth_request(pool).await } .map_err(|e| { log::error!("{:?}", e); @@ -91,7 +91,7 @@ async fn handle_google_oauth_callback( // 1. Fetch OAuth request, if this fails, probably a hacker is trying to p*wn us. let oauth_request = { let pool = pool.clone(); - web::block(move || fetch_oauth_request(pool, state)).await? + fetch_oauth_request(pool, state).await } .map_err(|e| { log::error!("{:?}", e); @@ -116,7 +116,7 @@ async fn handle_google_oauth_callback( // 3. Store tokens and create user. { let claims = claims.clone(); - web::block(move || upsert_user(pool, &claims, &oauth_response)).await? + upsert_user(pool, &claims, &oauth_response).await } .map_err(|err| { log::error!("{:?}", err); diff --git a/actix-api/src/main.rs b/actix-api/src/main.rs index 4ef287f..87436a3 100644 --- a/actix-api/src/main.rs +++ b/actix-api/src/main.rs @@ -1,142 +1,5 @@ use actix_api::{get_app, ACTIX_PORT}; -use crate::auth::{ - fetch_oauth_request, generate_and_store_oauth_request, request_token, upsert_user, -}; -use crate::{ - auth::AuthRequest, - db::{get_pool, PostgresPool}, -}; -use reqwest::header::LOCATION; -use types::HelloResponse; - -const OAUTH_CLIENT_ID: &str = std::env!("OAUTH_CLIENT_ID"); -const OAUTH_AUTH_URL: &str = std::env!("OAUTH_AUTH_URL"); -const OAUTH_TOKEN_URL: &str = std::env!("OAUTH_TOKEN_URL"); -const OAUTH_SECRET: &str = std::env!("OAUTH_CLIENT_SECRET"); -const OAUTH_REDIRECT_URL: &str = std::env!("OAUTH_REDIRECT_URL"); -const SCOPE: &str = "email%20profile%20openid"; -const ACTIX_PORT: &str = std::env!("ACTIX_PORT"); -const UI_PORT: &str = std::env!("TRUNK_SERVE_PORT"); -const UI_HOST: &str = std::env!("TRUNK_SERVE_HOST"); -const AFTER_LOGIN_URL: &str = concat!("http://localhost:", std::env!("TRUNK_SERVE_PORT")); - -pub mod auth; -pub mod db; - -/** - * Function used by the Web Application to initiate OAuth. - * - * The server responds with the OAuth login URL. - * - * The server implements PKCE (Proof Key for Code Exchange) to protect itself and the users. - */ -#[get("/login")] -async fn login(pool: web::Data) -> Result { - // TODO: verify if user exists in the db by looking at the session cookie, (if the client provides one.) - let pool2 = pool.clone(); - - // 2. Generate and Store OAuth Request. - let (csrf_token, pkce_challenge) = { - let pool = pool2.clone(); - generate_and_store_oauth_request(pool).await - } - .map_err(|e| { - log::error!("{:?}", e); - error::ErrorInternalServerError(e) - })?; - - // 3. Craft OAuth Login URL - let oauth_login_url = format!("{oauth_url}?client_id={client_id}&redirect_uri={redirect_url}&response_type=code&scope={scope}&prompt=select_account&pkce_challenge={pkce_challenge}&state={state}&access_type=offline", - oauth_url=OAUTH_AUTH_URL, - redirect_url=OAUTH_REDIRECT_URL, - client_id=OAUTH_CLIENT_ID, - scope=SCOPE, - pkce_challenge=pkce_challenge.as_str(), - state=&csrf_token.secret() - ); - - // 4. Redirect the browser to the OAuth Login URL. - let mut response = HttpResponse::Found(); - response.append_header((LOCATION, oauth_login_url)); - Ok(response.finish()) -} - -/** - * Handle OAuth callback from Web App. - * - * This service is responsible for using the provided authentication code to fetch - * the OAuth access_token and refresh token. - * - * It upserts the user using their email and stores the access_token & refresh_code. - */ -#[get("/login/callback")] -async fn handle_google_oauth_callback( - pool: web::Data, - info: web::Query, -) -> Result { - let state = info.state.clone(); - - // 1. Fetch OAuth request, if this fails, probably a hacker is trying to p*wn us. - let oauth_request = { - let pool = pool.clone(); - fetch_oauth_request(pool, state).await - } - .map_err(|e| { - log::error!("{:?}", e); - error::ErrorBadRequest("couldn't find a request, are you a hacker?") - })?; - - // 2. Request token from OAuth provider. - let (oauth_response, claims) = request_token( - OAUTH_REDIRECT_URL, - OAUTH_CLIENT_ID, - OAUTH_SECRET, - &oauth_request.pkce_verifier, - OAUTH_TOKEN_URL, - &info.code, - ) - .await - .map_err(|err| { - log::error!("{:?}", err); - error::ErrorBadRequest("couldn't find a request, are you a hacker?") - })?; - - // 3. Store tokens and create user. - { - let claims = claims.clone(); - upsert_user(pool, &claims, &oauth_response).await - } - .map_err(|err| { - log::error!("{:?}", err); - error::ErrorInternalServerError(err) - })?; - - // 4. Create session cookie with email. - let cookie = Cookie::build("email", claims.email) - .path("/") - .same_site(SameSite::Lax) - // Session lasts only 360 secs to test cookie expiration. - .expires(OffsetDateTime::now_utc().checked_add(Duration::seconds(360))) - .finish(); - - // 5. Send cookie and redirect browser to AFTER_LOGIN_URL - let mut response = HttpResponse::Found(); - response.append_header((LOCATION, AFTER_LOGIN_URL)); - response.cookie(cookie); - Ok(response.finish()) -} - -/** - * Sample service - */ -#[get("/hello/{name}")] -async fn greet(name: web::Path) -> Json { - Json(HelloResponse { - name: name.to_string(), - }) -} - #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init(); From 8b0197782c3791978717adc6bec2fd55d6abe6e1 Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Fri, 24 Nov 2023 22:33:21 -0500 Subject: [PATCH 08/10] get tests to pass --- actix-api/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/actix-api/src/main.rs b/actix-api/src/main.rs index 87436a3..113bf19 100644 --- a/actix-api/src/main.rs +++ b/actix-api/src/main.rs @@ -1,4 +1,5 @@ use actix_api::{get_app, ACTIX_PORT}; +use actix_web::HttpServer; #[actix_web::main] async fn main() -> std::io::Result<()> { From a2b9cd315a77d8cc4fa431f3e3e7c818498d938e Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Fri, 24 Nov 2023 22:47:38 -0500 Subject: [PATCH 09/10] got tests to pass --- actix-api/tests/server.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/actix-api/tests/server.rs b/actix-api/tests/server.rs index d901a55..9c990d3 100644 --- a/actix-api/tests/server.rs +++ b/actix-api/tests/server.rs @@ -2,6 +2,7 @@ mod common; use actix_api::get_app; use actix_web::test; +use types::HelloResponse; /// Test login inserts pkce_challenge, pkce_verifier, csrf_state /// And returns a login url with the pkce_challenge /// @@ -12,8 +13,17 @@ async fn test_login() { println!("DB_URL: {}", db_url); common::dbmate_up(&db_url); let mut app = test::init_service(get_app()).await; - let req = test::TestRequest::get().uri("/login").to_request(); + let req = test::TestRequest::get().uri("/hello/dario").to_request(); let resp = test::call_service(&mut app, req).await; - drop(app); - assert!(resp.status() == 302); + assert_eq!(resp.status(), 200); + // Get body as json + let body = test::read_body(resp).await; + let body = String::from_utf8(body.to_vec()).unwrap(); + let body: HelloResponse = serde_json::from_str(&body).unwrap(); + assert_eq!( + body, + HelloResponse { + name: "dario".to_string() + } + ); } From 1cbe15fc53f0b414c4fcdb448b6bb134ca1d7631 Mon Sep 17 00:00:00 2001 From: Dario Lencina Date: Fri, 24 Nov 2023 22:56:05 -0500 Subject: [PATCH 10/10] added changes --- CHANGES.md | 6 ++++++ VERSION | 2 +- actix-api/Cargo.lock | 4 ++-- actix-api/Cargo.toml | 2 +- types/Cargo.toml | 2 +- yew-ui/Cargo.lock | 4 ++-- yew-ui/Cargo.toml | 2 +- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f0df053..1a8eace 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ # Changelog All notable changes to this project will be documented in this file. +## [1.4.0] - Unreleased +## Changed +- Added integration tests for the backend. This change ensures that the backend is tested in a more realistic environment, providing more confidence in the application's functionality. + +(PR [#28](https://github.com/security-union/yew-actix-template/pull/25)) + ## [1.3.0] - 2023-11-24 ### Changed - Added versioning and changelog. diff --git a/VERSION b/VERSION index 589268e..e21e727 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0 \ No newline at end of file +1.4.0 \ No newline at end of file diff --git a/actix-api/Cargo.lock b/actix-api/Cargo.lock index 9cf4e78..a1c06b8 100644 --- a/actix-api/Cargo.lock +++ b/actix-api/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ [[package]] name = "actix-api" -version = "1.3.0" +version = "1.4.0" dependencies = [ "actix", "actix-cors", @@ -2539,7 +2539,7 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "types" -version = "1.3.0" +version = "1.4.0" dependencies = [ "serde", "serde_json", diff --git a/actix-api/Cargo.toml b/actix-api/Cargo.toml index 8e1a5fe..7f3ea74 100644 --- a/actix-api/Cargo.toml +++ b/actix-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-api" -version = "1.3.0" +version = "1.4.0" edition = "2021" repository = "https://github.com/security-union/yew-actix-template.git" description = "Actix-web backend" diff --git a/types/Cargo.toml b/types/Cargo.toml index c7ac44d..8996b02 100644 --- a/types/Cargo.toml +++ b/types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "types" -version = "1.3.0" +version = "1.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/yew-ui/Cargo.lock b/yew-ui/Cargo.lock index 5a00f5a..a843bad 100644 --- a/yew-ui/Cargo.lock +++ b/yew-ui/Cargo.lock @@ -1076,7 +1076,7 @@ dependencies = [ [[package]] name = "types" -version = "1.3.0" +version = "1.4.0" dependencies = [ "serde", "serde_json", @@ -1271,7 +1271,7 @@ dependencies = [ [[package]] name = "yew-ui" -version = "1.3.0" +version = "1.4.0" dependencies = [ "const_format", "gloo-console 0.2.3", diff --git a/yew-ui/Cargo.toml b/yew-ui/Cargo.toml index 2045b45..e5fe058 100644 --- a/yew-ui/Cargo.toml +++ b/yew-ui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yew-ui" -version = "1.3.0" +version = "1.4.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html