Skip to content

WIP - Implement Google OAuth #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: 4.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 145 additions & 30 deletions addons/supabase/Auth/auth.gd
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ signal reset_email_sent()
signal token_refreshed(refreshed_user: SupabaseUser)
signal user_invited()
signal error(supabase_error: SupabaseAuthError)
signal received_auth(auth_code: String)

const _auth_endpoint : String = "/auth/v1"
const _provider_endpoint : String = _auth_endpoint+"/authorize"
const _provider_token_endpoint : String = _auth_endpoint + "/token"
const _signin_endpoint : String = _auth_endpoint+"/token?grant_type=password"
const _signin_otp_endpoint : String = _auth_endpoint+"/otp"
const _verify_otp_endpoint : String = _auth_endpoint+"/verify"
Expand All @@ -55,7 +57,8 @@ var client : SupabaseUser
func _init(conf : Dictionary, head : PackedStringArray) -> void:
_config = conf
_header = head
name = "Authentication"
name = "Authentication"
tcp_timer.timeout.connect(_tcp_stream_timer)

func __get_session_header() -> PackedStringArray :
return PackedStringArray([_bearer[0] % ( _auth if not _auth.is_empty() else _config.supabaseKey )])
Expand All @@ -71,7 +74,7 @@ func sign_up(email : String, password : String, data: Dictionary = {}) -> AuthTa
var payload : Dictionary = {"email":email, "password":password, "data":data}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.SIGNUP,
_config.supabaseUrl + _signup_endpoint,
_config.supabaseUrl + _signup_endpoint,
_header,
JSON.stringify(payload)
)
Expand All @@ -86,7 +89,7 @@ func sign_up_phone(phone : String, password : String, data: Dictionary = {}) ->
var payload : Dictionary = {"phone":phone, "password":password, "data":data}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.SIGNUPPHONEPASSWORD,
_config.supabaseUrl + _signup_endpoint,
_config.supabaseUrl + _signup_endpoint,
_header,
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -99,7 +102,7 @@ func sign_in(email : String, password : String = "") -> AuthTask:
var payload : Dictionary = {"email":email, "password":password}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.SIGNIN,
_config.supabaseUrl + _signin_endpoint,
_config.supabaseUrl + _signin_endpoint,
_header,
JSON.stringify(payload)
)
Expand All @@ -114,7 +117,7 @@ func sign_in_phone(phone : String, password : String = "") -> AuthTask:
var payload : Dictionary = {"phone":phone, "password":password}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.SIGNIN,
_config.supabaseUrl + _signin_endpoint,
_config.supabaseUrl + _signin_endpoint,
_header,
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -128,7 +131,7 @@ func sign_in_otp(phone : String) -> AuthTask:
var payload : Dictionary = {"phone":phone}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.SIGNINOTP,
_config.supabaseUrl + _signin_otp_endpoint,
_config.supabaseUrl + _signin_otp_endpoint,
_header,
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -141,7 +144,7 @@ func verify_otp(phone : String, token : String) -> AuthTask:
var payload : Dictionary = {phone = phone, token = token, type = "sms"}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.VERIFYOTP,
_config.supabaseUrl + _verify_otp_endpoint,
_config.supabaseUrl + _verify_otp_endpoint,
_header,
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -153,7 +156,7 @@ func verify_otp_email(email : String, token : String, type : String) -> AuthTask
var payload : Dictionary = {email = email, token = token, type = type}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.VERIFYOTP,
_config.supabaseUrl + _verify_otp_endpoint,
_config.supabaseUrl + _verify_otp_endpoint,
_header,
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -168,20 +171,92 @@ func sign_in_anonymous() -> AuthTask:
return auth_task


# [ CURRENTLY UNSUPPORTED ]
# Sign in with a Provider
# @provider = Providers.PROVIDER
func sign_in_with_provider(provider : String, grab_from_browser : bool = true, port : int = 3000) -> void:
OS.shell_open(_config.supabaseUrl + _provider_endpoint + "?provider="+provider)
# ! to be implemented
pass

