DEV Community

sabbir alam
sabbir alam

Posted on • Edited on

How I Integrated Google Sign-In with Keycloak Token Exchange in Flutter β€” Step-by-Step πŸš€

Hey Dev Community! πŸ‘‹

I recently built a Flutter app that uses Google Sign-In for authentication and then exchanges the Google access token for a Keycloak token to secure backend APIs.

I ran into some challenges during the token exchange process, so here’s a walkthrough of how I solved it β€” plus a snippet of my Flutter Google Sign-In code!

The Challenge 🐞
When I first tried exchanging the Google access token for a Keycloak token, Keycloak kept rejecting the token with errors like:
TOKEN_EXCHANGE_ERROR: subject_token validation failure
and
invalid_request: Parameter 'subject_issuer' is not supported for standard token exchange

before any configuration a very important step is to start the keyclock server correctly
kc.bat start-dev --hostname=<your-host-name> --features="preview,token-exchange,admin-fine-grained-authz:v1"

rest all the configuration remains the same
First you need to create google OAuth Credentials

How to Create Google OAuth Credentials (Client ID & Secret)

Step 1: Go to Google Cloud Console


Step 2: Create or Select a Project

  • Select an existing project from the top dropdown, or
  • Click New Project, enter a project name, and create it.

Step 3: Enable the OAuth Consent Screen

  1. In the left sidebar, click OAuth consent screen.
  2. Choose External (for apps used by any Google user) or Internal (for G Suite users only).
  3. Click Create.
  4. Fill out the required fields:
    • App name
    • User support email
    • Developer contact email
  5. Save and continue through scopes and test users (default settings usually work).
  6. Click Save and Continue until done.

Step 4: Create OAuth 2.0 Credentials

  1. Go to Credentials on the left menu.
  2. Click Create Credentials > OAuth client ID.
  3. Select Application type:
    • Web application (if your app uses redirects or backend)
    • Other (for standalone apps like Flutter)
  4. Enter a name for the client (e.g., "Flutter App Client").

Step 5: Configure Authorized Redirect URIs (for Web Application)

  • If you chose Web application, add the redirect URI where Google will send users after authentication.

- For Keycloak, this is usually: https://<your-keycloak-domain>/realms/<realm-name>/broker/google/endpoint

Step 6: Save and Get Your Client ID and Secret

  • Click Create.
  • Copy the Client ID and Client Secret displayed.
  • Keep them safe to configure in Keycloak.

Summary

Step Action
1 Go to Google Cloud Console
2 Create/select a project
3 Configure OAuth consent screen
4 Create OAuth client credentials
5 Set redirect URIs if needed
6 Save and copy Client ID & Secret

Part 2: Configure Google as Identity Provider in Keycloak

Step 1: Login to Keycloak Admin Console

  • Open Keycloak Admin UI and select your realm.

Part 2: Configure Google Identity Provider in Keycloak

Login to Keycloak Admin Console and Open Your Realm

  • Open the Keycloak Admin UI.
  • Select your realm.

Add Google as an Identity Provider

  1. Go to Identity Providers in the sidebar.
  2. Click Add provider and select Google.
  3. Enter your Client ID and Client Secret from Google.
  4. Under Advanced Settings, enable Store Tokens.
  5. Click Save.

Configure Your Client (flutter-app) for Token Exchange

  1. Go to Clients and select your client (flutter-app).
  2. Enable the following settings:
    • Standard Flow
    • Direct Access Grants
    • Client Authentication
    • Authorization
  3. Check Service Account Roles.
  4. Click Save.

Enable and Configure Permissions (Crucial Step!)

⚠️ This is the crucial part of the whole process!

  • Under Permissions
  • If Permissions tab is not visible, start Keycloak with the command:
  kc.bat start-dev --hostname=<your-host-name> --features="preview,token-exchange,admin-fine-grained-authz:v1"
  1. Go to the Permissions tab inside your client settings.
  2. Enable Permissions Enabled.
  3. Click on token-exchange permission.
  4. Click Create Client Policy:
    • Name the policy.
    • Select the client (flutter-app).
    • Set decision to Positive.
    • Click Save.
  5. Go back to the token-exchange permission, select the policy you just created, and save.

Configure Permissions for the Google Identity Provider

  1. Go to the Google Identity Provider you added.
  2. Under Permissions, enable Permissions Enabled.
  3. Click token-exchange.
  4. Select the same policy you created for the client.
  5. Click Save.

Part 3: Token Exchange Request

Make this request to exchange a Google access token for a Keycloak token:

