Skip to main content
The login_client package provides a production-ready OAuth2 implementation that automatically handles authentication, token refresh, and credential storage. It’s designed to be easily integrated into existing codebases.

Installation

Add the package to your pubspec.yaml:
dependencies:
  login_client: ^3.2.0

Features

  • OAuth2 compliant authentication
  • Automatic token refresh
  • Secure credential storage
  • Multiple authentication strategies
  • Seamless integration with http.Client
  • Built-in error handling
  • Credential change notifications

Quick Start

1

Configure OAuth Settings

Define your OAuth2 configuration with the authorization endpoint and client credentials.
final oAuthSettings = OAuthSettings(
  authorizationUri: Uri.parse('https://api.example.com/auth'),
  clientId: 'com.example.myapp',
  clientSecret: 'secret',  // Optional
  scopes: ['read', 'write'],  // Optional
);
2

Create Login Client

Instantiate the LoginClient with your OAuth settings and a credentials storage implementation.
final loginClient = LoginClient(
  oAuthSettings: oAuthSettings,
  credentialsStorage: InMemoryCredentialsStorage(),
);
3

Initialize

Call initialize() to restore any previously saved credentials.
await loginClient.initialize();
4

Log In

Use an authentication strategy to log in.
await loginClient.logIn(
  ResourceOwnerPasswordStrategy('username', 'password'),
);
5

Make Authenticated Requests

Use the LoginClient as a standard http.Client.
final response = await loginClient.get(
  Uri.parse('https://api.example.com/user/profile'),
);

API Reference

LoginClient Class

Extends http.BaseClient and adds OAuth2 authentication capabilities.

Constructor

LoginClient({
  required OAuthSettings oAuthSettings,
  required CredentialsStorage credentialsStorage,
  http.Client? httpClient,
  void Function(String) logger,
})
oAuthSettings
OAuthSettings
required
OAuth2 authorization settings including client ID, secret, and authorization URI.
credentialsStorage
CredentialsStorage
required
Storage implementation for persisting OAuth2 credentials.
httpClient
http.Client?
Optional HTTP client for sending requests. Defaults to http.Client().
logger
void Function(String)?
Optional callback for logging debug information. Defaults to printing with [LoginClient] prefix.

Properties

loggedIn
bool get loggedIn
Returns true if the client is currently authenticated.
credentials
Future<Credentials?> get credentials
Reads the current credentials from storage.
onCredentialsChanged
Stream<Credentials?> get onCredentialsChanged
Stream that emits whenever credentials change (login, logout, refresh). Useful for reacting to session expiration.
loginClient.onCredentialsChanged.listen((credentials) {
  if (credentials == null) {
    // User logged out or session expired
    navigateToLogin();
  }
});

Methods

initialize
Future<void> initialize()
Restores saved credentials from storage. Call this after creating the LoginClient instance.
Always call initialize() before using the LoginClient to properly restore saved sessions.
logIn
Future<void> logIn(AuthorizationStrategy strategy)
Authenticates using the provided strategy. Automatically saves credentials on success and logs out on failure. Throws:
  • AuthorizationException - When authentication fails
logOut
Future<void> logOut()
Logs out the current user and clears stored credentials.
refresh
Future<void> refresh([List<String>? newScopes])
Manually refreshes the current credentials. Optionally request new scopes. Parameters:
  • newScopes - Optional list of new scopes to request during refresh
Throws:
  • RefreshException - When called on an unauthenticated client
  • AuthorizationException - When refresh fails (triggers automatic logout)
Token refresh happens automatically when a 401 response is received. Manual refresh is rarely needed.
dispose
Future<void> dispose()
Cleans up resources. Call when you’re done using the LoginClient.

OAuthSettings

Configuration for OAuth2 authorization.
class OAuthSettings {
  const OAuthSettings({
    required this.authorizationUri,
    required this.clientId,
    this.clientSecret = '',
    this.scopes = const [],
  });

  final Uri authorizationUri;
  final String clientId;
  final String clientSecret;
  final List<String> scopes;
}
authorizationUri
Uri
required
The OAuth2 authorization endpoint URL.
clientId
String
required
The client identifier as defined in RFC 6749.
clientSecret
String
default:"''"
The client secret. Not required for all grant types.
scopes
List<String>
default:"[]"
List of requested OAuth2 scopes.

CredentialsStorage

Interface for storing and retrieving OAuth2 credentials.
abstract interface class CredentialsStorage {
  Future<Credentials?> read();
  Future<void> save(Credentials credentials);
  Future<void> clear();
}

Built-in Implementations

InMemoryCredentialsStorage
Stores credentials in memory. Credentials are lost when the app restarts.
final storage = InMemoryCredentialsStorage();
InMemoryCredentialsStorage is not suitable for production use. Credentials will be lost on app restart.
For Flutter apps, use FlutterSecureCredentialsStorage from the login_client_flutter package.

Authentication Strategies

The LoginClient supports multiple OAuth2 authentication flows through different strategies.

ResourceOwnerPasswordStrategy

Uses the Resource Owner Password Credentials grant (username/password).
final strategy = ResourceOwnerPasswordStrategy(
  '[email protected]',
  'mySecretPassword',
);

