felectronic_clave
Cl@ve authentication for Spanish government services. Provides a clean API for authenticating via Spain's Cl@ve identity system using OAuth/OIDC.
Features
- Cl@ve PIN -- temporary 24-hour PIN via SMS
- Cl@ve Permanente -- permanent password (Social Security)
- Electronic Certificate -- client-side X.509 certificate (AFIRMA)
- European Credential -- eIDAS / STORK cross-border authentication
- Cl@ve Movil -- push notification to the Cl@ve mobile app
- Token Management -- secure storage, refresh, and validation
- LOA Elevation -- re-authenticate at a higher Level of Assurance
- Document Validation -- DNI/NIE check-letter and format validation
Installation
dependencies:
felectronic_clave: ^1.0.0
Dependencies
This package uses:
flutter_appauthfor OAuth/OIDC flowsflutter_secure_storagefor encrypted token persistencehttpfor Cl@ve Movil API calls
Follow the flutter_appauth setup guide for platform-specific configuration (redirect URI schemes, etc.).
Configuration
Create a ClaveConfig with your OAuth endpoints and client credentials:
import 'package:felectronic_clave/felectronic_clave.dart';
final config = ClaveConfig(
discoveryUrl: 'https://auth-api.redsara.es/auth/realms/.../.well-known/openid-configuration',
clientId: 'my-client-id',
redirectUri: 'com.example.app://login-callback',
clientSecret: 'my-client-secret',
userInfoUrl: 'https://auth-api.redsara.es/auth/realms/.../protocol/openid-connect/userinfo',
logoutUrl: 'https://auth-api.redsara.es/auth/realms/.../protocol/openid-connect/logout',
defaultLoa: ClaveLoaLevel.low,
enabledMethods: [
ClaveAuthMethod.clavePin,
ClaveAuthMethod.clavePermanente,
ClaveAuthMethod.electronicCertificate,
ClaveAuthMethod.claveMovil,
],
// Required only for Cl@ve Movil:
claveMobileCreateUrl: 'https://...',
claveMobileValidateUrl: 'https://...',
);
Use the ClaveConfigX extension to validate your config:
if (!config.isValid) throw Exception('Invalid configuration');
if (!config.hasClaveMobileUrls) print('Cl@ve Movil not configured');
Usage
Login
final repo = ClaveRepository(config);
// Login with Cl@ve PIN
final result = await repo.login(method: ClaveAuthMethod.clavePin);
print(result.accessToken);
// Login with a specific LOA level
final elevated = await repo.login(
method: ClaveAuthMethod.clavePermanente,
loa: ClaveLoaLevel.medium,
);
Note: ClaveAuthMethod.electronicCertificate always uses ClaveLoaLevel.medium regardless of the loa parameter.
Token Management
Tokens are persisted in encrypted secure storage automatically after login.
// Get stored token
final token = await repo.getStoredToken();
// Refresh token
final newToken = await repo.refreshToken();
// Validate token against the userInfo endpoint
final claims = await repo.validateToken();
if (claims.isEmpty) print('Token is invalid');
// Extract NIF from a JWT token
final nif = repo.getNifFromToken(token!);
// Logout and clear tokens
await repo.logout();
LOA Elevation
Re-authenticate at a higher Level of Assurance. If the elevated login fails, the previous tokens are restored automatically.
final result = await repo.elevateLoaLevel(
method: ClaveAuthMethod.clavePermanente,
loa: ClaveLoaLevel.high,
);
Cl@ve Movil Flow
Cl@ve Movil sends a push notification to the user's Cl@ve app. The user confirms on their phone, and the app polls for the result.
Manual Polling
// Step 1: Send notification
final session = await repo.sendNotificationCode(
document: '12345678Z', // DNI
contrast: '01-01-2025', // Validity date for DNI
);
print('Show code to user: ${session.displayCode}');
// Step 2: Poll for result
ClaveAuthResult? authResult;
while (authResult == null) {
try {
authResult = await repo.validateNotificationCode(session: session);
} on ClaveIdleError {
await Future.delayed(Duration(seconds: 5));
}
}
For NIE documents, the contrast is the support number (e.g. C12345678).
Stream-Based Polling with ClaveMobilePoller
A cleaner alternative using a stream:
final poller = ClaveMobilePoller(repo);
await for (final status in poller.poll(session: session)) {
switch (status) {
case ClavePollWaiting(:final elapsedSeconds):
print('Waiting... ${elapsedSeconds}s');
case ClavePollSuccess(:final result):
print('Authenticated: ${result.accessToken}');
case ClavePollError(:final error):
print('Failed: ${error.message}');
}
}
// Cancel polling early if needed
poller.cancel();
The poller accepts optional parameters:
initialDelay-- wait before first poll (default: 20 seconds)interval-- time between polls (default: 5 seconds)timeout-- maximum polling duration (default: 5 minutes)
Document Validation
DocumentValidator
Static utility for validating Spanish identity documents:
DocumentValidator.isValidDni('12345678Z'); // true/false (modulo-23 check)
DocumentValidator.isValidNie('X1234567L'); // true/false
DocumentValidator.isValid('12345678Z'); // validates either DNI or NIE
DocumentValidator.isValidSupportNumber('C12345678'); // NIE support number
DocumentValidator.isValidContrastDate('01-01-2025'); // dd-MM-yyyy
DocumentValidator.contrastTypeFor('12345678Z'); // 'date'
DocumentValidator.contrastTypeFor('X1234567L'); // 'support'
String Extensions
Convenient validation directly on strings:
'12345678Z'.isValidDni; // true
'X1234567L'.isValidNie; // true
'12345678Z'.isValidDocument; // true
'12345678Z'.documentType; // 'DNI'
'C12345678'.isValidSupportNumber; // true
'01-01-2025'.isValidContrastDate; // true
// For form validation — returns error message or null
final error = userInput.validateDocument();
final contrastError = dateInput.validateContrast(isDni: true);
Models and Enums
ClaveAuthMethod
| Value | IDP String | Description |
|---|---|---|
clavePin |
PIN24H |
Temporary 24-hour PIN via SMS |
clavePermanente |
SEGSOC |
Permanent password (Social Security) |
electronicCertificate |
AFIRMA |
Client-side X.509 certificate |
europeanCredential |
EIDAS |
eIDAS / STORK |
claveMovil |
CLVMOVIL |
Push notification to Cl@ve app |
ClaveLoaLevel
| Value | Level | Description |
|---|---|---|
low |
1 | Password or PIN-based |
medium |
2 | Two-factor or certificate-based |
high |
3 | Qualified electronic signature |
ClaveAuthResult
| Field | Type | Description |
|---|---|---|
accessToken |
String |
OAuth access token |
refreshToken |
String? |
OAuth refresh token |
expiresIn |
int? |
Token expiry in seconds |
Extensions (ClaveAuthResultX): hasRefreshToken, expiresAt, isTokenExpired.
ClaveMobileSession
| Field | Type | Description |
|---|---|---|
token |
String |
Session token for polling |
verificationCode |
String |
Code shown to user |
document |
String |
DNI or NIE used |
Extensions (ClaveMobileSessionX): isActive, displayCode (zero-padded to 6 digits).
Error Handling
All errors extend the sealed ClaveError class:
| Error | Description |
|---|---|
ClaveAuthCancelledError |
User cancelled the OAuth flow |
ClaveInvalidContrastError |
Contrast data does not match the document |
ClaveRequestAlreadySentError |
A Cl@ve Movil notification is already pending |
ClaveSessionExpiredError |
Session or token has expired |
ClaveRefusedError |
Authentication was refused |
ClaveDiscoveryFailedError |
Cannot reach the OpenID Connect discovery endpoint |
ClaveIdleError |
Cl@ve Movil validation is still pending (keep polling) |
ClaveUnknownError |
Unexpected error with a message |
try {
await repo.login(method: ClaveAuthMethod.clavePin);
} on ClaveAuthCancelledError {
print('User cancelled');
} on ClaveDiscoveryFailedError {
print('Network issue');
} on ClaveError catch (e) {
print('Cl@ve error: ${e.message}');
}
Cleanup
Call dispose() when the repository is no longer needed to release HTTP resources:
repo.dispose();
Libraries
- felectronic_clave
- Cl@ve authentication for Spanish government services.