security: LiveKit-Token vom Server statt Secret in der App
Die App signierte LiveKit-Tokens selbst – dafür steckte das SFU-Secret im App-Code (jeder mit der APK konnte sich Tokens für beliebige Räume und Identitäten ausstellen). Jetzt holt die App das Token von der bereits vorhandenen Server-Route /api/livekit-token (whoami-Prüfung, Identität = geprüfte Matrix-ID). Secret und JWT-Signieren sind aus dem Client entfernt. - Lesbare Fehlermeldungen (offline, 401, Serverfehler) statt stiller Fehler. - Gruppenanruf aus dem Chat-Kopf übergibt jetzt den Matrix-Client. - Tests: Unit-Test mit Mock-HTTP + echter LiveKit-Beitritt (Testkonto). - Offen: Handy-Anruftest; danach Secret-Rotation durch Bernd (Doku). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ae6f651f4c
commit
756486d1fe
@@ -0,0 +1,87 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:pyramid/core/livekit_token.dart';
|
||||
|
||||
/// Beitritts-Token kommt vom Server; das LiveKit-Secret steckt nicht mehr
|
||||
/// in der App (docs/LIVEKIT_TOKEN_MIGRATION.md).
|
||||
void main() {
|
||||
test('schickt Raum + Matrix-Token und liefert Token und Adresse', () async {
|
||||
late Map<String, dynamic> sent;
|
||||
final client = MockClient((req) async {
|
||||
expect(req.url.toString(), LiveKitTokenService.endpoint);
|
||||
sent = jsonDecode(req.body) as Map<String, dynamic>;
|
||||
return http.Response(
|
||||
jsonEncode({'token': 'abc.def.ghi', 'url': 'wss://lk.example'}),
|
||||
200,
|
||||
);
|
||||
});
|
||||
final r = await LiveKitTokenService.fetch(
|
||||
room: '!raum:server',
|
||||
matrixToken: 'mt',
|
||||
httpClient: client,
|
||||
);
|
||||
expect(sent, {'room': '!raum:server', 'matrix_token': 'mt'});
|
||||
expect(r.token, 'abc.def.ghi');
|
||||
expect(r.url, 'wss://lk.example');
|
||||
});
|
||||
|
||||
test('ohne brauchbare Adresse: Standard-LiveKit', () async {
|
||||
final client = MockClient(
|
||||
(_) async => http.Response(jsonEncode({'token': 't'}), 200),
|
||||
);
|
||||
final r = await LiveKitTokenService.fetch(
|
||||
room: 'r',
|
||||
matrixToken: 'm',
|
||||
httpClient: client,
|
||||
);
|
||||
expect(r.url, LiveKitTokenService.fallbackUrl);
|
||||
});
|
||||
|
||||
Future<String> failureFor(MockClient client) async {
|
||||
try {
|
||||
await LiveKitTokenService.fetch(
|
||||
room: 'r',
|
||||
matrixToken: 'm',
|
||||
httpClient: client,
|
||||
);
|
||||
} on LiveKitTokenException catch (e) {
|
||||
return e.message;
|
||||
}
|
||||
fail('LiveKitTokenException erwartet');
|
||||
}
|
||||
|
||||
test('Fehler werden verständlich gemeldet statt still geschluckt', () async {
|
||||
expect(
|
||||
await failureFor(MockClient((_) async => http.Response('{}', 401))),
|
||||
contains('neu anmelden'),
|
||||
);
|
||||
expect(
|
||||
await failureFor(MockClient((_) async => http.Response('kaputt', 500))),
|
||||
contains('HTTP 500'),
|
||||
);
|
||||
expect(
|
||||
await failureFor(MockClient((_) async => http.Response('{}', 200))),
|
||||
contains('kein Zugangstoken'),
|
||||
);
|
||||
expect(
|
||||
await failureFor(MockClient((_) async => http.Response('<html>', 200))),
|
||||
contains('unlesbare'),
|
||||
);
|
||||
expect(
|
||||
await failureFor(
|
||||
MockClient((_) async => throw const SocketException('offline')),
|
||||
),
|
||||
contains('nicht erreichbar'),
|
||||
);
|
||||
});
|
||||
|
||||
test('App-Code enthält kein LiveKit-Secret und signiert nicht selbst', () {
|
||||
final src = File('lib/core/livekit_token.dart').readAsStringSync();
|
||||
expect(src, isNot(contains('apiSecret')));
|
||||
expect(src, isNot(contains('jsonwebtoken')));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user