POST https://<keycloak-server>/realms/<your-releam>/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<GOOGLE_ACCESS_TOKEN>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&client_id=<your-client-id>
&client_secret=<CLIENT_SECRET>
&subject_issuer=google
&scope=openid profile email
Enter fullscreen mode Exit fullscreen mode

Note: subject_issuer is the Identity Provider’s name (usually "google").

Flutter Google Sign-In and Keycloak Token Exchange Code

// auth_service.dart

import 'package:google_sign_in/google_sign_in.dart';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class AuthService {
  final List<String> scopes = <String>[
    'email',
    'openid',
    'profile',
    'https://www.googleapis.com/auth/userinfo.email',
    'https://www.googleapis.com/auth/userinfo.profile',
  ];
  final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
  bool _initialized = false;

  // Google SignIn Instance
  // final GoogleSignIn _googleSignIn = GoogleSignIn(
  //   scopes: ['email', 'profile', 'openid'],
  // );

  // Secure Storage
  final FlutterSecureStorage _storage = const FlutterSecureStorage();

  // Keycloak Configuration
  final String keycloakUrl =
      "https://knows-pride-future-bon.trycloudflare.com";
  final String realm = "Medical";
  final String clientId = "flutter-app";

  Future<void> _initializeGoogle() async {
    if (_initialized) return;
    try {
      await _googleSignIn.initialize(
        clientId:
            "<your-android-client-id>",
        serverClientId:
            "<your-web-client-id>",
      );
      _initialized = true;
    } catch (e) {
      print("GoogleSign-In initialization failed: $e");
    }
    _initialized = true;
  }

  /// Step 1: Google Sign-In to get ID Token
  Future<String?> signInWithGoogle() async {
    try {
      await _initializeGoogle();
      final GoogleSignInAccount? account = await _googleSignIn.authenticate();
      if (account == null) return null;
      // print(account);

      final GoogleSignInClientAuthorization authorization = await account
          .authorizationClient
          .authorizeScopes(scopes);

      print("access token ${authorization.accessToken}");
      return authorization.accessToken; // <-- Return access token here
    } catch (e) {
      print("Google Sign-In Error: $e");
      return null;
    }
  }

  /// Step 2: Exchange Google ID Token with Keycloak for Access & Refresh Tokens
  Future<Map<String, dynamic>?> exchangeTokenWithKeycloak(
    String googleIdToken,
  ) async {
    final dio = Dio();

    try {
      print("Exchanging token with Keycloak... ${keycloakUrl}");
      final response = await dio.post(
        '$keycloakUrl/realms/$realm/protocol/openid-connect/token',
        data: {
          'client_id': clientId,
          'client_secret': '<your-client-secret>',
          'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
          'subject_token': googleIdToken,
          'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
          'requested_token_type':
              'urn:ietf:params:oauth:token-type:access_token',
          'subject_issuer': 'google',
        },
        options: Options(contentType: Headers.formUrlEncodedContentType),
      );

      return response.data;
    } catch (e) {
      print("Keycloak Token Exchange Error: $e");
      return null;
    }
  }

  /// Step 3: Store Tokens Securely
  Future<void> storeTokens(Map<String, dynamic> tokens) async {
    await _storage.write(key: 'access_token', value: tokens['access_token']);
    await _storage.write(key: 'refresh_token', value: tokens['refresh_token']);
  }

  /// Step 4: Get Dio Instance with Authorization Header
  Future<Dio> getAuthorizedDio() async {
    final dio = Dio();
    final token = await _storage.read(key: 'access_token');

    dio.options.headers['Authorization'] = 'Bearer $token';
    return dio;
  }

  // Step 5: Complete Login Flow

  Future<String?> login() async {
    try {
      // Step 1: Google Sign-In to get ID Token
      final googleIdToken = await signInWithGoogle();
      if (googleIdToken == null) return null;

      final keycloakTokens = await exchangeTokenWithKeycloak(googleIdToken);
      if (keycloakTokens == null) return null;

      await storeTokens(keycloakTokens);
      return keycloakTokens['access_token'];

      // Return Access Token
      return keycloakTokens['access_token'];
    } catch (e) {
      print("Login Error: $e");
      return null;
    }
  }

  /// Step 6: Logout from Google and Clear Tokens
  Future<void> logout() async {
    await _googleSignIn.signOut();
    await _storage.deleteAll();
    print("Logged out successfully");
  }
}
Enter fullscreen mode Exit fullscreen mode

Happy coding! πŸš€

Top comments (0)