Skip to main content
The Discord OAuth endpoints handle user authentication through Discord, guild membership verification, and session creation with JWT tokens.

OAuth Initiation

Endpoint

GET /api/auth/discord
Initiates the Discord OAuth flow by redirecting to Discord’s authorization page.

Implementation

src/web/api/mod.rs
#[get("/auth/discord")]
pub fn discord_auth() -> Redirect {
    let client_id = std::env::var("DISCORD_CLIENT_ID")
        .expect("DISCORD_CLIENT_ID must be set");
    let redirect_uri = std::env::var("DISCORD_REDIRECT_URI")
        .expect("DISCORD_REDIRECT_URI must be set");
    
    let discord_auth_url = format!(
        "https://discord.com/api/oauth2/authorize?client_id={}&redirect_uri={}&response_type=code&scope=identify+guilds",
        client_id, redirect_uri
    );
    
    Redirect::to(discord_auth_url)
}

OAuth Scopes

ScopePurpose
identifyAccess user ID, username, and avatar
guildsCheck server membership

Usage

Simply redirect users to this endpoint:
<a href="/api/auth/discord">Login with Discord</a>

OAuth Callback

Endpoint

GET /api/auth/discord/callback?code={authorization_code}
Handles the Discord OAuth callback, exchanges the authorization code for tokens, verifies guild membership, and creates a session.

Implementation

src/web/api/mod.rs
#[get("/auth/discord/callback?<code>")]
pub async fn discord_callback(
    code: String, 
    jar: &CookieJar<'_>
) -> Result<Template, rocket::http::Status> {
    let client = reqwest::Client::new();
    
    // Get OAuth tokens
    let token_response = get_discord_token(&client, &code).await
        .map_err(|_| rocket::http::Status::InternalServerError)?;
    
    // Get user info
    let user_info = get_discord_user(&client, &token_response.access_token).await
        .map_err(|_| rocket::http::Status::InternalServerError)?;

    let guilds = get_discord_guilds(&client, &token_response.access_token).await
        .map_err(|_| rocket::http::Status::InternalServerError)?;

    // Check guild membership
    let guild_id = std::env::var("DISCORD_SERVER_ID")
        .expect("DISCORD_SERVER_ID must be set")
        .parse::<u64>()
        .expect("DISCORD_SERVER_ID must be a number");
    
    let is_member = guilds.iter()
        .any(|guild| guild.id.parse::<u64>().unwrap() == guild_id);

    if !is_member {
        return Ok(Template::render("noAccess", &{
            let mut context = std::collections::HashMap::new();
            context.insert("serverName", "HS Kempten".to_string());
            context
        }));
    }

    // Create user token
    let user = User::new(user_info.id.parse().unwrap());
    let token = if user_info.id == "242385294123335690" {
        user.create_token(Roles::Admin)
    } else {
        user.create_token(Roles::User)
    };
    
    jar.add(Cookie::build(("token", token))
        .path("/")
        .secure(true)
        .http_only(true));

    // Render success template
    Ok(Template::render("discord_callback", &{
        let mut context = std::collections::HashMap::new();
        context.insert("user_id", user_info.id.clone());
        context.insert("username", user_info.username.clone());
        context.insert("avatar", user_info.get_avatar_url());
        context
    }))
}

Query Parameters

code
string
required
Discord authorization code provided by Discord after user authorization

Response

On success, renders discord_callback.html.hbs template with user information and sets authentication cookie. On failure (non-member), renders noAccess.html.hbs template.

OAuth Flow Diagram


Data Structures

TokenResponse

src/web/api/mod.rs
#[derive(Deserialize)]
pub struct TokenResponse {
    access_token: String,
}
Returned by Discord’s token exchange endpoint.

UserInfo

src/web/api/mod.rs
#[derive(Deserialize)]
pub struct UserInfo {
    id: String,
    username: String,
    avatar: String,
}

impl UserInfo {
    fn get_avatar_url(&self) -> String {
        let ext = if self.avatar.starts_with("a_") { "gif" } else { "png" };
        format!(
            "https://cdn.discordapp.com/avatars/{}/{}.{}",
            self.id, self.avatar, ext
        )
    }
}
Animated avatars (starting with “a_”) use .gif extension, static avatars use .png.

Guild

src/web/api/mod.rs
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Guild {
    id: String,
    name: String,
    icon: Option<String>,
    owner: bool,
    permissions: u64,
}

Helper Functions

Exchange Authorization Code

