# Authentication Tutorial Learn how to add user authentication to your AeThex game with email/password, OAuth, and guest login support. > [!TIP] > Authentication is the foundation for cloud saves, multiplayer, and social features. Start here to unlock all AeThex Cloud capabilities. --- ## What You'll Build A complete authentication system with: - Email/password registration and login - OAuth login (Google, GitHub, Discord) - Guest accounts - User profiles - Session management **Time:** 30 minutes **Difficulty:** Beginner **Prerequisites:** Basic GDScript knowledge ```mermaid flowchart TD Start[Game Launch] --> Check{Has Session?} Check -->|Yes| Welcome[Welcome Back!] Check -->|No| Login[Show Login Screen] Login --> Choice{Auth Method?} Choice -->|Email/Password| EmailAuth[Email Login] Choice -->|OAuth| OAuthFlow[OAuth Flow] Choice -->|Guest| GuestAuth[Guest Account] EmailAuth --> Validate{Valid?} Validate -->|No| Error1[Show Error] Error1 --> Login Validate -->|Yes| Auth[Authenticate] OAuthFlow --> Provider[Select Provider] Provider --> Browser[Open Browser] Browser --> Auth GuestAuth --> Auth Auth --> Success{Success?} Success -->|No| Error2[Show Error] Error2 --> Login Success -->|Yes| SaveSession[Save Session] SaveSession --> Welcome Welcome --> MainMenu[Main Menu] style Start fill:#00ffff22,stroke:#00ffff style Welcome fill:#00ff0022,stroke:#00ff00 style Auth fill:#ff00ff22,stroke:#ff00ff style MainMenu fill:#00ffff22,stroke:#00ffff ``` --- ## Why Add Authentication? Authentication enables: - **Cloud saves** - Save progress across devices - **Multiplayer** - Identify players in matches - **Social features** - Friends, leaderboards, chat - **Analytics** - Track user behavior - **Monetization** - In-app purchases, subscriptions > [!WARNING] > Never store passwords in plaintext. AeThex Cloud handles all password hashing and security automatically. --- ## Step 1: Connect to AeThex Cloud First, ensure cloud services are connected: > [!NOTE] > Session tokens are stored securely and persist across game sessions. Users stay logged in automatically. ```gdscript # main.gd extends Node func _ready(): # Connect to AeThex Cloud var result = await AeThexCloud.connect_to_cloud() if result.success: print("Connected to AeThex Cloud") check_existing_session() else: print("Failed to connect: ", result.error) show_error("Could not connect to servers") func check_existing_session(): if AeThexAuth.is_logged_in(): var user = AeThexAuth.get_current_user() print("Welcome back, ", user.display_name) go_to_main_menu() else: show_login_screen() ``` --- ## Step 2: Create Login UI Create a login screen with options for different auth methods: ```gdscript # login_screen.gd extends Control @onready var email_field = $VBox/EmailField @onready var password_field = $VBox/PasswordField @onready var login_btn = $VBox/LoginButton @onready var register_btn = $VBox/RegisterButton @onready var guest_btn = $VBox/GuestButton @onready var google_btn = $VBox/OAuthButtons/GoogleButton @onready var github_btn = $VBox/OAuthButtons/GitHubButton @onready var discord_btn = $VBox/OAuthButtons/DiscordButton @onready var status_label = $VBox/StatusLabel func _ready(): login_btn.pressed.connect(_on_login_pressed) register_btn.pressed.connect(_on_register_pressed) guest_btn.pressed.connect(_on_guest_pressed) google_btn.pressed.connect(_on_oauth_pressed.bind("google")) github_btn.pressed.connect(_on_oauth_pressed.bind("github")) discord_btn.pressed.connect(_on_oauth_pressed.bind("discord")) func _on_login_pressed(): var email = email_field.text var password = password_field.text if not validate_email(email): show_status("Invalid email address", Color.RED) return if password.length() < 6: show_status("Password must be at least 6 characters", Color.RED) return show_status("Logging in...", Color.YELLOW) set_buttons_enabled(false) var result = await AeThexAuth.login_email(email, password) set_buttons_enabled(true) if result.success: show_status("Login successful!", Color.GREEN) on_login_success() else: show_status("Login failed: " + result.error, Color.RED) func _on_register_pressed(): var email = email_field.text var password = password_field.text if not validate_email(email): show_status("Invalid email address", Color.RED) return if password.length() < 6: show_status("Password must be at least 6 characters", Color.RED) return show_status("Creating account...", Color.YELLOW) set_buttons_enabled(false) var result = await AeThexAuth.register_email(email, password) set_buttons_enabled(true) if result.success: show_status("Account created! Logging in...", Color.GREEN) on_login_success() else: show_status("Registration failed: " + result.error, Color.RED) func _on_guest_pressed(): show_status("Creating guest account...", Color.YELLOW) set_buttons_enabled(false) var result = await AeThexAuth.login_as_guest() set_buttons_enabled(true) if result.success: show_status("Logged in as guest!", Color.GREEN) on_login_success() else: show_status("Guest login failed: " + result.error, Color.RED) func _on_oauth_pressed(provider: String): show_status("Opening " + provider + " login...", Color.YELLOW) set_buttons_enabled(false) var result = await AeThexAuth.login_oauth(provider) set_buttons_enabled(true) if result.success: show_status("Logged in with " + provider + "!", Color.GREEN) on_login_success() else: show_status("OAuth login failed: " + result.error, Color.RED) func validate_email(email: String) -> bool: var regex = RegEx.new() regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") return regex.search(email) != null func show_status(message: String, color: Color): status_label.text = message status_label.modulate = color func set_buttons_enabled(enabled: bool): login_btn.disabled = !enabled register_btn.disabled = !enabled guest_btn.disabled = !enabled google_btn.disabled = !enabled github_btn.disabled = !enabled discord_btn.disabled = !enabled func on_login_success(): # Transition to main menu await get_tree().create_timer(1.0).timeout get_tree().change_scene_to_file("res://scenes/main_menu.tscn") ``` --- ## Step 3: Create Registration Flow Separate registration screen with additional fields: ```gdscript # registration_screen.gd extends Control @onready var email_field = $VBox/EmailField @onready var password_field = $VBox/PasswordField @onready var confirm_password_field = $VBox/ConfirmPasswordField @onready var display_name_field = $VBox/DisplayNameField @onready var terms_checkbox = $VBox/TermsCheckbox @onready var register_btn = $VBox/RegisterButton @onready var back_btn = $VBox/BackButton func _ready(): register_btn.pressed.connect(_on_register_pressed) back_btn.pressed.connect(_on_back_pressed) func _on_register_pressed(): # Validation if not validate_input(): return var email = email_field.text var password = password_field.text var display_name = display_name_field.text register_btn.disabled = true # Register with additional profile data var result = await AeThexAuth.register_email(email, password, { "display_name": display_name }) register_btn.disabled = false if result.success: # Send verification email (optional) await AeThexAuth.send_verification_email() show_success("Account created! Check your email to verify.") else: show_error("Registration failed: " + result.error) func validate_input() -> bool: # Check email if not validate_email(email_field.text): show_error("Invalid email address") return false # Check password if password_field.text.length() < 6: show_error("Password must be at least 6 characters") return false # Check password match if password_field.text != confirm_password_field.text: show_error("Passwords do not match") return false # Check display name if display_name_field.text.strip_edges().is_empty(): show_error("Display name is required") return false # Check terms if not terms_checkbox.button_pressed: show_error("You must accept the terms of service") return false return true func validate_email(email: String) -> bool: var regex = RegEx.new() regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") return regex.search(email) != null func _on_back_pressed(): get_tree().change_scene_to_file("res://scenes/login_screen.tscn") ``` --- ## Step 4: Handle Authentication State Create a global authentication manager: ```gdscript # autoload: auth_manager.gd extends Node signal login_changed(is_logged_in: bool) signal user_profile_updated(profile: Dictionary) var current_user: Dictionary = {} func _ready(): # Listen for auth state changes AeThexAuth.login_state_changed.connect(_on_login_state_changed) func _on_login_state_changed(is_logged_in: bool): login_changed.emit(is_logged_in) if is_logged_in: current_user = AeThexAuth.get_current_user() print("User logged in: ", current_user.display_name) else: current_user = {} print("User logged out") func is_logged_in() -> bool: return AeThexAuth.is_logged_in() func get_user_id() -> String: return current_user.get("user_id", "") func get_display_name() -> String: return current_user.get("display_name", "Guest") func get_email() -> String: return current_user.get("email", "") func is_guest() -> bool: return current_user.get("is_guest", false) func logout(): await AeThexAuth.logout() get_tree().change_scene_to_file("res://scenes/login_screen.tscn") ``` **Add to Project Settings:** ``` Project → Project Settings → Autoload Name: AuthManager Path: res://scripts/auth_manager.gd ``` --- ## Step 5: User Profiles Display and edit user profiles: ```gdscript # profile_screen.gd extends Control @onready var avatar_texture = $VBox/Avatar @onready var display_name_label = $VBox/DisplayName @onready var email_label = $VBox/Email @onready var user_id_label = $VBox/UserID @onready var edit_btn = $VBox/EditButton @onready var logout_btn = $VBox/LogoutButton func _ready(): edit_btn.pressed.connect(_on_edit_pressed) logout_btn.pressed.connect(_on_logout_pressed) load_profile() func load_profile(): var user = AeThexAuth.get_current_user() display_name_label.text = user.get("display_name", "Unknown") email_label.text = user.get("email", "No email") user_id_label.text = "ID: " + user.get("user_id", "unknown") # Load avatar if available if "avatar_url" in user: load_avatar(user.avatar_url) func load_avatar(url: String): var http = HTTPRequest.new() add_child(http) http.request_completed.connect(_on_avatar_loaded) http.request(url) func _on_avatar_loaded(result, response_code, headers, body): if response_code == 200: var image = Image.new() var error = image.load_png_from_buffer(body) if error == OK: avatar_texture.texture = ImageTexture.create_from_image(image) func _on_edit_pressed(): get_tree().change_scene_to_file("res://scenes/edit_profile.tscn") func _on_logout_pressed(): AuthManager.logout() ``` --- ## Step 6: Edit Profile Allow users to update their profile: ```gdscript # edit_profile.gd extends Control @onready var display_name_field = $VBox/DisplayNameField @onready var bio_field = $VBox/BioField @onready var save_btn = $VBox/SaveButton @onready var cancel_btn = $VBox/CancelButton var original_profile: Dictionary func _ready(): save_btn.pressed.connect(_on_save_pressed) cancel_btn.pressed.connect(_on_cancel_pressed) load_current_profile() func load_current_profile(): original_profile = AeThexAuth.get_current_user() display_name_field.text = original_profile.get("display_name", "") bio_field.text = original_profile.get("bio", "") func _on_save_pressed(): var new_profile = { "display_name": display_name_field.text, "bio": bio_field.text } save_btn.disabled = true var result = await AeThexAuth.update_profile(new_profile) save_btn.disabled = false if result.success: AuthManager.user_profile_updated.emit(new_profile) show_success("Profile updated!") await get_tree().create_timer(1.0).timeout go_back() else: show_error("Failed to update profile: " + result.error) func _on_cancel_pressed(): go_back() func go_back(): get_tree().change_scene_to_file("res://scenes/profile_screen.tscn") ``` --- ## Step 7: Password Reset Implement password reset functionality: ```gdscript # forgot_password.gd extends Control @onready var email_field = $VBox/EmailField @onready var send_btn = $VBox/SendButton @onready var back_btn = $VBox/BackButton @onready var status_label = $VBox/StatusLabel func _ready(): send_btn.pressed.connect(_on_send_pressed) back_btn.pressed.connect(_on_back_pressed) func _on_send_pressed(): var email = email_field.text if not validate_email(email): show_status("Invalid email address", Color.RED) return send_btn.disabled = true show_status("Sending reset email...", Color.YELLOW) var result = await AeThexAuth.send_password_reset_email(email) send_btn.disabled = false if result.success: show_status("Reset email sent! Check your inbox.", Color.GREEN) else: show_status("Failed: " + result.error, Color.RED) func validate_email(email: String) -> bool: var regex = RegEx.new() regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") return regex.search(email) != null func _on_back_pressed(): get_tree().change_scene_to_file("res://scenes/login_screen.tscn") ``` --- ## Step 8: Session Management Handle session timeouts and refresh: ```gdscript # session_manager.gd (autoload) extends Node const SESSION_CHECK_INTERVAL = 300 # 5 minutes const SESSION_TIMEOUT = 3600 # 1 hour var session_timer: Timer func _ready(): session_timer = Timer.new() session_timer.timeout.connect(_check_session) session_timer.wait_time = SESSION_CHECK_INTERVAL add_child(session_timer) if AuthManager.is_logged_in(): session_timer.start() func _check_session(): if not AeThexAuth.is_logged_in(): session_expired() return # Refresh session token var result = await AeThexAuth.refresh_session() if not result.success: session_expired() func session_expired(): session_timer.stop() show_session_expired_dialog() func show_session_expired_dialog(): var dialog = AcceptDialog.new() dialog.dialog_text = "Your session has expired. Please log in again." dialog.confirmed.connect(func(): get_tree().change_scene_to_file("res://scenes/login_screen.tscn")) get_tree().root.add_child(dialog) dialog.popup_centered() ``` --- ## Step 9: Account Linking Allow users to link multiple auth providers: ```gdscript # account_linking.gd extends Control @onready var linked_providers = $VBox/LinkedProviders @onready var available_providers = $VBox/AvailableProviders func _ready(): load_linked_providers() load_available_providers() func load_linked_providers(): var user = AeThexAuth.get_current_user() var providers = user.get("linked_providers", []) for provider in providers: var label = Label.new() label.text = "✓ " + provider.capitalize() + " (linked)" label.modulate = Color.GREEN linked_providers.add_child(label) var unlink_btn = Button.new() unlink_btn.text = "Unlink" unlink_btn.pressed.connect(_on_unlink_provider.bind(provider)) linked_providers.add_child(unlink_btn) func load_available_providers(): var all_providers = ["google", "github", "discord", "twitter"] var user = AeThexAuth.get_current_user() var linked = user.get("linked_providers", []) for provider in all_providers: if provider not in linked: var btn = Button.new() btn.text = "Link " + provider.capitalize() btn.pressed.connect(_on_link_provider.bind(provider)) available_providers.add_child(btn) func _on_link_provider(provider: String): var result = await AeThexAuth.link_oauth_provider(provider) if result.success: show_success("Linked " + provider.capitalize()) reload_ui() else: show_error("Failed to link: " + result.error) func _on_unlink_provider(provider: String): var result = await AeThexAuth.unlink_provider(provider) if result.success: show_success("Unlinked " + provider.capitalize()) reload_ui() else: show_error("Failed to unlink: " + result.error) func reload_ui(): # Clear and reload for child in linked_providers.get_children(): child.queue_free() for child in available_providers.get_children(): child.queue_free() await get_tree().process_frame load_linked_providers() load_available_providers() ``` --- ## Step 10: Guest Account Conversion Allow guests to create permanent accounts: ```gdscript # guest_conversion.gd extends Control @onready var email_field = $VBox/EmailField @onready var password_field = $VBox/PasswordField @onready var convert_btn = $VBox/ConvertButton func _ready(): convert_btn.pressed.connect(_on_convert_pressed) # Only show if user is guest if not AuthManager.is_guest(): queue_free() func _on_convert_pressed(): var email = email_field.text var password = password_field.text if not validate_input(email, password): return convert_btn.disabled = true var result = await AeThexAuth.convert_guest_to_account(email, password) convert_btn.disabled = false if result.success: show_success("Account created! Your progress is saved.") await get_tree().create_timer(2.0).timeout get_tree().change_scene_to_file("res://scenes/main_menu.tscn") else: show_error("Conversion failed: " + result.error) func validate_input(email: String, password: String) -> bool: if not validate_email(email): show_error("Invalid email address") return false if password.length() < 6: show_error("Password must be at least 6 characters") return false return true func validate_email(email: String) -> bool: var regex = RegEx.new() regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") return regex.search(email) != null ``` --- ## Security Best Practices ### 1. **Never Store Passwords** ```gdscript # ❌ DON'T var user_password = "secret123" # Never store passwords! # ✓ DO # Let AeThexAuth handle authentication securely ``` ### 2. **Validate Input** ```gdscript func validate_password(password: String) -> bool: if password.length() < 8: return false # Check for numbers var has_number = false var has_letter = false for c in password: if c.is_valid_int(): has_number = true if c.to_lower() >= 'a' and c.to_lower() <= 'z': has_letter = true return has_number and has_letter ``` ### 3. **Use HTTPS Only** ```gdscript # AeThex automatically uses HTTPS # Never disable SSL verification in production ``` ### 4. **Handle Tokens Securely** ```gdscript # Auth tokens are automatically managed by AeThexAuth # Don't try to extract or store them manually ``` ### 5. **Rate Limiting** ```gdscript var login_attempts = 0 var last_attempt_time = 0 func attempt_login(): var now = Time.get_ticks_msec() / 1000.0 if now - last_attempt_time > 60: login_attempts = 0 if login_attempts >= 5: show_error("Too many attempts. Try again later.") return login_attempts += 1 last_attempt_time = now # Proceed with login ``` --- ## Testing Test authentication flows: ```gdscript # test_auth.gd extends Node func _ready(): run_tests() func run_tests(): print("Testing authentication...") # Test guest login await test_guest_login() await AeThexAuth.logout() # Test email registration await test_email_registration() await AeThexAuth.logout() print("All auth tests passed!") func test_guest_login(): var result = await AeThexAuth.login_as_guest() assert(result.success, "Guest login failed") assert(AeThexAuth.is_logged_in(), "Not logged in after guest login") print("✓ Guest login works") func test_email_registration(): var test_email = "test%d@example.com" % Time.get_ticks_msec() var test_password = "Test123456" var result = await AeThexAuth.register_email(test_email, test_password) assert(result.success, "Registration failed") assert(AeThexAuth.is_logged_in(), "Not logged in after registration") print("✓ Email registration works") ``` --- ## Next Steps - **[Cloud Saves](FIRST_GAME_TUTORIAL.md#cloud-saves)** - Save user data - **[Analytics Tutorial](ANALYTICS_TUTORIAL.md)** - Track user behavior - **[API Reference](../API_REFERENCE.md#aethexauth-singleton)** - Complete auth API docs --- ## Summary You've learned how to: ✅ Implement email/password authentication ✅ Add OAuth login (Google, GitHub, Discord) ✅ Support guest accounts ✅ Manage user profiles ✅ Handle password resets ✅ Link multiple auth providers ✅ Convert guest accounts to permanent accounts Authentication is the foundation for cloud features, social systems, and user engagement! **Ready to save user data?** Check out [Cloud Saves](FIRST_GAME_TUTORIAL.md#cloud-saves)! 💾