Authentication System#
ContainerPub uses a dual-token JWT authentication system with a whitelist-based token storage approach for secure and scalable user authentication.
Overview#
The authentication system provides:
- Dual Token Architecture - Access tokens (1 hour) + Refresh tokens (30 days)
- Whitelist-Based Storage - Tokens stored as hashes in user-specific whitelists
- SHA-256 Hashing - Tokens hashed before storage (64 chars vs 255+ char JWTs)
- Encrypted Storage - Hive database with AES-256 encryption
- Token Blacklisting - Immediate token invalidation
- Multi-Session Support - Multiple active sessions per user
Token Architecture#
Access Token#
- Lifetime: 1 hour
- Purpose: API request authorization
- Storage: SHA-256 hash in user's whitelist
- Payload:
userId,email,type='access'
Refresh Token#
- Lifetime: 30 days
- Purpose: Obtain new access tokens
- Storage: SHA-256 hash with user ID mapping
- Payload:
userId,email,type='refresh'
Storage Architecture#
Hive Boxes#
The token service uses four encrypted Hive boxes:
| Box | Key | Value | Description |
|---|---|---|---|
auth_tokens |
userId | List<tokenHash> | User's whitelist of valid access tokens |
blacklist_tokens |
tokenHash | timestamp | Invalidated tokens |
refresh_tokens |
refreshHash | userId | Refresh token to user mapping |
token_links |
refreshHash | accessHash | Refresh to access token links |
Why Whitelist Approach?#
JWT tokens exceed Hive's 255-character key limit. The whitelist approach:
- Hashes tokens - SHA-256 produces 64-character hashes
- Uses userId as key - Short, predictable key length
- Stores token list - Supports multiple active sessions
- Enables fast lookup - O(n) where n = user's active sessions
Authentication Flow#
Registration#
POST /api/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "securePassword123"
}
Response:
{
"message": "Account created successfully"
}
Flow:
- Validate email and password
- Hash password with BCrypt
- Insert user into PostgreSQL
- Return success message
Login#
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "base64EncodedPassword"
}
Response:
{
"accessToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Flow:
- Validate credentials against PostgreSQL
- Generate access token (HS512, 1 hour)
- Generate refresh token (HS256, 30 days)
- Hash both tokens with SHA-256
- Add access token hash to user's whitelist
- Store refresh token hash with user mapping
- Link refresh hash to access hash
- Return both tokens
Token Validation (Middleware)#
// Request with Authorization header
GET /api/functions
Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...
Flow:
- Extract token from
Authorization: Bearer <token> - Verify JWT signature with secret
- Extract
userIdfrom JWT payload - Hash token with SHA-256
- Check if hash exists in user's whitelist
- Check if hash is NOT in blacklist
- Allow or deny request
Token Refresh#
POST /api/auth/refresh
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response:
{
"accessToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..."
}
Flow:
- Verify refresh token JWT signature
- Validate token type is 'refresh'
- Check refresh token hash is valid (exists and not blacklisted)
- Generate new access token
- Hash new access token
- Add new hash to user's whitelist
- Blacklist old access token hash
- Remove old hash from user's whitelist
- Update token link with new access hash
- Return new access token
Logout#
POST /api/auth/logout
Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response:
{
"message": "Logout successful"
}
Flow:
- Extract access token from header
- Extract refresh token from body
- Verify access token to get userId
- Hash access token, add to blacklist
- Remove access hash from user's whitelist
- Hash refresh token, add to blacklist
- Remove refresh token from storage
- Remove token link
Token Service API#
Adding Tokens#
// Add access token to user's whitelist
await TokenService.instance.addAuthToken(
token: accessToken,
userId: userId,
);
// Add refresh token with link to access token
await TokenService.instance.addRefreshToken(
refreshToken: refreshToken,
userId: userId,
accessToken: accessToken,
);
Validating Tokens#
// Check if access token is valid (async)
final isValid = await TokenService.instance.isTokenValid(
token,
userId,
);
// Check if refresh token is valid (sync)
final isRefreshValid = TokenService.instance.isRefreshTokenValid(
refreshToken,
);
// Check if token is blacklisted (sync)
final isBlacklisted = TokenService.instance.isTokenBlacklisted(token);
Invalidating Tokens#
// Blacklist access token and remove from whitelist
await TokenService.instance.blacklistToken(
token,
userId: userId,
);
// Blacklist refresh token
await TokenService.instance.blacklistRefreshToken(refreshToken);
// Remove all tokens for a user (logout from all devices)
await TokenService.instance.removeAllUserTokens(userId);
Refreshing Tokens#
// Update linked access token (blacklists old, links new)
await TokenService.instance.updateLinkedAccessToken(
refreshToken: refreshToken,
newAccessToken: newAccessToken,
userId: userId,
);
Security Features#
Token Hashing#
All tokens are hashed using SHA-256 before storage:
String _hashToken(String token) {
final bytes = utf8.encode(token);
final digest = sha256.convert(bytes);
return digest.toString(); // 64 characters
}
Benefits:
- Tokens never stored in plain text
- Fixed 64-character hash length
- One-way transformation (cannot recover token)
- Fast computation
Encrypted Storage#
Hive boxes use AES-256 encryption:
final cipher = HiveAesCipher(key); // 256-bit key
await Hive.openLazyBox<List<dynamic>>(
'auth_tokens',
encryptionCipher: cipher,
);
Key Management:
- Key stored in
data/key.txt - Auto-generated on first run
- Base64 encoded for storage
Blacklist Checking#
Blacklist is checked before whitelist:
Future<bool> isTokenValid(String token, String userId) async {
final tokenHash = _hashToken(token);
// Check blacklist first (fast rejection)
if (_blacklistBox.containsKey(tokenHash)) {
return false;
}
// Check whitelist
final existingTokens = await _authTokenBox.get(userId);
if (existingTokens == null) return false;
return existingTokens.cast<String>().contains(tokenHash);
}
Multi-Session Support#
Each user can have multiple active sessions:
// User's whitelist: ["hash1", "hash2", "hash3"]
// Each hash represents an active session/device
Session Management:
- Login adds new hash to list
- Logout removes specific hash
removeAllUserTokens()clears all sessions
JWT Configuration#
Access Token#
final accessJwt = JWT({
'userId': userId,
'email': email,
'type': 'access',
});
final accessToken = accessJwt.sign(
SecretKey(Config.jwtSecret),
algorithm: JWTAlgorithm.HS512,
expiresIn: Duration(hours: 1),
);
Refresh Token#
final refreshJwt = JWT({
'userId': userId,
'email': email,
'type': 'refresh',
});
final refreshToken = refreshJwt.sign(
SecretKey(Config.jwtSecret),
expiresIn: Duration(days: 30),
);
Middleware Integration#
The auth middleware validates tokens on protected routes:
Middleware get authMiddleware {
return (Handler handler) {
return (Request request) async {
final authHeader = request.headers['authorization'];
if (authHeader == null || !authHeader.startsWith('Bearer ')) {
return Response.forbidden(
jsonEncode({'error': 'Missing or invalid authorization header'}),
);
}
final token = authHeader.substring(7);
try {
final jwt = JWT.verify(token, SecretKey(Config.jwtSecret));
final userId = jwt.payload['userId'] as String;
// Validate against whitelist
final isValid = await TokenService.instance.isTokenValid(token, userId);
if (!isValid) {
return Response.forbidden(
jsonEncode({'error': 'Invalid or expired token'}),
);
}
// Add userId to request context
return await handler(request.change(context: {'userId': userId}));
} catch (e) {
return Response.forbidden(
jsonEncode({'error': 'Invalid or expired token'}),
);
}
};
};
}
Error Handling#
Common Errors#
| Error | Status | Cause |
|---|---|---|
| Missing authorization header | 403 | No Authorization header |
| Invalid token format | 403 | Not Bearer <token> format |
| Token expired | 403 | JWT expiry exceeded |
| Token blacklisted | 403 | Token in blacklist |
| Token not in whitelist | 403 | Token hash not found |
| Invalid credentials | 403 | Wrong email/password |
Error Responses#
{
"error": "Invalid or expired token"
}
Development Setup#
Dependencies#
dependencies:
dart_jsonwebtoken: ^3.3.1
hive_ce: ^2.15.1
crypto: ^3.0.6
bcrypt: ^1.1.3
Initialization#
// In server startup
await TokenService.instance.initialize();
Configuration#
// Environment variables
JWT_SECRET=your-secret-key-here
Best Practices#
Token Handling#
- Never log tokens - Use hashes for debugging
- Short access token lifetime - 1 hour maximum
- Rotate refresh tokens - Consider rotation on each use
- Secure transmission - Always use HTTPS
Storage#
- Encrypt at rest - Use HiveAesCipher
- Protect encryption key - Secure
key.txtfile - Regular cleanup - Purge old blacklist entries
- Backup tokens - Include in disaster recovery
Security#
- Validate on every request - Use middleware
- Check blacklist first - Fast rejection
- Log authentication events - Audit trail
- Rate limit auth endpoints - Prevent brute force
Next Steps#
- API Reference - Complete endpoint documentation
- Architecture Overview - System design details
- CLI Authentication - Client-side token handling