mirror of
https://github.com/foss42/apidash.git
synced 2025-12-02 18:57:05 +08:00
Implement OAuth2 secure token storage with automatic migration
Co-authored-by: animator <615622+animator@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
/// Secure storage service for OAuth2 credentials
|
||||
/// Uses platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android)
|
||||
class OAuth2SecureStorage {
|
||||
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
),
|
||||
iOptions: IOSOptions(
|
||||
accessibility: KeychainAccessibility.first_unlock,
|
||||
),
|
||||
);
|
||||
|
||||
/// Generate a unique storage key from client ID and token URL
|
||||
static String _generateKey(String clientId, String tokenUrl) {
|
||||
final combined = '$clientId:$tokenUrl';
|
||||
final bytes = utf8.encode(combined);
|
||||
final hash = sha256.convert(bytes);
|
||||
return 'oauth2_cred_${hash.toString().substring(0, 16)}';
|
||||
}
|
||||
|
||||
/// Store OAuth2 credentials securely
|
||||
static Future<void> storeCredentials({
|
||||
required String clientId,
|
||||
required String tokenUrl,
|
||||
required String credentialsJson,
|
||||
}) async {
|
||||
try {
|
||||
final key = _generateKey(clientId, tokenUrl);
|
||||
await _secureStorage.write(key: key, value: credentialsJson);
|
||||
} catch (e) {
|
||||
// Log error but don't fail - fallback to no storage
|
||||
// In production, consider proper logging
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve OAuth2 credentials
|
||||
static Future<String?> retrieveCredentials({
|
||||
required String clientId,
|
||||
required String tokenUrl,
|
||||
}) async {
|
||||
try {
|
||||
final key = _generateKey(clientId, tokenUrl);
|
||||
return await _secureStorage.read(key: key);
|
||||
} catch (e) {
|
||||
// Log error but return null - will trigger fresh auth
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete OAuth2 credentials
|
||||
static Future<void> deleteCredentials({
|
||||
required String clientId,
|
||||
required String tokenUrl,
|
||||
}) async {
|
||||
try {
|
||||
final key = _generateKey(clientId, tokenUrl);
|
||||
await _secureStorage.delete(key: key);
|
||||
} catch (e) {
|
||||
// Log error but don't fail
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all OAuth2 credentials
|
||||
static Future<void> clearAllCredentials() async {
|
||||
try {
|
||||
final allKeys = await _secureStorage.readAll();
|
||||
for (final key in allKeys.keys) {
|
||||
if (key.startsWith('oauth2_cred_')) {
|
||||
await _secureStorage.delete(key: key);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Log error but don't fail
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:oauth2/oauth2.dart' as oauth2;
|
||||
import '../../models/auth/auth_oauth2_model.dart';
|
||||
import '../../services/http_client_manager.dart';
|
||||
import '../../services/oauth_callback_server.dart';
|
||||
import '../../services/oauth2_secure_storage.dart';
|
||||
import '../platform_utils.dart';
|
||||
|
||||
/// Advanced OAuth2 authorization code grant handler that returns both the client and server
|
||||
@@ -23,13 +24,47 @@ Future<(oauth2.Client, OAuthCallbackServer?)> oAuth2AuthorizationCodeGrant({
|
||||
String? state,
|
||||
String? scope,
|
||||
}) async {
|
||||
// Check for existing credentials first
|
||||
// Check for existing credentials first - try secure storage, then file
|
||||
// Try secure storage first (preferred method)
|
||||
try {
|
||||
final secureCredJson = await OAuth2SecureStorage.retrieveCredentials(
|
||||
clientId: identifier,
|
||||
tokenUrl: tokenEndpoint.toString(),
|
||||
);
|
||||
|
||||
if (secureCredJson != null) {
|
||||
final credentials = oauth2.Credentials.fromJson(secureCredJson);
|
||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||
return (
|
||||
oauth2.Client(credentials, identifier: identifier, secret: secret),
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Secure storage failed, try file fallback
|
||||
}
|
||||
|
||||
// Fallback to file-based storage for backward compatibility
|
||||
if (credentialsFile != null && await credentialsFile.exists()) {
|
||||
try {
|
||||
final json = await credentialsFile.readAsString();
|
||||
final credentials = oauth2.Credentials.fromJson(json);
|
||||
|
||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||
// Migrate to secure storage for future use
|
||||
try {
|
||||
await OAuth2SecureStorage.storeCredentials(
|
||||
clientId: identifier,
|
||||
tokenUrl: tokenEndpoint.toString(),
|
||||
credentialsJson: json,
|
||||
);
|
||||
// Delete old file after successful migration
|
||||
await credentialsFile.delete();
|
||||
} catch (e) {
|
||||
// Migration failed, keep using file
|
||||
}
|
||||
|
||||
return (
|
||||
oauth2.Client(credentials, identifier: identifier, secret: secret),
|
||||
null,
|
||||
@@ -124,8 +159,22 @@ Future<(oauth2.Client, OAuthCallbackServer?)> oAuth2AuthorizationCodeGrant({
|
||||
callbackUriParsed.queryParameters,
|
||||
);
|
||||
|
||||
if (credentialsFile != null) {
|
||||
await credentialsFile.writeAsString(client.credentials.toJson());
|
||||
// Store credentials securely (preferred method)
|
||||
try {
|
||||
await OAuth2SecureStorage.storeCredentials(
|
||||
clientId: identifier,
|
||||
tokenUrl: tokenEndpoint.toString(),
|
||||
credentialsJson: client.credentials.toJson(),
|
||||
);
|
||||
} catch (e) {
|
||||
// Secure storage failed, fallback to file if available
|
||||
if (credentialsFile != null) {
|
||||
try {
|
||||
await credentialsFile.writeAsString(client.credentials.toJson());
|
||||
} catch (fileError) {
|
||||
// Both storage methods failed - credentials won't persist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (client, callbackServer);
|
||||
@@ -150,13 +199,46 @@ Future<oauth2.Client> oAuth2ClientCredentialsGrantHandler({
|
||||
required AuthOAuth2Model oauth2Model,
|
||||
required File? credentialsFile,
|
||||
}) async {
|
||||
// Try to use saved credentials
|
||||
// Try secure storage first
|
||||
try {
|
||||
final secureCredJson = await OAuth2SecureStorage.retrieveCredentials(
|
||||
clientId: oauth2Model.clientId,
|
||||
tokenUrl: oauth2Model.accessTokenUrl,
|
||||
);
|
||||
|
||||
if (secureCredJson != null) {
|
||||
final credentials = oauth2.Credentials.fromJson(secureCredJson);
|
||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||
return oauth2.Client(
|
||||
credentials,
|
||||
identifier: oauth2Model.clientId,
|
||||
secret: oauth2Model.clientSecret,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Secure storage failed, try file fallback
|
||||
}
|
||||
|
||||
// Fallback to file-based storage for backward compatibility
|
||||
if (credentialsFile != null && await credentialsFile.exists()) {
|
||||
try {
|
||||
final json = await credentialsFile.readAsString();
|
||||
final credentials = oauth2.Credentials.fromJson(json);
|
||||
|
||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||
// Migrate to secure storage
|
||||
try {
|
||||
await OAuth2SecureStorage.storeCredentials(
|
||||
clientId: oauth2Model.clientId,
|
||||
tokenUrl: oauth2Model.accessTokenUrl,
|
||||
credentialsJson: json,
|
||||
);
|
||||
await credentialsFile.delete();
|
||||
} catch (e) {
|
||||
// Migration failed
|
||||
}
|
||||
|
||||
return oauth2.Client(
|
||||
credentials,
|
||||
identifier: oauth2Model.clientId,
|
||||
@@ -184,12 +266,22 @@ Future<oauth2.Client> oAuth2ClientCredentialsGrantHandler({
|
||||
httpClient: baseClient,
|
||||
);
|
||||
|
||||
// Store credentials securely
|
||||
try {
|
||||
if (credentialsFile != null) {
|
||||
await credentialsFile.writeAsString(client.credentials.toJson());
|
||||
}
|
||||
await OAuth2SecureStorage.storeCredentials(
|
||||
clientId: oauth2Model.clientId,
|
||||
tokenUrl: oauth2Model.accessTokenUrl,
|
||||
credentialsJson: client.credentials.toJson(),
|
||||
);
|
||||
} catch (e) {
|
||||
// Ignore credential saving errors
|
||||
// Secure storage failed, try file fallback
|
||||
if (credentialsFile != null) {
|
||||
try {
|
||||
await credentialsFile.writeAsString(client.credentials.toJson());
|
||||
} catch (fileError) {
|
||||
// Both storage methods failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the HTTP client
|
||||
@@ -207,13 +299,46 @@ Future<oauth2.Client> oAuth2ResourceOwnerPasswordGrantHandler({
|
||||
required AuthOAuth2Model oauth2Model,
|
||||
required File? credentialsFile,
|
||||
}) async {
|
||||
// Try to use saved credentials
|
||||
// Try secure storage first
|
||||
try {
|
||||
final secureCredJson = await OAuth2SecureStorage.retrieveCredentials(
|
||||
clientId: oauth2Model.clientId,
|
||||
tokenUrl: oauth2Model.accessTokenUrl,
|
||||
);
|
||||
|
||||
if (secureCredJson != null) {
|
||||
final credentials = oauth2.Credentials.fromJson(secureCredJson);
|
||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||
return oauth2.Client(
|
||||
credentials,
|
||||
identifier: oauth2Model.clientId,
|
||||
secret: oauth2Model.clientSecret,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Secure storage failed, try file fallback
|
||||
}
|
||||
|
||||
// Fallback to file-based storage for backward compatibility
|
||||
if (credentialsFile != null && await credentialsFile.exists()) {
|
||||
try {
|
||||
final json = await credentialsFile.readAsString();
|
||||
final credentials = oauth2.Credentials.fromJson(json);
|
||||
|
||||
if (credentials.accessToken.isNotEmpty && !credentials.isExpired) {
|
||||
// Migrate to secure storage
|
||||
try {
|
||||
await OAuth2SecureStorage.storeCredentials(
|
||||
clientId: oauth2Model.clientId,
|
||||
tokenUrl: oauth2Model.accessTokenUrl,
|
||||
credentialsJson: json,
|
||||
);
|
||||
await credentialsFile.delete();
|
||||
} catch (e) {
|
||||
// Migration failed
|
||||
}
|
||||
|
||||
return oauth2.Client(
|
||||
credentials,
|
||||
identifier: oauth2Model.clientId,
|
||||
@@ -247,12 +372,22 @@ Future<oauth2.Client> oAuth2ResourceOwnerPasswordGrantHandler({
|
||||
httpClient: baseClient,
|
||||
);
|
||||
|
||||
// Store credentials securely
|
||||
try {
|
||||
if (credentialsFile != null) {
|
||||
await credentialsFile.writeAsString(client.credentials.toJson());
|
||||
}
|
||||
await OAuth2SecureStorage.storeCredentials(
|
||||
clientId: oauth2Model.clientId,
|
||||
tokenUrl: oauth2Model.accessTokenUrl,
|
||||
credentialsJson: client.credentials.toJson(),
|
||||
);
|
||||
} catch (e) {
|
||||
// Ignore credential saving errors
|
||||
// Secure storage failed, try file fallback
|
||||
if (credentialsFile != null) {
|
||||
try {
|
||||
await credentialsFile.writeAsString(client.credentials.toJson());
|
||||
} catch (fileError) {
|
||||
// Both storage methods failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the HTTP client
|
||||
|
||||
@@ -21,6 +21,7 @@ dependencies:
|
||||
convert: ^3.1.2
|
||||
crypto: ^3.0.6
|
||||
dart_jsonwebtoken: ^3.2.0
|
||||
flutter_secure_storage: ^9.0.0
|
||||
http: ^1.3.0
|
||||
http_parser: ^4.1.2
|
||||
json5: ^0.8.2
|
||||
|
||||
Reference in New Issue
Block a user