# This function initiates a provider login (e.g. Google) by opening the browser
# and waiting on a local TCPServer for the OAuth redirect.
func sign_in_with_provider(provider: String, port : int = 3000) -> void:
# Build the callback URL – this must match what you set in your Supabase project.
var callback_url = "http://127.0.0.1:%s" % port

# Start the TCPServer to listen for the redirect
var err = tcp_server.listen(port, "127.0.0.1")
if err != OK:
error.emit("TCPServer error: " + str(err))
return

add_child(tcp_timer)
tcp_timer.start(1)

# Construct the URL parameters
var params = {
"client_id": "YOUR CLIENT ID",
"redirect_uri": callback_url,
"response_type": "code",
"provider": provider
}

# Build query string (using percent encoding for safety)
var query_arr : Array = []
for key in params.keys():
query_arr.append("%s=%s" % [key, params[key].uri_encode()])
var query_string = "&".join(query_arr)

# Build the full authorization URL.
var auth_url = _config.supabaseUrl + _provider_endpoint + "?" + query_string
print(auth_url)
# Open the URL in the user's browser.
OS.shell_open(auth_url)

# Wait for the incoming HTTP redirect to provide the auth code.
var auth_code = await received_auth

# Exchange the authorization code for tokens.
var token_fetch_task = await sign_in_with_oauth(auth_code, callback_url)
await token_fetch_task.completed


var token_data = token_fetch_task.user.dict.get("code", "")

if not token_data:
error.emit("OAuth didn't send a code back")
return

# Update our internal authentication state.
_auth = token_data.access_token
# Assume token_data also contains refresh_token and expires_in.
client = SupabaseUser.new(token_data) # SupabaseUser should be defined to accept these details.
_expires_in = token_data.expires_in

# Emit the signed_in signal and start token refresh if needed.
signed_in.emit(client)
refresh_token()

func sign_in_with_oauth(auth_code: String, callback_url: String) -> AuthTask:
var payload : Dictionary = {
"provider": "google",
"code": auth_code,
"redirect_uri": callback_url,
"grant_type": "pkce",
"client_id": "your google client id",
"client_secret": "your google client secret"
}
var content = "&".join(payload.keys().map(func(name): return "%s=%s" % [name, payload[name].uri_encode()]))
print(content)
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.SIGNIN,
_config.supabaseUrl + _provider_token_endpoint,
PackedStringArray([
"apikey: %s"%[_config.supabaseKey],
"Content-Type: application/json",
]),
JSON.stringify(payload))
_process_task(auth_task)
return auth_task

