OAuth2 mit Flutter Web: Sichere Authentifizierung für Cross-Platform-Apps

App-Experte aus Köln
16. Feb 2023
5 Minuten Lesezeit

Implementieren Sie OAuth2 in Flutter Web mit dieser Schritt-für-Schritt-Anleitung. Erfahren Sie, wie Sie eine sichere Authentifizierung für Ihre Cross-Platform-Apps einrichten.


OAuth2 mit Flutter Web: Sichere Authentifizierung für Cross-Platform-Apps

Als führende App-Agentur aus Köln mit Spezialisierung auf Flutter-Entwicklung teilen wir heute unsere bewährte Implementierung von OAuth2 in Flutter Web. Diese Lösung haben wir in zahlreichen Kundenprojekten erfolgreich eingesetzt und kontinuierlich verbessert.

OAuth2 ist der Industriestandard für sichere Authentifizierung, und mit Flutter können wir diese Funktionalität plattformübergreifend implementieren - sowohl für mobile Apps als auch für Web-Anwendungen. In diesem Artikel zeigen wir Ihnen, wie Sie OAuth2 in Ihrer Flutter Web-Anwendung einrichten.

Schritt 1: Abhängigkeiten hinzufügen

Fügen Sie die erforderlichen Pakete zu Ihrer pubspec.yaml Datei hinzu:

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.3
  shared_preferences: ^2.0.6
  rxdart: ^0.27.1
  get_it: ^7.1.3
  url_launcher: ^6.0.9
  jwt_decoder: ^2.0.1

Schritt 2: AuthService implementieren

Erstellen Sie eine auth_service.dart Datei mit folgendem Inhalt:

import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:rxdart/rxdart.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:get_it/get_it.dart';

final getIt = GetIt.instance;

class AuthService {
  final SharedPreferences _prefs = getIt.get<SharedPreferences>();
  final BehaviorSubject<bool> _isLoggedInController = BehaviorSubject<bool>.seeded(false);
  
  Stream<bool> get isLoggedIn => _isLoggedInController.stream;
  
  // OAuth2 Konfiguration
  final String clientId = 'YOUR_CLIENT_ID';
  final String clientSecret = 'YOUR_CLIENT_SECRET';
  final String redirectUri = kIsWeb 
      ? 'http://localhost:8080/callback.html' 
      : 'com.yourcompany.app://callback';
  final String authorizationEndpoint = 'https://your-auth-provider.com/authorize';
  final String tokenEndpoint = 'https://your-auth-provider.com/oauth/token';
  final String scope = 'openid profile email';
  
  AuthService() {
    _checkIfLoggedIn();
  }
  
  Future<void> _checkIfLoggedIn() async {
    final accessToken = _prefs.getString('access_token');
    final refreshToken = _prefs.getString('refresh_token');
    final expiresAt = _prefs.getInt('expires_at');
    
    if (accessToken != null && expiresAt != null) {
      if (DateTime.now().millisecondsSinceEpoch < expiresAt) {
        _isLoggedInController.add(true);
      } else if (refreshToken != null) {
        try {
          await _refreshToken(refreshToken);
          _isLoggedInController.add(true);
        } catch (e) {
          _isLoggedInController.add(false);
        }
      } else {
        _isLoggedInController.add(false);
      }
    } else {
      _isLoggedInController.add(false);
    }
  }
  
  Future<void> loginAction() async {
    final authUrl = Uri.parse(authorizationEndpoint).replace(
      queryParameters: {
        'client_id': clientId,
        'redirect_uri': redirectUri,
        'response_type': 'code',
        'scope': scope,
      },
    );
    
    if (await canLaunch(authUrl.toString())) {
      await launch(authUrl.toString());
    } else {
      throw 'Could not launch $authUrl';
    }
  }
  
  Future<void> doAuthOnWeb(Map<String, String> queryParams) async {
    if (queryParams.containsKey('code')) {
      final code = queryParams['code'];
      await _getToken(code!);
    }
  }
  
