feat(auth): Add JWT token authentication support with validation

This commit is contained in:
cogwheel0
2025-12-05 11:24:03 +05:30
parent 1ad43d372a
commit bee8fda9f7
11 changed files with 117 additions and 13 deletions

View File

@@ -399,7 +399,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
icon: Platform.isIOS
? CupertinoIcons.lock_shield
: Icons.vpn_key_outlined,
label: AppLocalizations.of(context)!.apiKey,
label: AppLocalizations.of(context)!.token,
isSelected: _useApiKey,
onTap: () => setState(() => _useApiKey = true),
),
@@ -480,24 +480,43 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
);
}
/// Validates that a token is a JWT and not an API key.
/// API keys (sk-, api-, key-) don't work with WebSocket authentication.
String? _validateJwtToken(String? value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context)!.validationMissingRequired;
}
final trimmed = value.trim();
final lowerTrimmed = trimmed.toLowerCase();
// Reject API keys - they don't work with socket authentication
// Case-insensitive check to catch SK-, API-, KEY- variants
if (lowerTrimmed.startsWith('sk-') ||
lowerTrimmed.startsWith('api-') ||
lowerTrimmed.startsWith('key-')) {
return AppLocalizations.of(context)!.apiKeyNotSupported;
}
// Check minimum length
if (trimmed.length < 10) {
return AppLocalizations.of(context)!.tokenTooShort;
}
return null;
}
Widget _buildApiKeyForm() {
return Column(
key: const ValueKey('api_key_form'),
children: [
AccessibleFormField(
label: AppLocalizations.of(context)!.apiKey,
hint: 'sk-...',
label: AppLocalizations.of(context)!.token,
hint: 'eyJ...',
controller: _apiKeyController,
validator: InputValidationService.combine([
InputValidationService.validateRequired,
(value) => InputValidationService.validateMinLength(
value,
10,
fieldName: AppLocalizations.of(context)!.apiKey,
),
]),
validator: _validateJwtToken,
obscureText: _obscurePassword,
semanticLabel: AppLocalizations.of(context)!.enterApiKey,
semanticLabel: AppLocalizations.of(context)!.enterToken,
prefixIcon: Icon(
Platform.isIOS
? CupertinoIcons.lock_shield
@@ -520,6 +539,13 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
isRequired: true,
autofillHints: const [AutofillHints.password],
),
const SizedBox(height: Spacing.sm),
Text(
AppLocalizations.of(context)!.tokenHint,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
],
);
}
@@ -591,7 +617,7 @@ class _AuthenticationPageState extends ConsumerState<AuthenticationPage> {
text: _isSigningIn
? AppLocalizations.of(context)!.signingIn
: _useApiKey
? AppLocalizations.of(context)!.signInWithApiKey
? AppLocalizations.of(context)!.signInWithToken
: AppLocalizations.of(context)!.signIn,
icon: _isSigningIn
? null