Cloud Challenge CTF supports two authentication paths: Google OAuth for persistent accounts and Guest mode for anonymous exploration. Understanding the authentication flow is essential to understanding how the architecture supports both user types while maintaining security and simplicity.
When a player chooses to authenticate with Google (clicking "Initialize New Agent Protocol" or "Activate Returning Agent Protocol"), the following sequence occurs:
┌─────────────────────────────────────────────────────────────────┐
│ GOOGLE OAUTH FLOW │
│ │
│ ┌──────────────────────┐ │
│ │ Player Clicks Auth │ │
│ │ Button on /login │ │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 1. useGoogleLogin Hook Triggers │ │
│ │ • Opens Google OAuth consent popup│ │
│ │ • Client ID from env variable │ │
│ │ • Scope: email, profile │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 2. User Authenticates with Google │ │
│ │ • User logs into Google account │ │
│ │ • Grants permissions to app │ │
│ │ • Google returns access_token │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 3. Fetch User Info from Google API │ │
│ │ GET https://www.googleapis.com/ │ │
│ │ oauth2/v2/userinfo │ │
│ │ Headers: Authorization: Bearer │ │
│ │ {access_token} │ │
│ │ Returns: { │ │
│ │ email: "user@gmail.com", │ │
│ │ name: "John Doe", │ │
│ │ picture: "https://..." │ │
│ │ } │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 4. Store Session in localStorage │ │
│ │ localStorage.setItem('auth_user',│ │
│ │ JSON.stringify(userInfo)) │ │
│ │ localStorage.setItem('auth_token',│ │
│ │ access_token) │ │
│ │ AuthContext.setUser(userInfo) │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 5. Check if User Exists in Database │ │
│ │ GET /users/check?email=... │ │
│ │ → API Gateway │ │
│ │ → Lambda: awschallenge_checkusers│ │
│ │ → DynamoDB Query (EmailIndex) │ │
│ │ Response: { exists: true/false, │ │
│ │ user: {...} } │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ├─────────── User Exists? ──────────┐ │
│ │ │ │
│ ▼ YES ▼ NO │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ 6a. Returning User │ │ 6b. New User │ │
│ │ router.push( │ │ router.push( │ │
│ │ '/welcomeback')│ │ '/setup') │ │
│ │ • Show profile │ │ • Skill setup │ │
│ │ • Recent progress│ │ • Create profile│ │
│ └──────────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Performance: Entire OAuth flow happens in <3 seconds, including Google authentication, user info fetch, and database lookup.
Client-Side OAuth Implementation
The platform implements OAuth entirely client-side using the @react-oauth/google library. This design works seamlessly with Next.js static export, eliminates backend session management complexity, and provides immediate user feedback. OAuth tokens are stored in browser localStorage, which is appropriate for a CTF platform with no financial data or sensitive personal information.
When a player chooses to play as a guest (clicking "Play as Guest"), the following sequence occurs:
┌─────────────────────────────────────────────────────────────────┐
│ GUEST MODE FLOW │
│ │
│ ┌──────────────────────┐ │
│ │ Player Clicks │ │
│ │ "Play as Guest" │ │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 1. Generate Guest Credentials │ │
│ │ • guestId: 7-char random string │ │
│ │ (e.g., "ABC1234") │ │
│ │ • guestEmail: guest-{guestId}@ │ │
│ │ localhost.local │ │
│ │ • username: guestId │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 2. Auto-Create Guest Profile │ │
│ │ POST /users │ │
│ │ Body: { │ │
│ │ email: "guest-ABC1234@ │ │
│ │ localhost.local", │ │
│ │ username: "ABC1234", │ │
│ │ awsSkillLevel: "beginner", │ │
│ │ securitySkillLevel: "beginner",│ │
│ │ puzzleSkillLevel: "beginner" │ │
│ │ } │ │
│ │ → API Gateway │ │
│ │ → Lambda: awschallenge_base_user_│ │
│ │ function │ │
│ │ → DynamoDB Put Item │ │
│ │ Response: { │ │
│ │ success: true, │ │
│ │ userId: "uuid-v4" │ │
│ │ } │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 3. Store Session in sessionStorage │ │
│ │ sessionStorage.setItem( │ │
│ │ 'guest_user', │ │
│ │ JSON.stringify({ │ │
│ │ email: guestEmail, │ │
│ │ guestId: guestId, │ │
│ │ isGuest: true │ │
│ │ }) │ │
│ │ ) │ │
│ │ AuthContext.setUser({...}) │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 4. Redirect to │ │
│ │ /dashboard │ │
│ │ (Skip /setup) │ │
│ │ • Show challenges │ │
│ │ • Guest indicator│ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Guest Mode Design
Guest mode provides anonymous play with auto-generated profiles, lowering the barrier to entry for players who want to try challenges without authentication. This approach works particularly well in educational settings where students may not have Google accounts, and enables rapid testing and development without OAuth configuration. Guest sessions use sessionStorage for temporary persistence, automatically clearing when the browser closes.
Guest Session Management:
sessionStorage (automatically cleared when browser tab closes)All users (authenticated and guest) have the same profile structure in DynamoDB, ensuring consistent API interactions:
interface UserProfile {
userID: string; // UUID v4 for all users
email: string; // Real email for authenticated, guest-ABC1234@localhost.local for guests
username: string; // Display name (real name for authenticated, guest ID for guests)
awsSkillLevel: string; // "beginner" | "intermediate" | "advanced"
securitySkillLevel: string; // "beginner" | "intermediate" | "advanced"
puzzleSkillLevel: string; // "beginner" | "intermediate" | "advanced"
currentScore: number; // Total points earned across all challenges
currentLevel: number; // Security clearance level (1-3)
createdAt: string; // ISO 8601 timestamp
updatedAt: string; // ISO 8601 timestamp
wrongAttempts?: number; // Challenge attempt tracking (for lockout)
closeAttempts?: number; // Close answer attempt tracking
lockedUntil?: string; // ISO timestamp for lockout expiration
}
DynamoDB Table: awschallangeUsersTable
userID (String, UUID v4)EmailIndex on email fieldDynamoDB Table: awsChallengeChallengesTable
challengeStateId (String, UUID v4)user-challenge-index on userId + challengeId (composite)interface ChallengeState {
challengeStateId: string; // UUID v4
userId: string; // Foreign key to userID
challengeId: string; // "hallucination", "future_challenges", etc.
currentStage: number; // Current stage number (1-4 for Operation Hallucination)
stageStatus: { // Per-stage completion tracking
"1": { completed: 0 | 1, attempts: number },
"2": { completed: 0 | 1, attempts: number },
"3": { completed: 0 | 1, attempts: number },
"4": { completed: 0 | 1, attempts: number }
};
lastUpdated: string; // ISO 8601 timestamp
}
Profile Structure Benefits
The profile structure tracks three skill dimensions (AWS, Security, Puzzle) enabling personalized challenge recommendations. The design is challenge-agnostic, supporting any challenge type without schema changes. Built-in attempt tracking provides lockout mechanisms and anti-cheating measures, while DynamoDB's flexible schema allows adding new attributes without migrations.