Skip to content

Commit

Permalink
Add tests of using JWKS for obtaining the signer verification public key
Browse files Browse the repository at this point in the history
  • Loading branch information
starksm64 committed Apr 18, 2018
1 parent d5ec57a commit 35e7512
Show file tree
Hide file tree
Showing 10 changed files with 438 additions and 8 deletions.
7 changes: 6 additions & 1 deletion sandbox/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<properties>
<version.jose4j>0.6.2</version.jose4j>
<version.numbusds>4.23</version.numbusds>
<version.auth0>3.0.1</version.auth0>
<version.auth0>3.3.0</version.auth0>
<version.jsonwebtoken>0.9.0</version.jsonwebtoken>
</properties>

Expand Down Expand Up @@ -90,6 +90,11 @@
<artifactId>java-jwt</artifactId>
<version>${version.auth0}</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
Expand Down
152 changes: 152 additions & 0 deletions sandbox/src/test/java/jwks/AbstractJWKSTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright (c) 2016-2018 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package jwks;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URL;
import java.security.PrivateKey;

import com.auth0.jwt.exceptions.JWTVerificationException;
import com.nimbusds.jose.proc.BadJOSEException;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.eclipse.microprofile.jwt.tck.util.TokenUtils;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Test;

public abstract class AbstractJWKSTest {
private static String endpoint;
private static final String TEST_ISSUER = "https://server.example.com";

/**
* Start an embedded HttpServer that returns the test JWKS from a http://localhost:8080/jwks endpoint
* @throws IOException - on failure
*/
@BeforeSuite
public static void startHttpServer() throws IOException {
// Load the test JKWS from the signer-keyset.jwk resource
InputStream is = AbstractJWKSTest.class.getResourceAsStream("/signer-keyset.jwk");
byte[] response;
StringWriter sw = new StringWriter();
try(BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
String line = br.readLine();
while(line != null) {
sw.write(line);
sw.write('\n');
line = br.readLine();
}
}
response = sw.toString().getBytes();

// Start a server listening on 8080 with a /jwks context that returns the JWKS json data
HttpServer httpServer = HttpServer.create(new InetSocketAddress(8080), 0);
endpoint = "http://localhost:8080/jwks";
httpServer.createContext("/jwks", new HttpHandler() {
public void handle(HttpExchange exchange) throws IOException {
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length);
exchange.getResponseBody().write(response);
exchange.close();
System.out.printf("Handled jwks request\n");
}
});
httpServer.start();
System.out.printf("Started HttpServer at: %s\n", endpoint);
}

/**
* Loads the signer-keypair.jwk resource that was generated using https://mkjwk.org
* and returns the private key
*
* @return the private key from the key pair
*/
static PrivateKey loadPrivateKey() throws Exception {
String jwk = TokenUtils.readResource("/signer-keypair.jwk");
RsaJsonWebKey rsaJsonWebKey = (RsaJsonWebKey) JsonWebKey.Factory.newJwk(jwk);
return rsaJsonWebKey.getRsaPrivateKey();
}

/**
* Validate access to the http://localhost:8080/jwks endpoint
* @throws IOException - on failure
*/
@Test
public void validateGet() throws IOException {
URL jwksURL = new URL(endpoint);
InputStream is = jwksURL.openStream();
try(BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
String line = br.readLine();
while(line != null) {
System.out.println(line);
line = br.readLine();
}
}
}

/**
* Ensure a valid token is validated by the provider using the JWKS URL for the public key associated
* with the signer.
*
* @throws Exception
*/
@Test
public void testValidToken() throws Exception {
PrivateKey pk = loadPrivateKey();
String token = TokenUtils.generateTokenString(pk, "jwk-test", "/Token1.json", null, null);
int expGracePeriodSecs = 60;
validateToken(token, new URL(endpoint), TEST_ISSUER, expGracePeriodSecs);
}
/**
* Ensure a token is validated by the provider using the JWKS URL for the public key associated
* with the signer.
*
* @throws Exception
*/
@Test(expectedExceptions = {InvalidJwtException.class, BadJOSEException.class, JWTVerificationException.class})
public void testNoMatchingKID() throws Exception {
PrivateKey pk = loadPrivateKey();
String token = TokenUtils.generateTokenString(pk, "invalid-kid", "/Token1.json", null, null);
int expGracePeriodSecs = 60;
validateToken(token, new URL(endpoint), TEST_ISSUER, expGracePeriodSecs);
}

