-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add async capabilities to the library (#37)
Create asynchronous counterparts to JsonTokenParser, VerifierProvider, and VerifierProviders. Add new unit tests to verify the behavior of these async equivalents.
- Loading branch information
Showing
6 changed files
with
379 additions
and
6 deletions.
There are no files selected for viewing
140 changes: 140 additions & 0 deletions
140
src/main/java/net/oauth/jsontoken/AsyncJsonTokenParser.java
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,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); | ||
} | ||
|
||
} |
49 changes: 49 additions & 0 deletions
49
src/main/java/net/oauth/jsontoken/discovery/AsyncVerifierProvider.java
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,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); | ||
|
||
} |
41 changes: 41 additions & 0 deletions
41
src/main/java/net/oauth/jsontoken/discovery/AsyncVerifierProviders.java
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,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); | ||
} |
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
141 changes: 141 additions & 0 deletions
141
src/test/java/net/oauth/jsontoken/AsyncJsonTokenParserTest.java
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,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())); | ||
} | ||
|
||
} |
Oops, something went wrong.