await loginClient.logIn(strategy);
This is the most common authentication strategy for mobile apps. See RFC 6749 Section 4.3.

ClientCredentialsStrategy

Uses the Client Credentials grant. Useful for machine-to-machine authentication.
final strategy = ClientCredentialsStrategy();

await loginClient.logIn(strategy);
Requires clientSecret to be set in OAuthSettings.

RawCredentialsStrategy

Directly use pre-obtained OAuth2 credentials.
final credentials = Credentials(
  accessToken: 'access_token_value',
  refreshToken: 'refresh_token_value',
  tokenEndpoint: Uri.parse('https://api.example.com/auth'),
);

final strategy = RawCredentialsStrategy(credentials);
await loginClient.logIn(strategy);

CustomGrantStrategy

Implement custom OAuth2 grant types.
final strategy = CustomGrantStrategy(
  grantType: 'custom_grant',
  parameters: {
    'custom_param': 'value',
  },
);

await loginClient.logIn(strategy);

SmsTokenStrategy

Authenticate using SMS tokens.
final strategy = SmsTokenStrategy(
  phoneNumber: '+1234567890',
  token: '123456',
);

await loginClient.logIn(strategy);

Automatic Token Refresh

The LoginClient automatically handles token refresh:
  1. When a request receives a 401 response, the client attempts to refresh the token
  2. If refresh succeeds, the original request is retried with the new token
  3. If refresh fails, the client automatically logs out and throws an exception
// This request might trigger automatic token refresh
final response = await loginClient.get(
  Uri.parse('https://api.example.com/data'),
);

// If the token was expired:
// 1. Request returns 401
// 2. LoginClient refreshes the token
// 3. Request is retried with new token
// 4. Response is returned
Token refresh is transparent to your application code. You don’t need to handle it manually.

Error Handling

AuthorizationException

Thrown when authentication or authorization fails.
try {
  await loginClient.logIn(
    ResourceOwnerPasswordStrategy('user', 'wrong_password'),
  );
} on AuthorizationException catch (e) {
  print('Login failed: ${e.description}');
  showErrorMessage('Invalid username or password');
}

ExpirationException

Thrown when credentials have expired and cannot be refreshed.
try {
  await loginClient.get(Uri.parse('https://api.example.com/data'));
} on ExpirationException catch (e) {
  print('Session expired: ${e.description}');
  navigateToLogin();
}

RefreshException

Thrown when attempting to refresh without being logged in.
try {
  await loginClient.refresh();
} on RefreshException catch (e) {
  print('Not logged in: ${e.message}');
}

Complete Example

import 'package:login_client/login_client.dart';
import 'package:http/http.dart' as http;

void main() async {
  // Configure OAuth settings
  final oAuthSettings = OAuthSettings(
    authorizationUri: Uri.parse('https://api.example.com/oauth/token'),
    clientId: 'my_app_id',
  );

  // Create login client
  final loginClient = LoginClient(
    oAuthSettings: oAuthSettings,
    credentialsStorage: InMemoryCredentialsStorage(),
  );

  // Initialize (restore saved credentials if any)
  await loginClient.initialize();

  // Check if already logged in
  if (!loginClient.loggedIn) {
    // Log in with username/password
    try {
      await loginClient.logIn(
        ResourceOwnerPasswordStrategy('[email protected]', 'password'),
      );
      print('Login successful!');
    } on AuthorizationException catch (e) {
      print('Login failed: $e');
      return;
    }
  }

  // Make authenticated requests
  final response = await loginClient.get(
    Uri.parse('https://api.example.com/user/profile'),
  );

  print('Status: ${response.statusCode}');
  print('Body: ${response.body}');

  // Log out when done
  await loginClient.logOut();
  await loginClient.dispose();
}

Custom Credentials Storage

Implement CredentialsStorage for custom storage solutions:
import 'package:login_client/login_client.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SharedPreferencesCredentialsStorage implements CredentialsStorage {
  static const _key = 'oauth_credentials';

  @override
  Future<Credentials?> read() async {
    final prefs = await SharedPreferences.getInstance();
    final json = prefs.getString(_key);
    if (json == null) return null;

    try {
      return Credentials.fromJson(json);
    } on FormatException {
      return null;
    }
  }

  @override
  Future<void> save(Credentials credentials) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_key, credentials.toJson());
  }

  @override
  Future<void> clear() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_key);
  }
}
For production apps, use secure storage solutions like flutter_secure_storage instead of SharedPreferences.

login_client_flutter

Flutter Secure Storage implementation for credentials

cqrs

CQRS client that works seamlessly with LoginClient

Best Practices

Call await loginClient.initialize() immediately after instantiating to restore saved credentials.
Listen to onCredentialsChanged to handle session expiration and automatic logouts.
Never use InMemoryCredentialsStorage in production. Use FlutterSecureCredentialsStorage or implement secure storage.
Always wrap login calls in try-catch to handle authentication failures gracefully.
Call dispose() to clean up resources when the client is no longer needed.

Build docs developers (and LLMs) love