From cb9db1f3ba9f19754437cceac9b5af9878c3f027 Mon Sep 17 00:00:00 2001 From: Ben Sherriff Date: Tue, 21 Nov 2023 08:01:32 -0500 Subject: [PATCH] Updated auth to use pem keys instead of base64 keys in strings? --- .env.TEMPLATE | 16 +++------- .gitignore | 2 ++ Makefile | 14 ++++---- README.md | 9 +++++- docker-compose.yml | 4 ++- service/.env.TEMPLATE | 8 ++--- service/Cargo.lock | 1 - service/Cargo.toml | 1 - service/Dockerfile | 16 ++++++++-- service/Makefile | 7 ++++ service/README.md | 62 ------------------------------------ service/docker-compose.yml | 2 ++ service/src/auth/mod.rs | 29 +++++++---------- service/src/auth/model.rs | 4 +-- service/src/auth/routes.rs | 15 +++++---- service/src/error_handler.rs | 12 +++++++ ui/docker-compose.yml | 4 +-- 17 files changed, 86 insertions(+), 120 deletions(-) delete mode 100644 service/README.md diff --git a/.env.TEMPLATE b/.env.TEMPLATE index 4f8b783..7475297 100644 --- a/.env.TEMPLATE +++ b/.env.TEMPLATE @@ -1,31 +1,25 @@ RUST_LOG=warn,service=info -DATABASE_CONTAINER=weather-service - DATABASE_USER=weather DATABASE_PASSWORD= DATABASE_NAME=weather -DATABASE_HOST=localhost +DATABASE_HOST=db DATABASE_PORT=5432 -REDIS_HOST=localhost +REDIS_HOST=redis REDIS_PORT=6379 -MINIO_ROOT_USER=siren +MINIO_ROOT_USER=weather MINIO_ROOT_PASSWORD= MINIO_HOST=localhost MINIO_PORT=9000 MINIO_PORT_INTERNAL=9001 -SERVICE_HOST=localhost +SERVICE_HOST=service SERVICE_PORT=5000 -ACCESS_TOKEN_PRIVATE_KEY= -ACCESS_TOKEN_PUBLIC_KEY= +KEYS_DIR_PATH= ACCESS_TOKEN_MAXAGE=5 - -REFRESH_TOKEN_PRIVATE_KEY= -REFRESH_TOKEN_PUBLIC_KEY= REFRESH_TOKEN_MAXAGE=30 GOV_API_URL=https://aviationweather.gov/cgi-bin/data diff --git a/.gitignore b/.gitignore index 45eb45a..df08952 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ node_modules # misc .DS_Store *.pem +*.pem.pub +keys/ # debug npm-debug.log* diff --git a/Makefile b/Makefile index 9b0fdc6..5f5a113 100644 --- a/Makefile +++ b/Makefile @@ -20,9 +20,6 @@ up: ## Start Docker containers down: ## Stop Docker containers docker compose down -connect: ## Connect to the Weather DB - docker exec -it ${DATABASE_CONTAINER} psql -U postgres - clean: ## Cleanup Docker containers docker compose down && \ docker image rm weather-ui || \ @@ -30,8 +27,9 @@ clean: ## Cleanup Docker containers docker network rm weather-frontend || \ docker network rm weather-backend -clean-db: ## Remove database - docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"' - docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "CREATE DATABASE \"${DATABASE_NAME}\";"' || true - - \ No newline at end of file +generate: ## Generate RSA keys + mkdir keys + openssl genrsa -out keys/access_private_key.pem 4096 + openssl rsa -in keys/access_private_key.pem -pubout -outform PEM -out keys/access_public_key.pem + openssl genrsa -out keys/refresh_private_key.pem 4096 + openssl rsa -in keys/refresh_private_key.pem -pubout -outform PEM -out keys/refresh_public_key.pem diff --git a/README.md b/README.md index ae7b2b4..c88f2c4 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ # Aviation Weather ## Makefile -`make help` to list all commands \ No newline at end of file +`make help` to list all commands + +## Setup + +1. Copy `.env.TEMPLATE` to `.env` +2. Generate JWT RS256 (RSA Signature with SHA-256) Private/Public keys with `make generate` +3. Build the service and ui images with `make build` +4. Run the application with `make up` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 67468ef..cd7f34f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: minio: image: minio/minio - container_name: siren-minio + container_name: weather-minio environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} @@ -49,6 +49,8 @@ services: container_name: weather-service env_file: - .env + volumes: + - ${KEYS_DIR_PATH}:/keys ports: - "${SERVICE_PORT:-5000}:5000" build: diff --git a/service/.env.TEMPLATE b/service/.env.TEMPLATE index 2391b8c..c1013dc 100644 --- a/service/.env.TEMPLATE +++ b/service/.env.TEMPLATE @@ -11,7 +11,7 @@ DATABASE_PORT=5432 REDIS_HOST=localhost REDIS_PORT=6379 -MINIO_ROOT_USER=siren +MINIO_ROOT_USER=weather MINIO_ROOT_PASSWORD=7LtSkxU15ix40nu MINIO_HOST=localhost MINIO_PORT=9000 @@ -20,12 +20,8 @@ MINIO_PORT_INTERNAL=9001 SERVICE_HOST=localhost SERVICE_PORT=5000 -ACCESS_TOKEN_PRIVATE_KEY= -ACCESS_TOKEN_PUBLIC_KEY= +KEYS_DIR_PATH= ACCESS_TOKEN_MAXAGE=5 - -REFRESH_TOKEN_PRIVATE_KEY= -REFRESH_TOKEN_PUBLIC_KEY= REFRESH_TOKEN_MAXAGE=30 GOV_API_URL=https://aviationweather.gov/cgi-bin/data diff --git a/service/Cargo.lock b/service/Cargo.lock index 461ef08..78b9968 100644 --- a/service/Cargo.lock +++ b/service/Cargo.lock @@ -1805,7 +1805,6 @@ dependencies = [ "actix-web", "actix-web-httpauth", "argon2", - "base64", "chrono", "diesel", "diesel_migrations", diff --git a/service/Cargo.toml b/service/Cargo.toml index 34399e3..385bf54 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -32,5 +32,4 @@ log = "0.4.20" argon2 = "0.5.2" jsonwebtoken = "9.0.0" redis = { version = "0.23.3", features = ["tokio-comp", "connection-manager", "r2d2"] } -base64 = "0.21.4" rustix = "0.38.19" # https://github.com/imsnif/bandwhich/issues/284 diff --git a/service/Dockerfile b/service/Dockerfile index 0b06a41..7aba9cf 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -11,14 +11,26 @@ COPY Cargo.toml ./ RUN apt-get update && apt-get install -y cmake RUN cargo build --release +# ====== +# Keys +# ====== +FROM debian:bookworm-slim as keys +WORKDIR /keys + +RUN apt-get update && apt-get install -y openssl libpq-dev +RUN openssl genrsa -out access.pem 4096 +RUN openssl rsa -in access.pem -pubout -outform PEM -out access.pem.pub +RUN openssl genrsa -out refresh.pem 4096 +RUN openssl rsa -in refresh.pem -pubout -outform PEM -out refresh.pem.pub + # ========= # Runtime # ========= -FROM debian:bookworm-slim as runtime +FROM keys as runtime WORKDIR /service USER root COPY --from=builder /builder/target/release/service /usr/local/bin/service -COPY --from=packages /packages /usr/bin +COPY --from=keys /keys /keys CMD ["service"] diff --git a/service/Makefile b/service/Makefile index 1506881..1051e48 100644 --- a/service/Makefile +++ b/service/Makefile @@ -16,6 +16,7 @@ build: ## Build the Docker image utils: ## Start the utils docker compose up -d db docker compose up -d redis + docker compose up -d minio up: ## Start the Docker containers docker compose up -d @@ -36,4 +37,10 @@ clean-db: ## Remove database docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"' docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "CREATE DATABASE \"${DATABASE_NAME}\";"' || true +generate: ## Generate RSA keys + mkdir ../keys/ + openssl genrsa -out ../keys/access_private_key.pem 4096 + openssl rsa -in ../keys/access_private_key.pem -pubout -outform PEM -out ../keys/access_public_key.pem + openssl genrsa -out ../keys/refresh_private_key.pem 4096 + openssl rsa -in ../keys/refresh_private_key.pem -pubout -outform PEM -out ../keys/refresh_public_key.pem \ No newline at end of file diff --git a/service/README.md b/service/README.md deleted file mode 100644 index 7858e09..0000000 --- a/service/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Aviation Weather - -## UI - -## Service - -## REST API -The REST API for the weather service is described below. - -### Import Airports ---- -#### Request -`GET /import` - -#### Response -``` -``` - -### Get Airports ---- -#### Request -`GET /airports` - -#### Response -``` -``` - -### Get Airport ---- -#### Request -`GET /airports/{icao}` - -#### Response -``` -``` - -### Create Airport ---- -#### Request -`CREATE /airports` - -#### Response -``` -``` - -### Update Airport ---- -#### Request -`PUT /airports/{icao}` - -#### Response -``` -``` - -### Delete Airport ---- -#### Request -`DELETE /airports/{icao}` - -#### Response -``` -``` diff --git a/service/docker-compose.yml b/service/docker-compose.yml index b347a9c..9615867 100644 --- a/service/docker-compose.yml +++ b/service/docker-compose.yml @@ -54,6 +54,8 @@ services: REDIS_PORT: 6379 SERVICE_HOST: service SERVICE_PORT: 5000 + volumes: + - ${KEYS_DIR_PATH}:/keys ports: - "${SERVICE_PORT:-5000}:5000" build: diff --git a/service/src/auth/mod.rs b/service/src/auth/mod.rs index ee6f8fe..9c56134 100644 --- a/service/src/auth/mod.rs +++ b/service/src/auth/mod.rs @@ -1,7 +1,6 @@ use std::env; use argon2::{password_hash::{rand_core::OsRng, PasswordHasher, PasswordVerifier, SaltString, Error as HashError}, Argon2, PasswordHash}; -use base64::{engine::general_purpose, Engine as _}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm}; use serde::{Deserialize, Serialize}; @@ -31,9 +30,7 @@ pub struct TokenDetails { } pub fn verify_token(token: &str, public_key: &str) -> Result { - let bytes_public_key = general_purpose::STANDARD.decode(public_key).unwrap(); - let decoded_public_key = String::from_utf8(bytes_public_key).unwrap(); - let key = DecodingKey::from_rsa_pem(decoded_public_key.as_bytes())?; + let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?; let validation = Validation::new(Algorithm::RS256); let decoded = decode::(token, &key, &validation)?; let email = decoded.claims.sub; @@ -43,21 +40,21 @@ pub fn verify_token(token: &str, public_key: &str) -> Result Result { let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE") - .expect("ACCESS_TOKEN_MAXAGE must be set") - .parse::() - .expect("ACCESS_TOKEN_MAXAGE must be an integer"); - let access_private_key = env::var("ACCESS_TOKEN_PRIVATE_KEY") - .expect("ACCESS_TOKEN_PRIVATE_KEY must be set"); + .expect("ACCESS_TOKEN_MAXAGE must be set") + .parse::() + .expect("ACCESS_TOKEN_MAXAGE must be an integer"); + let keys_dir = env::var("KEYS_DIR_PATH")?; + let access_private_key = std::fs::read_to_string(format!("{}/access_private_key.pem", keys_dir))?; generate_token(&email, access_token_max_age, &access_private_key) } pub fn generate_refresh_token(email: &str) -> Result { let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE") - .expect("REFRESH_TOKEN_MAXAGE must be set") - .parse::() - .expect("REFRESH_TOKEN_MAXAGE must be an integer"); - let refresh_private_key = env::var("REFRESH_TOKEN_PRIVATE_KEY") - .expect("REFRESH_TOKEN_PRIVATE_KEY must be set"); + .expect("REFRESH_TOKEN_MAXAGE must be set") + .parse::() + .expect("REFRESH_TOKEN_MAXAGE must be an integer"); + let keys_dir = env::var("KEYS_DIR_PATH")?; + let refresh_private_key = std::fs::read_to_string(format!("{}/refresh_private_key.pem", keys_dir))?; generate_token(&email, refresh_token_max_age, &refresh_private_key) } @@ -78,9 +75,7 @@ pub fn generate_token(email: &str, ttl: i64, private_key: &str) -> Result token_details, diff --git a/service/src/auth/routes.rs b/service/src/auth/routes.rs index 1563df1..03630ec 100644 --- a/service/src/auth/routes.rs +++ b/service/src/auth/routes.rs @@ -150,8 +150,9 @@ async fn refresh(req: HttpRequest) -> HttpResponse { }) }; - let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY") - .expect("REFRESH_TOKEN_PUBLIC_KEY must be set"); + let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); + let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir)) + .expect("Unable to read refresh public key"); let refresh_token_details = match verify_token(&refresh_token, &public_key) { Ok(token_details) => token_details, Err(err) => return ResponseError::error_response(&err) @@ -181,8 +182,9 @@ async fn refresh(req: HttpRequest) -> HttpResponse { match req.cookie("access_token") { Some(cookie) => { let access_token = cookie.value().to_string(); - let public_key = env::var("ACCESS_TOKEN_PUBLIC_KEY") - .expect("ACCESS_TOKEN_PUBLIC_KEY must be set"); + let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); + let public_key = std::fs::read_to_string(format!("{}/access_public_key.pem", keys_dir)) + .expect("Unable to read access public key"); match verify_token(&access_token, &public_key) { Ok(token_details) => { let _: redis::RedisResult<()> = conn.del(token_details.token_uuid.to_string()).await; @@ -285,8 +287,9 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { message: "Refresh token not found".to_string() }) }; - let public_key = env::var("REFRESH_TOKEN_PUBLIC_KEY") - .expect("REFRESH_TOKEN_PUBLIC_KEY must be set"); + let keys_dir = env::var("KEYS_DIR_PATH").expect("KEYS_DIR_PATH must be set"); + let public_key = std::fs::read_to_string(format!("{}/refresh_public_key.pem", keys_dir)) + .expect("Unable to read refresh public key"); let refresh_token_details = match verify_token(&refresh_token, &public_key) { Ok(token_details) => token_details, Err(err) => return ResponseError::error_response(&err) diff --git a/service/src/error_handler.rs b/service/src/error_handler.rs index 53648ed..aae201c 100644 --- a/service/src/error_handler.rs +++ b/service/src/error_handler.rs @@ -38,6 +38,18 @@ impl fmt::Display for ServiceError { } } +impl From for ServiceError { + fn from(error: std::io::Error) -> ServiceError { + ServiceError::new(500, format!("Unknown IO error: {}", error)) + } +} + +impl From for ServiceError { + fn from(error: std::env::VarError) -> ServiceError { + ServiceError::new(500, format!("Unknown environment variable error: {}", error)) + } +} + impl From for ServiceError { fn from(error: DieselError) -> ServiceError { match error { diff --git a/ui/docker-compose.yml b/ui/docker-compose.yml index 66b6ff8..672d5a7 100644 --- a/ui/docker-compose.yml +++ b/ui/docker-compose.yml @@ -19,8 +19,8 @@ services: - ./public:/app/public - ./styles:/app/styles networks: - - weather-frontend + - frontend restart: unless-stopped networks: - weather-frontend: + frontend: