diff --git a/README.md b/README.md index 6a7501a..3a7edd6 100644 --- a/README.md +++ b/README.md @@ -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 | ## 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 diff --git a/addons/GodotGoogleSignIn/bin/release/GodotGoogleSignIn-release.aar b/addons/GodotGoogleSignIn/bin/release/GodotGoogleSignIn-release.aar index dd5ff3c..067864e 100644 Binary files a/addons/GodotGoogleSignIn/bin/release/GodotGoogleSignIn-release.aar and b/addons/GodotGoogleSignIn/bin/release/GodotGoogleSignIn-release.aar differ diff --git a/addons/GodotGoogleSignIn/google_sign_in.gd b/addons/GodotGoogleSignIn/google_sign_in.gd new file mode 100644 index 0000000..3e0c26f --- /dev/null +++ b/addons/GodotGoogleSignIn/google_sign_in.gd @@ -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() diff --git a/plugin/gradlew b/plugin/gradlew new file mode 100755 index 0000000..9c1e038 --- /dev/null +++ b/plugin/gradlew @@ -0,0 +1,117 @@ +#!/bin/sh + +# © 2026 Uche Ogbuji (uche@oori.dev), Oori Data LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 + +############################################################################## +# Gradle wrapper script for UN*X/BSD +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +app_path=$0 +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( absolute + *) app_path=$APP_HOME$link ;; #( relative + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS +# to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + command -v java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + ;; + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \ + '"-Dorg.gradle.appname=$APP_BASE_NAME"' \ + -classpath '"$CLASSPATH"' \ + org.gradle.wrapper.GradleWrapperMain \ + '"$@"' + +exec "$JAVACMD" "$@" diff --git a/plugin/src/main/kotlin/com/niquewrld/casino/googlesignin/GodotGoogleSignIn.kt b/plugin/src/main/kotlin/com/niquewrld/casino/googlesignin/GodotGoogleSignIn.kt index 0cb5735..777fe47 100644 --- a/plugin/src/main/kotlin/com/niquewrld/casino/googlesignin/GodotGoogleSignIn.kt +++ b/plugin/src/main/kotlin/com/niquewrld/casino/googlesignin/GodotGoogleSignIn.kt @@ -38,12 +38,13 @@ class GodotGoogleSignIn(godot: Godot) : GodotPlugin(godot) { // Signals private val signInSuccessSignal = SignalInfo("sign_in_success", String::class.java, String::class.java, String::class.java) private val signInFailedSignal = SignalInfo("sign_in_failed", String::class.java) + private val silentSignInFailedSignal = SignalInfo("silent_sign_in_failed", String::class.java) private val signOutCompleteSignal = SignalInfo("sign_out_complete") - + override fun getPluginName(): String = "GodotGoogleSignIn" - + override fun getPluginSignals(): Set { - return setOf(signInSuccessSignal, signInFailedSignal, signOutCompleteSignal) + return setOf(signInSuccessSignal, signInFailedSignal, silentSignInFailedSignal, signOutCompleteSignal) } private fun getOrCreateCredentialManager(): CredentialManager? { @@ -80,26 +81,73 @@ class GodotGoogleSignIn(godot: Godot) : GodotPlugin(godot) { */ @UsedByGodot fun signIn() { + signInInternal(null) + } + + /** + * Start Google Sign-In flow with a caller-supplied raw nonce. + * + * Use this variant when your app needs to verify the ID token on a backend + * (e.g. Supabase, Firebase, or a custom OIDC server). The caller generates + * a cryptographically random raw nonce, passes it here, and later sends that + * same raw nonce to the backend alongside the ID token. The plugin will + * SHA-256 hash the raw nonce before passing it to Google; Google embeds the + * hash in the ID token's `nonce` claim, allowing the backend to verify it. + * + * @param rawNonce The raw (unhashed) nonce string generated by the caller. + */ + @UsedByGodot + fun signInWithNonce(rawNonce: String) { + signInInternal(rawNonce) + } + + /** + * Attempt a silent (non-interactive) sign-in using a previously authorized account. + * + * Uses `setFilterByAuthorizedAccounts(true)` + `setAutoSelectEnabled(true)` so the + * Credential Manager resolves without showing any UI. If no single authorized account + * can be resolved automatically, the request fails immediately and the + * `silent_sign_in_failed` signal is emitted — no account picker is ever shown. + * On success the normal `sign_in_success` signal is emitted. + */ + @UsedByGodot + fun silentSignIn() { + signInInternal(null, silent = true) + } + + /** + * Attempt a silent sign-in with a caller-supplied raw nonce. + * See [silentSignIn] for the silent flow and [signInWithNonce] for nonce details. + * + * @param rawNonce The raw (unhashed) nonce string generated by the caller. + */ + @UsedByGodot + fun silentSignInWithNonce(rawNonce: String) { + signInInternal(rawNonce, silent = true) + } + + private fun signInInternal(rawNonce: String?, silent: Boolean = false) { + val failSignal = if (silent) silentSignInFailedSignal.name else signInFailedSignal.name + if (webClientId.isEmpty()) { - emitSignal(signInFailedSignal.name, "Plugin not initialized. Call initialize() first.") + emitSignal(failSignal, "Plugin not initialized. Call initialize() first.") return } - + val act = activity ?: run { - emitSignal(signInFailedSignal.name, "Activity not available") + emitSignal(failSignal, "Activity not available") return } - + val cm = getOrCreateCredentialManager() ?: run { - emitSignal(signInFailedSignal.name, "CredentialManager not available") + emitSignal(failSignal, "CredentialManager not available") return } - + coroutineScope.launch { try { - // Generate a nonce for security - val nonce = generateNonce() - + val nonce = if (rawNonce != null) hashNonce(rawNonce) else generateNonce() + // Try to sign in with existing authorized account first val googleIdOption = GetGoogleIdOption.Builder() .setFilterByAuthorizedAccounts(true) @@ -107,18 +155,24 @@ class GodotGoogleSignIn(godot: Godot) : GodotPlugin(godot) { .setAutoSelectEnabled(true) .setNonce(nonce) .build() - + val request = GetCredentialRequest.Builder() .addCredentialOption(googleIdOption) .build() - + try { val result = cm.getCredential(act, request) handleSignInResult(result) } catch (e: GetCredentialException) { - // No authorized accounts found, try with all accounts - Log.d(TAG, "No authorized accounts, trying with all accounts") - signInWithAllAccounts(act, nonce, cm) + if (silent) { + // Silent mode: never show UI — report failure immediately. + Log.d(TAG, "Silent sign-in failed (no auto-selectable account): ${e.message}") + emitSignal(silentSignInFailedSignal.name, e.message ?: "No authorized account available") + } else { + // Interactive mode: fall back to full account chooser. + Log.d(TAG, "No authorized accounts, trying with all accounts") + signInWithAllAccounts(act, nonce, cm) + } } } catch (e: Exception) { Log.e(TAG, "Sign-in failed", e) @@ -132,6 +186,21 @@ class GodotGoogleSignIn(godot: Godot) : GodotPlugin(godot) { */ @UsedByGodot fun signInWithAccountChooser() { + signInWithAccountChooserInternal(null) + } + + /** + * Sign in with account chooser and a caller-supplied raw nonce. + * See [signInWithNonce] for details on the nonce flow. + * + * @param rawNonce The raw (unhashed) nonce string generated by the caller. + */ + @UsedByGodot + fun signInWithAccountChooserWithNonce(rawNonce: String) { + signInWithAccountChooserInternal(rawNonce) + } + + private fun signInWithAccountChooserInternal(rawNonce: String?) { if (webClientId.isEmpty()) { emitSignal(signInFailedSignal.name, "Plugin not initialized. Call initialize() first.") return @@ -149,7 +218,7 @@ class GodotGoogleSignIn(godot: Godot) : GodotPlugin(godot) { coroutineScope.launch { try { - val nonce = generateNonce() + val nonce = if (rawNonce != null) hashNonce(rawNonce) else generateNonce() signInWithAllAccounts(act, nonce, cm) } catch (e: Exception) { Log.e(TAG, "Sign-in failed", e) @@ -163,6 +232,21 @@ class GodotGoogleSignIn(godot: Godot) : GodotPlugin(godot) { */ @UsedByGodot fun signInWithGoogleButton() { + signInWithGoogleButtonInternal(null) + } + + /** + * Sign in with the Google button flow and a caller-supplied raw nonce. + * See [signInWithNonce] for details on the nonce flow. + * + * @param rawNonce The raw (unhashed) nonce string generated by the caller. + */ + @UsedByGodot + fun signInWithGoogleButtonWithNonce(rawNonce: String) { + signInWithGoogleButtonInternal(rawNonce) + } + + private fun signInWithGoogleButtonInternal(rawNonce: String?) { if (webClientId.isEmpty()) { emitSignal(signInFailedSignal.name, "Plugin not initialized. Call initialize() first.") return @@ -180,8 +264,8 @@ class GodotGoogleSignIn(godot: Godot) : GodotPlugin(godot) { coroutineScope.launch { try { - val nonce = generateNonce() - + val nonce = if (rawNonce != null) hashNonce(rawNonce) else generateNonce() + val signInWithGoogleOption = GetSignInWithGoogleOption.Builder(webClientId) .setNonce(nonce) .build() @@ -275,13 +359,17 @@ class GodotGoogleSignIn(godot: Godot) : GodotPlugin(godot) { } /** - * Generate a secure nonce for the sign-in request + * Generate a random nonce and return its SHA-256 hex hash. + */ + private fun generateNonce(): String = hashNonce(UUID.randomUUID().toString()) + + /** + * SHA-256 hash a raw nonce string, returning a lowercase hex string. + * This is what gets passed to Google; the caller retains the raw value for + * backend verification. */ - private fun generateNonce(): String { - val rawNonce = UUID.randomUUID().toString() - val bytes = rawNonce.toByteArray() - val md = MessageDigest.getInstance("SHA-256") - val digest = md.digest(bytes) + private fun hashNonce(rawNonce: String): String { + val digest = MessageDigest.getInstance("SHA-256").digest(rawNonce.toByteArray(Charsets.UTF_8)) return digest.fold("") { str, it -> str + "%02x".format(it) } } }