-
Notifications
You must be signed in to change notification settings - Fork 3
Silent signin #8
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
Open
uogbuji
wants to merge
4
commits into
NiqueWrld:main
Choose a base branch
from
OoriData:feature/silent-signin
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,8 @@ A native Android plugin for Godot 4.2+ that enables Google Sign-In using the mod | |
| - ✅ Supports Godot 4.2+ (v2 plugin architecture) | ||
| - ✅ Auto-select previously signed-in accounts | ||
| - ✅ Account chooser support | ||
| - ✅ Custom nonce support for backend verification (Supabase, Firebase, custom OIDC) | ||
| - ✅ Silent (non-interactive) sign-in for auto-login and session recovery | ||
|
|
||
| ## Requirements | ||
|
|
||
|
|
@@ -104,16 +106,22 @@ func _on_sign_out_complete(): | |
| | `initialize(web_client_id: String)` | Initialize with your Web Client ID from Google Cloud Console | | ||
| | `isInitialized() -> bool` | Check if plugin is initialized | | ||
| | `signIn()` | Start sign-in flow (auto-selects if previously authorized) | | ||
| | `signInWithNonce(raw_nonce: String)` | Same as `signIn()` but uses a caller-supplied raw nonce for backend verification | | ||
| | `signInWithAccountChooser()` | Sign in with account picker | | ||
| | `signInWithAccountChooserWithNonce(raw_nonce: String)` | Same as `signInWithAccountChooser()` with a caller-supplied raw nonce | | ||
| | `signInWithGoogleButton()` | Sign in using Google's branded button flow | | ||
| | `signInWithGoogleButtonWithNonce(raw_nonce: String)` | Same as `signInWithGoogleButton()` with a caller-supplied raw nonce | | ||
| | `signOut()` | Sign out and clear credential state | | ||
| | `silentSignIn()` | Attempt non-interactive sign-in; emits `silent_sign_in_failed` if interaction would be required | | ||
| | `silentSignInWithNonce(raw_nonce: String)` | Same as `silentSignIn()` with a caller-supplied raw nonce | | ||
|
|
||
| ### Signals | ||
|
|
||
| | Signal | Parameters | Description | | ||
| |--------|------------|-------------| | ||
| | `sign_in_success` | `id_token: String, email: String, display_name: String` | Emitted on successful sign-in | | ||
| | `sign_in_failed` | `error: String` | Emitted when sign-in fails | | ||
| | `sign_in_success` | `id_token: String, email: String, display_name: String` | Emitted on successful sign-in (all methods including silent) | | ||
| | `sign_in_failed` | `error: String` | Emitted when an interactive sign-in fails | | ||
| | `silent_sign_in_failed` | `error: String` | Emitted when sign-in fails with an unrecoverable error (interactive or silent) | | ||
| | `sign_out_complete` | None | Emitted when sign-out completes | | ||
|
Comment on lines
+122
to
125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Signal descriptions are swapped. Based on the Kotlin implementation:
The current descriptions have these reversed. 📝 Proposed fix | Signal | Parameters | Description |
|--------|------------|-------------|
| `sign_in_success` | `id_token: String, email: String, display_name: String` | Emitted on successful sign-in (all methods including silent) |
-| `sign_in_failed` | `error: String` | Emitted when an interactive sign-in fails |
-| `silent_sign_in_failed` | `error: String` | Emitted when sign-in fails with an unrecoverable error (interactive or silent) |
+| `sign_in_failed` | `error: String` | Emitted when sign-in fails (interactive failures or unrecoverable errors in any mode) |
+| `silent_sign_in_failed` | `error: String` | Emitted when silent sign-in cannot auto-select an authorized account |🤖 Prompt for AI Agents |
||
|
|
||
| ## Firebase Authentication | ||
|
|
@@ -136,6 +144,99 @@ func _sign_in_with_firebase(google_id_token: String): | |
| http.request(url, ["Content-Type: application/json"], HTTPClient.METHOD_POST, JSON.stringify(body)) | ||
| ``` | ||
|
|
||
| ## Backend Verification with Nonce (Supabase / Firebase / Custom OIDC) | ||
|
|
||
| Backends that enforce OIDC replay protection (e.g. Supabase `signInWithIdToken`, Firebase Admin SDK) require a **raw nonce** to verify the ID token's `nonce` claim. | ||
|
|
||
| **How it works:** | ||
| 1. Your app generates a random raw nonce. | ||
| 2. You pass it to one of the `WithNonce` methods; the plugin SHA-256 hashes it and forwards the hash to Google. | ||
| 3. Google embeds that hash in the ID token's `nonce` claim. | ||
| 4. On `sign_in_success` you send both the `id_token` **and** your original raw nonce to your backend; the backend hashes the raw nonce and compares it to the claim. | ||
|
|
||
| ```gdscript | ||
| extends Node | ||
|
|
||
| var google_sign_in: Object = null | ||
| var _raw_nonce: String = "" | ||
|
|
||
| func _ready(): | ||
| if OS.get_name() == "Android" and Engine.has_singleton("GodotGoogleSignIn"): | ||
| google_sign_in = Engine.get_singleton("GodotGoogleSignIn") | ||
| google_sign_in.connect("sign_in_success", _on_sign_in_success) | ||
| google_sign_in.connect("sign_in_failed", _on_sign_in_failed) | ||
| google_sign_in.initialize("YOUR_WEB_CLIENT_ID.apps.googleusercontent.com") | ||
|
|
||
| func sign_in_with_backend(): | ||
| if google_sign_in: | ||
| # Generate a cryptographically random raw nonce | ||
| var crypto = Crypto.new() | ||
| _raw_nonce = crypto.generate_random_bytes(32).hex_encode() | ||
| google_sign_in.signInWithNonce(_raw_nonce) | ||
|
|
||
| func _on_sign_in_success(id_token: String, email: String, display_name: String): | ||
| # Send id_token + raw nonce to your backend | ||
| _authenticate_with_supabase(id_token, _raw_nonce) | ||
|
|
||
| func _authenticate_with_supabase(id_token: String, raw_nonce: String): | ||
| # Example: Supabase signInWithIdToken | ||
| var body = JSON.stringify({ | ||
| "provider": "google", | ||
| "id_token": id_token, | ||
| "nonce": raw_nonce # raw (unhashed) nonce | ||
| }) | ||
| # POST to your Supabase endpoint ... | ||
| ``` | ||
|
|
||
| ## Silent Sign-In (Auto-Login / Session Recovery) | ||
|
|
||
| Use `silentSignIn()` to attempt sign-in without showing any UI — ideal for auto-login on startup or recovering a session after a 401 error. | ||
|
|
||
| **How it works:** | ||
| - Credential Manager attempts to resolve the sign-in automatically using a previously authorized account with `setAutoSelectEnabled(true)`. | ||
| - If exactly one authorized account is available it resolves silently and `sign_in_success` is emitted — no UI is shown. | ||
| - If no authorized account can be resolved automatically, `silent_sign_in_failed` is emitted immediately. The app can then decide whether to call `signIn()` / `signInWithGoogleButton()` to show the full picker, or leave the user in an anonymous/logged-out state. | ||
|
|
||
| ```gdscript | ||
| # Using the google_sign_in.gd wrapper (recommended) | ||
| extends Node | ||
|
|
||
| @onready var auth = $GoogleSignIn # node with google_sign_in.gd attached | ||
|
|
||
| func _ready() -> void: | ||
| auth.initialize("YOUR_WEB_CLIENT_ID.apps.googleusercontent.com") | ||
| auth.sign_in_success.connect(_on_sign_in_success) | ||
| auth.sign_in_failed.connect(_on_sign_in_failed) | ||
| auth.silent_sign_in_failed.connect(_on_silent_sign_in_failed) | ||
|
|
||
| # Try silent sign-in first; only show UI if it fails. | ||
| auth.silent_sign_in() | ||
|
|
||
| func _on_sign_in_success(id_token: String, email: String, _display_name: String) -> void: | ||
| print("Signed in as: ", email) | ||
|
|
||
| func _on_sign_in_failed(error: String) -> void: | ||
| print("Interactive sign-in failed: ", error) | ||
|
|
||
| func _on_silent_sign_in_failed(_error: String) -> void: | ||
| # No authorized account — show the picker or stay logged out. | ||
| auth.sign_in_with_google_button() | ||
| ``` | ||
|
|
||
| For backend verification combine silent sign-in with a nonce: | ||
|
|
||
| ```gdscript | ||
| var _raw_nonce: String = "" | ||
|
|
||
| func try_auto_login() -> void: | ||
| var crypto = Crypto.new() | ||
| _raw_nonce = crypto.generate_random_bytes(32).hex_encode() | ||
| auth.silent_sign_in_with_nonce(_raw_nonce) | ||
|
|
||
| func _on_sign_in_success(id_token: String, _email: String, _display_name: String) -> void: | ||
| _authenticate_with_backend(id_token, _raw_nonce) | ||
| ``` | ||
|
|
||
| ## Building from Source | ||
|
|
||
| ### Requirements | ||
|
|
@@ -147,16 +248,52 @@ func _sign_in_with_firebase(google_id_token: String): | |
| ### Build Steps | ||
|
|
||
| 1. Clone this repository | ||
| 2. Navigate to `plugin/` | ||
| 3. Update `local.properties` with your Android SDK path: | ||
|
|
||
| 2. **Download the Godot AAR library** and place it in `plugin/libs/`: | ||
| - On the [Godot downloads page](https://godotengine.org/download), find the "AAR" link for your platform and version | ||
| - Or download directly, e.g.: | ||
| ```bash | ||
| # Adjust the version number to match your Godot version | ||
| cp ~/Downloads/godot-lib.*.aar plugin/libs/ | ||
| ``` | ||
| - See `plugin/libs/README.md` for more options (TuxFamily, local Godot installation) | ||
|
|
||
| 3. **Point Gradle at your Android SDK** — choose the method for your platform: | ||
|
|
||
| **macOS / Linux** — set the `ANDROID_HOME` environment variable: | ||
| ```bash | ||
| export ANDROID_HOME=~/Library/Android/sdk # macOS typical path | ||
| # or add to your shell profile to make it permanent | ||
| ``` | ||
|
|
||
| **Windows** — create `plugin/local.properties`: | ||
| ``` | ||
| sdk.dir=C\:\\Users\\YourName\\AppData\\Local\\Android\\Sdk | ||
| ``` | ||
| 4. Build: | ||
|
|
||
| 4. **Build** from the `plugin/` directory: | ||
|
|
||
| **macOS / Linux:** | ||
| ```bash | ||
| cd plugin && ./gradlew bundleReleaseAar | ||
| ``` | ||
|
|
||
| **Windows:** | ||
| ```bat | ||
| cd plugin | ||
| gradlew.bat assembleRelease | ||
| ``` | ||
|
|
||
| A successful build prints `BUILD SUCCESSFUL` and produces the AAR at: | ||
| ``` | ||
| plugin/build/outputs/aar/GoogleSignIn-release.aar | ||
| ``` | ||
|
|
||
| 5. Copy the AAR to your Godot project's `addons/GodotGoogleSignIn/` folder. | ||
| You may need to rename it: | ||
| ```bash | ||
| ./gradlew assembleRelease | ||
| mv GoogleSignIn-release.aar GodotGoogleSignIn-release.aar | ||
| ``` | ||
| 5. The AAR will be in `build/outputs/aar/` | ||
|
|
||
| ## Dependencies | ||
|
|
||
|
|
||
Binary file modified
BIN
+545 Bytes
(100%)
addons/GodotGoogleSignIn/bin/release/GodotGoogleSignIn-release.aar
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| ## GodotGoogleSignIn — GDScript wrapper | ||
| ## | ||
| ## Attach this script to an autoload node (or any Node) and call | ||
| ## initialize() before using any sign-in method. | ||
| ## | ||
| ## Signals forwarded from the native plugin: | ||
| ## sign_in_success(id_token, email, display_name) | ||
| ## sign_in_failed(error) | ||
| ## silent_sign_in_failed(error) | ||
| ## sign_out_complete() | ||
| extends Node | ||
|
|
||
| signal sign_in_success(id_token: String, email: String, display_name: String) | ||
| signal sign_in_failed(error: String) | ||
| signal silent_sign_in_failed(error: String) | ||
| signal sign_out_complete() | ||
|
|
||
| var _plugin: Object = null | ||
|
|
||
| func _ready() -> void: | ||
| if OS.get_name() == "Android" and Engine.has_singleton("GodotGoogleSignIn"): | ||
| _plugin = Engine.get_singleton("GodotGoogleSignIn") | ||
| _plugin.connect("sign_in_success", _on_sign_in_success) | ||
| _plugin.connect("sign_in_failed", _on_sign_in_failed) | ||
| _plugin.connect("silent_sign_in_failed", _on_silent_sign_in_failed) | ||
| _plugin.connect("sign_out_complete", _on_sign_out_complete) | ||
|
|
||
| ## Initialize with the Web Client ID from Google Cloud Console. | ||
| ## Must be called before any sign-in method. | ||
| func initialize(web_client_id: String) -> void: | ||
| if _plugin: | ||
| _plugin.initialize(web_client_id) | ||
|
|
||
| func is_initialized() -> bool: | ||
| return _plugin != null and _plugin.isInitialized() | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Interactive sign-in methods (may show account picker UI) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| ## Sign in, auto-selecting a previously authorized account if available, | ||
| ## otherwise falling back to the full account chooser. | ||
| func sign_in() -> void: | ||
| if _plugin: | ||
| _plugin.signIn() | ||
|
|
||
| ## Same as sign_in() but embeds a raw nonce in the ID token for backend | ||
| ## verification (Supabase, Firebase, custom OIDC). | ||
| func sign_in_with_nonce(nonce: String) -> void: | ||
| if _plugin: | ||
| _plugin.signInWithNonce(nonce) | ||
|
|
||
| ## Always show the full account chooser. | ||
| func sign_in_with_account_chooser() -> void: | ||
| if _plugin: | ||
| _plugin.signInWithAccountChooser() | ||
|
|
||
| ## Account chooser with nonce support. | ||
| func sign_in_with_account_chooser_with_nonce(nonce: String) -> void: | ||
| if _plugin: | ||
| _plugin.signInWithAccountChooserWithNonce(nonce) | ||
|
|
||
| ## Sign in with the branded "Sign in with Google" button flow. | ||
| func sign_in_with_google_button() -> void: | ||
| if _plugin: | ||
| _plugin.signInWithGoogleButton() | ||
|
|
||
| ## Google button flow with nonce support. | ||
| func sign_in_with_google_button_with_nonce(nonce: String) -> void: | ||
| if _plugin: | ||
| _plugin.signInWithGoogleButtonWithNonce(nonce) | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Silent sign-in methods (no UI — fail immediately if not resolvable) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| ## Attempt a non-interactive sign-in using a previously authorized account. | ||
| ## | ||
| ## Emits sign_in_success on success. | ||
| ## Emits silent_sign_in_failed (never sign_in_failed) if no account can be | ||
| ## resolved without user interaction — the caller can then decide whether to | ||
| ## show the full picker or leave the user signed out. | ||
| func silent_sign_in() -> void: | ||
| if _plugin: | ||
| _plugin.silentSignIn() | ||
|
|
||
| ## Silent sign-in with a caller-supplied raw nonce. | ||
| ## See silent_sign_in() for the silent flow and sign_in_with_nonce() for | ||
| ## nonce details. | ||
| func silent_sign_in_with_nonce(nonce: String) -> void: | ||
| if _plugin: | ||
| _plugin.silentSignInWithNonce(nonce) | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Sign-out | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| func sign_out() -> void: | ||
| if _plugin: | ||
| _plugin.signOut() | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Internal signal forwarders | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| func _on_sign_in_success(id_token: String, email: String, display_name: String) -> void: | ||
| sign_in_success.emit(id_token, email, display_name) | ||
|
|
||
| func _on_sign_in_failed(error: String) -> void: | ||
| sign_in_failed.emit(error) | ||
|
|
||
| func _on_silent_sign_in_failed(error: String) -> void: | ||
| silent_sign_in_failed.emit(error) | ||
|
|
||
| func _on_sign_out_complete() -> void: | ||
| sign_out_complete.emit() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarify
sign_in_failedsignal behavior for silent flows.The description states this signal is emitted "when an interactive sign-in fails," but according to the PR objectives' behavior contract, unrecoverable exceptions during silent sign-in also emit
sign_in_failed. The current wording may lead users to believe this signal is never emitted during silent flows.📝 Suggested clarification
📝 Committable suggestion
🤖 Prompt for AI Agents