keycloak_client 3.0.0
keycloak_client: ^3.0.0 copied to clipboard
A Flutter package for Keycloak authentication using the Authorization Code flow.
CHANGELOG #
3.0.0 #
New #
-
onTokenRefreshed— emits after every successful token refresh while a session is active, on both refresh paths (the scheduled timer and the inline refresh insidegetAuthToken()). Refreshes were previously invisible from outside the client, which is fine for code that callsgetAuthToken()per request but not for a connection authenticated once at dial time: a WebSocket or gRPC channel would hold a token that expired underneath it.The stream carries no value — call
getAuthToken()for the new token. UnlikeonAuthChangeandonUserChangeit does not replay on listen, and it stays silent while signed out, including for the refreshinitialize()performs on a cold start with an expired access token. -
ClientConfig.refreshTokenLifetime— how long a refresh token is assumed to stay valid, defaulting to 30 days.package:oauth2discards the token response'srefresh_expires_in, so this cannot be read from the server; set it to match the realm's SSO Session Max. -
ClientConfig.isOfflineSession— true whenoffline_accessis amongscopes. The only offline signal available on the client, for the same reason. -
PendingGrantis now exported. It appears inIPendingGrantStore's signature, so implementing that interface required a type you could not name.
Bug Fixes #
-
A second
login()while one is running no longer fails. It now joins the attempt already in flight instead of starting another. On desktop a second attempt could not work at all: the first one's loopback listener holds its port until the user finishes orDesktopConfig.loopbackTimeoutexpires — five minutes by default — so binding it again threw, and an impatient double-tap killed a login that was otherwise working. Since the user cannot tell a slow browser from a dropped one, show something while the call is outstanding; the example disables its button and says it is waiting for the browser. -
Offline tokens no longer degrade into 30-day ones.
isOfflineTokenwas never set anywhere: both credential-writing paths calledUserCredentials.fromOAuth2without it, so a session opened withoffline_accesswas stored as an ordinary one with an invented 30-day refresh expiry. On day 31initialize()ended a session Keycloak would still have honoured. Offline-ness and the assumed lifetime are now supplied on every write, taken fromClientConfig. -
Desktop and mobile logins now send an OAuth
stateparameter. Only the web strategy did.package:oauth2does not generate one for you, and it validates the callback'sstateonly when one was supplied — so both flows accepted any callback carrying acode. PKCE kept this from being directly exploitable, but the desktop loopback listener would accept a code from any local origin. RFC 6749 §10.12. -
WebLoginStrategy.withDependenciesnow honours itsredirectargument. It wasrequired, documented as recording the URL instead of navigating, and silently discarded — so a test written against its own documentation navigated the real browser tab. -
WebConfig.pendingGrantTTLnow has an effect. The strategy built its store with a hardcoded default and never read the configured value, so the documented setting did nothing and the two defaults disagreed (15 minutes documented, 10 applied). The TTL now rides on the persistedPendingGrant, which is also what letshandleCallbackjudge expiry after the page reload that ends the redirect. -
Mobile login no longer races the deep link.
AppLinkswas subscribed after the browser launched, so a user already signed in at Keycloak could be bounced back before anything was listening — hanging the login for the fulldeepLinkTimeout. The listener is now attached first, as the desktop strategy has always done with its loopback server. -
A login-listener port already in use now throws
KeycloakNetworkExceptioninstead of a rawSocketException, so callers catchingKeycloakExceptionsee it. -
MobileLoginStrategyhad atry { … } on KeycloakTimeoutException { rethrow; }with no other catch and no finally. Removed.
Breaking #
-
Logging now goes through
package:logginginstead ofpackage:logger, andClientConfig.logLeveland theLogLevelenum are gone with it. The client logs under the logger nameKeycloakClientand prints nothing on its own — the app installs a listener and picks the level. See the Logging section of the README.To migrate, drop
logLevelfrom yourClientConfigand configurepackage:loggingat startup:Logger.root.level = Level.INFO; Logger.root.onRecord.listen((r) => debugPrint('${r.level.name}: ${r.message}')); -
refreshToken()now throwsKeycloakSessionExpiredExceptionwhen the refresh turned out to be permanently dead. It previously returned normally, so an awaiting caller could not tell "refreshed" from "your session just ended" without separately watchingonAuthChange. This also makesKeycloakSessionExpiredExceptionreachable — it was exported and documented but never thrown by anything. -
A login strategy now throws
KeycloakServerExceptionwhen the IdP returns anerrorother thanaccess_denied. Every error code was previously reported asnull, i.e. "the user cancelled", so a misconfigured client or an invalid scope was indistinguishable from someone changing their mind.access_deniedstill returnsnull, because that genuinely is a cancellation. -
IWebLoginStrategy.handleCallbacktakes aclientSecretnamed argument, andPendingGrantno longer carries one. The grant is persisted tosessionStorage, readable by any script on the origin; the secret now comes from the liveClientConfigat callback time. A browser client should be a public one with no secret at all, but the record no longer publishes it if there is. -
PendingGrant.isExpired(Duration)is now the getterisExpired, reading thettlMsthe record carries, andSessionStoragePendingGrantStoreno longer takes attl. -
UserCredentials.fromApiremoved. It parsed a raw Keycloak token response — a shape this package never receives, sincepackage:oauth2performs the exchange — so nothing called it and nothing tested it. -
DesktopLoginStrategy.generateCodeVerifierremoved. It was the only public one of three identical copies; all three now sharegenerateCodeVerifier()/generateState()internally.
Bug Fixes #
- Logout again revokes the session at Keycloak when the user has no ID token.
The
id_token_hintfield was being sent as a literal null instead of being omitted, which made the request body aMap<String, String?>;httpthrows when it casts that to form fields, andrevokeSessionswallows the throw. The local session still cleared, so a logout looked successful while the refresh token stayed valid server-side.
2.1.1 #
Bug Fixes #
AccountCredential.fromJsonnow reads instances from the correct field on the Keycloak response. Keycloak's account REST API returns configured instances underuserCredentialMetadatas(each wrapped in a metadata envelope with the actual credential under.credential), notuserCredentials. The previous lookup missed every entry and reportedinstanceCount: 0/isConfigured: falsefor credentials the user had actually configured. The parser now readsuserCredentialMetadatasfirst and unwraps each envelope, falling back to the flatuserCredentialsshape for compatibility with older or alternative response paths.
2.1.0 #
New #
getAccountCredentials()— queries Keycloak's account REST API (/realms/{realm}/account/credentials) and returns the list of credential types configured for the current user as a sealedAccountCredentialfamily. Subtypes (PasswordCredential,OtpCredential,WebAuthnCredential) expose per-type fields parsed from the response, including OTPsubType/digits/period/algorithmand WebAuthnaaguid. Unknown credential types fall back toUnknownCredentialso realm-specific or future credential providers don't break the client.ClientConfig.accountCredentialsEndpoint— exposed for completeness.
2.0.0 #
Breaking Changes #
-
getAuthToken()now throwsKeycloakNetworkExceptionwhen the session is valid but a network error prevents token refresh. Previously it returnednull, making "device is offline" indistinguishable from "no session". Update call sites:// Before final token = await client.getAuthToken(); if (token == null) showLoginScreen(); // After try { final token = await client.getAuthToken(); if (token == null) showLoginScreen(); // genuinely no session } on KeycloakNetworkException { showOfflineBanner(); // transient — user is still signed in } -
KeycloakClientconstructor now accepts optionalwebConfig,mobileConfig, anddesktopConfignamed parameters directly instead of embedding them insideClientConfig. Pass platform-specific configuration at the client level:KeycloakClient( clientConfig: ClientConfig(...), mobileConfig: MobileConfig(redirectUri: 'myapp://auth'), desktopConfig: DesktopConfig(loopbackUri: Uri.parse('http://localhost:9000/cb')), )
Bug Fixes #
- Token refresh no longer hangs indefinitely on unresponsive networks. A configurable
refreshTimeout(default 15 s) is applied to every refresh HTTP request; a timeout is treated as a transient failure and retried automatically. initialize()no longer triggers a second token refresh when the access token is expired and the server is unreachable. Previously the offline startup time was up to 30 s (two sequential timeouts); it is now at mostrefreshTimeout(15 s by default).- The refresh retry loop no longer runs indefinitely past the local refresh token
expiry. Both
SocketExceptionand non-invalid_grantauthorization error retry paths now checkisRefreshExpiredbefore scheduling a retry; if expired, the session ends immediately withAuthState.sessionExpired.
New #
ClientConfig.refreshTimeout— controls the per-request HTTP timeout for token refresh. Defaults toDuration(seconds: 15). Lower it for faster offline detection; raise it for slow or high-latency Keycloak deployments.refreshToken()— forces an immediate token refresh regardless of access token expiry, then reloads the user profile. ThrowsKeycloakNetworkExceptionif the server is unreachable. Useful after returning from account management or when an admin has changed the user's roles and updated claims are needed immediately.
Improvements #
- User profile is automatically re-fetched when the device recovers from a network
outage (refresh transitions from failing to succeeding), preventing a stale
currentUserafter the app comes back online. - Internal architecture:
SessionManager(identity) andTokenService(transport) are now separate classes, matching Firebase Auth's separation of concerns.AuthStateandcurrentUserare never affected by transient network events.
1.1.1 #
- Fix for stale user info when refresh is unsuccessful
1.0.1 #
- Tiny tweaks
1.0.0 #
Breaking changes #
KeycloakClientconstructor now takes a singleClientConfigobject instead of individual parametersidTokenonUserCredentialsis now nullable (String?) — non-OIDC flows may not return an ID token
New features #
- *Platform-specific login strategies- — the library automatically selects the right strategy at runtime:
DesktopLoginStrategy— localhostHttpServerloopback + system browser (Windows, macOS, Linux)MobileLoginStrategy— system browser + deep-link callback viaapp_linksWebLoginStrategy— same-tab redirect flow; persists a pending grant insessionStorageacross the redirect
ClientConfig— single configuration object replacing individual constructor parameters; exposes computed endpoint URIs (authorizationEndpoint,tokenEndpoint,userInfoEndpoint,logoutEndpoint)PlatformConfigsealed hierarchy —DesktopConfig,MobileConfig,WebConfigwith platform-specific knobs (loopback URI, timeout, success page HTML, pending-grant TTL, custom launch callback)handleWebCallback(Uri)— call once on app startup to complete in-progress web redirect flowsKeycloakTimeoutException— new typed exception thrown when the browser does not redirect back within the configured timeout- PKCE (
code_verifier/code_challenge) enabled on all platforms UserCredentials.fromOAuth2andUserCredentials.toOAuth2Credentials— interop with theoauth2packageDesktopConfig.clientSecretsupport for confidential clients
Improvements #
- Replaced
dio+flutter_web_auth_2with theoauth2package — one transport, one token-exchange path onAuthChangeandonUserChangestreams share a single_bufferedStreamhelper — no more duplicated stream controller code- Log messages trimmed and made consistent
Dependency updates #
- Added
oauth2: ^2.0.5,url_launcher: ^6.3.2,web: ^1.1.1,app_links: ^7.0.0 - Updated
flutter_secure_storage:^9.2.4→^10.0.0,dio:^5.8.0+1→^5.9.2 - Removed
flutter_web_auth_2
Example app #
- Added web and Windows platform targets
- Updated example to demonstrate
ClientConfigandhandleWebCallback
0.0.1 #
-
Authorization Code flow login via system browser (
login()) -
Persistent session storage via
flutter_secure_storage -
Reactive authentication state stream (
onAuthChange) -
Reactive user profile stream (
onUserChange) -
On-demand access token retrieval with automatic refresh (
getAuthToken()) -
User profile reload from Keycloak userinfo endpoint (
reloadUser()) -
Typed exceptions:
KeycloakNetworkException,KeycloakServerException,KeycloakSessionExpiredException -
Configurable OAuth scopes
-
Configurable log verbosity via
LogLevel