Skip to content

Commit

Permalink
Add async capabilities to the library (#37)
Browse files Browse the repository at this point in the history
Create asynchronous counterparts to JsonTokenParser, VerifierProvider, and VerifierProviders. Add new unit tests to verify the behavior of these async equivalents.
  • Loading branch information
Will-Lin4 committed Jun 25, 2020
1 parent f903dd2 commit 86589b0
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 6 deletions.
140 changes: 140 additions & 0 deletions src/main/java/net/oauth/jsontoken/AsyncJsonTokenParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Copyright 2020 Google LLC
*
* 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 net.oauth.jsontoken;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import net.oauth.jsontoken.crypto.SignatureAlgorithm;
import net.oauth.jsontoken.crypto.Verifier;
import net.oauth.jsontoken.discovery.AsyncVerifierProvider;
import net.oauth.jsontoken.discovery.AsyncVerifierProviders;

/**
* The asynchronous counterpart of {@link JsonTokenParser}.
* Class that parses and verifies JSON Tokens asynchronously.
*/
public final class AsyncJsonTokenParser extends AbstractJsonTokenParser {
private final AsyncVerifierProviders asyncVerifierProviders;
private final Executor executor;

/**
* Creates a new {@link AsyncJsonTokenParser}.
*
* @param clock a clock object that will decide whether a given token is currently valid or not.
* @param asyncVerifierProviders an object that provides signature verifiers asynchronously
* based on a signature algorithm, the signer, and key ids.
* @param executor an executor to run the tasks before and after getting the verifiers
* @param checkers an array of checkers that validates the parameters in the JSON token.
*/
public AsyncJsonTokenParser(
Clock clock,
AsyncVerifierProviders asyncVerifierProviders,
Executor executor,
Checker... checkers) {
super(clock, checkers);
this.asyncVerifierProviders = Preconditions.checkNotNull(asyncVerifierProviders);
this.executor = Preconditions.checkNotNull(executor);
}

/**
* Verifies that the jsonToken has a valid signature and valid standard claims
* (iat, exp). Uses {@link AsyncVerifierProviders} to obtain the secret key.
* This method is not expected to throw exceptions when returning a future. However,
* when getting the result of the future, an {@link ExecutionException} may be thrown
* in which {@link ExecutionException#getCause()} includes the possible exceptions
* as thrown by {@link JsonTokenParser#verify(JsonToken)}.
*
* @param jsonToken
* @return a {@link ListenableFuture} that will fail if the token fails verification.
*/
public ListenableFuture<Void> verify(JsonToken jsonToken) {
ListenableFuture<List<Verifier>> futureVerifiers = provideVerifiers(jsonToken);
// Use AsyncFunction instead of Function to allow for checked exceptions to propagate forward
AsyncFunction<List<Verifier>, Void> verifyFunction =
verifiers -> {
verify(jsonToken, verifiers);
return Futures.immediateVoidFuture();
};

return Futures.transformAsync(futureVerifiers, verifyFunction, executor);
}

/**
* Parses and verifies a JSON Token.
* This method is not expected to throw exceptions when returning a future. However,
* when getting the result of the future, an {@link ExecutionException} may be thrown
* in which {@link ExecutionException#getCause()} includes the same possible exceptions
* thrown by {@link JsonTokenParser#verifyAndDeserialize(String)}.
*
* @param tokenString the serialized token that is to parsed and verified.
* @return a {@link ListenableFuture} that will return the deserialized {@link JsonObject},
* suitable for passing to the constructor of {@link JsonToken}
* or equivalent constructor of {@link JsonToken} subclasses.
*/
public ListenableFuture<JsonToken> verifyAndDeserialize(String tokenString) {
JsonToken jsonToken;
try {
jsonToken = deserialize(tokenString);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}

return Futures.transform(verify(jsonToken), unused -> jsonToken, executor);
}

/**
* Use {@link AsyncVerifierProviders} to get future that will return a list of verifiers
* for this token.
*
* @param jsonToken
* @return a {@link ListenableFuture} that will return a list of verifiers
*/
private ListenableFuture<List<Verifier>> provideVerifiers(JsonToken jsonToken) {
ListenableFuture<List<Verifier>> futureVerifiers;
try {
SignatureAlgorithm signatureAlgorithm = jsonToken.getSignatureAlgorithm();
AsyncVerifierProvider provider =
asyncVerifierProviders.getVerifierProvider(signatureAlgorithm);
if (provider == null) {
throw new IllegalArgumentException("Signature algorithm not supported: "
+ signatureAlgorithm);
}
futureVerifiers = provider.findVerifier(jsonToken.getIssuer(), jsonToken.getKeyId());
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}

Function<List<Verifier>, List<Verifier>> checkNullFunction =
verifiers -> {
if (verifiers == null) {
throw new IllegalStateException("No valid verifier for issuer: "
+ jsonToken.getIssuer());
}
return verifiers;
};

return Futures.transform(futureVerifiers, checkNullFunction, executor);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright 2020 Google LLC
*
* 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 net.oauth.jsontoken.discovery;

import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import net.oauth.jsontoken.AsyncJsonTokenParser;
import net.oauth.jsontoken.crypto.Verifier;

/**
* The asynchronous counterpart of {@link VerifierProvider}.
* An interface that must be implemented by JSON Token verifiers. The {@link AsyncJsonTokenParser}
* uses {@link AsyncVerifierProvider} implementations to find verification keys asynchronously with
* which to verify the parsed JSON Token. There are different implementations of this interface for
* different types of verification keys.
*
* For symmetric signing keys, an implementation of {@link AsyncVerifierProvider} presumably will
* always look up the key in a local database. For public signing keys, the
* {@link AsyncVerifierProvider} implementation may fetch the public verification keys when needed
* from the public internet.
*/
public interface AsyncVerifierProvider {

/**
* Returns a {@link ListenableFuture}, which asynchronously returns a {@link List<Verifier>}
* that represents a certain verification key, given the key's id and its issuer.
* @param issuer the id of the issuer that's using the key.
* @param keyId the id of the key, if keyId mismatches, return a list of
* possible verification keys.
* @return a {@link ListenableFuture} object that asynchronously returns a {@link List<Verifier>}
* that represents the verification key.
*/
ListenableFuture<List<Verifier>> findVerifier(String issuer, String keyId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright 2020 Google LLC
*
* 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 net.oauth.jsontoken.discovery;

import javax.annotation.Nullable;
import net.oauth.jsontoken.AsyncJsonTokenParser;
import net.oauth.jsontoken.crypto.SignatureAlgorithm;
import net.oauth.jsontoken.crypto.Verifier;

/**
* The asynchronous counterpart of {@link VerifierProviders}.
* An interface that must be implemented by JSON Token verifiers. The {@link AsyncJsonTokenParser}
* uses the {@link AsyncVerifierProviders} implementation to locate verification keys. In
* particular, it will first look up the {@link AsyncVerifierProvider} for the signature algorithm
* used in the JSON Token and the ask the {@link AsyncVerifierProvider} to provide a future that
* will return a {@link Verifier} to check the validity of the JSON Token.
*/
public interface AsyncVerifierProviders {

/**
* @param alg the signature algorithm of the JSON Token.
* @return a {@link AsyncVerifierProvider} corresponding to a given signature algorithm
* that allows for asynchronous retrieval of a verification key.
*/
@Nullable
AsyncVerifierProvider getVerifierProvider(SignatureAlgorithm alg);
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ public void testVerify_unsupportedSignatureAlgorithm() throws Exception {
}

public void testVerify_failChecker() throws Exception {
AbstractJsonTokenParser parser = getAbstractJsonTokenParser(new IgnoreAudience(), new AlwaysFailAudience());
AbstractJsonTokenParser parser =
getAbstractJsonTokenParser(new IgnoreAudience(), new AlwaysFailAudience());
JsonToken checkToken = naiveDeserialize(TOKEN_STRING);
assertThrows(
SignatureException.class,
Expand Down Expand Up @@ -294,7 +295,8 @@ private boolean verifyTimeFrame(Instant issuedAt, Instant expiration) throws Exc
}
}

private JsonToken getJsonTokenWithTimeRange(Instant issuedAt, Instant expiration) throws Exception {
private JsonToken getJsonTokenWithTimeRange(
Instant issuedAt, Instant expiration) throws Exception {
HmacSHA256Signer signer = new HmacSHA256Signer("google.com", "key2", SYMMETRIC_KEY);
JsonToken token = new JsonToken(signer, clock);
if (issuedAt != null) {
Expand Down
141 changes: 141 additions & 0 deletions src/test/java/net/oauth/jsontoken/AsyncJsonTokenParserTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package net.oauth.jsontoken;

import static org.junit.Assert.assertThrows;

import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.JsonParseException;
import java.security.SignatureException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import net.oauth.jsontoken.crypto.SignatureAlgorithm;
import net.oauth.jsontoken.discovery.AsyncVerifierProvider;
import net.oauth.jsontoken.discovery.AsyncVerifierProviders;
import org.junit.function.ThrowingRunnable;

public class AsyncJsonTokenParserTest extends JsonTokenTestBase {

private AsyncVerifierProviders asyncLocators;
private AsyncVerifierProviders asyncLocatorsFromRuby;
private Executor executor;

@Override
protected void setUp() throws Exception {
super.setUp();
AsyncVerifierProvider hmacLocator = (issuer, keyId) -> Futures.immediateFuture(
locators.getVerifierProvider(SignatureAlgorithm.HS256).findVerifier(issuer, keyId));
AsyncVerifierProvider rsaLocator = (issuer, keyId) -> Futures.immediateFuture(
locators.getVerifierProvider(SignatureAlgorithm.RS256).findVerifier(issuer, keyId));

asyncLocators = alg -> {
if (alg.equals(SignatureAlgorithm.HS256)) {
return hmacLocator;
} else if (alg.equals(SignatureAlgorithm.RS256)) {
return rsaLocator;
}
return null;
};

AsyncVerifierProvider hmacLocatorFromRuby = (issuer, keyId) -> Futures.immediateFuture(
locatorsFromRuby.getVerifierProvider(SignatureAlgorithm.HS256).findVerifier(issuer, keyId));

asyncLocatorsFromRuby = alg -> {
if (alg.equals(SignatureAlgorithm.HS256)) {
return hmacLocatorFromRuby;
}
return null;
};

executor = MoreExecutors.directExecutor();
}

public void testVerify_valid() throws Exception {
AsyncJsonTokenParser parser = getAsyncJsonTokenParser();
JsonToken checkToken = naiveDeserialize(TOKEN_STRING);
parser.verify(checkToken).get();
}

public void testVerify_badSignature() throws Exception {
AsyncJsonTokenParser parser = getAsyncJsonTokenParser();
JsonToken checkToken = naiveDeserialize(TOKEN_STRING_BAD_SIG);
assertFailsWithCause(
SignatureException.class,
() -> parser.verify(checkToken).get()
);
}

public void testVerify_unsupportedSignature() throws Exception {
AsyncJsonTokenParser parser = getAsyncJsonTokenParser();
JsonToken checkToken = naiveDeserialize(TOKEN_STRING_UNSUPPORTED_SIGNATURE_ALGORITHM);
assertFailsWithCause(
IllegalArgumentException.class,
() -> parser.verify(checkToken).get()
);
}

public void testVerify_noVerifiers() throws Exception {
AsyncVerifierProvider noLocator = (signerId, keyId) -> Futures.immediateFuture(null);
AsyncVerifierProviders noLocators = alg -> {
if (alg.equals(SignatureAlgorithm.HS256)) {
return noLocator;
}
return null;
};

AsyncJsonTokenParser parser = getAsyncJsonTokenParser(noLocators, new IgnoreAudience());
JsonToken checkToken = naiveDeserialize(TOKEN_STRING);
assertFailsWithCause(
IllegalStateException.class,
() -> parser.verify(checkToken).get()
);
}

public void testVerifyAndDeserialize_valid() throws Exception {
AsyncJsonTokenParser parser = getAsyncJsonTokenParser();
JsonToken token = parser.verifyAndDeserialize(TOKEN_STRING).get();
assertHeader(token);
assertPayload(token);
}

public void testVerifyAndDeserialize_deserializeFail() throws Exception {
AsyncJsonTokenParser parser = getAsyncJsonTokenParser();
assertFailsWithCause(
JsonParseException.class,
() -> parser.verifyAndDeserialize(TOKEN_STRING_CORRUPT_PAYLOAD).get()
);
}

public void testVerifyAndDeserialize_verifyFail() throws Exception {
AsyncJsonTokenParser parser = getAsyncJsonTokenParser();
assertFailsWithCause(
SignatureException.class,
() -> parser.verifyAndDeserialize(TOKEN_STRING_BAD_SIG).get()
);
}

public void testVerifyAndDeserialize_tokenFromRuby() throws Exception {
AsyncJsonTokenParser parser =
getAsyncJsonTokenParser(asyncLocatorsFromRuby, new IgnoreAudience());
JsonToken token = parser.verifyAndDeserialize(TOKEN_FROM_RUBY).get();

assertEquals(SignatureAlgorithm.HS256, token.getSignatureAlgorithm());
assertEquals("JWT", token.getHeader().get(JsonToken.TYPE_HEADER).getAsString());
assertEquals("world", token.getParamAsPrimitive("hello").getAsString());
}

private AsyncJsonTokenParser getAsyncJsonTokenParser() {
return new AsyncJsonTokenParser(clock, asyncLocators, executor, new IgnoreAudience());
}

private AsyncJsonTokenParser getAsyncJsonTokenParser(
AsyncVerifierProviders providers, Checker... checkers) {
return new AsyncJsonTokenParser(clock, providers, executor, checkers);
}

private <T extends Throwable> void assertFailsWithCause(
Class<T> throwableClass, ThrowingRunnable runnable) {
ExecutionException e = assertThrows(ExecutionException.class, runnable);
assertTrue(throwableClass.isInstance(e.getCause()));
}

}
Loading

0 comments on commit 86589b0

Please sign in to comment.