feat(auth): Add JWT token authentication support with validation
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user