Skip to main content

spring_boot_starter_flutter_security 0.6.1 copy "spring_boot_starter_flutter_security: ^0.6.1" to clipboard
spring_boot_starter_flutter_security: ^0.6.1 copied to clipboard

Security for Spring Boot Flutter apps: the OAuth2 / OpenID Connect app login, secrets in the Keychain / Keystore, widgets that follow the logged-in user.

0.6.1 #

  • No changes of its own; released with the family version 0.6.1.

0.6.0 #

  • Data starter (#17)

0.5.0 #

Breaking #

  • Linux: while the keyring is locked, a call that needs a value throws a PlatformSecretStoreException (libsecret_locked) instead of reading as "no value" or doing nothing: read of a stored key and write when the unlock prompt is dismissed, delete and deleteAll when there is a value to delete (a logout kept the tokens).
  • Windows and Linux: the app's values are kept under an application id, spring.flutter.security.application-id when set, else the app's own (<CompanyName>\<ProductName> of Runner.rc on Windows, the GTK APPLICATION_ID on Linux), else the executable's name (a blank id counts as unset); on Windows in %LOCALAPPDATA%\spring_secret_store\<app id>. The values 0.4.x kept under the executable's name (on Windows in %LOCALAPPDATA%\<executable>\spring_secret_store) are moved there once, at the first call after the update, and the old place is never searched again, so another app of the same executable name no longer mixes its values in.
  • iOS: a reinstalled app starts logged out. Keychain items outlive the app, so at the first start after a reinstall (found by an install id kept in a file of the app's container) the secret store's values and the DPoP key are removed; an update or cleared preferences keep them.

Changes #

  • The plugin declares dartFileName for its desktop class (APK, iOS and macOS builds of an app using the plugin failed).
  • Web server mode with the server on another origin: ServerCredentialsHttpTransport sends the cookies only with the requests to the server's origin, so a public API answering Access-Control-Allow-Origin: * keeps working and never sees them; an app with its own transport builds one with ServerCredentialsHttpTransport.forPage(page: ..., server: ...).
  • Android: a login whose window is destroyed (a singleTask main activity, a CLEAR_TOP intent) ends as cancelled instead of staying busy until the app restarts; a hot restart cancels a login the old code left running, and a window's answer reaches only its own login.
  • Android's redirect activity and the Windows and Linux loopback server end the login only for the redirect carrying its state: another site sending the browser to the redirect URI no longer ends a running login (the loopback answers 400 and keeps waiting).
  • Android's redirect matcher no longer takes an opaque redirect URI (books.neon.app:callback, which no link can match) or an authority that is no valid host as a match.
  • restore() brings the user back with the authorities of their login, those an application's own OAuth2UserService added included (kept in the login record); the SCOPE_ ones follow the current access token.
  • A login record of a registration removed from the configuration, or one that cannot be read, is removed with its tokens.
  • logout() clears the context and publishes the LogoutSuccessEvent even when the store cannot delete the stored login, then throws the store's error.
  • The stored tokens of two registrations can no longer share a key (a.b + c and a + b.c); tokens stored by 0.4.x are read and moved to the new key.
  • The loopback window keeps a redirect URI's localhost (or [::1]) for providers that want it exact, listening on 127.0.0.1 and ::1 with the same port.
  • spa mode: every login, the silent one too, makes a new WebCrypto DPoP key, and a key read back from IndexedDB is used only while it still is a non-extractable P-256 signing key.
  • Windows: keys longer than 64 bytes are stored under their SHA-256, each file is written under a unique temporary name, and the 0.4.x move never fails a call: a file in use or read-only is tried again at later starts, and a value deleted meanwhile never comes back.
  • SecretTemplate gives its errors in the returned Future (it threw some synchronously).
  • Every bean decides "a browser" the same way (the web profile), so no platform bean exists next to the web's.
  • The iOS and macOS privacy manifest states that the plugin uses no "required reason" API (the install id is a file, not UserDefaults), and a test fails when the Swift code starts using one.
  • The Android build uses Kotlin 2.4.21 and fixed versions of BouncyCastle, Guava, jose4j, jdom2 and commons-lang3 for the Gradle plugins (advisories on the build machine only; nothing of them is in the APK).
  • The docs say what each platform's store protects from: DPAPI and the Secret Service separate users, not apps; Keychain items survive the app's deletion and are in encrypted backups; a software Keychain DPoP key is in the app's memory while it signs.

0.4.3 #

  • The Windows (DPAPI) and Linux (libsecret) secret stores are tested against the real systems (test/desktop_secret_store_test.dart, CI's "Secret stores on Windows and Linux" jobs): reading, writing, keys(), deleteAll(), apps kept apart, the DPoP key kept across restarts.
  • The web login, spa mode (spring.flutter.security.web.mode=spa: the browser logs in by itself, for apps without a server of their own; server stays the default). SpaOAuth2LoginService: login builds the registration's authorization request (PKCE S256, state, nonce; dpop_jkt with DPoP), keeps it in sessionStorage for the round trip only and navigates the page to the provider; restore finishes the login when the page comes back with code and state (the state, RFC 9207 iss and error checks of the native login, the code exchanged as a public client, the ID token and nonce checked, the user loaded), removes the answer from the address bar (history.replaceState) and the request from sessionStorage; an answer the tab did not ask for, or older than 10 minutes, logs nobody in; after a reload or in a new tab a top-level prompt=none trip to the provider brings back a user who logged in in this browser (a hint in localStorage, the registration id only; spring.flutter.security.web.silent-restore, true by default), login_required meaning nobody; logout forgets the tokens, revokes them (best effort) and ends the provider's session at its end_session_endpoint. Tokens in the tab's memory only (TabOAuth2AuthorizedClientService, which warns when a refresh did not rotate the refresh token). WebCryptoDPoPKey, the app's DPoPKey in spa mode (webCryptoDPoPKey): an ECDSA P-256 WebCrypto key made with extractable: false, its CryptoKey handles kept in IndexedDB, made at the first proof (LoadableDPoPKey); BrowserKeyVault and BrowserKeyPair put WebCrypto behind interfaces. SpaBrowserPage (the base address, history.replaceState, sessionStorage and localStorage as BrowserStorage; InMemoryBrowserStorage), all through dart:js_interop, no package. A registration with a client secret is refused (a browser cannot keep it); an unreachable token endpoint names CORS. Beans: spaBrowserPage, tabAuthorizedClientService, webCryptoDPoPKey, spaOAuth2LoginService; the start fails in spa mode without a registration. browserPage is a server-mode bean now.
  • DPoP keys that survive a restart (RFC 9449): PlatformDPoPKey, the app's DPoPKey bean (platformDPoPKey, wherever a registration is configured, not on the web), ordered before the OAuth2 client's key of one run, which backs off. An EC P-256 key in the Secure Enclave on iOS and macOS (a Keychain key where there is no usable Secure Enclave), in the Android Keystore (StrongBox when there is one), signing in the platform (DER turned into R || S); on Windows and Linux SecretStoreDPoPKey, a key made in Dart and kept as a private JWK in the SecretStore (oauth2.dpop-key). Made at the first proof; a key gone (PlatformDPoPKeyException, no_key) is replaced at the next. DPoPKeyStorage says where the key is. The channel spring_boot_starter_flutter_security/dpop_key (dpopPublicJwk, dpopSign, dpopDelete).
  • SecretStoreOAuth2AuthorizedClientService keeps DPoP tokens (it read Bearer only): the token type, and with its new dPoPKey the thumbprint of the key a DPoP token is bound to; tokens bound to another key (lost, deleted, replaced) are dropped when loaded, so the user logs in again at once. Records without a type read as Bearer.
  • NativeOAuth2LoginService.logout() deletes the PlatformDPoPKey (new dPoPKey), so the next login gets a new key; restore() treats invalid_dpop_proof at the refresh as a refused login.
  • The web login, server mode (backend-for-frontend, spring.flutter.security.web.mode=server, the default): OAuth2LoginService keeps its calls in a browser, where the app's server logs the user in with oauth2Login() and the page holds only an HttpOnly session cookie. ServerOAuth2LoginService: login navigates the page to the server's /oauth2/authorization/{registrationId} (and returns null), restore asks the server's user endpoint ({"name", "authorities", "registrationId", "attributes"}) and puts an OAuth2AuthenticationToken or a PreAuthenticatedAuthenticationToken into the context, logout posts the server's logout with the CSRF token and follows a {"logoutUrl"} answer or a Location on a 2xx (http(s) only). CsrfCookieHttpTransport copies the XSRF-TOKEN cookie into X-XSRF-TOKEN on unsafe requests to the server only; in a browser the package declares it as the httpTransport bean around fetch (with FetchCredentials.include for a server on another origin), before Feign's, and backs off for an application's own HttpTransport. BrowserPage (address, cookies, navigation; a bean, faked in tests), FlutterWebSecurityProperties (spring.flutter.security.web.mode, server-url, user-endpoint, logout-url, authorization-base-uri). Outside a browser nothing changes.
  • Breaking: OAuth2LoginService is an interface (login returns Future<Authentication?>, restore Future<Authentication?>, authentication an Authentication?); the login of phones and desktops is NativeOAuth2LoginService (its constructor, user, sessionKey and the narrower OAuth2AuthenticationToken types are those of the former class). The module now comes before Feign's too.
  • First version: the login of an app with OAuth 2.0 / OpenID Connect and the platform code of logins and secrets (a Flutter plugin with its own platform code, no community plugins), on the registrations, token endpoint clients and authorized clients of spring_boot_starter_oauth2_client:
    • OAuth2LoginService, the app login: login (authorization code with PKCE in the AuthorizationWindow, the state, iss, ID token and user info checks), restore at startup from the stored tokens, logout with token revocation (OAuth2TokenRevocationClient, RFC 7009) and RP-initiated logout. In a web page the server logs in (see above).
    • AuthorizationWindow and AuthorizationCancelledException. PlatformAuthorizationWindow, the window of Android, iOS and macOS: ASWebAuthenticationSession (custom schemes; https redirects on iOS 17.4 / macOS 14.4; preferEphemeralSession) and Custom Tabs with the redirect caught by the package's RedirectReceiverActivity, which an app that logs in declares with its scheme in its AndroidManifest.xml; one login at a time; other failures are AuthorizationWindowExceptions. LoopbackAuthorizationWindow, the window of Windows and Linux: the default browser and a one-shot server on 127.0.0.1 (RFC 8252 §7.3).
    • SecretStore and InMemorySecretStore. PlatformSecretStore, the app's SecretStore bean: the Keychain on iOS and macOS (generic passwords, this device only, not synced; the login keychain for macOS builds without a team), an AES-256-GCM key of the Android Keystore with the values in a private preferences file, DPAPI files on Windows and the Secret Service through libsecret on Linux (both through dart:ffi, nothing native to build). None on the web (an InMemorySecretStore that the login and the template never fill). Errors are PlatformSecretStoreExceptions.
    • SecretStoreOAuth2AuthorizedClientService, the app's OAuth2AuthorizedClientService: the tokens as JSON in the SecretStore, so the login survives a restart.
    • SecretTemplate, the app's own secrets as JSON objects under a namespace; it throws on the web.
    • The beans are registered unless the application declares its own, before the OAuth2 client's auto-configuration, so the authorized client service replaces the client's in-memory one.
  • Depends on and re-exports spring_boot_starter_flutter, spring_boot_starter_oauth2_client and spring_security_core (the Windows and Linux stores use dart:ffi alone, no package:ffi).
0
likes
160
points
54
downloads

Documentation

API reference

Publisher

verified publisherfarum-azula.org

Weekly Downloads

Security for Spring Boot Flutter apps: the OAuth2 / OpenID Connect app login, secrets in the Keychain / Keystore, widgets that follow the logged-in user.

Repository (GitHub)
View/report issues

Topics

#flutter #oauth2 #security #spring

License

Apache-2.0 (license)

Dependencies

flutter, flutter_web_plugins, spring_boot_dart, spring_boot_starter_flutter, spring_boot_starter_oauth2_client, spring_security_core

More

Packages that depend on spring_boot_starter_flutter_security

Packages that implement spring_boot_starter_flutter_security