From 80d653927c47c01dd59e049dcf072cf764412360 Mon Sep 17 00:00:00 2001 From: Firuz Date: Tue, 12 May 2026 11:28:17 +0500 Subject: [PATCH] docs: Update Google sign-in setup and troubleshooting documentation for web redirect flow --- .../04-providers/03-google/01-setup.md | 154 ++++++++++++++---- .../03-google/02-customizations.md | 15 ++ .../03-google/03-customizing-the-ui.md | 11 +- .../03-google/04-troubleshooting.md | 60 +++---- 4 files changed, 175 insertions(+), 65 deletions(-) diff --git a/docs/06-concepts/11-authentication/04-providers/03-google/01-setup.md b/docs/06-concepts/11-authentication/04-providers/03-google/01-setup.md index 920f40c1..6c9b6a25 100644 --- a/docs/06-concepts/11-authentication/04-providers/03-google/01-setup.md +++ b/docs/06-concepts/11-authentication/04-providers/03-google/01-setup.md @@ -60,23 +60,36 @@ The People API is required for Serverpod to access basic user profile data durin All platforms (iOS, Android, and Web) require a **Web application** OAuth client for the server. This is the only client type that provides a **client secret**, which Serverpod needs to verify sign-in tokens on the server side. +This same Web application client is also used by the web sign-in flow. + +On web, Serverpod supports two sign-in modes: + +- The default mode uses the Google-hosted web sign-in button from `google_sign_in_web`. +- An optional redirect mode uses OAuth2 Authorization Code + PKCE with an `auth.html` callback page. + 1. In the Google Auth Platform, navigate to **Clients** and click **Create Client**. 2. Select **Web application** as the application type. 3. Add the following URIs: - - **Authorized JavaScript origins**: The origin that is allowed to make requests to Google's OAuth servers. For Serverpod, this is your **web server** address. - - **Authorized redirect URIs**: The URL Google redirects the user back to after they sign in. Serverpod handles this callback on the web server as well. + - **Authorized JavaScript origins**: The browser origins that are allowed to start the sign-in flow. + - **Authorized redirect URIs**: The URLs Google redirects back to after sign-in. - Serverpod runs three servers locally (see `config/development.yaml`): the API server on port 8080, the Insights server on 8081, and the **web server on port 8082**. The Google OAuth flow uses the web server, so both fields should point to port 8082: + Serverpod runs three servers locally (see `config/development.yaml`): the API server on port 8080, the Insights server on 8081, and the **web server on port 8082**. - | Environment | Authorized JavaScript origins | Authorized redirect URIs | - | --- | --- | --- | - | Local development | `http://localhost:8082` | `http://localhost:8082` | - | Production | Your web server's public URL (e.g., `https://my-awesome-project.serverpod.space`) | Your web server's public URL | + For the default popup / iFrame web flow, the main requirement is that your app's browser origin is listed under **Authorized JavaScript origins**. - You can find these ports in your server's `config/development.yaml` under `webServer`. + If you opt into redirect mode, you must also register the full callback URL under **Authorized redirect URIs**. + + | Environment | Authorized JavaScript origins | Authorized redirect URIs | + | --- | --- | --- | + | Local development | Your Flutter web app origin, for example `http://localhost:3000` or `http://localhost:8082` | `http://localhost:8082/auth.html` if you use redirect mode | + | Production | Your deployed web app origin, for example `https://your-domain.com` | `https://your-domain.com/auth.html` if you use redirect mode | + + In redirect mode, the redirect URI should point to an `auth.html` callback page. The browser is redirected there after sign-in, and that page sends the OAuth result back to your Flutter app. + + You can find the Serverpod web server port in your server's `config/development.yaml` under `webServer`. ![Clients configuration](/img/authentication/providers/google/5-clients.png) @@ -103,6 +116,8 @@ development: Replace `your-client-id` and `your-client-secret` with the values from the Google Auth Platform. The `redirect_uris` must match the **Authorized redirect URIs** you configured in the previous step. +If you use redirect mode on web, the `redirect_uris` entry should point to your callback page, for example `http://localhost:8082/auth.html` locally or `https://your-domain.com/auth.html` in production. + For production, add the same `googleClientSecret` entry to the `production:` section of `passwords.yaml` (with your production redirect URI), or set the `SERVERPOD_PASSWORD_googleClientSecret` environment variable on your production server. :::warning @@ -169,7 +184,9 @@ Skipping the migration will cause the server to crash at runtime when the Google ## Client-side configuration -The Android and iOS integrations use the [google_sign_in](https://pub.dev/packages/google_sign_in) package under the hood, so any documentation there should also apply to this setup. +The Android and iOS integrations use the [google_sign_in](https://pub.dev/packages/google_sign_in) package under the hood, so any documentation there should also apply to those platforms. + +On web, the default setup also uses the Google web integration from `google_sign_in_web`. If you provide a `redirectUri` when initializing Google sign-in, Serverpod switches to the redirect-based OAuth2 PKCE flow implemented with [flutter_web_auth_2](https://pub.dev/packages/flutter_web_auth_2). ### iOS @@ -248,49 +265,116 @@ The downloaded `google-services.json` may not include a web OAuth client entry, ### Web -Web uses the same server OAuth client you created earlier, so you don't need a separate client. However, for web, the sign-in request originates from the Flutter app running in the browser, not from the Serverpod web server. Google requires this origin to be listed as well. +Web uses the same server OAuth client you created earlier, so you don't need a separate client. -1. **Choose a fixed port for your Flutter web app.** Google OAuth requires exact origin matches, and Flutter picks a random port on each run by default. To keep things consistent, run Flutter on a fixed port using `--web-port`: +You have two setup options on web. - ```bash - flutter run -d chrome --web-hostname localhost --web-port=49660 +#### Option 1: Default popup / iFrame flow + +This is the existing behavior and requires the least setup. + +1. Make sure your Flutter web app origin is listed under **Authorized JavaScript origins** on the Web application OAuth client. + +2. Initialize Google sign-in in your Flutter app: + + ```dart + await client.auth.initialize(); + + await client.auth.initializeGoogleSignIn( + clientId: 'your-web-client-id.apps.googleusercontent.com', + ); + ``` + +3. Add `GoogleSignInWidget` to your UI. On web, this shows the Google-hosted web button in the default flow. + +#### Option 2: Redirect mode with OAuth2 PKCE + +Use this if you want the alternative redirect-based flow. + +1. **Create the OAuth callback page.** In your Flutter project's `web/` folder, create a file named `auth.html` with the following content: + + ```html + + Authentication complete +

