spring_boot_starter_flutter_security 0.4.3
spring_boot_starter_flutter_security: ^0.4.3 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.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;serverstays the default).SpaOAuth2LoginService:loginbuilds the registration's authorization request (PKCES256,state,nonce;dpop_jktwith DPoP), keeps it insessionStoragefor the round trip only and navigates the page to the provider;restorefinishes the login when the page comes back withcodeandstate(thestate, RFC 9207issanderrorchecks of the native login, the code exchanged as a public client, the ID token andnoncechecked, the user loaded), removes the answer from the address bar (history.replaceState) and the request fromsessionStorage; 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-levelprompt=nonetrip to the provider brings back a user who logged in in this browser (a hint inlocalStorage, the registration id only;spring.flutter.security.web.silent-restore,trueby default),login_requiredmeaning nobody;logoutforgets the tokens, revokes them (best effort) and ends the provider's session at itsend_session_endpoint. Tokens in the tab's memory only (TabOAuth2AuthorizedClientService, which warns when a refresh did not rotate the refresh token).WebCryptoDPoPKey, the app'sDPoPKeyin spa mode (webCryptoDPoPKey): an ECDSA P-256 WebCrypto key made withextractable: false, itsCryptoKeyhandles kept in IndexedDB, made at the first proof (LoadableDPoPKey);BrowserKeyVaultandBrowserKeyPairput WebCrypto behind interfaces.SpaBrowserPage(the base address,history.replaceState,sessionStorageandlocalStorageasBrowserStorage;InMemoryBrowserStorage), all throughdart: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.browserPageis a server-mode bean now. - DPoP keys that survive a restart (RFC 9449):
PlatformDPoPKey, the app'sDPoPKeybean (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 intoR || S); on Windows and LinuxSecretStoreDPoPKey, a key made in Dart and kept as a private JWK in theSecretStore(oauth2.dpop-key). Made at the first proof; a key gone (PlatformDPoPKeyException,no_key) is replaced at the next.DPoPKeyStoragesays where the key is. The channelspring_boot_starter_flutter_security/dpop_key(dpopPublicJwk,dpopSign,dpopDelete). SecretStoreOAuth2AuthorizedClientServicekeeps DPoP tokens (it readBeareronly): the token type, and with its newdPoPKeythe 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 asBearer.NativeOAuth2LoginService.logout()deletes thePlatformDPoPKey(newdPoPKey), so the next login gets a new key;restore()treatsinvalid_dpop_proofat the refresh as a refused login.- The web login, server mode (backend-for-frontend,
spring.flutter.security.web.mode=server, the default):OAuth2LoginServicekeeps its calls in a browser, where the app's server logs the user in withoauth2Login()and the page holds only anHttpOnlysession cookie.ServerOAuth2LoginService:loginnavigates the page to the server's/oauth2/authorization/{registrationId}(and returnsnull),restoreasks the server's user endpoint ({"name", "authorities", "registrationId", "attributes"}) and puts anOAuth2AuthenticationTokenor aPreAuthenticatedAuthenticationTokeninto the context,logoutposts the server's logout with the CSRF token and follows a{"logoutUrl"}answer or aLocationon a2xx(http(s)only).CsrfCookieHttpTransportcopies theXSRF-TOKENcookie intoX-XSRF-TOKENon unsafe requests to the server only; in a browser the package declares it as thehttpTransportbean aroundfetch(withFetchCredentials.includefor a server on another origin), before Feign's, and backs off for an application's ownHttpTransport.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:
OAuth2LoginServiceis an interface (loginreturnsFuture<Authentication?>,restoreFuture<Authentication?>,authenticationanAuthentication?); the login of phones and desktops isNativeOAuth2LoginService(its constructor,user,sessionKeyand the narrowerOAuth2AuthenticationTokentypes 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 theAuthorizationWindow, thestate,iss, ID token and user info checks),restoreat startup from the stored tokens,logoutwith token revocation (OAuth2TokenRevocationClient, RFC 7009) and RP-initiated logout. In a web page the server logs in (see above).AuthorizationWindowandAuthorizationCancelledException.PlatformAuthorizationWindow, the window of Android, iOS and macOS:ASWebAuthenticationSession(custom schemes;httpsredirects on iOS 17.4 / macOS 14.4;preferEphemeralSession) and Custom Tabs with the redirect caught by the package'sRedirectReceiverActivity, which an app that logs in declares with its scheme in itsAndroidManifest.xml; one login at a time; other failures areAuthorizationWindowExceptions.LoopbackAuthorizationWindow, the window of Windows and Linux: the default browser and a one-shot server on127.0.0.1(RFC 8252 §7.3).SecretStoreandInMemorySecretStore.PlatformSecretStore, the app'sSecretStorebean: 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 throughdart:ffi, nothing native to build). None on the web (anInMemorySecretStorethat the login and the template never fill). Errors arePlatformSecretStoreExceptions.SecretStoreOAuth2AuthorizedClientService, the app'sOAuth2AuthorizedClientService: the tokens as JSON in theSecretStore, 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_clientandspring_security_core(the Windows and Linux stores usedart:ffialone, nopackage:ffi).