fix(mobile): clear the app's cookies on logout (#32965)

This commit is contained in:
Santo Shakil authored and Codex committed 2026-11-05 20:35:16 +03:00
1 parent 9b5e1aae5b
commit 11e9a4b4da
7 files changed
+64 -4

No files matched your search

@@ -267,6 +267,10 @@ object HttpClientManager {
}
}
fun clearCookies() {
cookieJar.clear()
}
fun loadCookieHeader(url: String): String? {
val httpUrl = url.toHttpUrlOrNull() ?: return null
return cookieJar.loadForRequest(httpUrl).takeIf { it.isNotEmpty() }
@@ -470,6 +474,12 @@ object HttpClientManager {
return store.filter { it.matches(url) }
}
@Synchronized
fun clear() {
store.clear()
persist()
}
private fun syncAuthCookies(): Boolean {
val serverHosts = serverUrls.map { it.host }.toSet()
val now = System.currentTimeMillis()
@@ -84,4 +84,8 @@ private class NetworkApiImpl(private val context: Context) : NetworkApi {
override fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>, token: String?) {
HttpClientManager.setRequestHeaders(headers, serverUrls, token)
}
override fun clearCookies() {
HttpClientManager.clearCookies()
}
}
@@ -100,6 +100,10 @@ class NetworkApiImpl: NetworkApi {
URLSessionManager.shared.recreateSession()
}
}
func clearCookies() throws {
URLSessionManager.clearCookies()
}
}
private class CertImporter: NSObject, UIDocumentPickerDelegate {
+11 -2
View File
@@ -58,6 +58,7 @@ class URLSessionManager: NSObject {
static let cookieStorage = HTTPCookieStorage.sharedCookieStorage(forGroupContainerIdentifier: APP_GROUP)
private static var serverUrls: [String] = []
private static var isSyncing = false
private static let cookieQueue = DispatchQueue(label: "CookieSync")
var sessionPointer: UnsafeMutableRawPointer {
Unmanaged.passUnretained(session).toOpaque()
@@ -84,12 +85,20 @@ class URLSessionManager: NSObject {
guard urls != serverUrls else { return }
serverUrls = urls
UserDefaults.group.set(urls, forKey: SERVER_URLS_KEY)
syncAuthCookies()
cookieQueue.sync { syncAuthCookies() }
}
static func clearCookies() {
cookieQueue.sync {
for cookie in cookieStorage.cookies ?? [] {
cookieStorage.deleteCookie(cookie)
}
}
}
@objc private static func cookiesDidChange(_ notification: Notification) {
guard !isSyncing, !serverUrls.isEmpty else { return }
syncAuthCookies()
cookieQueue.async { syncAuthCookies() }
}
private static func syncAuthCookies() {
+2
View File
@@ -11,6 +11,7 @@ import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/repositories/auth.repository.dart';
import 'package:immich_mobile/repositories/auth_api.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
@@ -120,6 +121,7 @@ class AuthService {
_authRepository.clearLocalData(),
Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken),
networkApi.clearCookies(),
SettingsRepository.instance.clear(const [
.networkAutoEndpointSwitching,
.networkPreferredWifiName,
+2
View File
@@ -45,5 +45,7 @@ abstract class NetworkApi {
void setRequestHeaders(Map<String, String> headers, List<String> serverUrls, String? token);
void clearCookies();
String getAppGroupId();
}
+31 -2
View File
@@ -1,11 +1,13 @@
import 'package:drift/drift.dart' hide isNull;
import 'package:drift/native.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/data/db/main/database.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/platform/network_api.g.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:mocktail/mocktail.dart';
import 'package:openapi/api.dart';
@@ -34,9 +36,10 @@ void main() {
});
setUpAll(() async {
WidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized();
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
await StoreService.init(storeRepository: StoreRepository(db));
await SettingsRepository.ensureInitialized(db);
});
tearDownAll(() async {
@@ -238,4 +241,30 @@ void main() {
verify(() => apiService.resolveAndSetEndpoint('https://external.endpoint')).called(1);
});
});
group('logout', () {
test('Should clear native cookies after the server logout fails', () async {
final calls = <String>[];
when(() => authApiRepository.logout()).thenAnswer((_) async {
calls.add('logout');
throw ApiException(503, 'Server is not reachable');
});
when(() => backgroundSyncManager.cancel()).thenAnswer((_) async {});
when(() => authRepository.clearLocalData()).thenAnswer((_) async {});
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler(
const BasicMessageChannel<Object?>(
'dev.flutter.pigeon.immich_mobile.NetworkApi.clearCookies',
NetworkApi.pigeonChannelCodec,
),
(_) async {
calls.add('clearCookies');
return <Object?>[null];
},
);
await sut.logout();
expect(calls, ['logout', 'clearCookies']);
});
});
}