The API implements a role-based access control (RBAC) system that:
- Authenticates users via Discord OAuth2
- Verifies Discord server membership
- Checks user roles against required permissions
- Caches role information to avoid hitting Discord's rate limits
The API uses Discord OAuth2 middleware defined in internal/api/middleware/oauth.go to protect state-changing /v1 routes.
How it works:
-
Authorization Header: The middleware expects requests to include:
Authorization: Bearer <discord_access_token> -
Token Validation: The middleware validates the token by:
- Making a request to Discord's API to fetch user information
- Verifying the user is a member of the configured Discord guild/server
- Checking if the user has the required role
-
Role-Based Access Control:
- Roles are mapped in
RoleMap(e.g., Discord role ID1445971950584205554→"Board") - The middleware checks if the user has the required role for the endpoint
- Role hierarchy:
Presidentrole also grantsBoardaccess
- Roles are mapped in
-
Caching:
- Role information is cached for 5 minutes to prevent rate limit issues
- Cache key is the Authorization header value
- Expired cache entries are automatically removed
-
Development Mode:
- When
ENV=development, the special tokenBearer dev-tokenbypasses authentication - This allows local testing without setting up OAuth
- When
The CLI client (defined in utils/requests/request_with_auth.go) implements the OAuth2 authorization code flow with a local callback server.
How it works:
-
Token Persistence:
- Tokens are stored in
~/.config/acmcsuf-cli/token.jsonon Unix systems - The file contains:
access_token,refresh_token, andexpirytimestamp - Tokens are automatically loaded on subsequent CLI runs
- Tokens are stored in
-
OAuth Flow (when no valid token exists):
┌─────────┐ ┌─────────────┐ │ CLI │ │ Discord │ └────┬────┘ └──────┬──────┘ │ │ │ 1. Start local callback server │ │ on hardcoded port (:61234) │ │ │ │ 2. Open browser with OAuth URL │ ├──────────────────────────────────────────> │ │ https://discord.com/oauth2/authorize │ │ ?client_id=... │ │ &redirect_uri=http://localhost:61234 │ │ &scope=identify │ │ &response_type=code │ │ │ │ User approves in browser │ │ │ │ 3. Discord redirects to callback │ │ <────────────────────────────────────────── │ │ http://localhost:61234/?code=... │ │ │ │ 4. Exchange code for token │ ├──────────────────────────────────────────> │ │ POST /oauth2/token │ │ │ │ 5. Receive access token │ │ <────────────────────────────────────────── │ │ { access_token, refresh_token, ... } │ │ │ │ 6. Save token to ~/.config/acmcsuf-cli/ │ │ │ │ 7. Make authenticated API request │ │ Authorization: Bearer <access_token> │ └──────────────────────────────────────────────┘ -
Token Exchange:
- The callback server receives the authorization code from Discord
- Exchanges the code for an access token via
POST https://discord.com/api/oauth2/token - Stores the token with expiry information for future use
See developer-docs/env-vars.md for the complete list, but OAuth-specific variables include:
ENV: Set toproductionto enable OAuth (default:development)DISCORD_BOT_TOKEN: Bot token for server-side API authenticationGUILD_ID: Discord server/guild ID to verify membershipDISCORD_CLIENT_ID: OAuth2 application client ID (for CLI)DISCORD_CLIENT_SECRET: OAuth2 application client secret (for CLI)
During development (ENV=development), authentication is bypassed by supplying dev-token to the Authorization header. The CLI will do this automatically.
The CLI handles OAuth automatically:
# First run will trigger OAuth flow
./api.acmcsuf.com events get event-id
# Browser opens for authentication
# Token is saved to ~/.config/acmcsuf-cli/token.json
# Subsequent runs use the cached tokenYou need to manually pass the token when using a standard http client:
# Development mode (no real auth needed)
xh post :8080/v1/events --auth-type bearer --auth dev-token
# Or with short flags:
xh post :8080/v1/events -A bearer -a dev-token
# Or defining the header manually:
xh :8080/v1/events Authorization:'Bearer dev-token'To get a real Discord access token for testing, run the OAuth flow with the CLI and extract the token from ~/.config/acmcsuf-cli/token.json
Discord access tokens expire after a period (typically 1 week). When a token expires:
CLI: The OAuth flow automatically re-runs on the next command
Manual testing: You'll receive a 401 Unauthorized response and need a new token
To add or modify roles, edit the RoleMap in internal/api/middleware/oauth.go:
var RoleMap = map[string]string{
"1445971950584205554": "Board", // Discord role ID -> Role name
"another-role-id": "Developer",
}To find Discord role IDs:
- Enable Developer Mode in Discord settings
- Right-click a role in Server Settings → Roles
- Click "Copy ID"