Skip to content

Commit

Permalink
Add Test and Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsbox committed Jun 15, 2022
1 parent 60660cd commit 2364386
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ record AlgJwkPair(String alg, JWK jwk) {}
final List<String> audiencesClaimValue; // claim name is hard-coded to `aud` for OIDC ID Token compatibility
final String principalClaimName; // claim name is configurable, EX: Users (sub, oid, email, dn, uid), Clients (azp, appid, client_id)
final Map<String, User> principals; // principals with roles, for sending encoded JWTs into JWT realms for authc/authz verification
final List<AlgJwkPair> algAndJwksPkc;
final List<AlgJwkPair> algAndJwksHmac;
final AlgJwkPair algAndJwkHmacOidc;
List<AlgJwkPair> algAndJwksPkc;
List<AlgJwkPair> algAndJwksHmac;
AlgJwkPair algAndJwkHmacOidc;

// Computed values
final List<AlgJwkPair> algAndJwksAll;
final Set<String> algorithmsAll;
final String encodedJwkSetPkcPrivate;
final String encodedJwkSetPkcPublic;
final String encodedJwkSetHmac;
final String encodedKeyHmacOidc;
List<AlgJwkPair> algAndJwksAll;
Set<String> algorithmsAll;
String encodedJwkSetPkcPrivate;
String encodedJwkSetPkcPublic;
String encodedJwkSetHmac;
String encodedKeyHmacOidc;
final JwtIssuerHttpsServer httpsServer;

JwtIssuer(
Expand Down Expand Up @@ -94,6 +94,37 @@ record AlgJwkPair(String alg, JWK jwk) {}
}
}

public void rotate(
final List<AlgJwkPair> algAndJwksPkc,
final List<AlgJwkPair> algAndJwksHmac,
final AlgJwkPair algAndJwkHmacOidc
) {
this.algAndJwksPkc = algAndJwksPkc;
this.algAndJwksHmac = algAndJwksHmac;
this.algAndJwkHmacOidc = algAndJwkHmacOidc;

this.algAndJwksAll = new ArrayList<>(this.algAndJwksPkc.size() + this.algAndJwksHmac.size() + 1);
this.algAndJwksAll.addAll(this.algAndJwksPkc);
this.algAndJwksAll.addAll(this.algAndJwksHmac);
if (this.algAndJwkHmacOidc != null) {
this.algAndJwksAll.add(this.algAndJwkHmacOidc);
}

final JWKSet jwkSetPkc = new JWKSet(this.algAndJwksPkc.stream().map(p -> p.jwk).toList());
final JWKSet jwkSetHmac = new JWKSet(this.algAndJwksHmac.stream().map(p -> p.jwk).toList());

this.encodedJwkSetPkcPrivate = jwkSetPkc.getKeys().isEmpty() ? null : JwtUtil.serializeJwkSet(jwkSetPkc, false);
this.encodedJwkSetPkcPublic = jwkSetPkc.getKeys().isEmpty() ? null : JwtUtil.serializeJwkSet(jwkSetPkc, true);
this.encodedJwkSetHmac = jwkSetHmac.getKeys().isEmpty() ? null : JwtUtil.serializeJwkSet(jwkSetHmac, false);
this.encodedKeyHmacOidc = (algAndJwkHmacOidc == null) ? null : JwtUtil.serializeJwkHmacOidc(this.algAndJwkHmacOidc.jwk);

if (this.httpsServer != null) {
final byte[] encodedJwkSetPkcPublicBytes = this.encodedJwkSetPkcPublic.getBytes(StandardCharsets.UTF_8);
this.httpsServer.rotate(encodedJwkSetPkcPublicBytes);
}

}