# If a user is logged in, this will log it out
func sign_out() -> AuthTask:
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.LOGOUT,
_config.supabaseUrl + _logout_endpoint,
_config.supabaseUrl + _logout_endpoint,
_header + __get_session_header())
_process_task(auth_task)
return auth_task
Expand All @@ -194,7 +269,7 @@ func send_magic_link(email : String) -> AuthTask:
var payload : Dictionary = {"email":email}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.MAGICLINK,
_config.supabaseUrl + _magiclink_endpoint,
_config.supabaseUrl + _magiclink_endpoint,
_header,
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -205,7 +280,7 @@ func send_magic_link(email : String) -> AuthTask:
func user(user_access_token : String = _auth) -> AuthTask:
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.USER,
_config.supabaseUrl + _user_endpoint,
_config.supabaseUrl + _user_endpoint,
_header + __get_session_header())
_process_task(auth_task)
return auth_task
Expand All @@ -216,7 +291,7 @@ func update(email : String, password : String = "", data : Dictionary = {}) -> A
var payload : Dictionary = {"email":email, "password":password, "data" : data}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.UPDATE,
_config.supabaseUrl + _user_endpoint,
_config.supabaseUrl + _user_endpoint,
_header + __get_session_header(),
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -227,7 +302,7 @@ func update_email(email : String) -> AuthTask:
var payload : Dictionary = {"email":email}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.UPDATE,
_config.supabaseUrl + _user_endpoint,
_config.supabaseUrl + _user_endpoint,
_header + __get_session_header(),
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -238,7 +313,7 @@ func reset_password_for_email(email : String) -> AuthTask:
var payload : Dictionary = {"email":email}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.RECOVER,
_config.supabaseUrl + _reset_password_endpoint,
_config.supabaseUrl + _reset_password_endpoint,
_header,
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -250,7 +325,7 @@ func invite_user_by_email(email : String) -> AuthTask:
var payload : Dictionary = {"email":email}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.INVITE,
_config.supabaseUrl + _invite_endpoint,
_config.supabaseUrl + _invite_endpoint,
_header + __get_session_header(),
JSON.stringify(payload))
_process_task(auth_task)
Expand All @@ -264,11 +339,11 @@ func refresh_token(refresh_token : String = client.refresh_token, expires_in : f
var payload : Dictionary = {refresh_token = refresh_token}
var auth_task : AuthTask = AuthTask.new()._setup(
AuthTask.Task.REFRESH,
_config.supabaseUrl + _refresh_token_endpoint,
_config.supabaseUrl + _refresh_token_endpoint,
_header + __get_session_header(),
JSON.stringify(payload))
_process_task(auth_task)
return auth_task
return auth_task



Expand Down Expand Up @@ -313,7 +388,7 @@ func _on_task_completed(task : AuthTask) -> void:
signed_in.emit(client)
AuthTask.Task.SIGNINOTP:
signed_in_otp.emit(client)
AuthTask.Task.UPDATE:
AuthTask.Task.UPDATE:
user_updated.emit(client)
AuthTask.Task.REFRESH:
token_refreshed.emit(client)
Expand All @@ -322,7 +397,7 @@ func _on_task_completed(task : AuthTask) -> void:
AuthTask.Task.SIGNINANONYM:
signed_in_anonyous.emit()
refresh_token()
else:
else:
if task.data.is_empty() or task.data == null:
match task._code:
AuthTask.Task.MAGICLINK:
Expand All @@ -339,6 +414,46 @@ func _on_task_completed(task : AuthTask) -> void:

# A timer used to listen through TCP on the redirect uri of the request
func _tcp_stream_timer() -> void:
var peer : StreamPeer = tcp_server.take_connection()
# ! to be implemented
pass
var connection: StreamPeer = tcp_server.take_connection()
if not connection:
print("No connection")
return
# Read available data (assumes the entire HTTP request is available).
var request_data = connection.get_string(connection.get_available_bytes())
if not request_data:
print("No data")
return
# Example request: "GET /callback?code=AUTHCODE&state=... HTTP/1.1"
var parts = request_data.split(" ")
if parts.size() <= 1:
print("no parts")
return
var url_part = parts[1]
var query_index = url_part.find("?")
if query_index == -1:
print("no index")
print(request_data)
return

var query_string = url_part.substr(query_index + 1, url_part.length())
var query_set = {}

for param in query_string.split("&"):
print("param ", param)
var key_value = param.split("=")

# Other keys: state, scope="email+profile+
if key_value.size() != 2:
continue
query_set[key_value[0]] = key_value[1]

if "code" in query_set:
var auth_code = query_set["code"]

# Send a simple HTTP response back to the browser.
var response_html = "<html><body>Login successful. You can close this window.</body></html>"
var response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %s\r\n\r\n%s" % [str(response_html.length()), response_html]
connection.put_data(response.to_utf8_buffer())
tcp_server.stop() # Stop listening after obtaining the code.
received_auth.emit(auth_code)
return
2 changes: 0 additions & 2 deletions addons/supabase/Auth/auth_error.gd
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,3 @@ func _init(dictionary : Dictionary = {}) -> void:
code = str(_error.get("code", -1))
message = _error.get("msg", "(undefined)")
# different body for same api source ???


8 changes: 3 additions & 5 deletions addons/supabase/Auth/auth_task.gd
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ func _on_task_completed(result : int, response_code : int, headers : PackedStrin
if result != 0:
complete(null, {}, SupabaseAuthError.new({ error = "Could not connect", code = result }))
return

var result_body : Dictionary

if(!body.is_empty()):
result_body = JSON.parse_string(body.get_string_from_utf8())

match response_code:
200:
match _code:
Expand All @@ -60,5 +60,3 @@ func _on_task_completed(result : int, response_code : int, headers : PackedStrin
func complete(_user : SupabaseUser = null, _data : Dictionary = {}, _error : SupabaseAuthError = null) -> void:
user = _user
super._complete(_data, _error)