251 lines
7.9 KiB
GDScript
251 lines
7.9 KiB
GDScript
extends Node
|
|
|
|
signal auth_changed
|
|
signal session_warning(seconds_remaining: int)
|
|
signal session_countdown_changed(seconds_remaining: int)
|
|
signal session_refresh_started
|
|
signal session_refreshed
|
|
signal session_refresh_failed(message: String, terminal: bool)
|
|
signal session_expired(reason: String)
|
|
|
|
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
|
|
const DEFAULT_AUTH_BASE_URL := "https://pauth.ranaze.com/api/Auth"
|
|
const WARNING_SECONDS := 120
|
|
const REFRESH_TIMEOUT_SECONDS := 20.0
|
|
|
|
var is_logged_in := false
|
|
var username := ""
|
|
var role := ""
|
|
var access_token := ""
|
|
var refresh_token := ""
|
|
var expires_at := ""
|
|
var expiration_unix := 0.0
|
|
var refresh_in_progress := false
|
|
var warning_active := false
|
|
var requires_legal_acceptance := false
|
|
var terms_version := ""
|
|
var privacy_version := ""
|
|
var terms_url := ""
|
|
var privacy_url := ""
|
|
var auth_base_url := DEFAULT_AUTH_BASE_URL
|
|
var expiry_timer: Timer
|
|
var refresh_request: HTTPRequest
|
|
var session_revision := 0
|
|
var refresh_revision := -1
|
|
|
|
|
|
func _ready() -> void:
|
|
auth_base_url = EndpointConfig.resolve(
|
|
str(ProjectSettings.get_setting("step3/auth_api_base_url", DEFAULT_AUTH_BASE_URL)),
|
|
"/api/Auth")
|
|
expiry_timer = Timer.new()
|
|
expiry_timer.wait_time = 1.0
|
|
expiry_timer.timeout.connect(_evaluate_expiry)
|
|
add_child(expiry_timer)
|
|
refresh_request = HTTPRequest.new()
|
|
refresh_request.timeout = REFRESH_TIMEOUT_SECONDS
|
|
refresh_request.request_completed.connect(_on_refresh_completed)
|
|
add_child(refresh_request)
|
|
|
|
func set_session(new_username: String, token: String, new_refresh_token: String = "", new_role: String = "", token_expiration: String = "", legal: Dictionary = {}) -> void:
|
|
session_revision += 1
|
|
is_logged_in = not token.is_empty()
|
|
username = new_username
|
|
access_token = token
|
|
refresh_token = new_refresh_token
|
|
role = new_role
|
|
expires_at = token_expiration
|
|
expiration_unix = _parse_expiration(token_expiration, token)
|
|
refresh_in_progress = false
|
|
refresh_revision = -1
|
|
warning_active = false
|
|
set_legal_state(legal, false)
|
|
if is_logged_in and expiration_unix > 0.0:
|
|
expiry_timer.start()
|
|
else:
|
|
expiry_timer.stop()
|
|
auth_changed.emit()
|
|
if is_logged_in:
|
|
_evaluate_expiry.call_deferred()
|
|
|
|
|
|
func set_legal_state(legal: Dictionary, notify := true) -> void:
|
|
requires_legal_acceptance = bool(legal.get("requiresLegalAcceptance", false))
|
|
terms_version = str(legal.get("termsVersion", terms_version))
|
|
privacy_version = str(legal.get("privacyVersion", privacy_version))
|
|
terms_url = str(legal.get("termsUrl", terms_url))
|
|
privacy_url = str(legal.get("privacyUrl", privacy_url))
|
|
if notify:
|
|
auth_changed.emit()
|
|
|
|
|
|
func clear_session() -> void:
|
|
_clear_session_fields()
|
|
auth_changed.emit()
|
|
|
|
|
|
func expire_session(reason: String = "expired") -> void:
|
|
if not is_logged_in and access_token.is_empty():
|
|
return
|
|
_clear_session_fields()
|
|
auth_changed.emit()
|
|
session_expired.emit(reason)
|
|
|
|
|
|
func request_session_refresh() -> void:
|
|
if refresh_in_progress or not is_logged_in:
|
|
return
|
|
if refresh_token.is_empty() or username.is_empty():
|
|
expire_session("refresh_unavailable")
|
|
return
|
|
refresh_in_progress = true
|
|
refresh_revision = session_revision
|
|
session_refresh_started.emit()
|
|
var headers := PackedStringArray(["Content-Type: application/json"])
|
|
var payload := JSON.stringify({"username": username, "refreshToken": refresh_token})
|
|
var error := refresh_request.request(
|
|
"%s/refresh" % auth_base_url.rstrip("/"),
|
|
headers,
|
|
HTTPClient.METHOD_POST,
|
|
payload)
|
|
if error != OK:
|
|
refresh_in_progress = false
|
|
refresh_revision = -1
|
|
session_refresh_failed.emit("Unable to contact the authentication service. Check your connection and try again.", false)
|
|
_evaluate_expiry()
|
|
|
|
|
|
func can_send_authenticated_request() -> bool:
|
|
if not is_logged_in or access_token.is_empty():
|
|
return false
|
|
if expiration_unix <= 0.0 or Time.get_unix_time_from_system() < expiration_unix:
|
|
return true
|
|
if not refresh_in_progress:
|
|
expire_session("expired")
|
|
return false
|
|
|
|
|
|
func handle_unauthorized(token_used: String = "") -> void:
|
|
if not is_logged_in:
|
|
return
|
|
if not token_used.is_empty() and token_used != access_token:
|
|
return
|
|
expire_session("unauthorized")
|
|
|
|
|
|
func seconds_until_expiry() -> int:
|
|
if expiration_unix <= 0.0:
|
|
return 0
|
|
return maxi(0, int(ceil(expiration_unix - Time.get_unix_time_from_system())))
|
|
|
|
|
|
func _evaluate_expiry() -> void:
|
|
if not is_logged_in or expiration_unix <= 0.0:
|
|
return
|
|
var remaining := seconds_until_expiry()
|
|
if remaining <= 0:
|
|
if refresh_in_progress:
|
|
if warning_active:
|
|
session_countdown_changed.emit(0)
|
|
return
|
|
expire_session("expired")
|
|
return
|
|
if remaining <= WARNING_SECONDS:
|
|
if not warning_active:
|
|
warning_active = true
|
|
session_warning.emit(remaining)
|
|
session_countdown_changed.emit(remaining)
|
|
|
|
|
|
func _on_refresh_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
|
var completed_revision := refresh_revision
|
|
refresh_in_progress = false
|
|
refresh_revision = -1
|
|
if completed_revision != session_revision or not is_logged_in:
|
|
return
|
|
var succeeded := result == HTTPRequest.RESULT_SUCCESS and response_code >= 200 and response_code < 300
|
|
if not succeeded:
|
|
var terminal := response_code in [400, 401, 403] or seconds_until_expiry() <= 0
|
|
var message := "Unable to renew the session. Check your connection and try again."
|
|
if terminal:
|
|
message = "Your session could not be renewed. Please sign in again."
|
|
session_refresh_failed.emit(message, terminal)
|
|
if terminal:
|
|
expire_session("refresh_failed")
|
|
else:
|
|
_evaluate_expiry()
|
|
return
|
|
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
|
if typeof(parsed) != TYPE_DICTIONARY:
|
|
session_refresh_failed.emit("The authentication service returned an invalid response.", true)
|
|
expire_session("refresh_failed")
|
|
return
|
|
var new_access_token := str(parsed.get("accessToken", ""))
|
|
var new_expiration := str(parsed.get("exp", ""))
|
|
var new_expiration_unix := _parse_expiration(new_expiration, new_access_token)
|
|
if new_access_token.is_empty() or new_expiration_unix <= Time.get_unix_time_from_system():
|
|
session_refresh_failed.emit("The authentication service returned an invalid session.", true)
|
|
expire_session("refresh_failed")
|
|
return
|
|
access_token = new_access_token
|
|
expires_at = new_expiration
|
|
expiration_unix = new_expiration_unix
|
|
warning_active = false
|
|
var legal_value: Variant = parsed.get("legal", {})
|
|
if typeof(legal_value) == TYPE_DICTIONARY:
|
|
set_legal_state(legal_value, false)
|
|
session_revision += 1
|
|
expiry_timer.start()
|
|
auth_changed.emit()
|
|
session_refreshed.emit()
|
|
|
|
|
|
func _clear_session_fields() -> void:
|
|
session_revision += 1
|
|
if refresh_request != null and refresh_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
|
|
refresh_request.cancel_request()
|
|
if expiry_timer != null:
|
|
expiry_timer.stop()
|
|
is_logged_in = false
|
|
username = ""
|
|
role = ""
|
|
access_token = ""
|
|
refresh_token = ""
|
|
expires_at = ""
|
|
expiration_unix = 0.0
|
|
refresh_in_progress = false
|
|
refresh_revision = -1
|
|
warning_active = false
|
|
requires_legal_acceptance = false
|
|
terms_version = ""
|
|
privacy_version = ""
|
|
terms_url = ""
|
|
privacy_url = ""
|
|
|
|
|
|
func _parse_expiration(value: String, token: String = "") -> float:
|
|
var cleaned := value.strip_edges()
|
|
if not cleaned.is_empty() and cleaned != "<null>":
|
|
if cleaned.is_valid_float():
|
|
var numeric := float(cleaned)
|
|
return numeric / 1000.0 if numeric > 100000000000.0 else numeric
|
|
var iso_value := cleaned.trim_suffix("Z")
|
|
var parsed_iso := Time.get_unix_time_from_datetime_string(iso_value)
|
|
if parsed_iso > 0:
|
|
return float(parsed_iso)
|
|
return _expiration_from_jwt(token)
|
|
|
|
|
|
func _expiration_from_jwt(token: String) -> float:
|
|
var segments := token.split(".")
|
|
if segments.size() < 2:
|
|
return 0.0
|
|
var encoded := str(segments[1]).replace("-", "+").replace("_", "/")
|
|
while encoded.length() % 4 != 0:
|
|
encoded += "="
|
|
var payload_text := Marshalls.base64_to_raw(encoded).get_string_from_utf8()
|
|
var payload: Variant = JSON.parse_string(payload_text)
|
|
if typeof(payload) != TYPE_DICTIONARY:
|
|
return 0.0
|
|
return float(payload.get("exp", 0.0))
|