Building a Secure Authentication Server in Rust

Authentication is a critical component of most applications. As a developer, properly implementing auth ensures your users‘ data stays protected.

In this comprehensive guide, you‘ll learn how to build a secure authentication server in Rust using proven standards like JSON Web Tokens (JWT).

Why Authentication Matters

Without authentication, your app‘s data and functionality are exposed to anyone. Some key reasons properly implementing auth is important:

  • Security: Stops unauthorized access to sensitive user data.
  • Access control: Enforce fine-grained controls over who can access what.
  • Account ownership: Confirms a user owns an account.
  • Improved UX: Saves user data across sessions.

An Overview of JWT-Based Authentication

JWT (pronounced "jot") is an open standard that defines a secure way to transmit information between parties via digital signatures.

Here‘s a high-level overview:

  1. Authentication: User logs in with credentials
  2. JWT Generation: Server generates a JWT to identify authenticated sessions
  3. Sending JWTs: Tokens are included by client app in requests
  4. Verification: Server decrypts and verifies JWT signature to authenticate each request

The tokens are stored client-side and contain user data encoded into them.

JWTs provide key advantages like avoiding sessions stored on servers and scalability across infrastructure.

Project Setup

With the basics covered, let‘s setup a new Rust server app using:

  • Warp: High performance web server framework
  • [Other dependencies like bcrypt, jsonwebtoken, etc]
// Importing crates
use warp::Filter;

// HTTP GET route handler 
async fn get_posts() -> impl warp::Reply {
    "Fetching all posts..."  
}

#[tokio::main]
async fn main() { 
  // GET /posts route
  let posts_route = warp::path("posts")
      .and(warp::get())
      .and_then(get_posts);

  // Start server 
  warp::serve(posts_route)
      .run(([127, 0, 0, 1], 3030))  
      .await
}

This gives us a basic API that handles GET requests for /posts. Let‘s build upon it to implement authentication.

User Structs and Serialization

Our app needs to define user accounts with attributes like emails, passwords etc that can be persisted. For this, we can define a User struct and implement serialization with serde:

use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]  
struct User {
    id: i32,
    email: String,        
    password: String,
    created_at: DateTime<Utc>
}

Serializing it allows converting user data to formats like JSON for storage or transmission.

For securely storing user info like emails and password hashes, we‘ll integrate a PostgreSQL database later using Diesel ORM.

Hashing Passwords

To protect user passwords before persisting them, we should hash them using bcrypt:

use bcrypt::{hash, verify, DEFAULT_COST};

// Hash plain-text password 
let hashed = hash("my_password", DEFAULT_COST)?;   

// Verify correct password
let valid = verify("my_password", &hashed)?; 

// Won‘t match incorrect password  
let invalid = verify("wrong", &hashed)?;

bcrypt allows secure password hashing and salting to maximize security.

User Registration with Hashing

When a new user signs up, we need to hash their password and store the account details.

Our register endpoint can handle this:

use crate::models::User;
use crate::utils::hash_password;

#[post("/signup")]
async fn signup(user_data: Json<UserInput>) -> impl Reply {

    // Hash plain-text password
    let hashed = hash_password(user_data.password);              

    // Create user model
    let new_user = User {
        email: user.email,
        password: hashed,
    };

    // Insert account into DB
    insert_user(&new_user); 

    // 201 Created 
    StatusCode::CREATED  
} 

Only the hashed password gets stored, maximizing protection!

JWT Generation

Upon login with valid credentials, we want to generate a signed JWT so clients can use it for authenticated requests later.

use jsonwebtoken::{encode, Header, EncodingKey};

fn generate_token(user: &User) -> Result<String> {

    let iss = "my_issuer";

    // JWT claims 
    let mut claims = Claims::new();
    claims.insert("sub".to_owned(), user.id);   
    claims.insert("email".to_owned(), user.email);

    // Expiration
    let exp = Utc::now()
        .checked_add(Duration::hours(1))
        .unwrap()
        .timestamp();

    // JWT header   
    let header = Header::new(Algorithm::HS256);  

    // Generate signed JWT
    encode(header, &claims, &EncodingKey::from_secret("secret")) 
}

Breaking it down:

  • Construct claims including user ID
  • Set expiration like 1hr from now
  • Pass header & claims to encode()
  • Sign the JWT with a secret key

We return this upon login success for apps.

JWT Verification Middleware

To authenticate requests using JWTs, we need middleware that:

  1. Extract tokens from requests
  2. Decode and validate their signature
  3. Extract user data like identifiers
  4. Attach to the request before routes execute

Here is an example VerifyJWT middleware:

use jsonwebtoken::{decode, Validation, Algorithm};
use warp::{http::StatusCode, Filter, Rejection};

// Rejection type if token missing  
struct MissingToken; 

async fn verify_jwt(token: String) -> Result<Claims, Rejection> {
   let secret_key = SECRET.clone();

   // Decode and validate   
   let token_data = decode::<Claims>(
        &token,
        &DecodingKey::from_secret(secret_key),
        &Validation::new(Algorithm::HS256),
    )?;

   Ok(token_data.claims)    
}

// Attach user data to filters  
fn with_user_data(filters: impl Filter + Clone) -> BoxedFilter<(Claims,)> {
    let auth = warp::any()
        .map(verify_jwt)
        .untuple_one();

    filters.and(auth).boxed()
}    

pub fn routes(
 user_routes
    .and(with_user_data(user_routes)) 
    .recover(handle_missing_token)
)

Now all requests must have valid tokens, propagating user data to subsequent filters! Custom error handling like above allows handling cases like missing tokens.

This protects all routes and data under /users/*.

Role-Based Authorization

Beyond simple authentication, we often want fine-grained control over what users can access.

For example, only admin users may access reports or analytics:

fn can_view_reports(user: &Claims) -> bool {
  user.role == "admin"
}

pub fn reports_route() -> BoxedFilter<(Claims,)> {
    let view_reports = warp::any()
        .and(with_user_data(verify_role(can_view_reports)));

    // GET /reports route        
    warp::path("reports")
        .and(warp::get())
        .and(view_reports)
        .boxed()  
}

The custom verify_role() middleware checks the user‘s role before allowing access.

Handling Expired JWTs

Since JWT expiration times are short-lived, we need a way to allow clients to refresh tokens without logging in again.

The solution is refresh tokens – long-lived tokens only used to refresh soon-to-expire access tokens.

#[post("/login", data = "<credentials>")]
async fn login(credentials: LoginForm) -> impl Reply {

  // Verify and get user 
  let user = verify_credentials(credentials)?;  

  // Generate access + refresh JWT    
  let access_token = generate_access_token(&user);
  let refresh_token = generate_refresh_token(&user);

  // Send tokens
}  

#[post("/refresh")]  
async fn refresh(token: String) {

  // Decode refresh token
  let token_data = decode_token(&token)?;

  // Issue new access token
  let new_access_token = generate_access_token(&token_data.user); 

  // Send back 
}

The access tokens can be short-lived (15 minutes) while refreshes grant new access without re-auth for longer periods before itself expiring (30 days).

Recap

That wraps up a high-level overview building a secure authentication server in Rust using JWT and common techniques:

  • Hashed password storage
  • JWT generation upon login
  • Verification middleware protecting routes
  • Role-based authorization restricting access
  • Refresh token flows for renewed access

With these foundations, you can implement robust auth meeting production standards.

Let me know in the comments what authentication topics you want covered deeper!

Similar Posts