AeThex-Engine-Core/docs/tutorials/AUTH_TUTORIAL.md

21 KiB

Authentication Tutorial

Learn how to add user authentication to your AeThex game with email/password, OAuth, and guest login support.


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


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

Step 1: Connect to AeThex Cloud

First, ensure cloud services are connected:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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:

# 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

# ❌ DON'T
var user_password = "secret123"  # Never store passwords!

# ✓ DO
# Let AeThexAuth handle authentication securely

2. Validate Input

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

# AeThex automatically uses HTTPS
# Never disable SSL verification in production

4. Handle Tokens Securely

# Auth tokens are automatically managed by AeThexAuth
# Don't try to extract or store them manually

5. Rate Limiting

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:

# 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


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! 💾