src/web/api/mod.rs
async fn get_discord_token(
    client: &reqwest::Client, 
    code: &str
) -> Result<TokenResponse, reqwest::Error> {
    client.post("https://discord.com/api/oauth2/token")
        .form(&[
            ("client_id", std::env::var("DISCORD_CLIENT_ID").expect("DISCORD_CLIENT_ID must be set")),
            ("client_secret", std::env::var("DISCORD_CLIENT_SECRET").expect("DISCORD_CLIENT_SECRET must be set")),
            ("grant_type", "authorization_code".to_string()),
            ("code", code.to_string()),
            ("redirect_uri", std::env::var("DISCORD_REDIRECT_URI").expect("DISCORD_REDIRECT_URI must be set")),
            ("scope", "identify".to_string()),
        ])
        .send()
        .await?
        .json()
        .await
}

Get User Information

src/web/api/mod.rs
async fn get_discord_user(
    client: &reqwest::Client, 
    access_token: &str
) -> Result<UserInfo, reqwest::Error> {
    client.get("https://discord.com/api/users/@me")
        .header("Authorization", format!("Bearer {}", access_token))
        .send()
        .await?
        .json()
        .await
}

Get User Guilds

src/web/api/mod.rs
pub async fn get_discord_guilds(
    client: &reqwest::Client, 
    access_token: &str
) -> Result<Vec<Guild>, reqwest::Error> {
    client.get("https://discord.com/api/users/@me/guilds")
        .header("Authorization", format!("Bearer {}", access_token))
        .send()
        .await?
        .json::<Vec<Guild>>()
        .await
}

OAuth Client (Alternative Implementation)

A reusable OAuth client is also available:
src/web/api/mod.rs
#[derive(Clone)]
pub struct DiscordOAuthClient {
    client: Client,
    client_id: String,
    client_secret: String,
    redirect_uri: String,
    api_base: String,
}

impl DiscordOAuthClient {
    pub fn new() -> Result<Self, std::env::VarError> {
        Ok(Self {
            client: Client::new(),
            client_id: std::env::var("DISCORD_CLIENT_ID")?,
            client_secret: std::env::var("DISCORD_CLIENT_SECRET")?,
            redirect_uri: std::env::var("DISCORD_REDIRECT_URI")?,
            api_base: "https://discord.com/api".to_string(),
        })
    }

    pub async fn exchange_code(&self, code: &str) -> Result<TokenResponse, reqwest::Error> {
        let form = [
            ("client_id", &self.client_id),
            ("client_secret", &self.client_secret),
            ("grant_type", &"authorization_code".to_string()),
            ("code", &code.to_string()),
            ("redirect_uri", &self.redirect_uri),
            ("scope", &"identify".to_string()),
        ];

        self.client.post(format!("{}/oauth2/token", self.api_base))
            .form(&form)
            .send()
            .await?
            .json()
            .await
    }

    pub async fn get_current_user(&self, access_token: &str) -> Result<UserInfo, reqwest::Error> {
        self.get_authenticated_resource("users/@me", access_token).await
    }

    pub async fn get_user_guilds(&self, access_token: &str) -> Result<Vec<Guild>, reqwest::Error> {
        self.get_authenticated_resource("users/@me/guilds", access_token).await
    }
}

Environment Configuration

Required environment variables:
DISCORD_CLIENT_ID
string
required
Your Discord application’s client ID
DISCORD_CLIENT_SECRET
string
required
Your Discord application’s client secret (keep private!)
DISCORD_REDIRECT_URI
string
required
OAuth callback URL (must match Discord app settings)Example: https://bot.example.com/api/auth/discord/callback
DISCORD_SERVER_ID
string
required
Discord server ID that users must be members of

Security Considerations

Critical Security Requirements:
  1. HTTPS Only: OAuth should only run over HTTPS in production
  2. Secret Protection: Never expose DISCORD_CLIENT_SECRET
  3. Cookie Security: Cookies are marked secure and http_only
  4. Guild Verification: Always verify guild membership
  5. Token Validation: JWT tokens expire after 24 hours
jar.add(Cookie::build(("token", token))
    .path("/")           // Available site-wide
    .secure(true)        // HTTPS only
    .http_only(true));   // No JavaScript access

Error Handling

Error ScenarioResponseTemplate
Invalid authorization code500 Internal Server ErrorNone
Discord API failure500 Internal Server ErrorNone
Not guild member200 OKnoAccess.html.hbs
Success200 OKdiscord_callback.html.hbs

Testing

Test the OAuth flow locally:
  1. Set environment variables in .env
  2. Configure Discord app with redirect URI: http://localhost:8000/api/auth/discord/callback
  3. Visit http://localhost:8000/api/auth/discord
  4. Authorize the application
  5. Check for JWT token in browser cookies
For local testing, Discord allows http://localhost redirect URIs. Production must use HTTPS.

Authentication System

Learn about JWT tokens and roles

User Management

Managing authenticated users

Admin Dashboard

Admin role assignment and access

Build docs developers (and LLMs) love