Overview
Biscuit is a decentralized authorization token inspired by Macaroons. It supports offline delegation, attenuation, and cryptographic verification. This guide covers implementing Biscuit tokens in Java.
1. Setup and Dependencies
Maven Dependencies
<dependencies> <!-- Biscuit Java implementation --> <dependency> <groupId>com.clever-cloud</groupId> <artifactId>biscuit-java</artifactId> <version>3.0.0</version> </dependency> <!-- For cryptographic operations --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <!-- For JSON processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- For Base64 encoding --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version> </dependency> </dependencies>
2. Basic Biscuit Token Implementation
import com.clevercloud.biscuit.token.Biscuit;
import com.clevercloud.biscuit.token.Verifier;
import com.clevercloud.biscuit.datalog.SymbolTable;
import com.clevercloud.biscuit.datalog.Check;
import com.clevercloud.biscuit.datalog.Rule;
import com.clevercloud.biscuit.datalog.Fact;
import com.clevercloud.biscuit.crypto.KeyPair;
import java.util.Base64;
import java.util.List;
import java.util.Arrays;
public class BasicBiscuitExample {
public static void main(String[] args) throws Exception {
// Generate root key pair
KeyPair rootKey = KeyPair.generate();
// Create a basic biscuit token
Biscuit token = createBasicToken(rootKey);
// Serialize token to string
String serializedToken = serializeToken(token);
System.out.println("Serialized Token: " + serializedToken);
// Deserialize and verify token
Biscuit verifiedToken = verifyToken(serializedToken, rootKey.public_key());
// Check authorization
boolean authorized = checkAuthorization(verifiedToken, "read", "file1");
System.out.println("Authorization result: " + authorized);
}
public static Biscuit createBasicToken(KeyPair rootKey) throws Exception {
Biscuit.Builder builder = Biscuit.builder(rootKey.private_key());
// Add basic facts
builder.add_fact("user(\"alice\")");
builder.add_fact("resource(\"file1\")");
builder.add_fact("operation(\"read\")");
// Add checks
builder.add_check("check if user($user), resource($resource), operation($operation)");
// Add authority rules
builder.add_rule("right($user, $resource, $operation) <- user($user), resource($resource), operation($operation)");
return builder.build();
}
public static String serializeToken(Biscuit token) {
byte[] serialized = token.serialize();
return Base64.getUrlEncoder().withoutPadding().encodeToString(serialized);
}
public static Biscuit verifyToken(String serializedToken, PublicKey publicKey) throws Exception {
byte[] tokenData = Base64.getUrlDecoder().decode(serializedToken);
return Biscuit.from_bytes(tokenData, publicKey);
}
public static boolean checkAuthorization(Biscuit token, String operation, String resource) throws Exception {
Verifier verifier = token.verify();
// Add facts from the request context
verifier.add_fact("operation(\"" + operation + "\")");
verifier.add_fact("resource(\"" + resource + "\")");
// Add authorization check
verifier.add_check("check if right($user, $resource, $operation)");
try {
verifier.verify();
return true;
} catch (Exception e) {
System.err.println("Authorization failed: " + e.getMessage());
return false;
}
}
}
3. Advanced Biscuit Token with Attenuation
import com.clevercloud.biscuit.token.Biscuit;
import com.clevercloud.biscuit.token.Verifier;
import com.clevercloud.biscuit.token.Block;
import com.clevercloud.biscuit.crypto.KeyPair;
import com.clevercloud.biscuit.datalog.*;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
public class AdvancedBiscuitExample {
public static class BiscuitManager {
private final KeyPair rootKey;
private final SymbolTable symbols;
public BiscuitManager() {
this.rootKey = KeyPair.generate();
this.symbols = new SymbolTable();
initializeSymbols();
}
private void initializeSymbols() {
// Predefine common symbols for better performance
symbols.add("user");
symbols.add("resource");
symbols.add("operation");
symbols.add("role");
symbols.add("right");
symbols.add("time");
symbols.add("expiration");
symbols.add("tenant");
}
public Biscuit createUserToken(String userId, String tenantId,
List<String> roles, Duration validity) throws Exception {
Biscuit.Builder builder = Biscuit.builder(rootKey.private_key());
// Add user identity facts
builder.add_fact("user(\"" + userId + "\")");
builder.add_fact("tenant(\"" + tenantId + "\")");
// Add role facts
for (String role : roles) {
builder.add_fact("role(\"" + role + "\")");
}
// Add expiration
Instant expiration = Instant.now().plus(validity);
builder.add_fact("expiration(" + expiration.getEpochSecond() + ")");
// Add authority rules
builder.add_rule("right($user, $resource, $operation) <- " +
"user($user), role($role), resource_right($role, $resource, $operation)");
builder.add_rule("valid($user) <- user($user), expiration($exp), time($now), $now < $exp");
// Add checks
builder.add_check("check if valid($user)");
return builder.build();
}
public Biscuit attenuateToken(Biscuit token, List<String> restrictions) throws Exception {
Biscuit.Attenuated attenuated = token.attenuate();
for (String restriction : restrictions) {
attenuated.add_check(restriction);
}
return attenuated.build();
}
public boolean authorizeResourceAccess(Biscuit token, String userId,
String resource, String operation) throws Exception {
Verifier verifier = token.verify();
// Add request context
verifier.add_fact("user(\"" + userId + "\")");
verifier.add_fact("resource(\"" + resource + "\")");
verifier.add_fact("operation(\"" + operation + "\")");
verifier.add_fact("time(" + Instant.now().getEpochSecond() + ")");
// Add resource-specific rules (from application)
addResourceRules(verifier);
try {
verifier.verify();
return true;
} catch (Exception e) {
System.err.println("Authorization failed: " + e.getMessage());
return false;
}
}
private void addResourceRules(Verifier verifier) {
// Define resource rights based on roles
verifier.add_rule("resource_right(\"admin\", $resource, $operation) <- " +
"resource($resource), operation($operation)");
verifier.add_rule("resource_right(\"reader\", $resource, \"read\") <- " +
"resource($resource)");
verifier.add_rule("resource_right(\"writer\", $resource, \"write\") <- " +
"resource($resource)");
verifier.add_rule("resource_right(\"writer\", $resource, \"read\") <- " +
"resource($resource)");
}
public PublicKey getPublicKey() {
return rootKey.public_key();
}
}
public static void main(String[] args) throws Exception {
BiscuitManager manager = new BiscuitManager();
// Create a user token
List<String> roles = Arrays.asList("admin", "reader");
Biscuit userToken = manager.createUserToken(
"user123", "tenant1", roles, Duration.ofHours(24)
);
System.out.println("Original token created");
// Attenuate token with restrictions
List<String> restrictions = Arrays.asList(
"check if resource($resource), $resource == \"file1\"",
"check if operation($operation), $operation == \"read\""
);
Biscuit attenuatedToken = manager.attenuateToken(userToken, restrictions);
System.out.println("Token attenuated with restrictions");
// Test authorization
boolean canReadFile1 = manager.authorizeResourceAccess(
attenuatedToken, "user123", "file1", "read"
);
System.out.println("Can read file1: " + canReadFile1);
boolean canWriteFile1 = manager.authorizeResourceAccess(
attenuatedToken, "user123", "file1", "write"
);
System.out.println("Can write file1: " + canWriteFile1);
boolean canReadFile2 = manager.authorizeResourceAccess(
attenuatedToken, "user123", "file2", "read"
);
System.out.println("Can read file2: " + canReadFile2);
}
}
4. Spring Boot Integration
Configuration Class
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.clevercloud.biscuit.crypto.KeyPair;
import com.clevercloud.biscuit.token.Biscuit;
@Configuration
@ConfigurationProperties(prefix = "biscuit")
public class BiscuitConfig {
private String privateKeyBase64;
private String publicKeyBase64;
private long defaultTokenValidityHours = 24;
@Bean
public KeyPair biscuitKeyPair() {
if (privateKeyBase64 != null && publicKeyBase64 != null) {
// Load from configuration
byte[] privateKey = Base64.getDecoder().decode(privateKeyBase64);
byte[] publicKey = Base64.getDecoder().decode(publicKeyBase64);
return new KeyPair(privateKey, publicKey);
} else {
// Generate new key pair
return KeyPair.generate();
}
}
@Bean
public BiscuitService biscuitService(KeyPair keyPair) {
return new BiscuitService(keyPair, defaultTokenValidityHours);
}
// Getters and setters
public String getPrivateKeyBase64() { return privateKeyBase64; }
public void setPrivateKeyBase64(String privateKeyBase64) { this.privateKeyBase64 = privateKeyBase64; }
public String getPublicKeyBase64() { return publicKeyBase64; }
public void setPublicKeyBase64(String publicKeyBase64) { this.publicKeyBase64 = publicKeyBase64; }
public long getDefaultTokenValidityHours() { return defaultTokenValidityHours; }
public void setDefaultTokenValidityHours(long defaultTokenValidityHours) {
this.defaultTokenValidityHours = defaultTokenValidityHours;
}
}
Biscuit Service
import org.springframework.stereotype.Service;
import com.clevercloud.biscuit.token.Biscuit;
import com.clevercloud.biscuit.token.Verifier;
import com.clevercloud.biscuit.crypto.KeyPair;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
@Service
public class BiscuitService {
private final KeyPair keyPair;
private final long defaultTokenValidityHours;
public BiscuitService(KeyPair keyPair, long defaultTokenValidityHours) {
this.keyPair = keyPair;
this.defaultTokenValidityHours = defaultTokenValidityHours;
}
public String createAuthenticationToken(String userId, List<String> roles,
Map<String, String> attributes) throws Exception {
Biscuit.Builder builder = Biscuit.builder(keyPair.private_key());
// Add user identity
builder.add_fact("user(\"" + userId + "\")");
// Add roles
for (String role : roles) {
builder.add_fact("role(\"" + role + "\")");
}
// Add custom attributes
for (Map.Entry<String, String> attr : attributes.entrySet()) {
builder.add_fact("attribute(\"" + attr.getKey() + "\", \"" + attr.getValue() + "\")");
}
// Add expiration
Instant expiration = Instant.now().plus(Duration.ofHours(defaultTokenValidityHours));
builder.add_fact("expiration(" + expiration.getEpochSecond() + ")");
// Add authority rules
builder.add_rule("right($user, $resource, $operation) <- " +
"user($user), role($role), resource_right($role, $resource, $operation)");
builder.add_rule("valid($user) <- user($user), expiration($exp), time($now), $now < $exp");
// Add checks
builder.add_check("check if valid($user)");
Biscuit token = builder.build();
return Base64.getUrlEncoder().withoutPadding().encodeToString(token.serialize());
}
public BiscuitAuthorizationResult verifyToken(String tokenString,
String resource,
String operation) throws Exception {
try {
byte[] tokenData = Base64.getUrlDecoder().decode(tokenString);
Biscuit token = Biscuit.from_bytes(tokenData, keyPair.public_key());
Verifier verifier = token.verify();
verifier.add_fact("time(" + Instant.now().getEpochSecond() + ")");
if (resource != null) {
verifier.add_fact("resource(\"" + resource + "\")");
}
if (operation != null) {
verifier.add_fact("operation(\"" + operation + "\")");
}
// Add application-specific rules
addApplicationRules(verifier);
verifier.verify();
return new BiscuitAuthorizationResult(true, "Authorization successful", extractUserContext(token));
} catch (Exception e) {
return new BiscuitAuthorizationResult(false, "Authorization failed: " + e.getMessage(), null);
}
}
public String attenuateToken(String tokenString, List<String> additionalChecks) throws Exception {
byte[] tokenData = Base64.getUrlDecoder().decode(tokenString);
Biscuit token = Biscuit.from_bytes(tokenData, keyPair.public_key());
Biscuit.Attenuated attenuated = token.attenuate();
for (String check : additionalChecks) {
attenuated.add_check(check);
}
Biscuit newToken = attenuated.build();
return Base64.getUrlEncoder().withoutPadding().encodeToString(newToken.serialize());
}
private void addApplicationRules(Verifier verifier) {
// Define role-based access control rules
verifier.add_rule("resource_right(\"admin\", $resource, $operation) <- " +
"resource($resource), operation($operation)");
verifier.add_rule("resource_right(\"user\", $resource, \"read\") <- " +
"resource($resource), resource_public($resource)");
verifier.add_rule("resource_right(\"user\", $resource, \"write\") <- " +
"user($user), resource($resource), user_resource($user, $resource)");
// Add public resources
verifier.add_fact("resource_public(\"public-data\")");
verifier.add_fact("resource_public(\"catalog\")");
}
private Map<String, Object> extractUserContext(Biscuit token) {
// Extract user context from token for use in application
Map<String, Object> context = new HashMap<>();
// Implementation would parse facts from the token
return context;
}
public static class BiscuitAuthorizationResult {
private final boolean authorized;
private final String message;
private final Map<String, Object> userContext;
public BiscuitAuthorizationResult(boolean authorized, String message,
Map<String, Object> userContext) {
this.authorized = authorized;
this.message = message;
this.userContext = userContext;
}
// Getters
public boolean isAuthorized() { return authorized; }
public String getMessage() { return message; }
public Map<String, Object> getUserContext() { return userContext; }
}
}
5. Spring Security Integration
Security Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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
public class SecurityConfig {
private final BiscuitAuthenticationFilter biscuitAuthenticationFilter;
public SecurityConfig(BiscuitAuthenticationFilter biscuitAuthenticationFilter) {
this.biscuitAuthenticationFilter = biscuitAuthenticationFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated()
)
.addFilterBefore(biscuitAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Authentication Filter
import org.springframework.security.core.context.SecurityContextHolder;
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;
public class BiscuitAuthenticationFilter extends OncePerRequestFilter {
private final BiscuitService biscuitService;
public BiscuitAuthenticationFilter(BiscuitService biscuitService) {
this.biscuitService = biscuitService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
// Extract resource and operation from request
String resource = extractResourceFromRequest(request);
String operation = extractOperationFromRequest(request);
// Verify token
BiscuitService.BiscuitAuthorizationResult result =
biscuitService.verifyToken(token, resource, operation);
if (result.isAuthorized()) {
// Create authentication object
BiscuitAuthentication authentication =
new BiscuitAuthentication(token, result.getUserContext());
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
sendError(response, HttpServletResponse.SC_UNAUTHORIZED,
"Invalid token: " + result.getMessage());
return;
}
} catch (Exception e) {
sendError(response, HttpServletResponse.SC_UNAUTHORIZED,
"Token verification failed: " + e.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
private String extractResourceFromRequest(HttpServletRequest request) {
String path = request.getRequestURI();
// Extract resource identifier from path
// Example: /api/users/123 -> "users/123"
return path.replace("/api/", "");
}
private String extractOperationFromRequest(HttpServletRequest request) {
String method = request.getMethod().toLowerCase();
// Map HTTP methods to operations
switch (method) {
case "get": return "read";
case "post": return "create";
case "put": case "patch": return "update";
case "delete": return "delete";
default: return method;
}
}
private void sendError(HttpServletResponse response, int status, String message)
throws IOException {
response.setStatus(status);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"" + message + "\"}");
}
}
Authentication Object
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
public class BiscuitAuthentication implements Authentication {
private final String token;
private final Map<String, Object> userContext;
private boolean authenticated = true;
public BiscuitAuthentication(String token, Map<String, Object> userContext) {
this.token = token;
this.userContext = userContext != null ? userContext : Collections.emptyMap();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// Extract roles from user context and convert to authorities
// This is a simplified example
return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public Object getCredentials() {
return token;
}
@Override
public Object getDetails() {
return userContext;
}
@Override
public Object getPrincipal() {
return userContext.get("user"); // Or extract username from context
}
@Override
public boolean isAuthenticated() {
return authenticated;
}
@Override
public void setAuthenticated(boolean authenticated) throws IllegalArgumentException {
this.authenticated = authenticated;
}
@Override
public String getName() {
Object principal = getPrincipal();
return principal != null ? principal.toString() : "anonymous";
}
}
6. REST Controller Examples
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import java.util.*;
@RestController
@RequestMapping("/api")
public class BiscuitController {
private final BiscuitService biscuitService;
public BiscuitController(BiscuitService biscuitService) {
this.biscuitService = biscuitService;
}
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest request) {
try {
List<String> roles = Arrays.asList("user"); // Get from user service
Map<String, String> attributes = new HashMap<>();
attributes.put("email", request.getEmail());
String token = biscuitService.createAuthenticationToken(
request.getEmail(), roles, attributes);
return ResponseEntity.ok(Collections.singletonMap("token", token));
} catch (Exception e) {
return ResponseEntity.status(401)
.body(Collections.singletonMap("error", "Login failed: " + e.getMessage()));
}
}
@PostMapping("/token/attenuate")
public ResponseEntity<Map<String, String>> attenuateToken(
@RequestHeader("Authorization") String authHeader,
@RequestBody AttenuationRequest request) {
try {
String token = authHeader.substring(7); // Remove "Bearer "
String newToken = biscuitService.attenuateToken(token, request.getChecks());
return ResponseEntity.ok(Collections.singletonMap("token", newToken));
} catch (Exception e) {
return ResponseEntity.status(400)
.body(Collections.singletonMap("error", "Attenuation failed: " + e.getMessage()));
}
}
@GetMapping("/protected-data")
public ResponseEntity<Map<String, String>> getProtectedData() {
// Authorization is handled by the filter
return ResponseEntity.ok(Collections.singletonMap("data", "This is protected data"));
}
@PostMapping("/admin/operation")
public ResponseEntity<Map<String, String>> adminOperation() {
// Requires ROLE_ADMIN authority
return ResponseEntity.ok(Collections.singletonMap("result", "Admin operation successful"));
}
// DTO classes
public static class LoginRequest {
private String email;
private String password;
// Getters and setters
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
public static class AttenuationRequest {
private List<String> checks;
// Getters and setters
public List<String> getChecks() { return checks; }
public void setChecks(List<String> checks) { this.checks = checks; }
}
}
7. Advanced Features - Third-Party Verification
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class ThirdPartyBiscuitService {
private final Map<String, PublicKey> trustedIssuers;
public ThirdPartyBiscuitService() {
this.trustedIssuers = new HashMap<>();
// Load trusted issuer public keys
// In production, this would come from configuration or discovery service
}
public void addTrustedIssuer(String issuerId, PublicKey publicKey) {
trustedIssuers.put(issuerId, publicKey);
}
public VerificationResult verifyThirdPartyToken(String tokenString,
String expectedIssuer,
List<String> requiredChecks) throws Exception {
try {
byte[] tokenData = Base64.getUrlDecoder().decode(tokenString);
// Get the appropriate public key for verification
PublicKey publicKey = trustedIssuers.get(expectedIssuer);
if (publicKey == null) {
return new VerificationResult(false, "Unknown issuer: " + expectedIssuer);
}
Biscuit token = Biscuit.from_bytes(tokenData, publicKey);
Verifier verifier = token.verify();
// Add time fact
verifier.add_fact("time(" + Instant.now().getEpochSecond() + ")");
// Add required checks
for (String check : requiredChecks) {
verifier.add_check(check);
}
verifier.verify();
return new VerificationResult(true, "Token verified successfully",
extractTokenInfo(token));
} catch (Exception e) {
return new VerificationResult(false, "Verification failed: " + e.getMessage());
}
}
private Map<String, Object> extractTokenInfo(Biscuit token) {
Map<String, Object> info = new HashMap<>();
// Extract information from token blocks
// This would require parsing the token's Datalog facts
return info;
}
public static class VerificationResult {
private final boolean verified;
private final String message;
private final Map<String, Object> tokenInfo;
public VerificationResult(boolean verified, String message) {
this(verified, message, null);
}
public VerificationResult(boolean verified, String message,
Map<String, Object> tokenInfo) {
this.verified = verified;
this.message = message;
this.tokenInfo = tokenInfo;
}
// Getters
public boolean isVerified() { return verified; }
public String getMessage() { return message; }
public Map<String, Object> getTokenInfo() { return tokenInfo; }
}
}
Key Features Covered
- Token Creation: Generate Biscuit tokens with facts, rules, and checks
- Token Verification: Cryptographically verify tokens and evaluate authorization logic
- Attenuation: Create restricted tokens from existing ones
- Spring Integration: Seamless integration with Spring Boot and Spring Security
- REST API: Complete authentication and authorization flow
- Third-Party Tokens: Verify tokens from external issuers
- Fine-grained Authorization: Complex authorization logic using Datalog
This implementation provides a robust foundation for using Biscuit tokens in Java applications, offering decentralized authorization with offline delegation capabilities.