secure_token_storage

pub package License: MIT

A tiny, opinionated wrapper around flutter_secure_storage for persisting OAuth-style access + refresh tokens with separate expiry timestamps in the platform's secure enclave (Android Keystore / iOS Keychain).

If you've written this code in three different apps already, this is that code — extracted, tested, and reusable.

Features

  • One TokenBundle value type holds both tokens and both expiries.
  • isAccessExpired() / isRefreshExpired() helpers with injectable clock for testing.
  • Atomic per-key writes with all-or-nothing read semantics — partial state returns null instead of half-decoded garbage.
  • keyPrefix for multi-account or dev/staging isolation on the same device.
  • Pure flutter_secure_storage under the hood: no extra native code, no extra permissions.

Install

flutter pub add secure_token_storage

Usage

import 'package:secure_token_storage/secure_token_storage.dart';

final storage = const SecureTokenStorage();

// After successful login:
await storage.save(
  TokenBundle(
    accessToken: response.accessToken,
    refreshToken: response.refreshToken,
    accessExpiresAt:
        DateTime.now().toUtc().add(Duration(seconds: response.expiresIn)),
    refreshExpiresAt:
        DateTime.now().toUtc().add(const Duration(days: 30)),
  ),
);

// On app start:
final tokens = await storage.read();
if (tokens == null || tokens.isRefreshExpired()) {
  // user must log in again
} else if (tokens.isAccessExpired()) {
  // call your refresh endpoint, then storage.save(...) the new bundle
} else {
  // use tokens.accessToken
}

// On logout:
await storage.clear();

Multi-account / staging isolation

final prod = const SecureTokenStorage();
final staging = const SecureTokenStorage(keyPrefix: 'staging_');

The two stores never collide — clear() on one leaves the other intact.

API

TokenBundle

Field Type Notes
accessToken String Sent as Authorization: Bearer ….
refreshToken String Used to mint a new access token.
accessExpiresAt DateTime Stored as ISO-8601 UTC.
refreshExpiresAt DateTime Stored as ISO-8601 UTC.
Method Returns
isAccessExpired({DateTime? now}) true when now ≥ accessExpiresAt.
isRefreshExpired({DateTime? now}) true when now ≥ refreshExpiresAt.

SecureTokenStorage

Constructor parameter Default Notes
storage platform default Inject your own for testing.
keyPrefix '' Namespaces every entry.
Method Behavior
save(TokenBundle) Writes all 4 entries.
read() Returns the bundle, or null if any entry is missing.
clear() Deletes all 4 entries under the configured prefix.

Testing your own code

SecureTokenStorage accepts an injected FlutterSecureStorage so you can mock it. See test/secure_token_storage_test.dart for a worked example using mocktail.

flutter test

Example app

A small interactive demo lives in example/:

cd example
flutter run

License

MIT — see LICENSE.

Libraries

secure_token_storage
Persists OAuth-style access and refresh tokens with separate expiry timestamps in the platform's secure enclave.