Phase B: Keycloak OIDC (PKCE) statt Cookie-Session-Login

App-Code:
- KeycloakOidcTokenProvider: PKCE-Login via flutter_appauth, Refresh via
  Refresh-Token aus flutter_secure_storage, Session-Restore beim
  App-Start, Logout.
- AuthSessionEvent als Provider→Bloc-Brücke (LoggedIn/LoggedOut/
  SessionExpired) auf einem Broadcast-Stream.
- AuthBloc komplett umgebaut: nimmt jetzt den KeycloakOidcTokenProvider
  statt UserInfoService, mappt eingehende Provider-Events auf eigene
  Zustände. Authenticated.fromClaims() liest personalnummer + Name aus
  dem ID-Token-Payload.
- LoginPage: kein Browser+Deep-Link mehr — Button feuert
  LoginRequested, der Provider übernimmt den restlichen Flow.
- network_locator: produktiver KeycloakOidcTokenProvider, doppelt
  registriert (KeycloakOidcTokenProvider für AuthBloc,
  AuthTokenProvider für Interceptor).
- Auth-State trägt zusätzlich personalnummer/displayName/email; das
  Legacy-User-Objekt + sessionId bleiben temporär drin, damit die
  alten ERPframe-Services (Phase D) noch kompilieren.

Plattform-Setup:
- Android: appAuthRedirectScheme=holzleitner in build.gradle.kts,
  NetworkSecurityConfig erlaubt HTTP zu localhost/10.0.2.2/127.0.0.1.
- iOS: holzleitner als URL-Scheme im Info.plist, ATS-Ausnahme für
  localhost (HTTP-Keycloak im Dev-Setup).

Out of scope:
- Keine echte App-Run-Smoke — kommt mit dem User-Test.
- iOS-pod-install läuft beim ersten 'flutter run ios' automatisch.
- Old ERPframe-Services bleiben aktiv und werfen ab jetzt 401 (kein
  Cookie-Session-Token mehr) — wird in Phase D entfernt.
This commit is contained in:
Dennis Nemec
2026-05-14 22:59:36 +02:00
parent 8cf4045e44
commit 6d7e58fc0f
15 changed files with 738 additions and 281 deletions

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/bloc/app_bloc.dart';
import 'package:hl_lieferservice/data/network/keycloak_oidc_token_provider.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/feature/authentication/presentation/login_enforcer.dart';
import 'package:hl_lieferservice/feature/authentication/service/userinfo.dart';
import 'package:hl_lieferservice/main.dart' show locator;
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
import 'package:hl_lieferservice/feature/car_selection/presentation/car_selection_enforcer.dart';
import 'package:hl_lieferservice/feature/car_selection/repository/car_selection_repository.dart';
@ -36,7 +38,6 @@ class _DeliveryAppState extends State<DeliveryApp> {
return BlocBuilder<AppBloc, AppState>(
builder: (context, state) {
if (state is AppConfigLoaded) {
final currentAppState = state;
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => NavigationBloc()),
@ -44,11 +45,14 @@ class _DeliveryAppState extends State<DeliveryApp> {
BlocProvider(
create:
(context) => AuthBloc(
service: UserInfoService(
url: currentAppState.config.backendUrl,
),
operationBloc: context.read<OperationBloc>(),
),
tokenProvider:
locator<KeycloakOidcTokenProvider>(),
operationBloc: context.read<OperationBloc>(),
)
// Beim ersten Build: prüfen, ob ein
// Refresh-Token aus der Secure Storage da ist,
// und ggf. direkt einloggen.
..add(const RestoreSessionRequested()),
),
BlocProvider(
create: