awesome_flutter_auth 1.10.7
awesome_flutter_auth: ^1.10.7 copied to clipboard
A Flutter/Dart authentication client for awesome-lang-auth servers. Supports web (including WASM) via cookie + CSRF and native platforms via Bearer token.
Changelog #
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
1.10.7 #
Fixed #
AuthState.userStreamno longer loses or delays state changes (#36):userStreamwas anasync*generator. It read the current user and subscribed to the changes only once the generator body had run, some microtasks afterlisten. AsetUserin that window was lost, and the first value could already be the newer user instead of the one atlistentime. This happened on every platform; the window differs between the VM, dart2js and dart2wasm.- Each change also went through extra asynchronous hops. Under dart2js the
nullfromlogout()had not arrived yet whenlogout()completed, so a subscription cancelled right after it missed the logout. - Apps that listen to
userStreamright after creating the client (for example aStreamBuilderin the first frame while the startup session check runs) could miss the initial login or logout. userStreamnow reads the current user and subscribes to the changes synchronously inlisten, and forwards each change as soon as the state delivers it. The first event is still delivered asynchronously, and every event arrives in order. Pause, resume and cancel are forwarded, anddispose()still closes the stream.
- No refresh for a late 401/403 after the session ended (#33):
- A request still in flight when
logout(),clearLocalSession()or a successfuldeleteAccount()ended the session could get 401/403 (for example 403No access token provided) after the server had cleared the cookies. The client then sentPOST /refresh, on web withoutX-CSRF-Tokenbecause the csrf cookie was gone, and emitted a secondloggedOutevent when that refresh failed. - Once the session has ended locally (
logout(),clearLocalSession(), a successfuldeleteAccount(), a failed refresh), a 401/403 is returned to the caller unchanged until the next successfulGET /me: no refresh, no retry, nologgedOutorsessionRevokedevent. This covers requests in flight when the session ended and requests started afterwards, such as route guards, polling orcheckSession(). Every login flow ends withGET /me, so the next login restores the usual refresh. This applies to theapi*methods and tohttpClient.send(), on web and native. logout()marks the session as ended before it sendsPOST /logout, because the server can clear the cookies before its response arrives.- A refresh in flight when the session ends has its result dropped: on native its tokens are no longer written back to the cleared
TokenStorageor kept in memory, so a logged-out app no longer sends a freshAuthorizationheader or resumes the session on the next start. The original request is not retried, and the logout callback is not called again. A request of a new session no longer joins a refresh started for the ended one. logout()waits for a refresh in flight before it sendsPOST /logout, so the server revokes the refreshed session. On web the browser stores the refresh response's cookies itself and the client cannot undo that;clearLocalSession(), which is local, does not wait, so on web those cookies can still be stored after it.- A retry is not sent when the session ended while its headers were being read.
- A refresh with no csrf cookie is still sent when the session was not ended locally. The csrf cookie expires with the access token while the refresh cookie lives on, and
/refreshis CSRF-exempt on the servers, so this refresh is how an idle web session resumes.
- A request still in flight when
logout()sends the refresh token on native (#33): in bearer modePOST /logoutcarries{"refreshToken": "..."}, read before the local tokens are cleared, so the server can revoke the session even when the access token has expired. Both servers accept it. Cookie mode sends no body, as before.
Changed #
- When several requests get 401/403 and share one refresh that fails,
loggedOutis emitted once instead of once per request. - When a refresh fails while
logout()waits for it,logout()sendsPOST /logoutbut emits no secondloggedOut. - A request of an ended session whose 401/403 arrives after a new login is returned unchanged instead of being refreshed with the new session.
- The wire protocol is unchanged apart from the optional
refreshTokenin the nativePOST /logoutbody.
Tests #
- Added
test/auth_state_test.dart: no change is lost when made right afterlisten, changes are delivered one microtask aftersetUser, late and multiple subscribers, pause, anddispose(). - The test "userStream emits user on login and null on logout" now checks the exact sequence (null, user, null).
- CI runs the whole suite on Chrome with dart2js again, not only the offline token tests.
- A request in flight across
logout(),clearLocalSession()anddeleteAccount(), in cookie mode and native, throughapiGetandhttpClient.get, resolved with 403No access token provided: noPOST /refresh, the original error is returned, oneloggedOutevent. The mock server fails/refreshonce the session is gone, as the real servers do, so the event count is checked. - Cookie mode with the csrf cookie cleared by logout: no
POST /refresh(and no other unsafe request) withoutX-CSRF-Tokenis sent. - A refresh in flight across
clearLocalSession()andlogout(): no token written back (native),logout()sendsPOST /logoutonly after the refresh, with the refreshed token in the body (native). - A request of a new session does not join the ended session's refresh; a retry is not sent when the session ends while its headers are read.
- Two 401s sharing one failed refresh: one refresh, one
loggedOut. - After logout with no new login, a 401/403 and
checkSession()do not refresh and emit no secondloggedOut; after a new login a 401 refreshes as before. logout()sends the refresh token in the body on native and no body in cookie mode.- Regression guard: with no logout and an expired csrf cookie the client still refreshes and retries.
1.10.6 #
Fixed #
OfflineTokenVerifierworks under dart2wasm (#34):- Under
flutter build web --wasmevery token was rejected withsignature_invalid: Ed25519 verification internal error. - The cause was
ed25519_edwards. It does its arithmetic throughadaptive_number, whose conditional import has no dart2wasm branch, so every operation threwUnsupportedError. - Ed25519 verification now uses the TweetNaCl arithmetic of
pinenacl. It is pure Dart and synchronous, and it gives the same result on the VM, dart2js and dart2wasm. - The public API and the order of the checks are unchanged.
- Under
Changed #
- New direct dependency:
pinenacl^0.6.0, which replacesed25519_edwards.pinenaclhas no dependencies, so this drops five packages from the dependency graph:ed25519_edwards,adaptive_number,fixnum,convertandcrypto.- An app or package that already depends on
pinenaclbelow 0.6 gets a version-solving conflict with this release.
- An app or package that already depends on
Tests #
- Added the RFC 8032 section 7.1 Ed25519 vectors.
- Added the licence token vectors, with the outcome recorded on the VM before the change: valid, next-kid, expired, wrong-alg, wrong-aud, tampered, tier-, next-tier- and trial-premium.
- Added negative cases: wrong key, tampered payload, a flipped bit in R or S, and non-canonical S (S + L on the tokens; S + L and S = L on the Ed25519 check itself).
- All of these run on the VM, on Chrome with dart2js and on Chrome with dart2wasm.
- CI has a
webjob that runs the suite on Chrome with dart2wasm, and the offline token tests with dart2js (the rest of the suite has a known dart2js-only test failure intest/auth_client_test.dart, unrelated to this change).
1.10.5 #
Security #
- CSRF token never sent to a scheme-relative or foreign URI (#31):
isSameOriginPuretreats a scheme-relative URL (//host/...) as cross-origin, including one pointing at the same host.AuthHttpClienttakes the CSRF decision on the URI that is actually sent.httpClient.send()used to rebuildrequest.urlunder the API prefix, so//evil.com/xpassed the same-origin check as/x. It never calls the CSRF provider for a scheme-relative target.deleteAccount(path:)andAuthOptions.deleteAccountPathreject scheme-relative paths (//evil.com/x, and/\evil.com/x, whichUri.parsenormalises to//evil.com/x) and non-http(s) schemes. They returnAuthResult.failurewitherrorCode: 'INVALID_PATH'and send no request.apiGet/apiPost/apiPatch/apiDeletethrowArgumentErrorfor a scheme-relative path.
Fixed #
- Path segments encoded again (#31):
revokeSession()andunlinkAccount()encode the interpolated segments (session handle, provider, providerAccountId) withUri.encodeComponent. A?,#or/in an id now stays inside its segment instead of becoming a query, fragment or extra segment; this was a regression since 1.10.2.getOAuthUrl()encodes the provider segment too. - Prefix and endpoint joined by path segments (#31): the prefix path segments and the endpoint path segments are joined, with the same result for absolute and relative prefixes. Before, with an absolute prefix, the prefix was skipped when the endpoint merely started with the prefix path as a string:
https://ita.app/s+/sessionsresolved tohttps://ita.app/sessionsand now resolves tohttps://ita.app/s/sessions. NativeAuthClient.clearLocalSession()awaits the token storage (#31):- It awaits
TokenStorage.clear()and returns aFuturethat completes once the storage is cleared. - Requests issued before then wait for the clear even if the call is not awaited, so they never carry the old
Authorization: Bearerheader with an asynchronous storage. - User state and the
loggedOutevent are still updated synchronously.
- It awaits
deleteAccount(path: '')falls back to/account(#31): an empty or blankpath(orAuthOptions.deleteAccountPath) is treated as unset. It falls back todeleteAccountPathif that is set, otherwise to{apiPrefix}/account.available2faMethodsvalidated eagerly (#31): non-string entries are dropped and a non-list value yields an empty list. ReadingLoginResult.availableMethodsno longer throws. A non-booleanrequires2FASetupreads asfalseinstead of throwing.
Changed #
AuthClient.clearLocalSession()returnsFuture<void>(wasvoid). Existing calls such asauth.clearLocalSession();still compile. Classes that implement or overrideclearLocalSession()with avoidreturn type must change it toFuture<void>.- Endpoint paths are no longer de-duplicated against an absolute prefix. With an absolute prefix,
httpClient.apiGet('/api/auth/me')used to resolve tohttps://host/api/auth/mewhen the prefix washttps://host/api/auth. It now resolves tohttps://host/api/auth/api/auth/me, the same as with a relative prefix. Pass paths relative to the prefix (/me), or use a full URL.
1.10.4 #
Fixed #
- Token clearing on Native platforms (#29):
clearLocalSession()and successfuldeleteAccount()onNativeAuthClientnow explicitly clearTokenStorageand resetAuthHttpClient._refreshToken. Subsequent requests do not leak staleAuthorization: Bearerheaders.
- Robust error message extraction (#29):
login()andregister()now safely inspect non-stringerrorvalues (such as nested objects{"error": {"message": "..."}}or numbers{"error": 500}) without throwing_TypeError. Nested error messages and codes are extracted when present.
deleteAccount(path:)URL resolution (#29):- Paths without a leading slash (e.g.
deleteAccount(path: 'account')) are resolved to{apiPrefix}/accountrather than concatenating without a separator. - Paths containing query parameters (e.g.
deleteAccount(path: '/api/account?hard=1')) preserve the query string instead of percent-encoding?into the path (%3F).
- Paths without a leading slash (e.g.
Documentation #
- Updated README to document
deleteAccountPath,clearLocalSession(), andAuthResult.statusCode. - Added clock synchronization note for
verify()andtryVerify()fallback toDateTime.now().toUtc().
1.10.3 #
Added #
AuthResult.statusCode/LoginResult.statusCode: carries the HTTP status code of the failed response (#27).clearLocalSession()onAuthClient: resets local session state (user, tokens) and emitsAuthEventType.loggedOutwithout sending network requests or triggering redirects (#27).AuthOptions.deleteAccountPathand optionalpathparameter ondeleteAccount({String? path}): enables consumers mounting account deletion at custom endpoints (e.g. root-relative/api/account) to usedeleteAccountwith full CSRF protection and automatic local state reset (#27).
Fixed #
login()andregister()now preserve server error codes and status codes:errorCodeis extracted fromcode(falling back toerror), anderrorfalls back todata['error']whenmessageis absent (#27).- All client operation failures through
BaseAuthClientconsistently populatestatusCodeanderrorCode.
1.10.2 #
Added #
- Offline token verification (
package:awesome_flutter_auth/offline_tokens.dart) (#25):- Offline verification for JWS compact tokens signed with
alg: EdDSA(Ed25519, RFC 8037 / RFC 8725) against pre-provisioned JWK OKP public keys. - Implemented in pure Dart: runs in Flutter (mobile, desktop, web including WASM) as well as standalone Dart applications without Flutter SDK dependencies.
- Evaluates 10 sequential security checks: malformed structure, algorithm allow-list (
EdDSAonly, rejectingnone,HS256,Ed25519), expectedtyp, rejection ofcritheaders,kidlookup in key set, canonical Ed25519 signature verification (S < L, high bit clear), claim type validation,audcheck,isscheck, and expiration check (exp <= now) with an injectable clock. - Typed failure reasons with
OfflineTokenReasonandOfflineTokenException(noErrorleaks for arbitrary input). - Supports JWK OKP key sets via
OkpKeySet.fromJwks()and SubjectPublicKeyInfo (SPKI) PEM keys viaOkpKey.fromPem(). - Re-exported from root
package:awesome_flutter_auth/awesome_flutter_auth.dart.
- Offline verification for JWS compact tokens signed with
Changed #
- Dropped the
environment.flutterconstraint inpubspec.yaml, enabling the package to be resolved and tested in pure Dart environments (dart pub get+dart test).
1.10.1 #
Changed #
- Renamed to
awesome_flutter_auth(formerlyawesome_node_auth_flutter, whose last standalone release is 1.10.0). To migrate, replace the dependency inpubspec.yamland thepackage:imports:package:awesome_node_auth_flutter/awesome_node_auth_flutter.dartbecomespackage:awesome_flutter_auth/awesome_flutter_auth.dart. The library file is nowlib/awesome_flutter_auth.dart; the rename changes no behaviour. - The repository moved to the
awesome-lang-authorganisation: https://github.com/awesome-lang-auth/awesome-flutter-auth.homepage,repository,issue_trackeranddocumentationinpubspec.yamlfollow it.
Fixed #
getActiveSessions()no longer throws against a conforming server (#21).SessionInfo.fromJsonreadssessionHandle, the key the servers send, with a fallback tohandle; optional fields of an unexpected type becomenullinstead of throwing, an entry with no handle is skipped, and a 200 whose body is not JSON (an HTML fallback page, an empty body) gives an empty list, asgetLinkedAccounts()already does.SessionInfo.toJson()now writessessionHandle. TheGET /sessionsandDELETE /sessions/<handle>requests are unchanged.setup2fa()no longer throws when the server sends noqrCode, as awesome-go-auth and awesome-lambda-auth do (#22). A response without asecretstring now comes back asAuthResult.failureinstead of an exception. ThePOST /2fa/setuprequest is unchanged.
Added #
TotpSetupData.otpauthUrl: theotpauth://provisioning URI that awesome-node-auth, awesome-go-auth and awesome-lambda-auth send, so an app can draw the QR code itself.
Documentation #
- README, "Forced 2FA enrolment": says what
login()does today when the servers answer403 {"requires2FASetup": true, "code": "2FA_SETUP_REQUIRED"}. It returns a plain failure withrequires2FASetup == false,errorCode == nulland notempToken, so enrol a second factor before 2FA becomes mandatory for the account.
Migration #
TotpSetupData.qrCodeis nowString?(it wasString). Code that passes it where aStringis expected, such asImage.network(setup.data!.qrCode), needs a null check, withotpauthUrlas the fallback.SessionInfo.toJson()writes the handle undersessionHandle(it washandle);SessionInfo.fromJsonstill reads either.
1.10.0 #
Added #
cleanupSessions()— callsPOST /auth/sessions/cleanupto prune expired/invalid sessions from the server store (closes section 7 of the v1.10 parity audit).getOAuthUrl(String provider)— returns the full OAuth redirect URL for a given provider (e.g.'github','google'). Use this to initiate a redirect-based OAuth flow on web or open the URL in a webview/system browser on native.handleOAuthCallback()— picks up the authenticated session after a successful OAuth callback. Internally callscheckSession()and, on success, emits anAuthEventType.loggedInevent.
Changed #
- Version bumped to 1.10.0 to align with
awesome-node-authbackend v1.10.
Notes #
- Section 15 (UI widgets): this library is intentionally headless — no pre-built Flutter widgets are provided. Build your own UI against the
AuthClientAPI andauth.state.userStream. - Background token refresh on app resume: the library does not hook into the Flutter app lifecycle automatically. Add a call to
auth.checkSession()inside yourAppLifecycleListener.onResume(orWidgetsBindingObserver.didChangeAppLifecycleState) to restore the session after the app returns to the foreground. - Deep-link handling (native): wire your platform deep-link handler (Android
intent-filter/ iOSCFBundleURLTypes) to callauth.verifyMagicLink(token),auth.resetPassword(password, token), orauth.verifyEmail(token)as appropriate. For OAuth callbacks, callauth.handleOAuthCallback()once the redirect is complete.
1.9.4 #
- Doc fix: clarify that
requestConflictLinkingEmailis a semantic alias ofrequestLinkingEmailwith identical payload, and that the backend infers the context from the presence or absence of a valid auth token in the request. TheisConflictfield is no longer sent in the request body, but if your backend version supports it, it is safely ignored if unused.
1.9.3 #
- Fixed auth/httpClient wrapper
1.9.2 #
- Added example project with a simple Node.js backend and Flutter frontend demonstrating usage of the package.
1.9.1 #
- README updated to give comprehensive usage instructions.
1.9.0 #
- version bump to 1.9.0, aligned with awesome-node-auth backend v1.9.0
1.8.5 #
- Initial public release, aligned with awesome-node-auth backend v1.8.5.
- Web (WASM-compatible) cookie + CSRF authentication via HttpOnly cookies and
X-CSRF-Tokenheader, same-origin only. - Native (iOS, Android, Desktop) Bearer token authentication with pluggable
TokenStorage. - Automatic token refresh with concurrent-request deduplication.
- Full API coverage: login, register, logout, 2FA (TOTP / SMS / magic-link), email verification and change, password reset and change, account linking, session management, account deletion.
AuthState.userStreamreplays the current user to new subscribers immediately upon subscription.- WASM-compatible: no
dart:html, nodart:io, no native plugins.
0.1.0 #
- Initial release.
- Web (WASM-compatible) cookie + CSRF authentication.
- Native Bearer token authentication with pluggable
TokenStorage. - Full API coverage: login, register, 2FA (TOTP/SMS/magic-link), email verification, account linking, session management.