Updated auth to use pem keys instead of base64 keys in strings?

This commit is contained in:
2023-11-21 08:01:32 -05:00
parent 0dad6be93d
commit cb9db1f3ba
17 changed files with 86 additions and 120 deletions

View File

@@ -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

2
.gitignore vendored
View File

@@ -18,6 +18,8 @@ node_modules
# misc
.DS_Store
*.pem
*.pem.pub
keys/
# debug
npm-debug.log*

View File

@@ -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
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

View File

@@ -2,3 +2,10 @@
## Makefile
`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`

View File

@@ -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:

View File

@@ -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

1
service/Cargo.lock generated
View File

@@ -1805,7 +1805,6 @@ dependencies = [
"actix-web",
"actix-web-httpauth",
"argon2",
"base64",
"chrono",
"diesel",
"diesel_migrations",

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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
```
```

View File

@@ -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:

View File

@@ -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<TokenDetails, ServiceError> {
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::<TokenClaims>(token, &key, &validation)?;
let email = decoded.claims.sub;
@@ -43,21 +40,21 @@ pub fn verify_token(token: &str, public_key: &str) -> Result<TokenDetails, Servi
pub fn generate_access_token(email: &str) -> Result<TokenDetails, ServiceError> {
let access_token_max_age = env::var("ACCESS_TOKEN_MAXAGE")
.expect("ACCESS_TOKEN_MAXAGE must be set")
.parse::<i64>()
.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::<i64>()
.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<TokenDetails, ServiceError> {
let refresh_token_max_age = env::var("REFRESH_TOKEN_MAXAGE")
.expect("REFRESH_TOKEN_MAXAGE must be set")
.parse::<i64>()
.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::<i64>()
.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<TokenD
nbf: now.timestamp()
};
let header = Header::new(Algorithm::RS256);
let bytes_private_key = general_purpose::STANDARD.decode(private_key).unwrap();
let decoded_private_key = String::from_utf8(bytes_private_key).unwrap();
let key = EncodingKey::from_rsa_pem(decoded_private_key.as_bytes())?;
let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?;
let token = encode(&header, &claims, &key)?;
token_details.token = Some(token);
Ok(token_details)

View File

@@ -157,8 +157,8 @@ impl FromRequest for JwtAuth {
})))
};
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("Failed to read access public key");
let access_token_details = match verify_token(&access_token, &public_key) {
Ok(token_details) => token_details,

View File

@@ -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)

View File

@@ -38,6 +38,18 @@ impl fmt::Display for ServiceError {
}
}
impl From<std::io::Error> for ServiceError {
fn from(error: std::io::Error) -> ServiceError {
ServiceError::new(500, format!("Unknown IO error: {}", error))
}
}
impl From<std::env::VarError> for ServiceError {
fn from(error: std::env::VarError) -> ServiceError {
ServiceError::new(500, format!("Unknown environment variable error: {}", error))
}
}
impl From<DieselError> for ServiceError {
fn from(error: DieselError) -> ServiceError {
match error {

View File

@@ -19,8 +19,8 @@ services:
- ./public:/app/public
- ./styles:/app/styles
networks:
- weather-frontend
- frontend
restart: unless-stopped
networks:
weather-frontend:
frontend: