Skip to content

The Authentification Module

ValleTrapiellaMatiasUO300652 edited this page Mar 19, 2026 · 2 revisions

The Authentication Module

Description of the Microservice

| yovi_en2a/authService

The purpose of this microservice is to handle all user identity operations: registration and login. It exposes a small HTTP API (port 4001) that the API Gateway proxies under the /api prefix. It connects to Firestore for persistent user storage and uses Argon2 for password hashing.


user_data.rs — Data Structures

user_data.rs is the data layer of the module. It defines the structures that travel between the API, the business logic, and Firestore, as well as the shared DBData trait.

DBData trait

DBData is a marker trait that bundles the constraints required to work with Firestore's Fluent API:

pub trait DBData: Serialize + for<'de> Deserialize<'de> + Debug + Send + Sync {}

Any struct that implements DBData can be passed to the generic read_db and insert_db functions in firebase.rs. This keeps the database layer fully generic — it never needs to know the concrete type it is reading or writing.

User

The persisted representation of a user in Firestore. The email is used as the document ID, so it doubles as the unique key.

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub email: String,
    pub username: String,
    pub password_hash: String,  // Argon2 hash — the raw password is never stored
}
impl DBData for User {}

API Request / Response models

Struct Direction Fields
RegisterRequest Inbound (POST /register) email, username, password
LoginRequest Inbound (POST /login) email, password
LoginResponse Outbound username, email, message

firebase.rs — Firestore Connection

firebase.rs provides two generic read/write helpers and a pair of concrete shorthands for the User collection. Its design mirrors the GameManager's Firebase module.

Crypto provider setup

The firestore crate uses rustls for TLS. Since Rust 1.x, rustls no longer ships with a default crypto provider, so one must be installed at startup. A Once guard ensures this happens exactly once, no matter how many times get_connection() is called concurrently:

static INIT_CRYPTO: Once = Once::new();
 
INIT_CRYPTO.call_once(|| {
    let provider = rustls::crypto::ring::default_provider();
    let _ = provider.install_default();
});

Why Ring? See the GameManager docs for the full rationale. In short: it is the most mature Rust crypto library, is optimized at the assembly level for common CPU architectures, supports only secure algorithms, and compiles reliably across Linux (Docker) and Windows.

read_db<T>

Fetches a single document from a Firestore collection and deserializes it into any type that implements DBData.

pub async fn read_db<T>(table_name: &str, id: &str) -> Result<T, Box<dyn Error>>
where
    for<'de> T: DBData

Internally it opens a connection, queries using the Fluent API, and maps the Option<T> result into a Result, returning an error if the document does not exist:

let object: Option<T> = db.fluent()
    .select()
    .by_id_in(table_name)
    .obj()
    .one(id)
    .await?;
 
match object {
    Some(data) => Ok(data),
    None => Err(format!("Document with ID {} not found in {}", id, table_name).into()),
}

insert_db<T>

Inserts a document and then immediately reads it back to verify the write was durable. If the post-insertion read fails, the error is propagated to the caller.

db.fluent()
    .insert()
    .into(table_name)
    .document_id(id)
    .object(data)
    .execute::<()>()
    .await?;
 
read_db::<T>(table_name, id).await?;  // Verify the write succeeded

Concrete shorthands

// Read a user by email (used as document ID)
pub async fn get_user_by_id(id: &str) -> Result<User, Box<dyn Error>>
 
// Insert a user into the "Users" collection
pub async fn insert_user_by_id(id: &str, user_data: &User) -> Result<(), Box<dyn Error>>

auth_utils.rs — Password Hashing

auth_utils.rs contains the two cryptographic helpers used by the registration and login flows. Both are thin wrappers around the argon2 crate.

hash_password

Generates a random salt via OsRng (the OS cryptographic random source) and produces an Argon2 hash string that encodes the algorithm parameters, salt, and digest in a single portable string:

pub fn hash_password(password: &str) -> Result<String, Box<dyn Error>> {
    let salt = SaltString::generate(&mut OsRng);
    let hash = Argon2::default().hash_password(password.as_bytes(), &salt)?;
    Ok(hash.to_string())
}

The resulting string is what gets stored in Firestore — the raw password is never persisted anywhere.

verify_password

Parses the stored hash string back into a PasswordHash object and uses Argon2's built-in constant-time comparison to check the candidate password. Using constant-time comparison is important because a naive string comparison would leak timing information that could help an attacker brute-force credentials.

pub fn verify_password(password: &str, hash: &str) -> Result<bool, Box<dyn Error>> {
    let parsed_hash = PasswordHash::new(hash)?;
    Ok(Argon2::default()
        .verify_password(password.as_bytes(), &parsed_hash)
        .is_ok())
}

user_auth.rs — Business Logic

user_auth.rs contains the two core functions that implement the registration and login workflows. Both sit between the HTTP handlers and the database layer.

register_user

email, username, password
       │
       ▼
1. read_db("Users", email)  →  already exists?  →  Err("User already exists")
       │
       ▼
2. hash_password(password)
       │
       ▼
3. insert_db("Users", email, User { email, username, password_hash })
       │
       ▼
4. Ok(())

The email is used as both the login identifier and the Firestore document ID. Attempting to look up the document before inserting is the uniqueness check — if read_db returns Ok, the email is already taken.

if let Ok(_) = read_db::<User>("Users", email).await {
    return Err("User already exists".into());
}

login_user

email, password
       │
       ▼
1. read_db("Users", email)  →  not found?  →  Err (from read_db)
       │
       ▼
2. verify_password(password, user.password_hash)
       │
    ┌──┴──┐
  true   false
    │      │
  Ok(user) Err("Invalid password")

api_rest.rs — HTTP Server & Route Handlers

api_rest.rs defines the Axum HTTP server and the two route handlers. It is the entry point to the microservice and delegates all business logic to user_auth.rs.

Routes

Method Path Handler Description
POST /register register_handler Creates a new user account
POST /login login_handler Authenticates an existing user

The server binds to 0.0.0.0:4001. Using 0.0.0.0 instead of 127.0.0.1 is required for Docker so the port is reachable from outside the container.

register_handler

Deserializes the JSON body into a RegisterRequest and calls register_user. On success it returns HTTP 200 with a confirmation message; on failure it returns HTTP 400 with the error string.

match register_user(&payload.email, &payload.username, &payload.password).await {
    Ok(_)  => (StatusCode::OK,          Json(json!({ "message": "User registered successfully" }))).into_response(),
    Err(e) => (StatusCode::BAD_REQUEST,  Json(json!({ "error": e.to_string() }))).into_response(),
}

login_handler

Deserializes the body into a LoginRequest and calls login_user. On success it returns HTTP 200 with a LoginResponse containing username, email, and a welcome message; on failure it returns HTTP 401.

match login_user(&payload.email, &payload.password).await {
    Ok(user) => (StatusCode::OK,           Json(LoginResponse { ... })).into_response(),
    Err(e)   => (StatusCode::UNAUTHORIZED,  Json(json!({ "error": e.to_string() }))).into_response(),
}

users-service.js — API Gateway

users-service.js is a Node.js/Express service running on port 3000. It is the single entry point the frontend talks to. It handles CORS, CSRF protection, metrics, and Swagger docs, then proxies requests to the appropriate backend microservice.

Middleware stack (in order)

# Middleware Purpose
1 cookie-parser Parses incoming cookies into req.cookies
2 express-prom-bundle Exposes a /metrics endpoint for Prometheus scraping
3 Custom CORS handler Echoes back the origin header for allowed origins; handles OPTIONS preflight responses
4 swagger-ui-express Serves the OpenAPI spec at /api-docs
5 express.json() Parses JSON request bodies — must come before local POST routes

CSRF protection

The gateway implements the Double Submit Cookie pattern:

  1. GET /api/csrf-token generates a UUID token, sets it as an httpOnly cookie (csrf_token), and returns the same value in the JSON response body.
  2. The frontend stores the body value and attaches it to every mutating request as the X-CSRF-Token header.
  3. The verifyCsrf middleware (applied to the /api proxy) compares the cookie value against the header value. A mismatch returns HTTP 403.
const cookieToken = req.cookies.csrf_token;
const headerToken = req.headers['x-csrf-token'];
 
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
    return res.status(403).json({ error: 'Invalid or missing CSRF token' });
}

GET, OPTIONS, and HEAD requests are exempt from the check since they carry no side effects.

Proxy routes

Prefix Target CSRF required Path rewrite
/api Auth service (AUTH_URL, default :4001) /api/login/login
/game Game Manager (GAMEMANAGER_URL, default :5000) /game/bestTimes/bestTimes

fixRequestBody is applied to both proxies to rebuild the request body stream that express.json() already consumed, otherwise the downstream service would receive an empty body.

Environment variables

Variable Default Description
GAMEMANAGER_URL http://localhost:5000 URL of the GameManager microservice
AUTH_URL http://localhost:4001 URL of the Auth microservice
NODE_ENV Set to production to enable secure cookies and strict CORS

Clone this wiki locally