Skip to main content
Faculty Bot uses JWT (JSON Web Tokens) for stateless authentication with role-based access control. Tokens are stored in HTTP-only cookies and validated on every request.

User Structure

The User struct represents an authenticated user:
src/web/auth.rs
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
    id: u64,
    role: Roles,
}

Roles

Four role levels are available, each with increasing privileges:
src/web/auth.rs
#[derive(Debug, Serialize, Deserialize)]
pub enum Roles {
    Unprivileged,  // No access
    User,          // Basic authenticated user
    Moderator,     // Moderation privileges
    Admin,         // Full administrative access
}
Roles are stored as numeric values in JWT claims, allowing for hierarchical permission checks.

JWT Token Creation

Tokens are created using HMAC-SHA256 signing:
src/web/auth.rs
pub fn create_token(&self, role: Roles) -> String {
    let secret_key = std::env::var("SECRET_KEY")
        .expect("SECRET_KEY must be set");
    let key: Hmac<Sha256> = Hmac::new_from_slice(secret_key.as_bytes())
        .expect("HMAC can take key of any size");
    
    let mut claims = BTreeMap::new();
    claims.insert("id", self.id);
    claims.insert("exp", chrono::Utc::now().timestamp() as u64 + 86400); // 24h
    claims.insert("iat", chrono::Utc::now().timestamp() as u64);
    claims.insert("role", role as u64);

    claims.sign_with_key(&key).unwrap()
}

Token Claims

ClaimTypeDescription
idu64Discord user ID
roleu64User’s role level (0-3)
iatu64Issued at timestamp
expu64Expiration timestamp (24 hours)

Token Verification

Tokens are verified on every protected request:
src/web/auth.rs
pub fn verify_token(token: &str) -> bool {
    let secret_key = std::env::var("SECRET_KEY")
        .expect("SECRET_KEY must be set");
    let key: Hmac<Sha256> = Hmac::new_from_slice(secret_key.as_bytes())
        .expect("HMAC can take key of any size");

    token.verify_with_key(&key)
        .ok()
        .and_then(|claims: BTreeMap<String, u64>| {
            let exp = claims.get("exp")?;
            let current_time = chrono::Utc::now().timestamp() as u64;
            
            if current_time > *exp || !claims.contains_key("id") {
                None
            } else {
                Some(true)
            }
        })
        .unwrap_or(false)
}
The verification process:
  1. Validates HMAC signature
  2. Checks token expiration
  3. Ensures required claims exist

Role Verification

Check if a user has a specific role or higher:
src/web/auth.rs
pub fn user_has_role(token: &str, role: Roles) -> bool {
    let secret_key = std::env::var("SECRET_KEY")
        .expect("SECRET_KEY must be set");
    let key: Hmac<Sha256> = Hmac::new_from_slice(secret_key.as_bytes())
        .expect("HMAC can take key of any size");

    token.verify_with_key(&key)
        .ok()
        .and_then(|claims: BTreeMap<String, u64>| {
            let exp = claims.get("exp")?;
            let role_claim = claims.get("role")?;
            let current_time = chrono::Utc::now().timestamp() as u64;

            if current_time > *exp {
                None
            } else if role_claim >= &(role as u64) {
                Some(true)
            } else {
                None
            }
        })
        .unwrap_or(false)
}
Role checking uses >= comparison, so Admin role (3) will pass checks for User role (1).

Request Guards

Rocket request guards automatically protect routes based on authentication level:

AuthenticatedUser Guard

Requires any authenticated user (User role or higher):
src/web/auth.rs
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthenticatedUser<'r> {
    type Error = ApiKeyError;

    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        fn is_valid(key: &str) -> bool {
            let is_valid = User::verify_token(key);
            let has_role = User::user_has_role(key, Roles::User);
            is_valid && has_role
        }

        let key = req.cookies().get("token").map(|cookie| cookie.value());
        match key {
            None => Outcome::Error((Status::Unauthorized, ApiKeyError::Missing)),
            Some(key) if is_valid(key) => Outcome::Success(AuthenticatedUser(key)),
            Some(_) => Outcome::Error((Status::Unauthorized, ApiKeyError::Invalid)),
        }
    }
}

AdminUser Guard

Requires Admin role:
src/web/auth.rs
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AdminUser<'r> {
    type Error = ApiKeyError;

    async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
        fn is_valid(key: &str) -> bool {
            let is_valid = User::verify_token(key);
            let has_role = User::user_has_role(key, Roles::Admin);
            is_valid && has_role
        }

        let key = req.cookies().get("token").map(|cookie| cookie.value());
        match key {
            None => Outcome::Error((Status::Unauthorized, ApiKeyError::Missing)),
            Some(key) if is_valid(key) => Outcome::Success(AdminUser(key)),
            Some(_) => Outcome::Error((Status::Unauthorized, ApiKeyError::Invalid)),
        }
    }
}

Using Request Guards

Simply add the guard type to route parameters:
src/web/mod.rs
// Requires authenticated user
#[get("/verify")]
pub fn verify(_user: AuthenticatedUser<'_>) -> Template {
    Template::render("verify", &{})
}

// Requires admin
#[get("/admin")]
pub fn admin(_user: AdminUser<'_>) -> Template {
    Template::render("admin", &{})
}
If authentication fails, the request returns a 401 status code and renders the unauthorized error page.

Authentication Flow

Login

  1. User clicks “Login with Discord”
  2. Redirect to /api/auth/discord
  3. Discord OAuth flow begins
  4. Callback to /api/auth/discord/callback
  5. JWT token created and set in cookie

Logout

src/web/mod.rs
#[get("/logout")]
pub fn logout(jar: &CookieJar<'_>) -> Template {
    jar.remove("token");
    Template::render("logout", &{})
}
Simply removes the token cookie, effectively logging out the user.

Security Configuration

Ensure the SECRET_KEY environment variable is set to a strong, random value in production. This key signs all JWT tokens.
Token cookies are configured with security flags:
src/web/api/mod.rs
jar.add(Cookie::build(("token", token))
    .path("/")
    .secure(true)      // HTTPS only
    .http_only(true)); // No JavaScript access

Example: Generate Test Token

The application generates an admin token on startup for testing:
src/main.rs
let example_user = User::new(1234567890).create_token(web::auth::Roles::Admin);
println!("Example admin token: {}", example_user);

Discord OAuth

Learn about Discord OAuth integration

Admin Dashboard

Admin-only features and pages

Build docs developers (and LLMs) love