Add complete authentication system supporting both web (NPM forward auth) and mobile (OIDC) authentication flows. Web flow: - Check /auth/me on startup to detect NPM forward auth session - Cookies handled by proxy, no Bearer tokens needed Mobile flow: - flutter_appauth for OIDC Authorization Code + PKCE - POST /auth/sync to get user profile and roles - Token storage in SharedPreferences Shared: - Permission system with Domain/Action enums and Role class - PermissionGate and AdminGate widgets for UI permission checks - Route guards redirecting unauthenticated users to login - Login page with platform-specific messaging Platform config: - iOS: CFBundleURLTypes for net.schweitz.tatlock:// - Android: appAuthRedirectScheme, minSdk 23 Docs: - Added Freezed 3.x sealed class documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
116 lines
3.2 KiB
Dart
116 lines
3.2 KiB
Dart
import 'package:flutter_appauth/flutter_appauth.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
import '../config/app_config.dart';
|
|
|
|
part 'oidc_service.g.dart';
|
|
|
|
/// OIDC token response containing access and refresh tokens.
|
|
class OidcTokens {
|
|
const OidcTokens({
|
|
required this.accessToken,
|
|
required this.refreshToken,
|
|
required this.expiresAt,
|
|
this.idToken,
|
|
});
|
|
|
|
final String accessToken;
|
|
final String? refreshToken;
|
|
final DateTime expiresAt;
|
|
final String? idToken;
|
|
}
|
|
|
|
/// Service for OIDC authentication using flutter_appauth.
|
|
///
|
|
/// Handles the Authorization Code flow with PKCE for secure authentication
|
|
/// against Authentik.
|
|
class OidcService {
|
|
OidcService({FlutterAppAuth? appAuth}) : _appAuth = appAuth ?? const FlutterAppAuth();
|
|
|
|
final FlutterAppAuth _appAuth;
|
|
|
|
/// OIDC scopes to request.
|
|
static const _scopes = ['openid', 'profile', 'email', 'offline_access'];
|
|
|
|
/// Redirect URI for the app.
|
|
static String get _redirectUri => '${AppConfig.authRedirectScheme}://callback';
|
|
|
|
/// Start the authorization code flow.
|
|
///
|
|
/// Opens a browser/webview for user to authenticate with Authentik,
|
|
/// then exchanges the authorization code for tokens.
|
|
///
|
|
/// Throws [OidcException] if authentication fails.
|
|
Future<OidcTokens> signIn() async {
|
|
try {
|
|
final result = await _appAuth.authorizeAndExchangeCode(
|
|
AuthorizationTokenRequest(
|
|
AppConfig.authClientId,
|
|
_redirectUri,
|
|
discoveryUrl: AppConfig.authDiscoveryUrl,
|
|
scopes: _scopes,
|
|
),
|
|
);
|
|
|
|
if (result.accessToken == null) {
|
|
throw OidcException('Authorization failed: no access token');
|
|
}
|
|
|
|
return OidcTokens(
|
|
accessToken: result.accessToken!,
|
|
refreshToken: result.refreshToken,
|
|
expiresAt: result.accessTokenExpirationDateTime ?? DateTime.now().add(const Duration(hours: 1)),
|
|
idToken: result.idToken,
|
|
);
|
|
} on Exception catch (e) {
|
|
throw OidcException('Authorization failed: $e');
|
|
}
|
|
}
|
|
|
|
/// Refresh the access token using a refresh token.
|
|
///
|
|
/// Throws [OidcException] if refresh fails.
|
|
Future<OidcTokens> refreshToken(String refreshToken) async {
|
|
try {
|
|
final result = await _appAuth.token(
|
|
TokenRequest(
|
|
AppConfig.authClientId,
|
|
_redirectUri,
|
|
discoveryUrl: AppConfig.authDiscoveryUrl,
|
|
refreshToken: refreshToken,
|
|
scopes: _scopes,
|
|
),
|
|
);
|
|
|
|
if (result.accessToken == null) {
|
|
throw OidcException('Token refresh failed: no access token');
|
|
}
|
|
|
|
return OidcTokens(
|
|
accessToken: result.accessToken!,
|
|
refreshToken: result.refreshToken ?? refreshToken,
|
|
expiresAt: result.accessTokenExpirationDateTime ?? DateTime.now().add(const Duration(hours: 1)),
|
|
idToken: result.idToken,
|
|
);
|
|
} on Exception catch (e) {
|
|
throw OidcException('Token refresh failed: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Exception thrown when OIDC operations fail.
|
|
class OidcException implements Exception {
|
|
const OidcException(this.message);
|
|
|
|
final String message;
|
|
|
|
@override
|
|
String toString() => 'OidcException: $message';
|
|
}
|
|
|
|
/// Provider for the OIDC service.
|
|
@riverpod
|
|
OidcService oidcService(Ref ref) {
|
|
return OidcService();
|
|
}
|