Skip to content

Authentication

MDU uses its own JWT-based authentication system with two login methods: Email OTP and Google OAuth.

Overview

  • Access token: Short-lived JWT (HMAC-SHA256)
  • Refresh token: Long-lived JWT for token renewal
  • Storage: localStorage keys mdu_access_token, mdu_refresh_token, mdu_user
  • Header: Authorization: Bearer <access_token>

Endpoints

Method Path Auth Description
POST /api/auth/send-otp - Send 6-digit OTP to email
POST /api/auth/verify-otp - Verify OTP, return JWT
GET /api/auth/google - Google OAuth redirect
GET /api/auth/google/callback - Google OAuth callback
POST /api/auth/refresh - Refresh access token
POST /api/auth/logout Required Invalidate refresh token

Email OTP Flow

1. Send OTP

POST /api/auth/send-otp
Content-Type: application/json

{ "email": "user@example.com" }

Response:

{ "success": true }

Rules:

  • 6-digit numeric code
  • 10-minute expiry
  • Max 3 sends per 10 minutes per email

2. Verify OTP

POST /api/auth/verify-otp
Content-Type: application/json

{ "email": "user@example.com", "code": "123456" }

Response:

{
  "access_token": "eyJhbGci...",
  "refresh_token": "eyJhbGci...",
  "user": { "id": "uuid", "email": "user@example.com", "name": "User" }
}

Google OAuth Flow

1. Redirect to Google

GET /api/auth/google?redirect=https://app.minidreamuniverse.com/create

Redirects the user to Google's OAuth consent screen. The optional redirect parameter sets where to return after authentication.

2. Callback

Google redirects back to /api/auth/google/callback with an authorization code. The server exchanges it for user info, creates/updates the profile, generates JWT tokens, and redirects to the frontend with tokens in the URL hash.

Token Refresh

POST /api/auth/refresh
Content-Type: application/json

{ "refresh_token": "eyJhbGci..." }

Response:

{
  "access_token": "eyJhbGci...",
  "refresh_token": "eyJhbGci..."
}

JWT Payload

{
  "sub": "user-uuid",
  "email": "user@example.com",
  "iat": 1709800000,
  "exp": 1709803600
}

Warning

The user ID is in payload.sub, NOT payload.userId. All middleware extracts req.userId = payload.sub.

Frontend Integration

// authService.ts
const token = localStorage.getItem('mdu_access_token');

// API calls
const response = await fetch('/api/generate-model', {
  headers: { 'Authorization': `Bearer ${token}` }
});

// Token refresh on 401
if (response.status === 401) {
  const newToken = await refreshToken();
  // Retry request with new token
}

The getValidToken() helper in authService.ts handles JWT expiry checking and automatic refresh.

Logout

POST /api/auth/logout
Authorization: Bearer <access_token>

Clears the refresh token server-side. Frontend clears localStorage and dispatches mdu:auth:logout CustomEvent.