pokepurple/addons/twitcher/lib/oOuch/oauth_token_handler.gd

246 lines
7.9 KiB
GDScript3
Raw Normal View History

@icon("./security-icon.svg")
@tool
extends Node
class_name OAuthTokenHandler
const OAuthHTTPClient = preload("res://addons/twitcher/lib/http/buffered_http_client.gd")
const OAuthDeviceCodeResponse = preload("./oauth_device_code_response.gd")
## Handles refreshing and resolving access and refresh tokens.
const HEADERS = {
"Accept": "*/*",
"Content-Type": "application/x-www-form-urlencoded"
}
const SECONDS_TO_CHECK_EARLIER = 60
## Called when new access token is available
signal token_resolved(tokens: OAuthToken)
## Called when token can't be refreshed cause auth was removed or refresh token expired
signal unauthenticated()
## Where to get the tokens from
@export var oauth_setting: OAuthSetting
## Holds the current set of tokens
@export var token: OAuthToken: set = _update_token
## Client to request new tokens
var _http_client : OAuthHTTPClient
## Is currently requesting tokens
var _requesting_token: bool = false
## Timer to refresh tokens
var _expiration_check_timer: Timer
func _ready() -> void:
_http_client = OAuthHTTPClient.new()
_http_client.name = "OAuthTokenClient"
add_child(_http_client)
_expiration_check_timer = Timer.new()
_expiration_check_timer.name = "ExpirationCheck"
_expiration_check_timer.timeout.connect(refresh_tokens)
add_child(_expiration_check_timer)
update_expiration_check()
func _enter_tree() -> void:
if not is_instance_valid(token):
token = OAuthToken.new()
else:
token.changed.connect(update_expiration_check)
func _exit_tree() -> void:
if is_instance_valid(token):
token.changed.disconnect(update_expiration_check)
func _update_token(val: OAuthToken) -> void:
if is_instance_valid(token) and is_inside_tree():
token.changed.disconnect(update_expiration_check)
token = val
if is_instance_valid(token) and is_inside_tree():
token.changed.connect(update_expiration_check)
func update_expiration_check() -> void:
var current_time = Time.get_unix_time_from_system()
var expiration = token.get_expiration()
if expiration == 0:
_expiration_check_timer.stop()
return
_expiration_check_timer.start(expiration - current_time - SECONDS_TO_CHECK_EARLIER)
logDebug("Refresh token (%s) in %s seconds" % [token._identifier, roundf(_expiration_check_timer.wait_time)])
## Checks if tokens expires and starts refreshing it. (called often hold footprintt small)
func _check_token_refresh() -> void:
if _requesting_token: return
if token_needs_refresh():
logInfo("Token (%s) needs refresh" % token._identifier)
refresh_tokens()
## Requests the tokens
func request_token(grant_type: String, auth_code: String = ""):
if _requesting_token: return
_requesting_token = true
logInfo("Request token (for %s) via '%s'" % [token._identifier, grant_type])
var request_params = [
"grant_type=%s" % grant_type,
"client_id=%s" % oauth_setting.client_id,
"client_secret=%s" % oauth_setting.get_client_secret()
]
if auth_code != "":
request_params.append("code=%s" % auth_code)
if grant_type == "authorization_code":
request_params.append("&redirect_uri=%s" % oauth_setting.redirect_url)
var request_body = "&".join(request_params)
var request = _http_client.request(oauth_setting.token_url, HTTPClient.METHOD_POST, HEADERS, request_body)
await _handle_token_request(request)
_requesting_token = false
func request_device_token(device_code_repsonse: OAuthDeviceCodeResponse, scopes: String, grant_type: String = "urn:ietf:params:oauth:grant-type:device_code") -> void:
if _requesting_token: return
_requesting_token = true
logInfo("request token (for %s) via urn:ietf:params:oauth:grant-type:device_code" % token._identifier)
var parameters = [
"client_id=%s" % oauth_setting.client_id,
"grant_type=%s" % grant_type,
"device_code=%s" % device_code_repsonse.device_code,
"scopes=%s" % scopes
]
var request_body = "&".join(parameters)
# Time when the code is expired and we don't poll anymore
var expire_data = Time.get_unix_time_from_system() + device_code_repsonse.expires_in
while expire_data > Time.get_unix_time_from_system():
var request = _http_client.request(oauth_setting.token_url, HTTPClient.METHOD_POST, HEADERS, request_body)
var response = await _http_client.wait_for_request(request)
var response_string: String = response.response_data.get_string_from_utf8()
var response_data = JSON.parse_string(response_string)
if response.response_code == 200:
_update_tokens_from_response(response_data)
_requesting_token = false
return
elif response.response_code == 400 && response_string.contains("authorization_pending"):
# Awaits for this amount of time until retry
await get_tree().create_timer(device_code_repsonse.interval, true, false, true).timeout
elif response.response_code == 400:
unauthenticated.emit()
_requesting_token = false
return
# Handle Timeout
unauthenticated.emit()
_requesting_token = false
## Uses the refresh token if possible to refresh all tokens
func refresh_tokens() -> void:
if not oauth_setting.is_valid():
logDebug("Try to refresh token (%s) but oauth settings are invalid. Can't refresh token." % token._identifier)
return
if _requesting_token: return
_requesting_token = true
logInfo("use refresh (%s) token" % token._identifier)
if token.has_refresh_token():
var request_body = "client_id=%s&client_secret=%s&refresh_token=%s&grant_type=refresh_token" % [oauth_setting.client_id, oauth_setting.get_client_secret(), token.get_refresh_token()]
var request = _http_client.request(oauth_setting.token_url, HTTPClient.METHOD_POST, HEADERS, request_body)
if await _handle_token_request(request):
logInfo("token (%s) got refreshed" % token._identifier)
else:
unauthenticated.emit()
else:
unauthenticated.emit()
_requesting_token = false
## Gets information from the response and update values returns true when success otherwise false
func _handle_token_request(request: OAuthHTTPClient.RequestData) -> bool:
var response = await _http_client.wait_for_request(request)
var response_string = response.response_data.get_string_from_utf8()
var result = JSON.parse_string(response_string)
if response.response_code == 200:
_update_tokens_from_response(result)
return true
else:
# Reset expiration cause token wasn't refreshed correctly.
token.invalidate()
logError("token (for %s) could not be fetched ResponseCode %s / Body %s" % [token._identifier, response.response_code, response_string])
return false
func _update_tokens_from_response(result: Dictionary):
var scopes: Array[String] = []
for scope in result.get("scope", []): scopes.append(scope)
update_tokens(result["access_token"], \
result.get("refresh_token", ""), \
result.get("expires_in", -1), \
scopes)
## Updates the token. Result is the response data of an token request.
func update_tokens(access_token: String, refresh_token: String = "", expires_in: int = -1, scopes: Array[String] = []):
token.update_values(access_token, refresh_token, expires_in, scopes)
token_resolved.emit(token)
logInfo("token (%s) resolved" % token._identifier)
func get_token_expiration() -> String:
return Time.get_datetime_string_from_unix_time(token._expire_date)
## Checks if the token are valud
func is_token_valid() -> bool:
return token.is_token_valid()
## Checks if the token is expired and can be refreshed
func token_needs_refresh() -> bool:
return !token.is_token_valid() && token.has_refresh_token()
func get_access_token() -> String: return await token.get_access_token()
func has_refresh_token() -> bool: return token.has_refresh_token()
func get_scopes() -> PackedStringArray: return token.get_scopes()
# === LOGGER ===
static var logger: Dictionary = {}
static func set_logger(error: Callable, info: Callable, debug: Callable) -> void:
logger.debug = debug
logger.info = info
logger.error = error
static func logDebug(text: String) -> void:
if logger.has("debug"): logger.debug.call(text)
static func logInfo(text: String) -> void:
if logger.has("info"): logger.info.call(text)
static func logError(text: String) -> void:
if logger.has("error"): logger.error.call(text)