@Override
public void close() {
if (this.httpsServer != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public JwtIssuerHttpsServer(final byte[] encodedJwkSetPkcPublicBytes) throws Exc
LOGGER.debug("Started [{}]", this.url);
}

public void rotate(final byte[] encodedJwkSetPkcPublicBytes) {
this.httpsServer.removeContext(PATH);
this.httpsServer.createContext(PATH, new JwtIssuerHttpHandler(encodedJwkSetPkcPublicBytes));
}

@Override
public void close() throws IOException {
if (this.httpsServer != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,53 @@ public void testPkcJwkSetUrlNotFound() throws Exception {
}
}

/**
* Verify that a JWT realm successfully connects to HTTPS server, and can handle an HTTP 404 Not Found response correctly.
* @throws Exception Unexpected test failure
*/
public void testPkcJwkSetRotation() throws Exception {
final JwtRealmsService jwtRealmsService = this.generateJwtRealmsService(this.createJwtRealmsSettingsBuilder());
final String principalClaimName = randomFrom(jwtRealmsService.getPrincipalClaimNames());

final List<Realm> allRealms = new ArrayList<>(); // authc and authz realms
final boolean createHttpsServer = true; // force issuer to create HTTPS server for its PKC JWKSet
final JwtIssuer jwtIssuer = this.createJwtIssuer(0, principalClaimName, 12, 1, 1, 1, createHttpsServer);
assertThat(jwtIssuer.httpsServer, is(notNullValue()));
try {
final JwtRealmSettingsBuilder jwtRealmSettingsBuilder = this.createJwtRealmSettingsBuilder(jwtIssuer, 0, 0);
this.jwtIssuerAndRealms = new ArrayList<>(1);
final JwtRealm jwtRealm = this.createJwtRealm(allRealms, jwtRealmsService, jwtIssuer, jwtRealmSettingsBuilder);
final JwtIssuerAndRealm jwtIssuerAndRealm = new JwtIssuerAndRealm(jwtIssuer, jwtRealm, jwtRealmSettingsBuilder);

jwtRealm.initialize(allRealms, super.licenseState);
this.jwtIssuerAndRealms.add(jwtIssuerAndRealm); // add them so the test will clean them up

final User user = this.randomUser(jwtIssuerAndRealm.issuer());
SecureString jwt = this.randomJwt(jwtIssuerAndRealm, user);
final SecureString clientSecret = jwtIssuerAndRealm.realm().clientAuthenticationSharedSecret;
final MinMax jwtAuthcRange = new MinMax(2, 3);

// Indirectly verify authentication works
this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcRange);

// Rotate
this.rotateJWKsJwtIssuer(jwtIssuer);

SecureString rotatedJwt = this.randomJwt(jwtIssuerAndRealm, user);
// Verify authentication works before performing any failure scenarios
this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, rotatedJwt, clientSecret, jwtAuthcRange);

// Should fail as we are using an old token and have rotated
expectThrows(
Exception.class,
() -> this.doMultipleAuthcAuthzAndVerifySuccess(jwtIssuerAndRealm.realm(), user, jwt, clientSecret, jwtAuthcRange)
);

} finally {
jwtIssuer.close();
}
}

/**
* Test token parse failures and authentication failures.
* @throws Exception Unexpected test failure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -189,6 +190,37 @@ protected List<JwtIssuerAndRealm> generateJwtIssuerRealmPairs(
return this.jwtIssuerAndRealms;
}

protected void rotateJWKsJwtIssuer(
final JwtIssuer jwtIssuer
) throws Exception {
final String issuer = jwtIssuer.issuerClaimValue;

// Allow algorithm repeats, to cover testing of multiple JWKs for same algorithm
final Set<String> algs = jwtIssuer.algorithmsAll;
final List<String> algsPkc = algs.stream().filter(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_PKC::contains).toList();
final List<String> algsHmac = algs.stream().filter(JwtRealmSettings.SUPPORTED_SIGNATURE_ALGORITHMS_HMAC::contains).toList();
final List<JwtIssuer.AlgJwkPair> algJwkPairsPkc = JwtTestCase.randomJwks(algsPkc);
// Key setting vs JWKSet setting are mutually exclusive, do not populate both
final List<JwtIssuer.AlgJwkPair> algJwkPairsHmac = new ArrayList<>(JwtTestCase.randomJwks(algsHmac)); // allow remove/add below
final JwtIssuer.AlgJwkPair algJwkPairHmacOidc;
if ((algJwkPairsHmac.size() == 0) || (randomBoolean())) {
algJwkPairHmacOidc = null; // list(0||1||N) => Key=null and JWKSet(N)
} else {
// Change one of the HMAC random bytes keys to an OIDC UTF8 key. Put it in either the Key setting or JWKSet setting.
final JwtIssuer.AlgJwkPair algJwkPairRandomBytes = algJwkPairsHmac.get(0);
final OctetSequenceKey jwkHmacRandomBytes = JwtTestCase.conditionJwkHmacForOidc((OctetSequenceKey) algJwkPairRandomBytes.jwk());
final JwtIssuer.AlgJwkPair algJwkPairUtf8Bytes = new JwtIssuer.AlgJwkPair(algJwkPairRandomBytes.alg(), jwkHmacRandomBytes);
if ((algJwkPairsHmac.size() == 1) && (randomBoolean())) {
algJwkPairHmacOidc = algJwkPairUtf8Bytes; // list(1) => Key=OIDC and JWKSet(0)
algJwkPairsHmac.remove(0);
} else {
algJwkPairHmacOidc = null; // list(N) => Key=null and JWKSet(OIDC+N-1)
algJwkPairsHmac.set(0, algJwkPairUtf8Bytes);
}
}
jwtIssuer.rotate(algJwkPairsPkc, algJwkPairsHmac, algJwkPairHmacOidc);
}

protected JwtIssuer createJwtIssuer(
final int i,
final String principalClaimName,
Expand Down Expand Up @@ -618,6 +650,7 @@ protected void doMultipleAuthcAuthzAndVerifySuccess(
}
} catch (Throwable t) {
final Exception authcFailed = new Exception("Authentication test failed.");
LOGGER.error(t.getMessage());
realmFailureExceptions.forEach(authcFailed::addSuppressed); // realm exceptions
authcFailed.addSuppressed(t); // final throwable (ex: assertThat)
LOGGER.error("Unexpected exception.", authcFailed);
Expand Down

0 comments on commit 2364386

Please sign in to comment.