-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests of using JWKS for obtaining the signer verification public key
- Loading branch information
Showing
10 changed files
with
438 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
} | ||
} |
Oops, something went wrong.