Macaroons for Caveats in Java

Overview

Macaroons are flexible authorization credentials that support decentralized delegation and attenuation through caveats. They are like cookies but with embedded restrictions that can be verified by the service.

Core Concepts

Macaroon Structure

  • Location: Service identifier
  • Identifier: Unique ID
  • Signature: HMAC-based
  • Caveats: List of restrictions

Dependencies

<dependencies>
<dependency>
<groupId>org.macaroon</groupId>
<artifactId>java-macaroon</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Alternative: Custom implementation -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20231013</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
</dependencies>

Basic Macaroon Implementation

1. Macaroon Class

public class Macaroon {
private final String location;
private final String identifier;
private final byte[] signature;
private final List<Caveat> caveats;
public Macaroon(String location, String identifier, byte[] key) {
this.location = location;
this.identifier = identifier;
this.caveats = new ArrayList<>();
this.signature = generateSignature(key, identifier.getBytes());
}
private byte[] generateSignature(byte[] key, byte[] data) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
mac.init(keySpec);
return mac.doFinal(data);
} catch (Exception e) {
throw new RuntimeException("Failed to generate signature", e);
}
}
}

2. Caveat Types

public interface Caveat {
String getCondition();
byte[] getVerificationId();
}
// First-party caveat (verified by service)
public class FirstPartyCaveat implements Caveat {
private final String condition;
private final byte[] verificationId;
public FirstPartyCaveat(String condition, byte[] verificationId) {
this.condition = condition;
this.verificationId = verificationId;
}
@Override
public String getCondition() { return condition; }
@Override
public byte[] getVerificationId() { return verificationId; }
}
// Third-party caveat (requires external verification)
public class ThirdPartyCaveat implements Caveat {
private final String condition;
private final byte[] verificationId;
private final String location;
private final byte[] caveatKey;
public ThirdPartyCaveat(String condition, byte[] verificationId, 
String location, byte[] caveatKey) {
this.condition = condition;
this.verificationId = verificationId;
this.location = location;
this.caveatKey = caveatKey;
}
}

Macaroon Builder with Caveats

public class MacaroonBuilder {
private Macaroon macaroon;
public MacaroonBuilder(String location, String identifier, byte[] key) {
this.macaroon = new Macaroon(location, identifier, key);
}
// Add first-party caveat
public MacaroonBuilder addFirstPartyCaveat(String condition, byte[] key) {
byte[] verificationId = generateVerificationId(condition, key);
FirstPartyCaveat caveat = new FirstPartyCaveat(condition, verificationId);
macaroon.addCaveat(caveat);
return this;
}
// Add third-party caveat
public MacaroonBuilder addThirdPartyCaveat(String condition, String location, 
byte[] caveatKey, byte[] key) {
byte[] verificationId = generateVerificationId(condition, caveatKey);
ThirdPartyCaveat caveat = new ThirdPartyCaveat(condition, verificationId, 
location, caveatKey);
macaroon.addCaveat(caveat);
return this;
}
private byte[] generateVerificationId(String condition, byte[] key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256");
mac.init(keySpec);
return mac.doFinal(condition.getBytes());
} catch (Exception e) {
throw new RuntimeException("Failed to generate verification ID", e);
}
}
public Macaroon build() {
return macaroon;
}
}

Caveat Verification System

1. Caveat Verifier

public interface CaveatVerifier {
boolean verify(String condition, Map<String, Object> context);
}
public class FirstPartyCaveatVerifier implements CaveatVerifier {
private final Map<String, Function<Map<String, Object>, Boolean>> verifiers;
public FirstPartyCaveatVerifier() {
this.verifiers = new HashMap<>();
registerDefaultVerifiers();
}
private void registerDefaultVerifiers() {
// Time-based caveat
verifiers.put("time", context -> {
String before = (String) context.get("before");
if (before != null) {
return Instant.now().isBefore(Instant.parse(before));
}
String after = (String) context.get("after");
if (after != null) {
return Instant.now().isAfter(Instant.parse(after));
}
return false;
});
// Resource access caveat
verifiers.put("resource", context -> {
String allowedResource = (String) context.get("allowed");
String requestedResource = (String) context.get("requested");
return allowedResource != null && allowedResource.equals(requestedResource);
});
// IP restriction caveat
verifiers.put("ip", context -> {
String allowedIP = (String) context.get("allowed_ip");
String clientIP = (String) context.get("client_ip");
return allowedIP != null && allowedIP.equals(clientIP);
});
}
@Override
public boolean verify(String condition, Map<String, Object> context) {
String[] parts = condition.split(":", 2);
if (parts.length < 2) return false;
String type = parts[0];
String value = parts[1];
Function<Map<String, Object>, Boolean> verifier = verifiers.get(type);
if (verifier != null) {
context.put("value", value);
return verifier.apply(context);
}
return false;
}
}

