Skip to content

Commit

Permalink
feat: add support for break-glass tokens in java (#100)
Browse files Browse the repository at this point in the history
* add support for break-glass tokens in java

* flip default
  • Loading branch information
raserva authored Jul 28, 2022
1 parent 065e7ee commit a7ed86e
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ public JVSClientBuilder withCacheTimeout(Duration cacheTimeout) {
return this;
}

public JVSClientBuilder withAllowBreakglass(boolean allowBreakglass) {
configuration.setBreakglassAllowed(allowBreakglass);
return this;
}

public JvsClient build() {
// Load env vars and validate config
updateConfigFromEnvironmentVars();
Expand All @@ -122,6 +127,6 @@ public JvsClient build() {
.rateLimited(true)
.build();

return new JvsClient(provider);
return new JvsClient(provider, configuration.isBreakglassAllowed());
}
}
42 changes: 38 additions & 4 deletions client-lib/java/src/main/java/com/abcxyz/jvs/JvsClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.security.interfaces.ECPublicKey;
import java.util.List;
import java.util.Map;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -32,20 +34,52 @@
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@Slf4j
public class JvsClient {
// This postfix is added by the cli tool when creating breakglass tokens.
private static final String UNSIGNED_POSTFIX = ".NOT_SIGNED";

// this category is added by the cli tool when creating breakglass tokens.
private static final String BREAKGLASS_CATEGORY = "breakglass";

private final JwkProvider provider;
private final boolean allowBreakglass;

public DecodedJWT validateJWT(String jwtString) throws JwkException {
DecodedJWT jwt = JWT.decode(jwtString);
Jwk jwk;

// Handle Break-glass tokens.
if (jwtString.endsWith(UNSIGNED_POSTFIX)) {
if (unsignedTokenValidAndAllowed(jwt)) {
return jwt;
} else {
throw new JwkException("Token unsigned and could not be validated.");
}
}

try {
jwk = provider.get(jwt.getKeyId());
Jwk jwk = provider.get(jwt.getKeyId());
Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) jwk.getPublicKey(), null);
algorithm.verify(jwt);
} catch (SigningKeyNotFoundException e) {
log.info("No public key found with id: {}", jwt.getKeyId());
throw new JwkException("Public key not found", e);
}
Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) jwk.getPublicKey(), null);
algorithm.verify(jwt);

return jwt;
}

// Check that the break-glass token is valid and that we allow break-glass tokens.
boolean unsignedTokenValidAndAllowed(DecodedJWT jwt) {
if (!allowBreakglass) {
log.info("break glass tokens not allowed, denying.");
return false;
}
List<Map> justifications = jwt.getClaim("justs").asList(Map.class);
for (Map<String, String> justification : justifications) {
if (justification.getOrDefault("category", "").equals(BREAKGLASS_CATEGORY)) {
return true;
}
}
log.info("unable to find correct break-glass category, denying.");
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public class JvsConfiguration {
@JsonProperty("cache_timeout")
private Duration cacheTimeout = Duration.ofMinutes(5);

@JsonProperty("allow_breakglass")
private boolean breakglassAllowed = true;

public void validate() throws IllegalArgumentException {
if (!version.equals(EXPECTED_VERSION)) {
throw new IllegalArgumentException(
Expand Down
81 changes: 79 additions & 2 deletions client-lib/java/src/test/java/com/abcxyz/jvs/JvsClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.security.SecureRandom;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
Expand Down Expand Up @@ -75,7 +76,7 @@ public void testValidateJWT() throws Exception {
Jwk jwk = mock(Jwk.class);
when(jwk.getPublicKey()).thenReturn(key1.getPublic());
when(provider.get(keyId)).thenReturn(jwk);
JvsClient client = new JvsClient(provider);
JvsClient client = new JvsClient(provider, false);
DecodedJWT returnVal = client.validateJWT(token);
Assertions.assertEquals(claims.get("id"), returnVal.getClaims().get("id").asString());
Assertions.assertEquals(claims.get("role"), returnVal.getClaims().get("role").asString());
Expand All @@ -101,9 +102,85 @@ public void testValidateJWT_WrongKey() throws Exception {

when(provider.get(keyId))
.thenThrow(new SigningKeyNotFoundException("", new RuntimeException()));
JvsClient client = new JvsClient(provider);
JvsClient client = new JvsClient(provider, false);
JwkException thrown =
Assertions.assertThrows(JwkException.class, () -> client.validateJWT(token));
Assertions.assertTrue(thrown.getMessage().contains("Public key not found"));
}

@Test
public void testValidateJWT_Unsigned() throws Exception {
String keyId = "key2";

Map<String, Object> claims = new HashMap<>();
claims.put("id", "jwt-id");
claims.put("role", "user");
claims.put("created", new Date());

Map<String, String> justification = new HashMap<>();
justification.put("category", "breakglass");
justification.put("value", "issues/12345");
claims.put("justs", List.of(justification));

String token =
Jwts.builder().setClaims(claims).setHeaderParam("kid", keyId).compact()
+ "NOT_SIGNED"; // dot is already added by the builder

JvsClient client = new JvsClient(provider, false);
JwkException thrown =
Assertions.assertThrows(JwkException.class, () -> client.validateJWT(token));
Assertions.assertTrue(
thrown.getMessage().contains("Token unsigned and could not be validated"));
}

@Test
public void testValidateJWT_UnsignedAllowed() throws Exception {
String keyId = "key2";

Map<String, Object> claims = new HashMap<>();
claims.put("id", "jwt-id");
claims.put("role", "user");
claims.put("created", new Date());

Map<String, String> justification = new HashMap<>();
justification.put("category", "breakglass");
justification.put("value", "issues/12345");
claims.put("justs", List.of(justification));

String token =
Jwts.builder().setClaims(claims).setHeaderParam("kid", keyId).compact()
+ "NOT_SIGNED"; // dot is already added by the builder

JvsClient client = new JvsClient(provider, true);
DecodedJWT returnVal = client.validateJWT(token);
Assertions.assertEquals(claims.get("id"), returnVal.getClaims().get("id").asString());
Assertions.assertEquals(claims.get("role"), returnVal.getClaims().get("role").asString());
Assertions.assertEquals(
claims.get("created"), new Date(returnVal.getClaims().get("created").asLong()));
Assertions.assertEquals(claims.get("justs"), List.of(justification));
}

@Test
public void testValidateJWT_UnsignedAllowed_Invalid() throws Exception {
String keyId = "key2";

Map<String, Object> claims = new HashMap<>();
claims.put("id", "jwt-id");
claims.put("role", "user");
claims.put("created", new Date());

Map<String, String> justification = new HashMap<>();
justification.put("category", "something_else");
claims.put("justs", List.of(justification));

String token =
Jwts.builder().setClaims(claims).setHeaderParam("kid", keyId).compact()
+ "NOT_SIGNED"; // dot is already added by the builder

JvsClient client = new JvsClient(provider, true);
JwkException thrown =
Assertions.assertThrows(JwkException.class, () -> client.validateJWT(token));
Assertions.assertTrue(
thrown.getMessage().contains("Token unsigned and could not be validated"));
}
}

0 comments on commit a7ed86e

Please sign in to comment.