Skip to content

Commit

Permalink
Add API test for /login endpoint (#25)
Browse files Browse the repository at this point in the history
* Some refactor for testing

* Progress mofo

* Save

* Passing basic test

* Clippy fixes

* fmt

* move main to lib

* get tests to pass

* got tests to pass

* added changes

---------

Co-authored-by: Dario A Lencina-Talarico <darioalessandrolencina@gmail.com>
Co-authored-by: Dario Lencina <dario@securityunion.dev>
  • Loading branch information
3 people committed Nov 25, 2023
1 parent a0ccf4a commit 93bb8dd
Show file tree
Hide file tree
Showing 15 changed files with 269 additions and 198 deletions.
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -21,7 +24,8 @@ 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"
docker compose -f docker/docker-compose.yaml run actix-api bash -c "cd app/actix-api && cargo clippy --all -- --deny warnings && cargo fmt --check"

2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.0
1.4.0
4 changes: 2 additions & 2 deletions actix-api/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion actix-api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
180 changes: 180 additions & 0 deletions actix-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
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,
};

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<PostgresPool>) -> Result<HttpResponse, Error> {
// 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<PostgresPool>,
info: web::Query<AuthRequest>,
) -> Result<HttpResponse, Error> {
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<String>) -> Json<HelloResponse> {
Json(HelloResponse {
name: name.to_string(),
})
}

pub fn get_app() -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<EitherBody<BoxBody>>,
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)
}
Loading

0 comments on commit 93bb8dd

Please sign in to comment.