2. Macaroon Verifier

public class MacaroonVerifier {
private final CaveatVerifier caveatVerifier;
private final Map<String, byte[]> thirdPartyKeys;
public MacaroonVerifier(CaveatVerifier caveatVerifier) {
this.caveatVerifier = caveatVerifier;
this.thirdPartyKeys = new HashMap<>();
}
public void registerThirdPartyKey(String location, byte[] key) {
thirdPartyKeys.put(location, key);
}
public boolean verify(Macaroon macaroon, byte[] rootKey, 
Map<String, Object> context) {
// Verify signature
if (!verifySignature(macaroon, rootKey)) {
return false;
}
// Verify all caveats
for (Caveat caveat : macaroon.getCaveats()) {
if (!verifyCaveat(caveat, context)) {
return false;
}
}
return true;
}
private boolean verifySignature(Macaroon macaroon, byte[] rootKey) {
// Recalculate signature and compare
byte[] calculatedSig = recalculateSignature(macaroon, rootKey);
return Arrays.equals(calculatedSig, macaroon.getSignature());
}
private boolean verifyCaveat(Caveat caveat, Map<String, Object> context) {
if (caveat instanceof FirstPartyCaveat) {
return caveatVerifier.verify(caveat.getCondition(), context);
} else if (caveat instanceof ThirdPartyCaveat) {
return verifyThirdPartyCaveat((ThirdPartyCaveat) caveat, context);
}
return false;
}
private boolean verifyThirdPartyCaveat(ThirdPartyCaveat caveat, 
Map<String, Object> context) {
byte[] key = thirdPartyKeys.get(caveat.getLocation());
if (key == null) {
// Need to fetch key from third party
key = fetchThirdPartyKey(caveat.getLocation(), caveat.getVerificationId());
}
if (key != null) {
// Verify with third party
return verifyWithThirdParty(caveat, key, context);
}
return false;
}
}

Practical Usage Examples

1. API Authorization with Caveats

public class SecureAPI {
private final MacaroonVerifier verifier;
private final byte[] rootKey = "secret-root-key".getBytes();
public SecureAPI() {
FirstPartyCaveatVerifier caveatVerifier = new FirstPartyCaveatVerifier();
this.verifier = new MacaroonVerifier(caveatVerifier);
}
public String createAccessToken(String userId, List<String> caveatConditions) {
MacaroonBuilder builder = new MacaroonBuilder(
"https://api.example.com", 
userId, 
rootKey
);
// Add caveats
for (String condition : caveatConditions) {
builder.addFirstPartyCaveat(condition, rootKey);
}
// Add time limit
Instant expiry = Instant.now().plus(1, ChronoUnit.HOURS);
builder.addFirstPartyCaveat("time:before:" + expiry.toString(), rootKey);
return Base64.getEncoder().encodeToString(builder.build().serialize());
}
public boolean authorizeRequest(String macaroonBase64, String resource, 
String clientIP) {
try {
Macaroon macaroon = Macaroon.deserialize(
Base64.getDecoder().decode(macaroonBase64)
);
Map<String, Object> context = new HashMap<>();
context.put("requested_resource", resource);
context.put("client_ip", clientIP);
return verifier.verify(macaroon, rootKey, context);
} catch (Exception e) {
return false;
}
}
}

2. Complex Caveat Scenarios

public class AdvancedCaveatExamples {
public void demonstrateComplexCaveats() {
byte[] rootKey = "super-secret-key".getBytes();
// Multi-tenant access control
Macaroon multiTenantMacaroon = new MacaroonBuilder(
"saas-platform", "user123", rootKey
)
.addFirstPartyCaveat("tenant:acme-corp", rootKey)
.addFirstPartyCaveat("role:developer", rootKey)
.addFirstPartyCaveat("project:project-alpha", rootKey)
.addFirstPartyCaveat("environment:production", rootKey)
.build();
// Time and location based access
Macaroon timeLocationMacaroon = new MacaroonBuilder(
"mobile-app", "device456", rootKey
)
.addFirstPartyCaveat("time:before:2024-12-31T23:59:59Z", rootKey)
.addFirstPartyCaveat("location:country:US", rootKey)
.addFirstPartyCaveat("ip:192.168.1.0/24", rootKey)
.build();
// Delegated access with third-party caveats
Macaroon delegatedMacaroon = new MacaroonBuilder(
"storage-service", "delegated-access", rootKey
)
.addFirstPartyCaveat("operation:read", rootKey)
.addFirstPartyCaveat("path:/documents/", rootKey)
.addThirdPartyCaveat("audit:required", "https://audit.example.com", 
"audit-key".getBytes(), rootKey)
.build();
}
}