/**
* This method is implemented by the JWT provider library to validate the token
*
* @param token - the signed, base64 encoded header.content.sig JWT string
* @param jwksURL - URL to a JWKS that contains the public key to verify the JWT signature
* @param issuer - the expected iss claim value
* @param expGracePeriodSecs - grace period in seconds for evaluating the exp claim
* @throws Exception
*/
abstract protected void validateToken(String token, URL jwksURL, String issuer, int expGracePeriodSecs)
throws Exception;

}
80 changes: 80 additions & 0 deletions sandbox/src/test/java/jwks/Auth0JWKSTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2016-2018 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package jwks;

import java.net.URL;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.RSAKeyProvider;
import com.auth0.jwt.interfaces.Verification;

/**
* Validate the auth0 jwt library
* https://github.com/auth0/java-jwt
*/
public class Auth0JWKSTest extends AbstractJWKSTest {
@Override
protected void validateToken(String token, URL jwksURL, String issuer, int expGracePeriodSecs) throws Exception {
JwkProvider jwkStore = new UrlJwkProvider(jwksURL);
RSAKeyProvider keyProvider = new RSAKeyProvider() {
@Override
public RSAPublicKey getPublicKeyById(String kid) throws JWTVerificationException {
//Received 'kid' value might be null if it wasn't defined in the Token's header
RSAPublicKey publicKey = null;
try {
Jwk jwk = jwkStore.get(kid);
publicKey = (RSAPublicKey) jwk.getPublicKey();
return publicKey;
}
catch (Exception e) {
throw new JWTVerificationException("Failed to retrieve key", e);
}
}

@Override
public RSAPrivateKey getPrivateKey() {
return null;
}

@Override
public String getPrivateKeyId() {
return null;
}
};
Algorithm algorithm = Algorithm.RSA256(keyProvider);

Verification builder = JWT.require(algorithm)
.withIssuer(issuer);
if(expGracePeriodSecs > 0) {
builder = builder.acceptLeeway(expGracePeriodSecs);
}
JWTVerifier verifier = builder.build();
DecodedJWT jwt = verifier.verify(token);
}
}
66 changes: 66 additions & 0 deletions sandbox/src/test/java/jwks/Jose4jJWKSTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2016-2018 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package jwks;

import java.net.URL;

import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jwk.HttpsJwks;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jwt.NumericDate;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.jwt.consumer.JwtContext;
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;

/**
* Validate the jose4j JWT library
* https://bitbucket.org/b_c/jose4j/overview
*/
public class Jose4jJWKSTest extends AbstractJWKSTest {
@Override
protected void validateToken(String token, URL jwksURL, String issuer, int expGracePeriodSecs) throws Exception {
JwtConsumerBuilder builder = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setRequireSubject()
.setSkipDefaultAudienceValidation()
.setExpectedIssuer(issuer)
.setJwsAlgorithmConstraints(
new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.WHITELIST,
AlgorithmIdentifiers.RSA_USING_SHA256));

HttpsJwks keySource = new HttpsJwks(jwksURL.toExternalForm());
builder.setVerificationKeyResolver(new HttpsJwksVerificationKeyResolver(keySource));

if (expGracePeriodSecs > 0) {
builder.setAllowedClockSkewInSeconds(expGracePeriodSecs);
}
else {
builder.setEvaluationTime(NumericDate.fromSeconds(0));
}

JwtConsumer jwtConsumer = builder.build();
JwtContext jwtContext = jwtConsumer.process(token);
String type = jwtContext.getJoseObjects().get(0).getHeader("typ");
// Validate the JWT and process it to the Claims
jwtConsumer.processContext(jwtContext);

}
}
Loading

0 comments on commit 35e7512

Please sign in to comment.