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
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
);
Create Login Client
Instantiate the LoginClient with your OAuth settings and a credentials storage implementation. final loginClient = LoginClient (
oAuthSettings : oAuthSettings,
credentialsStorage : InMemoryCredentialsStorage (),
);
Initialize
Call initialize() to restore any previously saved credentials. await loginClient. initialize ();
Log In
Use an authentication strategy to log in. await loginClient. logIn (
ResourceOwnerPasswordStrategy ( 'username' , 'password' ),
);
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,
})
OAuth2 authorization settings including client ID, secret, and authorization URI.
credentialsStorage
CredentialsStorage
required
Storage implementation for persisting OAuth2 credentials.
Optional HTTP client for sending requests. Defaults to http.Client().
Optional callback for logging debug information. Defaults to printing with [LoginClient] prefix.
Properties
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
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
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;
}
The OAuth2 authorization endpoint URL.
The client identifier as defined in RFC 6749.
The client secret. Not required for all grant types.
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);
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:
When a request receives a 401 response, the client attempts to refresh the token
If refresh succeeds, the original request is retried with the new token
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
Basic Usage
With Credentials Monitoring
Integration with CQRS
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
Always initialize after creating the client
Call await loginClient.initialize() immediately after instantiating to restore saved credentials.
Monitor credential changes
Listen to onCredentialsChanged to handle session expiration and automatic logouts.
Use secure storage in production
Never use InMemoryCredentialsStorage in production. Use FlutterSecureCredentialsStorage or implement secure storage.
Handle AuthorizationException
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.