3. Custom Caveat Verifiers

public class CustomCaveatVerifiers {
public static class RateLimitVerifier implements CaveatVerifier {
private final RateLimiter rateLimiter;
public RateLimitVerifier(RateLimiter rateLimiter) {
this.rateLimiter = rateLimiter;
}
@Override
public boolean verify(String condition, Map<String, Object> context) {
if (condition.startsWith("rate_limit:")) {
String[] parts = condition.split(":");
if (parts.length >= 3) {
String userId = (String) context.get("user_id");
int requests = Integer.parseInt(parts[1]);
int period = Integer.parseInt(parts[2]);
return rateLimiter.isAllowed(userId, requests, period);
}
}
return false;
}
}
public static class JWTClaimVerifier implements CaveatVerifier {
private final JwtParser jwtParser;
public JWTClaimVerifier(JwtParser jwtParser) {
this.jwtParser = jwtParser;
}
@Override
public boolean verify(String condition, Map<String, Object> context) {
if (condition.startsWith("jwt_claim:")) {
String jwt = (String) context.get("jwt_token");
if (jwt != null) {
try {
Jws<Claims> claims = jwtParser.parseClaimsJws(jwt);
String[] claimParts = condition.split(":", 3);
String claimName = claimParts[1];
String expectedValue = claimParts[2];
Object claimValue = claims.getBody().get(claimName);
return expectedValue.equals(claimValue.toString());
} catch (Exception e) {
return false;
}
}
}
return false;
}
}
}

Serialization and Storage

public class MacaroonSerializer {
public byte[] serialize(Macaroon macaroon) {
JSONObject json = new JSONObject();
json.put("location", macaroon.getLocation());
json.put("identifier", macaroon.getIdentifier());
json.put("signature", Base64.getEncoder().encodeToString(macaroon.getSignature()));
JSONArray caveatsArray = new JSONArray();
for (Caveat caveat : macaroon.getCaveats()) {
JSONObject caveatJson = new JSONObject();
caveatJson.put("condition", caveat.getCondition());
caveatJson.put("verification_id", 
Base64.getEncoder().encodeToString(caveat.getVerificationId()));
if (caveat instanceof ThirdPartyCaveat) {
ThirdPartyCaveat tpCaveat = (ThirdPartyCaveat) caveat;
caveatJson.put("type", "third_party");
caveatJson.put("location", tpCaveat.getLocation());
} else {
caveatJson.put("type", "first_party");
}
caveatsArray.put(caveatJson);
}
json.put("caveats", caveatsArray);
return json.toString().getBytes(StandardCharsets.UTF_8);
}
public Macaroon deserialize(byte[] data) {
JSONObject json = new JSONObject(new String(data, StandardCharsets.UTF_8));
// Implementation for deserialization
return null; // Simplified for example
}
}

Testing Macaroons with Caveats

class MacaroonTest {
@Test
void testFirstPartyCaveatVerification() {
byte[] rootKey = "test-key".getBytes();
Macaroon macaroon = new MacaroonBuilder("test", "user1", rootKey)
.addFirstPartyCaveat("time:before:2024-12-31T23:59:59Z", rootKey)
.addFirstPartyCaveat("resource:/api/data", rootKey)
.build();
FirstPartyCaveatVerifier verifier = new FirstPartyCaveatVerifier();
MacaroonVerifier macVerifier = new MacaroonVerifier(verifier);
Map<String, Object> context = new HashMap<>();
context.put("requested_resource", "/api/data");
assertTrue(macVerifier.verify(macaroon, rootKey, context));
}
@Test
void testCaveatFailure() {
byte[] rootKey = "test-key".getBytes();
Macaroon macaroon = new MacaroonBuilder("test", "user1", rootKey)
.addFirstPartyCaveat("resource:/api/admin", rootKey)
.build();
FirstPartyCaveatVerifier verifier = new FirstPartyCaveatVerifier();
MacaroonVerifier macVerifier = new MacaroonVerifier(verifier);
Map<String, Object> context = new HashMap<>();
context.put("requested_resource", "/api/user"); // Different resource
assertFalse(macVerifier.verify(macaroon, rootKey, context));
}
}

Best Practices

  1. Key Management: Store root keys securely (HSM, KMS)
  2. Caveat Design: Make caveats specific and unambiguous
  3. Verification Context: Provide complete context for caveat verification
  4. Error Handling: Don't reveal why verification failed
  5. Performance: Cache verification results when appropriate
  6. Audit Logging: Log all macaroon verification attempts

This implementation provides a robust foundation for using macaroons with caveats in Java applications, enabling fine-grained, decentralized authorization with flexible attenuation capabilities.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper