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.