keycloak_client 0.0.1
keycloak_client: ^0.0.1 copied to clipboard
A Flutter package for Keycloak authentication using the Authorization Code flow.
keycloak_client #
A Flutter package for Keycloak authentication. Handles login, token refresh, session persistence, and user profile — so you don't have to.
Features #
- Browser login — full Authorization Code flow via the system browser
- Automatic token refresh — silently refreshes access tokens before they expire
- Persistent sessions — credentials stored securely via
flutter_secure_storage - Reactive auth state —
Stream<AuthState>for signed-in / signed-out / session-expired / unknown - Reactive user profile —
Stream<UserInfo?>that updates whenever user data changes - On-demand tokens —
getAuthToken()returns a fresh token, refreshing automatically if needed - Typed exceptions —
KeycloakNetworkException,KeycloakServerException,KeycloakSessionExpiredException— no rawDioExceptionleaks - Configurable logging — from silent to trace-level with structured output
Installation #
Add to your pubspec.yaml:
dependencies:
keycloak_client: ^0.0.1
Setup #
Create a single KeycloakClient instance for your app and call initialize() early (e.g. in initState or your DI setup). initialize() is non-blocking — it restores any persisted session in the background.
final client = KeycloakClient(
baseUrl: 'https://auth.example.com', // Keycloak server URL
realm: 'my-realm', // Keycloak realm name
clientId: 'my-app', // Client ID registered in Keycloak
redirectUri: 'myapp://auth', // Redirect URI registered in Keycloak
scopes: const ['openid', 'email', 'profile'], // Optional — these are the defaults
);
@override
void initState() {
super.initState();
client.initialize();
}
@override
void dispose() {
client.dispose();
super.dispose();
}
Redirect URI: Must be registered in your Keycloak client settings. The scheme (e.g.
myapp) must also be registered as a custom URL scheme on each platform — see Platform setup below.
Platform setup #
login() uses the system browser via flutter_web_auth_2. You must register your redirect URI scheme on each platform before it will work.
Android #
Add a CallbackActivity to android/app/src/main/AndroidManifest.xml inside <application>:
<activity
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
android:exported="true">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="auth"/>
</intent-filter>
</activity>
Replace myapp with the scheme part of your redirectUri.
iOS #
Add your scheme to ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Replace myapp with the scheme part of your redirectUri.
Usage #
Listening to authentication state #
onAuthChange emits the current state immediately on subscribe, then on every change.
StreamBuilder<AuthState>(
stream: client.onAuthChange,
builder: (context, snapshot) {
final state = snapshot.data;
if (state == null || state.isUnknown) {
return const CircularProgressIndicator();
}
if (state.isSignedIn) {
return const HomeScreen();
}
if (state.isSessionExpired) {
return const SessionExpiredScreen(); // prompt re-login with a message
}
return const LoginScreen();
},
);
| State | Meaning |
|---|---|
AuthState.unknown |
Initial state; session restore in progress |
AuthState.signedIn |
User has a valid session |
AuthState.signedOut |
User explicitly logged out |
AuthState.sessionExpired |
Refresh token expired; user must log in again |
Listening to user changes #
onUserChange emits the current UserInfo? immediately on subscribe, then whenever the profile is updated.
StreamBuilder<UserInfo?>(
stream: client.onUserChange,
builder: (context, snapshot) {
final user = snapshot.data;
if (user == null) return const SizedBox.shrink();
return Text('Hello, ${user.username}');
},
);
UserInfo fields:
| Field | Type | Scope required | Description |
|---|---|---|---|
id |
String |
(always present) | Keycloak subject (sub) |
username |
String? |
profile |
Preferred username |
givenName |
String? |
profile |
First name |
familyName |
String? |
profile |
Last name |
email |
String? |
email |
Email address |
emailVerified |
bool? |
email |
Whether the email is verified |
Fields are null when the corresponding scope was not requested or the claim was not set on the account.
Login #
Opens the system browser for Keycloak's login page and completes the Authorization Code flow automatically.
await client.login();
// Auth state stream updates to signedIn on success.
Returns without throwing if the user cancels the browser flow.
Logout #
Invalidates the session on the Keycloak server and clears local credentials.
await client.logout();
Getting a fresh access token #
Use getAuthToken() when making authenticated API calls. It returns the current access token, refreshing it automatically if expired.
final token = await client.getAuthToken();
if (token != null) {
dio.options.headers['Authorization'] = 'Bearer $token';
}
Returns null if the user is not signed in or the session has fully expired.
Reloading user profile #
Fetches the latest user info from the Keycloak /userinfo endpoint and updates the onUserChange stream.
final user = await client.reloadUser();
Error handling #
login() and reloadUser() throw typed exceptions — no dependency on Dio internals required.
| Exception | When thrown |
|---|---|
KeycloakNetworkException |
No connectivity or request timeout |
KeycloakServerException |
Keycloak returned an HTTP error (statusCode available) |
KeycloakSessionExpiredException |
Refresh token expired; user must log in again |
try {
await client.login();
} on KeycloakNetworkException {
// show "check your connection"
} on KeycloakServerException catch (e) {
// show "server error ${e.statusCode}"
}
getAuthToken() never throws — it returns null when the session is unavailable.
Log Levels #
Control log verbosity with the logLevel constructor parameter:
KeycloakClient(
// ...
logLevel: LogLevel.info,
);
| Level | Description |
|---|---|
LogLevel.trace |
Everything, including internal state transitions (default) |
LogLevel.debug |
Debug-level messages |
LogLevel.info |
Informational messages (logins, token refreshes) |
LogLevel.warning |
Warnings (session expiry, cancelled logins) |
LogLevel.error |
Errors only |
LogLevel.fatal |
Fatal errors only |
LogLevel.off |
Disable all logging |
Disposal #
Always call dispose() when the client is no longer needed to cancel timers, close streams, and release the HTTP client.
@override
void dispose() {
client.dispose();
super.dispose();
}