  Future<void> _getToken(String code) async {
    final response = await http.post(
      Uri.parse(tokenEndpoint),
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
      body: {
        'grant_type': 'authorization_code',
        'client_id': clientId,
        'client_secret': clientSecret,
        'code': code,
        'redirect_uri': redirectUri,
      },
    );
    
    if (response.statusCode == 200) {
      final Map<String, dynamic> data = json.decode(response.body);
      
      await _prefs.setString('access_token', data['access_token']);
      await _prefs.setString('id_token', data['id_token']);
      await _prefs.setString('refresh_token', data['refresh_token']);
      
      final expiresIn = data['expires_in'] as int;
      final expiresAt = DateTime.now().millisecondsSinceEpoch + expiresIn * 1000;
      await _prefs.setInt('expires_at', expiresAt);
      
      if (data.containsKey('id_token')) {
        final Map<String, dynamic> profile = parseIdToken(data['id_token']);
        await _prefs.setString('user_profile', json.encode(profile));
      }
      
      _isLoggedInController.add(true);
    } else {
      throw Exception('Failed to get token');
    }
  }
  
  Future<void> _refreshToken(String refreshToken) async {
    final response = await http.post(
      Uri.parse(tokenEndpoint),
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
      body: {
        'grant_type': 'refresh_token',
        'client_id': clientId,
        'client_secret': clientSecret,
        'refresh_token': refreshToken,
      },
    );
    
    if (response.statusCode == 200) {
      final Map<String, dynamic> data = json.decode(response.body);
      
      await _prefs.setString('access_token', data['access_token']);
      if (data.containsKey('refresh_token')) {
        await _prefs.setString('refresh_token', data['refresh_token']);
      }
      
      final expiresIn = data['expires_in'] as int;
      final expiresAt = DateTime.now().millisecondsSinceEpoch + expiresIn * 1000;
      await _prefs.setInt('expires_at', expiresAt);
      
      _isLoggedInController.add(true);
    } else {
      throw Exception('Failed to refresh token');
    }
  }
  
  Map<String, dynamic> parseIdToken(String idToken) {
    return JwtDecoder.decode(idToken);
  }
  
  Future<String?> getAccessTokenIfLoggedIn() async {
    final accessToken = _prefs.getString('access_token');
    final expiresAt = _prefs.getInt('expires_at');
    
    if (accessToken != null && expiresAt != null) {
      if (DateTime.now().millisecondsSinceEpoch < expiresAt) {
        return accessToken;
      } else {
        final refreshToken = _prefs.getString('refresh_token');
        if (refreshToken != null) {
          try {
            await _refreshToken(refreshToken);
            return _prefs.getString('access_token');
          } catch (e) {
            return null;
          }
        }
      }
    }
    return null;
  }
  
  Future<void> logout() async {
    await _prefs.remove('access_token');
    await _prefs.remove('id_token');
    await _prefs.remove('refresh_token');
    await _prefs.remove('expires_at');
    await _prefs.remove('user_profile');
    
    _isLoggedInController.add(false);
  }
  
  void dispose() {
    _isLoggedInController.close();
  }
}

Schritt 3: Callback-Seite erstellen

Erstellen Sie eine callback.html im web-Ordner Ihres Flutter-Projekts:

<html>
    <body>
    </body>
    <script>
        function findGetParameter(parameterName) {
            var result = null,
            tmp = [];
            location.search
                .substr(1)
                .split("&")
                .forEach(function (item) {
                    tmp = item.split("=");
                    if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
                 });
            return result;
         }
        let code = findGetParameter('code');
        // Get Hostname
        var url = window.location.href
        var arr = url.split("/");
        var currentUrl = arr[0] + "//" + arr[2]
        // Build new URL
        let newUrl = currentUrl + "/#/callback?code=" + code;
        // Send to new URL
        window.location.href = newUrl;
    </script>
</html>

Schritt 4: Dependency Injection einrichten

Initialisieren Sie die Dependency Injection in Ihrer main.dart Datei:

import 'package:shared_preferences/shared_preferences.dart';
import 'auth_service.dart';
void setupDependencyInjection() async {
  final sharedPreferences = await SharedPreferences.getInstance();
  getIt.registerSingleton<SharedPreferences>(sharedPreferences);
  getIt.registerSingleton<AuthService>(AuthService());
}
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await setupDependencyInjection();
  runApp(MyApp());
}

