Skip to content

Authentication

PolitikTok uses Keycloak as its identity provider, implementing the OpenID Connect Authorization Code flow with PKCE (Proof Key for Code Exchange). This eliminates the need for a client secret in the browser, making the setup suitable for public-facing single-page applications.

Authentication Flow

1. User clicks "Login"
       |
       v
2. Browser -> GET /auth?redirect_url=/dashboard
       |
       v
3. Server generates PKCE verifier + challenge + state
   Stores (state -> verifier, redirect_url) in PendingOAuthStore
       |
       v
4. Server redirects to Keycloak authorize endpoint
   with client_id, code_challenge (S256), state
       |
       v
5. User authenticates in Keycloak
       |
       v
6. Keycloak redirects to /auth/callback?code=...&state=...
       |
       v
7. Server looks up PendingOAuthStore by state
   Exchanges authorization code + code_verifier for tokens
       |
       v
8. Server fetches userinfo from Keycloak
   Extracts role from JWT realm_access.roles
       |
       v
9. Server creates session with UserSessionState
   Redirects user to original redirect_url

PKCE Implementation

PKCE prevents authorization code interception attacks. The implementation is in src/infrastructure/auth.rs:

  • Code verifier: 43-character base64url string generated from 32 random bytes
  • Code challenge: SHA-256 hash of the verifier, base64url-encoded (S256 method)
  • State parameter: 64-character hex string for CSRF protection

The pending OAuth entries are stored in an in-memory PendingOAuthStore (behind Arc<RwLock<HashMap>>), which survives Dioxus CLI proxy restarts better than cookie-based state.

Session Management

After successful authentication, the server stores a UserSessionState in a signed session cookie:

rust
pub struct UserSessionState {
    pub sub: String,           // Keycloak subject ID
    pub email: String,
    pub name: String,
    pub role: String,          // "admin", "staff", "volunteer", "readonly"
    pub access_token: String,
    pub refresh_token: String,
}

Sessions are managed by tower-sessions with:

  • Memory store for session data
  • Signed cookies using a randomly generated key
  • 24-hour inactivity expiry
  • SameSite=Lax cookie policy

Role-Based Access Control

PolitikTok defines four roles, mapped from Keycloak realm roles:

RoleAccess LevelTypical User
AdminFull access to all modules + admin panelCampaign manager, system administrator
StaffAll operational modules, no admin panelCampaign staff, communications director
VolunteerVolunteer-facing modules onlyField volunteers
ReadOnlyView-only access across modulesObservers, auditors

Roles are extracted from the JWT access token's realm_access.roles claim. The extraction checks for roles in priority order: admin, staff, volunteer, readonly. If none match, the default role is staff.

Enforcing Roles in Server Functions

Server functions use helper methods from src/infrastructure/auth.rs:

rust
// Require any authenticated user
let user = require_user().await?;

// Require a specific role (admin always passes)
let user = require_role("staff").await?;

// Extract session without role check
let session = require_session().await?;

The require_role function grants access if the user's role matches the required role OR if the user is an admin.

Keycloak Configuration

Keycloak endpoints are derived from the base URL and realm name:

EndpointURL Pattern
Authorization{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/auth
Token{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/token
Userinfo{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/userinfo
Logout{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/logout

Logout

The logout flow (GET /logout):

  1. Flushes the server-side session
  2. Redirects to Keycloak's logout endpoint with post_logout_redirect_uri set to APP_URL
  3. Keycloak terminates its own session and redirects the user back to the landing page

Auth Middleware

The Axum middleware in src/infrastructure/middleware/auth.rs runs on every request. For routes that require authentication, it checks for a valid session cookie. Unauthenticated requests to protected routes are redirected to /auth.

The OAuth routes (/auth, /auth/callback, /logout) and static assets are excluded from auth enforcement.

Development Mode

During development without Keycloak running, the check_auth server function falls back to a mock authenticated user:

rust
Ok(AuthInfo {
    authenticated: true,
    email: "dev@politiktok.local".to_string(),
    name: "Dev User".to_string(),
    role: "admin".to_string(),
})

This allows UI development without requiring the full Docker Compose stack. Remove this fallback before deploying to production.