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
- Open your browser and visit: https://console.cloud.google.com/apis/credentials
- Sign in with your Google account.
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
- In the left sidebar, click OAuth consent screen.
- Choose External (for apps used by any Google user) or Internal (for G Suite users only).
- Click Create.
- Fill out the required fields:
- App name
- User support email
- Developer contact email
- Save and continue through scopes and test users (default settings usually work).
- Click Save and Continue until done.
Step 4: Create OAuth 2.0 Credentials
- Go to Credentials on the left menu.
- Click Create Credentials > OAuth client ID.
- Select Application type:
- Web application (if your app uses redirects or backend)
- Other (for standalone apps like Flutter)
- 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
- Go to Identity Providers in the sidebar.
- Click Add provider and select Google.
- Enter your Client ID and Client Secret from Google.
- Under Advanced Settings, enable Store Tokens.
- Click Save.
Configure Your Client (flutter-app) for Token Exchange
- Go to Clients and select your client (
flutter-app). - Enable the following settings:
- Standard Flow
- Direct Access Grants
- Client Authentication
- Authorization
- Check Service Account Roles.
- 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"
- Go to the Permissions tab inside your client settings.
- Enable Permissions Enabled.
- Click on token-exchange permission.
- Click Create Client Policy:
- Name the policy.
- Select the client (
flutter-app). - Set decision to Positive.
- Click Save.
- Go back to the token-exchange permission, select the policy you just created, and save.
Configure Permissions for the Google Identity Provider
- Go to the Google Identity Provider you added.
- Under Permissions, enable Permissions Enabled.
- Click token-exchange.
- Select the same policy you created for the client.
- 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
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");
}
}
Happy coding! π
Top comments (0)