Authentication is complete. If this does not happen automatically, please close the window.

+ ``` - - `-d chrome`: Run on the Chrome browser. - - `--web-hostname localhost`: Bind to localhost. - - `--web-port=49660`: Use a fixed port (pick any available port). This is the value you will add to **Authorized JavaScript origins** in the next step. +2. **Update the Web application OAuth client.** Go back to the Web application client you created in the [previous section](#create-the-server-oauth-client-web-application) and make sure the following are configured: -2. **Update the server OAuth client.** Go back to the server OAuth client you created in the [previous section](#create-the-server-oauth-client-web-application) and add your Flutter web app's origin to **Authorized JavaScript origins**: + - **Authorized redirect URIs** includes the full URL to your callback page. + - **Authorized JavaScript origins** includes the browser origin where your Flutter web app runs. - - For local development: `http://localhost:49660` (or whichever port you chose) - - For production: your Flutter web app's domain (e.g., `https://my-awesome-project.serverpod.space`) + Example values: - The **Authorized redirect URIs** should already contain your Serverpod web server's address (`http://localhost:8082`) from the earlier setup. You don't need to change it. + | Environment | Authorized JavaScript origins | Authorized redirect URIs | + | --- | --- | --- | + | Local development | `http://localhost:8082` or your Flutter web origin if different | `http://localhost:8082/auth.html` | + | Production | `https://your-domain.com` | `https://your-domain.com/auth.html` | ![Web credentials configuration](/img/authentication/providers/google/2-credentials.png) -3. **Add the client ID to your Flutter project's `web/index.html`** (e.g., `my_project_flutter/web/index.html`). In the `` section, add: +3. **Initialize Google sign-in with a redirect URI.** In your app startup code, pass the Web application client ID and the same redirect URI you registered in Google Cloud. - ```html - - ... - - + ```dart + await client.auth.initialize(); + + await client.auth.initializeGoogleSignIn( + clientId: 'your-web-client-id.apps.googleusercontent.com', + redirectUri: 'http://localhost:8082/auth.html', + ); ``` - Replace `your_server_client_id` with the client ID from your Web application OAuth client. + For production, replace the redirect URI with your deployed `auth.html` URL. + +:::note +You only need one `auth.html` file in your Flutter web project. It can be reused by multiple identity providers that rely on the same OAuth2 web callback mechanism. +::: ## Present the authentication UI ### Initialize the Google sign-in service -In your Flutter app's `main.dart` file (e.g., `my_project_flutter/lib/main.dart`), the template already sets up the `Client` and calls `client.auth.initialize()`. Add `client.auth.initializeGoogleSignIn()` right after it: +In your Flutter app's `main.dart` file (e.g., `my_project_flutter/lib/main.dart`), the template already sets up the `Client` and calls `client.auth.initialize()`. + +For iOS, Android, and the default web popup / iFrame flow, add `client.auth.initializeGoogleSignIn()` right after it: ```dart client.auth.initialize(); client.auth.initializeGoogleSignIn(); ``` +If you want the optional redirect-based web flow, pass `redirectUri` when initializing: + +```dart +await client.auth.initialize(); + +await client.auth.initializeGoogleSignIn( + clientId: 'your-web-client-id.apps.googleusercontent.com', + redirectUri: 'http://localhost:8082/auth.html', +); +``` + +If your app targets multiple platforms, you can keep using the same `initializeGoogleSignIn(...)` API and only add `redirectUri` for the web redirect mode. + ### Add the sign-in widget If you have configured the `SignInWidget` as described in the [setup section](../../setup#present-the-authentication-ui), the Google identity provider will be automatically detected and displayed in the sign-in widget. @@ -324,9 +408,9 @@ This renders a Google sign-in button like this: The widget automatically handles: - Google Sign-In flow for iOS, Android, and Web. -- Lightweight sign-in (One Tap, FedCM) support. +- Lightweight sign-in support on platforms where the underlying Google Sign-In package provides it. - Token management. -- Underlying Google Sign-In package error handling. +- Underlying platform-specific authentication flow handling. For details on how to customize the Google Sign-In UI in your Flutter app, see the [customizing the UI section](./customizing-the-ui). @@ -340,10 +424,10 @@ Before going live, complete the following steps: ### 1. Update the OAuth redirect URIs -Go back to the [server OAuth client](#create-the-server-oauth-client-web-application) in the Google Auth Platform and add your production server's public URL to both **Authorized JavaScript origins** and **Authorized redirect URIs**: +Go back to the [server OAuth client](#create-the-server-oauth-client-web-application) in the Google Auth Platform and update the production values you actually use: -- **Authorized JavaScript origins**: `https://your-domain.serverpod.space` -- **Authorized redirect URIs**: `https://your-domain.serverpod.space` +- **Authorized JavaScript origins**: `https://your-domain.com` +- **Authorized redirect URIs**: `https://your-domain.com/auth.html` if you use redirect mode on web Replace the URL with your actual production web server address. @@ -359,11 +443,13 @@ production: "web": { "client_id": "your-client-id.apps.googleusercontent.com", "client_secret": "your-client-secret", - "redirect_uris": ["https://your-domain.serverpod.space"] + "redirect_uris": ["https://your-domain.com/auth.html"] } } ``` + If you only use the default popup / iFrame web flow, the `redirect_uris` entry is still part of the Google client secret JSON, but the browser flow does not depend on `auth.html`. + Alternatively, set the `SERVERPOD_PASSWORD_googleClientSecret` [environment variable](../../../07-configuration.md#2-via-environment-variables) on your production server with the same JSON value. If you're deploying to Serverpod Cloud, set the password with the `scloud` CLI instead. Save the JSON to a file and run: diff --git a/docs/06-concepts/11-authentication/04-providers/03-google/02-customizations.md b/docs/06-concepts/11-authentication/04-providers/03-google/02-customizations.md index 99c44fbd..215a13dd 100644 --- a/docs/06-concepts/11-authentication/04-providers/03-google/02-customizations.md +++ b/docs/06-concepts/11-authentication/04-providers/03-google/02-customizations.md @@ -169,6 +169,8 @@ If lightweight sign-in fails (e.g., no previous session exists or the user dismi :::note The lightweight sign-in attempt happens automatically when the controller is initialized, typically at app launch. If successful, users will be signed in without any additional interaction. + +On web, lightweight sign-in behavior depends on which mode you use. The default web popup / iFrame flow continues to rely on `google_sign_in_web`, while the redirect-based OAuth2 flow is opt-in and does not use lightweight sign-in as its primary interaction model. ::: ### Configuring Client IDs on the App @@ -188,6 +190,19 @@ client.auth.initializeGoogleSignIn( This approach is useful when you need different client IDs per platform and want to manage them in your Dart code. +To opt into the redirect-based web flow, pass `redirectUri` to the same initializer: + +```dart +await client.auth.initializeGoogleSignIn( + clientId: '.apps.googleusercontent.com', + redirectUri: 'https://your-domain.com/auth.html', +); +``` + +If you omit `redirectUri` on web, Serverpod keeps using the default popup / iFrame flow. + +Use the same Web application client ID and redirect URI you configured in Google Cloud. + #### Using Environment Variables Alternatively, you can pass client IDs during build time using the `--dart-define` option. The Google Sign-In provider supports the following environment variables: diff --git a/docs/06-concepts/11-authentication/04-providers/03-google/03-customizing-the-ui.md b/docs/06-concepts/11-authentication/04-providers/03-google/03-customizing-the-ui.md index 4eb8d407..9219f954 100644 --- a/docs/06-concepts/11-authentication/04-providers/03-google/03-customizing-the-ui.md +++ b/docs/06-concepts/11-authentication/04-providers/03-google/03-customizing-the-ui.md @@ -43,7 +43,7 @@ GoogleSignInWidget( 'https://www.googleapis.com/auth/userinfo.profile', ], - // Whether to attempt lightweight sign-in (One Tap, FedCM) + // Whether to attempt lightweight sign-in on supported platforms attemptLightweightSignIn: false, onAuthenticated: () { @@ -87,8 +87,13 @@ final controller = GoogleAuthController( await controller.signIn(); ``` -:::warning -When using Google Sign-In on web, be mindful that the button will be rendered by the underlying `google_sign_in` package, so customizing the button might not work as expected. The included `GoogleSignInWidget` is a wrapper around the original widgets that already applies some customizations to make its design compatible between all platforms. +:::note +On web, the rendered button depends on how you initialize sign-in: + +- If you call `initializeGoogleSignIn()` without `redirectUri`, the widget uses the Google-hosted web button from the default popup / iFrame flow. +- If you call `initializeGoogleSignIn(..., redirectUri: ...)`, the widget switches to the Flutter-rendered button and starts the redirect-based OAuth2 flow. + +This means button customization on web is most consistent when you opt into redirect mode. ::: ### GoogleAuthController State Management diff --git a/docs/06-concepts/11-authentication/04-providers/03-google/04-troubleshooting.md b/docs/06-concepts/11-authentication/04-providers/03-google/04-troubleshooting.md index 339aec05..4cea8113 100644 --- a/docs/06-concepts/11-authentication/04-providers/03-google/04-troubleshooting.md +++ b/docs/06-concepts/11-authentication/04-providers/03-google/04-troubleshooting.md @@ -29,8 +29,10 @@ Go through this before investigating a specific error. Most problems come from a - [ ] Surface Google sign-in in the UI with `SignInWidget` or `GoogleSignInWidget` (see [Present the authentication UI](./setup#present-the-authentication-ui)). - [ ] Create an **iOS** OAuth client in the **same** Google Cloud project as the Web client, using the same **Bundle ID** as the app; set `GIDClientID` from the iOS client, `GIDServerClientID` to the **Web** client's ID, and add the reversed-client-ID **URL scheme** in `Info.plist` (*iOS only*). - [ ] Create an **Android** OAuth client in the **same** project, with the same **package name** and **SHA-1** as the build you run; place `google-services.json` in `android/app/` (*Android only*). -- [ ] Add the `google-signin-client_id` **meta tag** to `web/index.html` (*Web only*). -- [ ] On **Web**, list **both** the Serverpod web server origin (e.g., `http://localhost:8082`) and your Flutter app origin under **Authorized JavaScript origins** on the Web OAuth client; use a **fixed** `--web-port` for Flutter so that second origin does not change every run (see [Web setup](./setup#web)). +- [ ] On **Web**, make sure **Authorized JavaScript origins** includes the browser origin where your Flutter web app runs. +- [ ] If you use the optional redirect-based web flow, create `web/auth.html` exactly as shown in the [setup guide](./setup#web). +- [ ] If you use the optional redirect-based web flow, register the full callback URL (for example `http://localhost:8082/auth.html`) under **Authorized redirect URIs** on the Web OAuth client. +- [ ] If you use the optional redirect-based web flow, call `client.auth.initializeGoogleSignIn(..., redirectUri: ...)` with the same Web application client ID and redirect URI you configured in Google Cloud. ## Sign-in fails with redirect_uri_mismatch @@ -38,13 +40,14 @@ Go through this before investigating a specific error. Most problems come from a **Cause:** The redirect URI in your OAuth client configuration does not match the URI your app is actually using. Google requires an exact match. -**Resolution:** In the Google Auth Platform, navigate to **Clients**, select your Web application client, and verify that the URIs under **Authorized JavaScript origins** and **Authorized redirect URIs** match your server's address exactly. For local development, both should be `http://localhost:8082` (the Serverpod **web server** port, not the API server port 8080). Trailing slashes, port differences, and `http` vs `https` all count as mismatches. +**Resolution:** In the Google Auth Platform, navigate to **Clients**, select your Web application client, and verify that the URIs under **Authorized JavaScript origins** and **Authorized redirect URIs** match what your app actually uses. This error is mainly relevant when you use the optional redirect-based web flow. For local development with that flow, a common value is `http://localhost:8082/auth.html` for the redirect URI. Trailing slashes, port differences, path differences, and `http` vs `https` all count as mismatches. Common mistakes: -* Using port `8080` (the API server) instead of `8082` (the web server). Check `config/development.yaml` under `webServer` for the correct port. -* Adding a trailing slash (e.g., `http://localhost:8082/` instead of `http://localhost:8082`). -* For Web apps: not adding the Flutter web app's origin (e.g., `http://localhost:49660`) to **Authorized JavaScript origins**. This is separate from the Serverpod web server address. See the [Web setup section](./setup#web) for details. +* Using port `8080` (the API server) instead of the configured web callback host and port. +* Registering `http://localhost:8082` instead of `http://localhost:8082/auth.html`. +* Adding a trailing slash or using the wrong scheme. +* Not adding the actual Flutter web app origin to **Authorized JavaScript origins** when the app runs on a different host or port. ## Sign-in works for you but not for other users @@ -64,17 +67,23 @@ Common mistakes: ## Flutter web sign-in fails with origin mismatch -**Problem:** Google Sign-In on Flutter web fails with an origin mismatch error, even though you added `http://localhost:8082` to the OAuth client. +**Problem:** Google Sign-In on Flutter web fails with an origin mismatch error, even though the redirect URI looks correct. -**Cause:** The Flutter web app runs on a different port than the Serverpod web server. The sign-in request originates from the Flutter app's port (e.g., `http://localhost:49660`), which also needs to be listed in **Authorized JavaScript origins**. Flutter also picks a random port by default, so the origin changes on every run. +**Cause:** The sign-in request starts from the browser origin where your Flutter web app is running. If that origin is not listed under **Authorized JavaScript origins**, Google rejects the request. -**Resolution:** Run Flutter on a fixed port and add that origin to your OAuth client: +**Resolution:** Add the exact browser origin of your Flutter web app to **Authorized JavaScript origins** in the Google Auth Platform. If your local workflow changes ports often, pick a stable local origin for development so your Google OAuth configuration stays predictable. -```bash -flutter run -d chrome --web-hostname localhost --web-port=49660 -``` +For example, if your app runs from `http://localhost:3000`, add `http://localhost:3000` as an authorized JavaScript origin. + +The redirect URI is a separate setting and, if you use redirect mode, should still point to your callback page, for example `http://localhost:8082/auth.html`. + +## Web callback page is missing or not returning control to the app -Then add `http://localhost:49660` to **Authorized JavaScript origins** in the Google Auth Platform. +**Problem:** Google opens the sign-in page, but after authentication the app does not complete sign-in, or the browser shows a blank callback page. + +**Cause:** The `auth.html` callback page is missing, does not contain the expected `flutter_web_auth_2` script, or the registered redirect URI points somewhere else. + +**Resolution:** This only applies to the optional redirect-based web flow. Create `web/auth.html` exactly as shown in the [setup guide](./setup#web), then make sure the same full URL is registered under **Authorized redirect URIs** and passed to `client.auth.initializeGoogleSignIn(..., redirectUri: ...)`. ## Server fails to parse googleClientSecret from passwords.yaml @@ -163,9 +172,9 @@ dart run bin/main.dart --apply-migrations **Problem:** You enabled `attemptLightweightSignIn: true` but the One Tap prompt never appears on Web, or the silent sign-in doesn't trigger on mobile. -**Cause:** Lightweight sign-in requires the user to have previously signed in with Google on this device or browser. It also depends on platform-specific conditions: on Web, FedCM or One Tap must be supported by the browser; on mobile, the user must have a Google account configured on the device. +**Cause:** Lightweight sign-in requires the user to have previously signed in with Google on the device and depends on platform-specific behavior from the native Google Sign-In package. -**Resolution:** This is expected behavior for first-time users. The lightweight sign-in prompt only appears for returning users. If the user dismisses One Tap multiple times, Google may suppress it temporarily. The regular sign-in button remains available as a fallback. +**Resolution:** This is expected behavior for first-time users. The lightweight sign-in prompt only appears for returning users where the platform supports it. The regular sign-in button remains available as a fallback. On web, this applies to the default popup / iFrame flow. If you opt into redirect mode, the interaction model is different and does not center on lightweight sign-in. ## iOS sign-in prompt doesn't show @@ -181,25 +190,20 @@ dart run bin/main.dart --apply-migrations ## Web sign-in button doesn't render -**Problem:** The Google Sign-In button doesn't appear on Web, or the page shows a JavaScript error related to Google Identity Services. +**Problem:** The Google Sign-In button doesn't appear on Web, or tapping it does nothing useful. -**Cause:** The `google-signin-client_id` meta tag is missing from `web/index.html`, or its value doesn't match the server's Web application client ID. +**Cause:** The underlying cause depends on which web mode you use. In the default popup / iFrame flow, check that the Google web setup is complete and the browser origin is authorized. In the optional redirect flow, common causes are missing initialization with `redirectUri`, a bad redirect URI, or a missing `auth.html` callback page. -**Resolution:** Add or verify the meta tag in `web/index.html`: - -```html - - ... - - -``` +**Resolution:** Verify the web setup end to end: -Replace `your_server_client_id` with the `client_id` from your Web application OAuth client JSON file. +1. Call `client.auth.initializeGoogleSignIn(...)` during app startup. +2. If you use redirect mode, make sure `web/auth.html` exists and matches the documented contents. +3. If you use redirect mode, make sure the registered redirect URI exactly matches the URL passed as `redirectUri`. ## Google API calls fail after one hour on Web **Problem:** Your app calls Google APIs (e.g., Calendar, Drive) using the access token from sign-in, but requests start returning `401 Unauthorized` after about an hour. This only affects the Web platform. -**Cause:** On Web, the `accessToken` returned by the `google_sign_in` package expires after 3,600 seconds (one hour) and is not automatically refreshed. +**Cause:** On Web, access tokens obtained through the OAuth2 flow are short-lived. If your app keeps using the same token for Google API calls after it expires, Google returns `401 Unauthorized`. -**Resolution:** When making Google API calls on Web, check the token age and prompt the user to re-authenticate if the token has expired. On mobile platforms, the token is refreshed automatically and this is not an issue. See the [google_sign_in_web documentation](https://pub.dev/packages/google_sign_in_web) for details on token lifecycle. +**Resolution:** When making Google API calls on Web, treat the access token as time-limited and prompt the user to authenticate again when it expires. If your app depends heavily on long-lived access to Google APIs, design that flow explicitly instead of assuming the initial sign-in token remains valid indefinitely.