Secure, Encrypted, and Stateless Authentication Tokens
Article
Branca tokens provide a modern, secure alternative to JWT (JSON Web Tokens) by offering built-in encryption, smaller token size, and better security defaults. Unlike JWT, which is signed but not encrypted by default, Branca tokens are always encrypted, protecting token payloads from inspection.
Branca Token Overview
Key Features:
- Encrypted by default using XChaCha20-Poly1305
- Small token size compared to JWT
- Built-in timestamp for expiration handling
- Simple specification with versioning
- No external dependencies beyond libsodium
Token Structure:
Version (1B) | Timestamp (4B) | Nonce (24B) | Ciphertext (variable) | Tag (16B)
1. Project Setup and Dependencies
Add the required dependencies to your pom.xml:
<dependencies> <!-- Branca Java Implementation --> <dependency> <groupId>io.github.tomakehurst</groupId> <artifactId>branca</artifactId> <version>0.4.0</version> </dependency> <!-- Bouncy Castle for cryptographic operations --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- For Spring Boot integration --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
2. Core Branca Token Service
import com.github.tomakehurst.branca.Branca;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@Service
public class BrancaTokenService {
private final Branca branca;
private final ObjectMapper objectMapper;
private final long tokenTtlSeconds;
// 32-byte key for Branca (256 bits)
private static final String BRANCA_KEY_ENV = "BRANCA_SECRET_KEY";
private static final int DEFAULT_TTL_SECONDS = 3600; // 1 hour
public BrancaTokenService() {
String secretKey = getSecretKeyFromEnv();
this.branca = new Branca(secretKey.getBytes(StandardCharsets.UTF_8));
this.objectMapper = new ObjectMapper();
this.tokenTtlSeconds = DEFAULT_TTL_SECONDS;
}
public BrancaTokenService(String secretKey, long tokenTtlSeconds) {
this.branca = new Branca(secretKey.getBytes(StandardCharsets.UTF_8));
this.objectMapper = new ObjectMapper();
this.tokenTtlSeconds = tokenTtlSeconds;
}
private String getSecretKeyFromEnv() {
String key = System.getenv(BRANCA_KEY_ENV);
if (key == null || key.length() < 32) {
throw new IllegalStateException(
"BRANCA_SECRET_KEY environment variable must be set with 32+ characters");
}
// Ensure key is exactly 32 bytes
return key.length() > 32 ? key.substring(0, 32) : key;
}
/**
* Create a Branca token with user claims
*/
public String createToken(String userId, Map<String, Object> additionalClaims) {
try {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", userId); // Subject
claims.put("iat", Instant.now().getEpochSecond()); // Issued at
claims.put("exp", Instant.now().getEpochSecond() + tokenTtlSeconds); // Expiration
if (additionalClaims != null) {
claims.putAll(additionalClaims);
}
String payload = objectMapper.writeValueAsString(claims);
return branca.encode(payload);
} catch (JsonProcessingException e) {
throw new TokenException("Failed to create token payload", e);
}
}
/**
* Create a simple token with user ID and role
*/
public String createUserToken(String userId, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role);
claims.put("iss", "my-app"); // Issuer
return createToken(userId, claims);
}
/**
* Verify and decode a Branca token
*/
public TokenPayload verifyToken(String token) {
try {
String payload = branca.decode(token);
Map<String, Object> claims = objectMapper.readValue(payload, Map.class);
// Validate expiration
validateExpiration(claims);
return new TokenPayload(claims);
} catch (Exception e) {
throw new TokenException("Invalid or expired token", e);
}
}
/**
* Validate token expiration
*/
private void validateExpiration(Map<String, Object> claims) {
Object expClaim = claims.get("exp");
if (expClaim instanceof Number) {
long expiration = ((Number) expClaim).longValue();
if (Instant.now().getEpochSecond() > expiration) {
throw new TokenException("Token has expired");
}
} else {
throw new TokenException("Missing expiration claim");
}
}
/**
* Get token expiration time
*/
public Instant getExpiration(String token) {
TokenPayload payload = verifyToken(token);
Number exp = (Number) payload.getClaim("exp");
return Instant.ofEpochSecond(exp.longValue());
}
/**
* Check if token is about to expire (within threshold)
*/
public boolean isTokenExpiringSoon(String token, long thresholdSeconds) {
Instant expiration = getExpiration(token);
return Instant.now().plusSeconds(thresholdSeconds).isAfter(expiration);
}
/**
* Refresh a token (create new one with same claims but new expiration)
*/
public String refreshToken(String token) {
TokenPayload oldPayload = verifyToken(token);
Map<String, Object> claims = new HashMap<>(oldPayload.getClaims());
// Remove timestamp claims
claims.remove("iat");
claims.remove("exp");
String userId = (String) claims.get("sub");
return createToken(userId, claims);
}
/**
* Token payload wrapper
*/
public static class TokenPayload {
private final Map<String, Object> claims;
public TokenPayload(Map<String, Object> claims) {
this.claims = claims;
}
public String getUserId() {
return (String) claims.get("sub");
}
public String getRole() {
return (String) claims.get("role");
}
public Instant getIssuedAt() {
Number iat = (Number) claims.get("iat");
return iat != null ? Instant.ofEpochSecond(iat.longValue()) : null;
}
public Instant getExpiration() {
Number exp = (Number) claims.get("exp");
return exp != null ? Instant.ofEpochSecond(exp.longValue()) : null;
}
public Object getClaim(String claimName) {
return claims.get(claimName);
}
public Map<String, Object> getClaims() {
return new HashMap<>(claims);
}
public boolean hasClaim(String claimName) {
return claims.containsKey(claimName);
}
}
public static class TokenException extends RuntimeException {
public TokenException(String message) {
super(message);
}
public TokenException(String message, Throwable cause) {
super(message, cause);
}
}
}
3. Spring Security Integration
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@Component
public class BrancaAuthenticationFilter extends OncePerRequestFilter {
private final BrancaTokenService tokenService;
private static final String AUTH_HEADER = "Authorization";
private static final String TOKEN_PREFIX = "Bearer ";
public BrancaAuthenticationFilter(BrancaTokenService tokenService) {
this.tokenService = tokenService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader(AUTH_HEADER);
if (header != null && header.startsWith(TOKEN_PREFIX)) {
String token = header.substring(TOKEN_PREFIX.length());
try {
BrancaTokenService.TokenPayload payload = tokenService.verifyToken(token);
Authentication auth = createAuthentication(payload);
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (BrancaTokenService.TokenException e) {
// Token is invalid or expired
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\": \"Invalid token\"}");
return;
}
}
filterChain.doFilter(request, response);
}
private Authentication createAuthentication(BrancaTokenService.TokenPayload payload) {
String userId = payload.getUserId();
String role = payload.getRole();
List<GrantedAuthority> authorities = Collections.singletonList(
new SimpleGrantedAuthority("ROLE_" + role.toUpperCase())
);
UserDetails userDetails = User.withUsername(userId)
.password("") // No password in token-based auth
.authorities(authorities)
.build();
return new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
}
}
4. Spring Security Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final BrancaAuthenticationFilter brancaAuthenticationFilter;
public SecurityConfig(BrancaAuthenticationFilter brancaAuthenticationFilter) {
this.brancaAuthenticationFilter = brancaAuthenticationFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/user/**").hasRole("USER")
.anyRequest().authenticated()
.and()
.addFilterBefore(brancaAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
5. Authentication Controller
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final BrancaTokenService tokenService;
private final UserService userService;
public AuthController(BrancaTokenService tokenService, UserService userService) {
this.tokenService = tokenService;
this.userService = userService;
}
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(@RequestBody LoginRequest request) {
// Authenticate user (simplified - use proper authentication in production)
User user = userService.authenticate(request.getUsername(), request.getPassword());
if (user != null) {
String token = tokenService.createUserToken(user.getId(), user.getRole());
Map<String, Object> response = new HashMap<>();
response.put("token", token);
response.put("token_type", "branca");
response.put("expires_in", 3600);
response.put("user", Map.of(
"id", user.getId(),
"username", user.getUsername(),
"role", user.getRole()
));
return ResponseEntity.ok(response);
} else {
return ResponseEntity.status(401).body(Map.of("error", "Invalid credentials"));
}
}
@PostMapping("/refresh")
public ResponseEntity<Map<String, Object>> refreshToken(@RequestHeader("Authorization") String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.badRequest().body(Map.of("error", "Missing or invalid token"));
}
String token = authHeader.substring(7);
try {
String newToken = tokenService.refreshToken(token);
Map<String, Object> response = new HashMap<>();
response.put("token", newToken);
response.put("token_type", "branca");
response.put("expires_in", 3600);
return ResponseEntity.ok(response);
} catch (BrancaTokenService.TokenException e) {
return ResponseEntity.status(401).body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/verify")
public ResponseEntity<Map<String, Object>> verifyToken(@RequestHeader("Authorization") String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.badRequest().body(Map.of("error", "Missing or invalid token"));
}
String token = authHeader.substring(7);
try {
BrancaTokenService.TokenPayload payload = tokenService.verifyToken(token);
Map<String, Object> response = new HashMap<>();
response.put("valid", true);
response.put("user_id", payload.getUserId());
response.put("role", payload.getRole());
response.put("expires_at", payload.getExpiration());
return ResponseEntity.ok(response);
} catch (BrancaTokenService.TokenException e) {
return ResponseEntity.ok(Map.of("valid", false, "error", e.getMessage()));
}
}
// DTO classes
public static class LoginRequest {
private String username;
private String password;
// getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
}
6. Protected Resource Controller
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class ResourceController {
@GetMapping("/user/profile")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public ResponseEntity<Map<String, Object>> getUserProfile(@AuthenticationPrincipal UserDetails userDetails) {
String username = userDetails.getUsername();
// Fetch user profile from database
Map<String, Object> profile = Map.of(
"username", username,
"email", username + "@example.com",
"preferences", Map.of("theme", "dark", "language", "en")
);
return ResponseEntity.ok(profile);
}
@GetMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<Map<String, Object>>> getAllUsers() {
// Admin-only endpoint
List<Map<String, Object>> users = List.of(
Map.of("id", "1", "username", "admin", "role", "ADMIN"),
Map.of("id", "2", "username", "user1", "role", "USER"),
Map.of("id", "3", "username", "user2", "role", "USER")
);
return ResponseEntity.ok(users);
}
@PostMapping("/user/data")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<Map<String, Object>> processUserData(
@RequestBody Map<String, Object> data,
@AuthenticationPrincipal UserDetails userDetails) {
// Process data for authenticated user
String processedData = "Processed: " + data.toString() + " for user: " + userDetails.getUsername();
return ResponseEntity.ok(Map.of(
"result", processedData,
"processed_by", userDetails.getUsername(),
"timestamp", System.currentTimeMillis()
));
}
}
7. Advanced Branca Token Features
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class AdvancedBrancaService {
private final BrancaTokenService tokenService;
private final Map<String, TokenMetadata> tokenMetadata = new ConcurrentHashMap<>();
public AdvancedBrancaService(BrancaTokenService tokenService) {
this.tokenService = tokenService;
}
/**
* Create token with additional security features
*/
public String createSecureToken(String userId, String role, String clientInfo) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role);
claims.put("iss", "my-secure-app");
claims.put("client", clientInfo);
claims.put("jti", generateTokenId()); // Unique token ID
String token = tokenService.createToken(userId, claims);
// Store token metadata
storeTokenMetadata(token, userId, clientInfo);
return token;
}
/**
* Revoke a specific token
*/
public void revokeToken(String token) {
try {
BrancaTokenService.TokenPayload payload = tokenService.verifyToken(token);
String tokenId = (String) payload.getClaim("jti");
if (tokenId != null) {
tokenMetadata.remove(tokenId);
}
} catch (BrancaTokenService.TokenException e) {
// Token is already invalid
}
}
/**
* Revoke all tokens for a user
*/
public void revokeAllUserTokens(String userId) {
tokenMetadata.entrySet().removeIf(entry ->
entry.getValue().getUserId().equals(userId));
}
/**
* Check if token is revoked
*/
public boolean isTokenRevoked(String token) {
try {
BrancaTokenService.TokenPayload payload = tokenService.verifyToken(token);
String tokenId = (String) payload.getClaim("jti");
return tokenId != null && !tokenMetadata.containsKey(tokenId);
} catch (BrancaTokenService.TokenException e) {
return true; // Invalid tokens are considered revoked
}
}
/**
* Get active tokens for a user
*/
public Map<String, TokenMetadata> getUserActiveTokens(String userId) {
Map<String, TokenMetadata> userTokens = new HashMap<>();
tokenMetadata.forEach((tokenId, metadata) -> {
if (metadata.getUserId().equals(userId) &&
metadata.getExpiresAt().isAfter(Instant.now())) {
userTokens.put(tokenId, metadata);
}
});
return userTokens;
}
private String generateTokenId() {
return java.util.UUID.randomUUID().toString();
}
private void storeTokenMetadata(String token, String userId, String clientInfo) {
try {
BrancaTokenService.TokenPayload payload = tokenService.verifyToken(token);
String tokenId = (String) payload.getClaim("jti");
if (tokenId != null) {
TokenMetadata metadata = new TokenMetadata(
tokenId, userId, clientInfo,
payload.getIssuedAt(), payload.getExpiration()
);
tokenMetadata.put(tokenId, metadata);
}
} catch (BrancaTokenService.TokenException e) {
// Should not happen for newly created token
}
}
public static class TokenMetadata {
private final String tokenId;
private final String userId;
private final String clientInfo;
private final Instant issuedAt;
private final Instant expiresAt;
public TokenMetadata(String tokenId, String userId, String clientInfo,
Instant issuedAt, Instant expiresAt) {
this.tokenId = tokenId;
this.userId = userId;
this.clientInfo = clientInfo;
this.issuedAt = issuedAt;
this.expiresAt = expiresAt;
}
// getters
public String getTokenId() { return tokenId; }
public String getUserId() { return userId; }
public String getClientInfo() { return clientInfo; }
public Instant getIssuedAt() { return issuedAt; }
public Instant getExpiresAt() { return expiresAt; }
}
}
8. Key Management and Rotation
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Base64;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
@Component
public class KeyManagementService {
private final AtomicReference<String> currentKey;
private final ConcurrentHashMap<String, Instant> retiredKeys;
private final long keyRotationInterval = 30 * 24 * 60 * 60; // 30 days in seconds
public KeyManagementService() {
this.currentKey = new AtomicReference<>(generateNewKey());
this.retiredKeys = new ConcurrentHashMap<>();
}
/**
* Get current active key
*/
public String getCurrentKey() {
return currentKey.get();
}
/**
* Rotate to a new key
*/
public synchronized String rotateKey() {
String newKey = generateNewKey();
String oldKey = currentKey.get();
// Retire old key
retiredKeys.put(oldKey, Instant.now().plusSeconds(keyRotationInterval));
// Set new key as current
currentKey.set(newKey);
return newKey;
}
/**
* Check if a key is valid (current or recently retired)
*/
public boolean isValidKey(String key) {
if (key.equals(currentKey.get())) {
return true;
}
Instant retirementTime = retiredKeys.get(key);
return retirementTime != null && retirementTime.isAfter(Instant.now());
}
/**
* Clean up expired retired keys
*/
@Scheduled(cron = "0 0 2 * * ?") // Daily at 2 AM
public void cleanupExpiredKeys() {
Instant now = Instant.now();
retiredKeys.entrySet().removeIf(entry -> entry.getValue().isBefore(now));
}
/**
* Generate a new cryptographically secure key
*/
private String generateNewKey() {
byte[] keyBytes = new byte[32]; // 256 bits for Branca
new java.security.SecureRandom().nextBytes(keyBytes);
return Base64.getEncoder().encodeToString(keyBytes);
}
/**
* Branca service that supports key rotation
*/
public static class RotatingBrancaService {
private final KeyManagementService keyManagement;
private final ConcurrentHashMap<String, BrancaTokenService> brancaServices;
public RotatingBrancaService(KeyManagementService keyManagement) {
this.keyManagement = keyManagement;
this.brancaServices = new ConcurrentHashMap<>();
}
public String encode(String payload) {
String currentKey = keyManagement.getCurrentKey();
BrancaTokenService service = getBrancaService(currentKey);
return service.createToken("system", Map.of("payload", payload));
}
public String decode(String token) {
// Try current key first
try {
String currentKey = keyManagement.getCurrentKey();
BrancaTokenService service = getBrancaService(currentKey);
BrancaTokenService.TokenPayload payload = service.verifyToken(token);
return (String) payload.getClaim("payload");
} catch (BrancaTokenService.TokenException e) {
// Try retired keys
for (String retiredKey : keyManagement.retiredKeys.keySet()) {
if (keyManagement.isValidKey(retiredKey)) {
try {
BrancaTokenService service = getBrancaService(retiredKey);
BrancaTokenService.TokenPayload payload = service.verifyToken(token);
return (String) payload.getClaim("payload");
} catch (BrancaTokenService.TokenException ignored) {
// Continue to next key
}
}
}
throw new BrancaTokenService.TokenException("Token verification failed with all keys");
}
}
private BrancaTokenService getBrancaService(String key) {
return brancaServices.computeIfAbsent(key,
k -> new BrancaTokenService(k, 3600));
}
}
}
9. Testing Branca Tokens
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class BrancaTokenTest {
@Autowired
private BrancaTokenService tokenService;
@Test
public void testTokenCreationAndVerification() {
String userId = "user123";
String role = "USER";
String token = tokenService.createUserToken(userId, role);
assertNotNull(token);
assertTrue(token.length() > 0);
BrancaTokenService.TokenPayload payload = tokenService.verifyToken(token);
assertEquals(userId, payload.getUserId());
assertEquals(role, payload.getRole());
assertNotNull(payload.getIssuedAt());
assertNotNull(payload.getExpiration());
}
@Test
public void testTokenExpiration() {
String token = tokenService.createUserToken("testuser", "USER");
// Token should be valid initially
assertDoesNotThrow(() -> tokenService.verifyToken(token));
// Test expiration validation is working
BrancaTokenService.TokenPayload payload = tokenService.verifyToken(token);
assertTrue(payload.getExpiration().isAfter(payload.getIssuedAt()));
}
@Test
public void testTokenWithCustomClaims() {
Map<String, Object> claims = Map.of(
"role", "ADMIN",
"permissions", new String[]{"read", "write", "delete"},
"department", "engineering",
"custom_data", Map.of("preference", "dark_mode")
);
String token = tokenService.createToken("admin123", claims);
BrancaTokenService.TokenPayload payload = tokenService.verifyToken(token);
assertEquals("admin123", payload.getUserId());
assertEquals("ADMIN", payload.getRole());
assertTrue(payload.hasClaim("permissions"));
assertTrue(payload.hasClaim("department"));
assertTrue(payload.hasClaim("custom_data"));
}
@Test
public void testInvalidToken() {
String invalidToken = "invalid.token.here";
assertThrows(BrancaTokenService.TokenException.class,
() -> tokenService.verifyToken(invalidToken));
}
}
Best Practices for Branca Tokens
1. Security Configuration:
public class SecurityBestPractices {
// Use strong, randomly generated keys
public static String generateSecureKey() {
byte[] key = new byte[32];
new java.security.SecureRandom().nextBytes(key);
return java.util.Base64.getEncoder().encodeToString(key);
}
// Configure proper token expiration
public static final long SHORT_LIVED_TOKEN_TTL = 15 * 60; // 15 minutes
public static final long LONG_LIVED_TOKEN_TTL = 7 * 24 * 60 * 60; // 7 days
public static final long REFRESH_TOKEN_TTL = 30 * 24 * 60 * 60; // 30 days
}
2. Token Types Strategy:
public enum TokenType {
ACCESS(SecurityBestPractices.SHORT_LIVED_TOKEN_TTL),
REFRESH(SecurityBestPractices.REFRESH_TOKEN_TTL),
API_KEY(SecurityBestPractices.LONG_LIVED_TOKEN_TTL);
private final long ttl;
TokenType(long ttl) {
this.ttl = ttl;
}
public long getTtl() { return ttl; }
}
Comparison: Branca vs JWT
| Feature | Branca | JWT |
|---|---|---|
| Encryption | Always encrypted | Signed by default, encrypted optional |
| Token Size | Smaller | Larger due to Base64 encoding |
| Security | Better defaults | Requires careful configuration |
| Libraries | Fewer options | Extensive ecosystem |
| Complexity | Simple specification | More complex standard |
Conclusion
Branca tokens offer a modern, secure approach to token-based authentication in Java applications. Key advantages include:
- Built-in Encryption: All tokens are encrypted by default
- Compact Size: Smaller than equivalent JWTs
- Security First: Better cryptographic defaults
- Simplicity: Straightforward specification and usage
Implementation Checklist:
- ✅ Generate secure 32-byte keys
- ✅ Implement proper token expiration
- ✅ Add Spring Security integration
- ✅ Create token refresh mechanism
- ✅ Implement key rotation strategy
- ✅ Add comprehensive error handling
- ✅ Write tests for token operations
- ✅ Monitor token usage and performance
Branca tokens are particularly well-suited for:
- Microservices architectures
- Mobile applications
- API gateway authentication
- Systems requiring encrypted tokens
- Environments where token size matters
By following this guide, you can implement a robust, secure authentication system using Branca tokens that protects your applications and provides excellent user experience.