Schritt 5: Callback-Route in Ihrer App einrichten

Fügen Sie eine Callback-Route zu Ihrem Router hinzu:

import 'package:flutter/material.dart';
import 'auth_service.dart';
class CallbackPage extends StatefulWidget {
  final Map<String, String> queryParameters;
  CallbackPage({this.queryParameters});
  @override
  _CallbackPageState createState() => _CallbackPageState();
}
class _CallbackPageState extends State<CallbackPage> {
  final AuthService _authService = getIt.get<AuthService>();
  @override
  void initState() {
    super.initState();
    _handleCallback();
  }
  void _handleCallback() async {
    await _authService.doAuthOnWeb(widget.queryParameters);
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

Schritt 6: Login-Button implementieren

Implementieren Sie einen Login-Button in Ihrer App:

ElevatedButton(
  onPressed: () {
    final AuthService _authService = getIt.get<AuthService>();
    _authService.loginAction();
  },
  child: Text('Anmelden'),
)

Schritt 7: Authentifizierungsstatus überwachen

Verwenden Sie StreamBuilder, um den Authentifizierungsstatus zu überwachen:

StreamBuilder<bool>(
  stream: _authService.isLoggedIn,
  builder: (context, snapshot) {
    if (snapshot.hasData && snapshot.data) {
      return Text('Angemeldet');
    } else {
      return Text('Nicht angemeldet');
    }
  },
)

Schritt 8: API-Aufrufe mit Authentifizierung

Verwenden Sie den Access-Token für authentifizierte API-Aufrufe:

Future<void> callSecureApi() async {
  final token = await _authService.getAccessTokenIfLoggedIn();
  if (token != null) {
    final response = await http.get(
      Uri.parse('https://your-api.com/secure-endpoint'),
      headers: {
        'Authorization': 'Bearer $token',
      },
    );
    // Verarbeiten Sie die Antwort
  }
}

Vorteile dieser Implementierung

Diese OAuth2-Implementierung bietet mehrere Vorteile:

  1. Cross-Platform-Kompatibilität: Funktioniert sowohl in Flutter Web als auch in mobilen Apps
  2. Token-Persistenz: Speichert Tokens sicher für automatische Anmeldungen
  3. Refresh-Token-Unterstützung: Ermöglicht langfristige Authentifizierung ohne erneute Anmeldung
  4. Reaktive Programmierung: Verwendet RxDart für einfaches State-Management

Häufige Fehler und Lösungen

CORS-Probleme in Flutter Web

Wenn Sie CORS-Probleme in Flutter Web haben, stellen Sie sicher, dass Ihr OAuth2-Provider die richtige Origin zulässt. Bei Auth0 können Sie dies in den Anwendungseinstellungen konfigurieren.

Redirect-URI-Fehler

Stellen Sie sicher, dass die Redirect-URI in Ihrem Code exakt mit der in Ihrem OAuth2-Provider konfigurierten URI übereinstimmt.

Token-Parsing-Fehler

Wenn Sie Probleme beim Parsen des ID-Tokens haben, überprüfen Sie das Format des zurückgegebenen Tokens und passen Sie die parseIdToken-Methode entsprechend an.

Fazit

Mit dieser Implementierung haben Sie eine robuste OAuth2-Authentifizierungslösung für Ihre Flutter-Anwendungen, die sowohl auf Web- als auch auf mobilen Plattformen funktioniert. Diese Lösung bietet eine sichere und benutzerfreundliche Anmeldeerfahrung für Ihre Nutzer. Als App-Agentur aus Köln mit Fokus auf Flutter-Entwicklung helfen wir Ihnen gerne bei der Implementierung sicherer Authentifizierungslösungen für Ihre individuellen App-Projekte. Kontaktieren Sie uns für eine Beratung zu Ihrem nächsten Projekt.

Diesen Post teilen:

Empfohlene Artikel

Ihre App-Idee verwirklichen?

Lassen Sie uns gemeinsam Ihre App-Idee in Köln zum Leben erwecken. Unsere Experten beraten Sie gerne zu Ihrem individuellen Projekt.

Kostenlose Erstberatung