From 5a368174a748cfb1d7cee295af433861e1e11148 Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Wed, 19 Feb 2025 15:10:08 +0100 Subject: [PATCH 1/2] 5.0.0 jakarta / gson / osgi --- .gitignore | 3 + AllOauthClientTests.launch | 23 + build.properties | 4 + pom.xml | 628 ++++++++++-------- readme.md | 14 + .../io/curity/oauth/AbstractJwtValidator.java | 404 +++++------ .../io/curity/oauth/AuthenticatedUser.java | 66 +- .../AuthenticatedUserRequestWrapper.java | 6 +- src/main/java/io/curity/oauth/JsonData.java | 50 +- src/main/java/io/curity/oauth/JsonUtils.java | 52 +- src/main/java/io/curity/oauth/JsonWebKey.java | 127 ++-- .../oauth/JsonWebKeyNotFoundException.java | 4 +- .../java/io/curity/oauth/JsonWebKeyType.java | 74 +-- src/main/java/io/curity/oauth/JwkManager.java | 187 +++--- .../java/io/curity/oauth/JwksResponse.java | 57 +- .../io/curity/oauth/JwtValidatorWithCert.java | 32 +- .../io/curity/oauth/JwtValidatorWithJwk.java | 108 ++- .../curity/oauth/OAuthIntrospectResponse.java | 28 +- .../java/io/curity/oauth/OAuthJwtFilter.java | 14 +- .../io/curity/oauth/OAuthOpaqueFilter.java | 79 +-- .../io/curity/oauth/OpaqueTokenValidator.java | 115 ++-- .../oauth/TokenValidationException.java | 5 +- .../AuthenticatedUserRequestWrapperTest.java | 24 +- .../java/io/curity/oauth/JwtWithCertTest.java | 33 +- .../java/io/curity/oauth/JwtWithJwksTest.java | 304 +++++---- 25 files changed, 1174 insertions(+), 1267 deletions(-) create mode 100644 AllOauthClientTests.launch create mode 100644 build.properties diff --git a/.gitignore b/.gitignore index 5e6e75d..99f8bb0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ Thumbs.db .idea *.iml +/bin-test/ +/target-test/ +/test-classes/ diff --git a/AllOauthClientTests.launch b/AllOauthClientTests.launch new file mode 100644 index 0000000..cd07cbe --- /dev/null +++ b/AllOauthClientTests.launch @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.properties b/build.properties new file mode 100644 index 0000000..f1e110b --- /dev/null +++ b/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +bin.includes = META-INF/,\ + . +additional.bundles = org.junit diff --git a/pom.xml b/pom.xml index 8eedd38..05fa15f 100644 --- a/pom.xml +++ b/pom.xml @@ -14,296 +14,370 @@ ~ limitations under the License. --> - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" + xmlns="http://maven.apache.org/POM/4.0.0"> + 4.0.0 - io.curity - oauth-filter - 4.0.0 - OAuth API Filter - A Servlet Filter that authenticates and authorizes requests using OAuth access tokens of various kinds. - https://github.com/curityio/oauth-filter-for-java + io.curity + oauth-filter + 5.0.0 + OAuth API Filter + A Servlet Filter that authenticates and authorizes requests + using OAuth access tokens of various kinds. + https://github.com/curityio/oauth-filter-for-java - - Curity AB - https://curity.io - + + Curity AB + https://curity.io + - - - The Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - - + + + The Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + - - - Travis Spencer - travis.spencer@curity.io - Curity AB - https://curity.io - - - Michal Trojanowski - michal.trojanowski@curity.io - Curity AB - https://curity.io - - - Judith Kahrer - judith.kahrer@curity.io - Curity AB - https://curity.io - - + + + Travis Spencer + travis.spencer@curity.io + Curity AB + https://curity.io + + + Michal Trojanowski + michal.trojanowski@curity.io + Curity AB + https://curity.io + + + Judith Kahrer + judith.kahrer@curity.io + Curity AB + https://curity.io + + + Marco Descher + descher@medevit.at + MEDEVIT OG + http://medevit.at + + - - scm:git:git://github.com:curityio/oauth-filter-for-java.git - scm:git:ssh://github.com:curityio/oauth-filter-for-java.git - https://github.com/curityio/oauth-filter-for-java - + + scm:git:git://github.com:curityio/oauth-filter-for-java.git + + scm:git:ssh://github.com:curityio/oauth-filter-for-java.git + https://github.com/curityio/oauth-filter-for-java + - - UTF-8 - + + UTF-8 + - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 17 - 17 - true - false - - + + + + + org.apache.felix + maven-bundle-plugin + 6.0.0 + true + + + <_failok>true + ${buildNumber} + A Servlet Filter that + authenticates and authorizes requests using + OAuth access tokens of various kinds. + ${project.version} + oauth-filter + * + ${api.package} + ${project.version} + ${spec.version} + <_versionpolicy> + [$(version;==;$(@)),$(version;+;$(@))) + <_nodefaultversion>false + + + + + osgi-bundle + package + + bundle + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 17 + 17 + true + false + + - - - maven-failsafe-plugin - 2.22.2 - - - org.apache.logging.log4j.jul.LogManager - - - - **/integration/**/IntegrationTest*.java - **/integration/**/*IntegrationTest.java - - UTF-8 - - - - - integration-test - verify - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.16 - - - org.apache.logging.log4j.jul.LogManager - - - - **/integration/**/*.java - - - + + + maven-failsafe-plugin + 2.22.2 + + + + org.apache.logging.log4j.jul.LogManager + + + + **/integration/**/IntegrationTest*.java + **/integration/**/*IntegrationTest.java + + UTF-8 + + + + + integration-test + verify + + + + - - + + org.apache.maven.plugins + maven-surefire-plugin + 2.16 + + + + org.apache.logging.log4j.jul.LogManager + + + + **/integration/**/*.java + + + + + org.cyclonedx + cyclonedx-maven-plugin + 2.9.1 + + + package + + makeAggregateBom + + + + + library + 1.4 + true + true + false + true + true + false + false + true + all + CycloneDX-Sbom + + - - - jakarta.servlet - jakarta.servlet-api - 6.0.0 - - - javax.json - javax.json-api - - - org.apache.httpcomponents - httpclient - true - + + org.apache.felix + maven-bundle-plugin + + - + - - com.owlike - genson - test - - - junit - junit - test - - - org.mockito - mockito-core - test - - - org.apache.logging.log4j - log4j-core - test - - - org.apache.logging.log4j - log4j-jul - test - - - org.apache.logging.log4j - log4j-jcl - test - - - org.bitbucket.b_c - jose4j - test - - + + + jakarta.servlet + jakarta.servlet-api + + + org.apache.httpcomponents + httpclient + true + - - - - org.apache.httpcomponents - httpclient - 4.5.13 - - - javax.servlet - javax.servlet-api - 4.0.1 - - - com.google.guava - guava - 32.0.0-jre - - - javax.json - javax.json-api - 1.1.4 - - - com.owlike - genson - 1.6 - - - org.bitbucket.b_c - jose4j - 0.9.4 - - - junit - junit - 4.13.2 - - - org.mockito - mockito-core - 3.10.0 - test - - - org.apache.logging.log4j - log4j-bom - 2.14.1 - import - pom - - - + - - - release - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.3.0 - - - attach-javadocs - - jar - - - - - 8 - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.0.1 - - - sign-artifacts - verify - - sign - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - true - - ossrh - https://oss.sonatype.org/ - true - - - - - - + + com.owlike + genson + test + + + junit + junit + test + + + org.mockito + mockito-core + test + + + org.apache.logging.log4j + log4j-core + test + + + org.apache.logging.log4j + log4j-jul + test + + + org.apache.logging.log4j + log4j-jcl + test + + + com.google.code.gson + gson + + + org.bitbucket.b_c + jose4j + + + + + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + jakarta.servlet + jakarta.servlet-api + 6.0.0 + + + com.owlike + genson + 1.6 + + + org.bitbucket.b_c + jose4j + 0.9.6 + + + junit + junit + 4.13.2 + + + org.mockito + mockito-core + 3.10.0 + test + + + org.apache.logging.log4j + log4j-bom + 2.14.1 + import + pom + + + com.google.code.gson + gson + 2.10 + + + + + + + release + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.0 + + + attach-javadocs + + jar + + + + + 8 + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.0.1 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + ossrh + https://oss.sonatype.org/ + true + + + + + + diff --git a/readme.md b/readme.md index 058d418..db610e7 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,8 @@ [![Quality](https://img.shields.io/badge/quality-test-yellow)](https://curity.io/resources/code-examples/status/) [![Availability](https://img.shields.io/badge/availability-source-blue)](https://curity.io/resources/code-examples/status/) + + This project contains a Servlet Filter that authenticates and authorizes requests using OAuth access tokens of various kinds. There are two `OAuthFilter` implementations. `OAuthJwtFilter` and `OAuthOpaqueFilter`. Both implement `jakarta.servlet.Filter`, and can be used to protect APIs built using Java. Depending on the format of the access token, these two concrete implementations can be used in the following manner: 1. If the token is a Json Web Token (JWT) then validate the token using a public key @@ -11,6 +13,18 @@ This project contains a Servlet Filter that authenticates and authorizes request An example of how to use this filter can be found in a [separate repository](https://github.com/curityio/example-java-oauth-protected-api). +## FORK INFO + +This is a fork of the original repo + +It updates to version 5 with the following - breaking - changes + +* Fully move to `jakarta.servlet` namespace +* Change JSON parser to `com.google.gson` +* Update some dependent libraries +* Generate OSGi capable `META-INF/MANIFEST.MF` +* Fix some build warnings + ## Filter Overview The filter is build to perform two tasks. diff --git a/src/main/java/io/curity/oauth/AbstractJwtValidator.java b/src/main/java/io/curity/oauth/AbstractJwtValidator.java index da2a4f1..aa5cc26 100644 --- a/src/main/java/io/curity/oauth/AbstractJwtValidator.java +++ b/src/main/java/io/curity/oauth/AbstractJwtValidator.java @@ -16,10 +16,6 @@ package io.curity.oauth; -import javax.json.JsonObject; -import javax.json.JsonReader; -import javax.json.JsonReaderFactory; -import java.io.StringReader; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.PublicKey; @@ -33,221 +29,187 @@ import java.util.logging.Level; import java.util.logging.Logger; -abstract class AbstractJwtValidator implements JwtValidator -{ - private static final Logger _logger = Logger.getLogger(AbstractJwtValidator.class.getName()); - - // Caches with object scope that will ensure that we only decode the same JWT parts once per the lifetime of this - // object - private final Map _decodedJwtBodyByEncodedBody = new HashMap<>(1); - private final Map _decodedJwtHeaderByEncodedHeader = new HashMap<>(1); - private final JsonReaderFactory _jsonReaderFactory; - private final String _audience; - private final String _issuer; - - AbstractJwtValidator(String issuer, String audience, JsonReaderFactory jsonReaderFactory) - { - _issuer = issuer; - _audience = audience; - _jsonReaderFactory = jsonReaderFactory; - } - - public final JsonData validate(String jwt) throws TokenValidationException - { - String[] jwtParts = jwt.split("\\."); - - if (jwtParts.length != 3) - { - throw new InvalidTokenFormatException(); - } - - JsonObject jwtBody = decodeJwtBody(jwtParts[1]); - JwtHeader jwtHeader = decodeJwtHeader(jwtParts[0]); - byte[] jwtSignature = Base64.getUrlDecoder().decode(jwtParts[2]); - byte[] headerAndPayload = convertToBytes(jwtParts[0] + "." + jwtParts[1]); - - validateSignature(jwtHeader, jwtSignature, headerAndPayload); - - try - { - long exp = JsonUtils.getLong(jwtBody, "exp"); - long iat = JsonUtils.getLong(jwtBody, "iat"); - - String aud = JsonUtils.getString(jwtBody, "aud"); - String iss = JsonUtils.getString(jwtBody, "iss"); - - assert aud != null && aud.length() > 0 : "aud claim is not present in JWT"; - assert iss != null && iss.length() > 0 : "iss claim is not present in JWT"; - - if (!aud.equals(_audience)) - { - throw new InvalidAudienceException(_audience, aud); - } - - if (!iss.equals(_issuer)) - { - throw new InvalidIssuerException(_issuer, iss); - } - - Instant now = Instant.now(); - - if (now.getEpochSecond() > exp) - { - throw new ExpiredTokenException(); - } - - if (now.getEpochSecond() < iat) - { - throw new InvalidIssuanceInstantException(); - } - } - catch (Exception e) - { - _logger.log(Level.INFO, "Could not extract token data", e); - - throw new InvalidTokenFormatException("Failed to extract data from Token"); - } - - return new JsonData(jwtBody); - } - - private void validateSignature(JwtHeader jwtHeader, byte[] jwtSignatureData, - byte[] headerAndPayload) - throws TokenValidationException - { - String algorithm = jwtHeader.getAlgorithm(); - - if (algorithm == null || algorithm.length() <= 0) - { - throw new MissingAlgorithmException(); - } - - if (canRecognizeAlg(algorithm)) - { - Optional signatureVerificationKey = getPublicKey(jwtHeader); - - if (signatureVerificationKey.isEmpty()) - { - _logger.warning("Received token but could not find matching key"); - - throw new UnknownSignatureVerificationKey(); - } - - if (!verifySignature(algorithm, headerAndPayload, jwtSignatureData, signatureVerificationKey.get())) - { - throw new InvalidSignatureException(); - } - } - else - { - _logger.warning(() -> String.format("Requested JsonWebKey using unrecognizable alg: %s", algorithm)); - - throw new UnknownAlgorithmException(algorithm); - } - } - - protected abstract Optional getPublicKey(JwtHeader jwtHeader); - - /** - * Convert base64 to bytes (ASCII) - * - * @param input input - * @return The array of bytes - */ - private byte[] convertToBytes(String input) - { - byte[] bytes = new byte[input.length()]; - - for (int i = 0; i < input.length(); i++) - { - //Convert and treat as ascii. - int integer = input.charAt(i); - - //Since byte is signed in Java we cannot use normal conversion - //but must drop it into a byte array and truncate. - byte[] rawBytes = ByteBuffer.allocate(4).putInt(integer).array(); - //Only store the least significant byte (the others should be 0 TODO check) - bytes[i] = rawBytes[3]; - } - - return bytes; - } - - private boolean verifySignature(String algorithm, byte[] signingInput, byte[] signature, PublicKey publicKey) - { - try - { - Signature verifier = switch (algorithm) { - case "RS256" -> Signature.getInstance("SHA256withRSA"); - case "EdDSA" -> Signature.getInstance(((EdECPublicKey) publicKey).getParams().getName()); - default -> throw new UnknownAlgorithmException(String.format("Unsupported signature algorithm '%s'", algorithm)); - }; - - verifier.initVerify(publicKey); - verifier.update(signingInput); - - return verifier.verify(signature); - } - catch (Exception e) - { - throw new RuntimeException("Unable to validate JWT signature", e); - } - } - - private boolean canRecognizeAlg(String alg) - { - return switch (alg) { - case "RS256", "EdDSA" -> true; - default -> false; - }; - } - - private JsonObject decodeJwtBody(String body) - { - return _decodedJwtBodyByEncodedBody.computeIfAbsent(body, key -> - { - // TODO: Switch to stream - String decodedBody = new String(Base64.getUrlDecoder().decode(body), StandardCharsets.UTF_8); - JsonReader jsonBodyReader = _jsonReaderFactory.createReader(new StringReader(decodedBody)); - - return jsonBodyReader.readObject(); - }); - } - - private JwtHeader decodeJwtHeader(String header) - { - return _decodedJwtHeaderByEncodedHeader.computeIfAbsent(header, key -> - { - Base64.Decoder base64 = Base64.getDecoder(); - String decodedHeader = new String(base64.decode(header), StandardCharsets.UTF_8); - JsonReader jsonHeaderReader = _jsonReaderFactory.createReader(new StringReader(decodedHeader)); - - return new JwtHeader(jsonHeaderReader.readObject()); - }); - } - - class JwtHeader - { - private final JsonObject _jsonObject; - - JwtHeader(JsonObject jsonObject) - { - _jsonObject = jsonObject; - } - - String getAlgorithm() - { - return getString("alg"); - } - - String getKeyId() - { - return getString("kid"); - } - - String getString(String name) - { - return JsonUtils.getString(_jsonObject, name); - } - } +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; + +abstract class AbstractJwtValidator implements JwtValidator { + private static final Logger _logger = Logger.getLogger(AbstractJwtValidator.class.getName()); + + // Caches with object scope that will ensure that we only decode the same JWT + // parts once per the lifetime of this + // object + private final Map _decodedJwtBodyByEncodedBody = new HashMap<>(1); + private final Map _decodedJwtHeaderByEncodedHeader = new HashMap<>(1); + private final Gson gson = new GsonBuilder().create(); + private final String _audience; + private final String _issuer; + + AbstractJwtValidator(String issuer, String audience) { + _issuer = issuer; + _audience = audience; + } + + public final JsonData validate(String jwt) throws TokenValidationException { + String[] jwtParts = jwt.split("\\."); + + if (jwtParts.length != 3) { + throw new InvalidTokenFormatException(); + } + + JsonObject jwtBody = decodeJwtBody(jwtParts[1]); + JwtHeader jwtHeader = decodeJwtHeader(jwtParts[0]); + byte[] jwtSignature = Base64.getUrlDecoder().decode(jwtParts[2]); + byte[] headerAndPayload = convertToBytes(jwtParts[0] + "." + jwtParts[1]); + + validateSignature(jwtHeader, jwtSignature, headerAndPayload); + + try { + long exp = JsonUtils.getLong(jwtBody, "exp"); + long iat = JsonUtils.getLong(jwtBody, "iat"); + + String aud = JsonUtils.getString(jwtBody, "aud"); + String iss = JsonUtils.getString(jwtBody, "iss"); + + assert aud != null && aud.length() > 0 : "aud claim is not present in JWT"; + assert iss != null && iss.length() > 0 : "iss claim is not present in JWT"; + + if (!aud.equals(_audience)) { + throw new InvalidAudienceException(_audience, aud); + } + + if (!iss.equals(_issuer)) { + throw new InvalidIssuerException(_issuer, iss); + } + + Instant now = Instant.now(); + + if (now.getEpochSecond() > exp) { + throw new ExpiredTokenException(); + } + + if (now.getEpochSecond() < iat) { + throw new InvalidIssuanceInstantException(); + } + } catch (Exception e) { + _logger.log(Level.INFO, "Could not extract token data", e); + + throw new InvalidTokenFormatException("Failed to extract data from Token"); + } + + return new JsonData(jwtBody); + } + + private void validateSignature(JwtHeader jwtHeader, byte[] jwtSignatureData, byte[] headerAndPayload) + throws TokenValidationException { + String algorithm = jwtHeader.getAlgorithm(); + + if (algorithm == null || algorithm.length() <= 0) { + throw new MissingAlgorithmException(); + } + + if (canRecognizeAlg(algorithm)) { + Optional signatureVerificationKey = getPublicKey(jwtHeader); + + if (signatureVerificationKey.isEmpty()) { + _logger.warning("Received token but could not find matching key"); + + throw new UnknownSignatureVerificationKey(); + } + + if (!verifySignature(algorithm, headerAndPayload, jwtSignatureData, signatureVerificationKey.get())) { + throw new InvalidSignatureException(); + } + } else { + _logger.warning(() -> String.format("Requested JsonWebKey using unrecognizable alg: %s", algorithm)); + + throw new UnknownAlgorithmException(algorithm); + } + } + + protected abstract Optional getPublicKey(JwtHeader jwtHeader); + + /** + * Convert base64 to bytes (ASCII) + * + * @param input input + * @return The array of bytes + */ + private byte[] convertToBytes(String input) { + byte[] bytes = new byte[input.length()]; + + for (int i = 0; i < input.length(); i++) { + // Convert and treat as ascii. + int integer = input.charAt(i); + + // Since byte is signed in Java we cannot use normal conversion + // but must drop it into a byte array and truncate. + byte[] rawBytes = ByteBuffer.allocate(4).putInt(integer).array(); + // Only store the least significant byte (the others should be 0 TODO check) + bytes[i] = rawBytes[3]; + } + + return bytes; + } + + private boolean verifySignature(String algorithm, byte[] signingInput, byte[] signature, PublicKey publicKey) { + try { + Signature verifier = switch (algorithm) { + case "RS256" -> Signature.getInstance("SHA256withRSA"); + case "EdDSA" -> Signature.getInstance(((EdECPublicKey) publicKey).getParams().getName()); + default -> + throw new UnknownAlgorithmException(String.format("Unsupported signature algorithm '%s'", algorithm)); + }; + + verifier.initVerify(publicKey); + verifier.update(signingInput); + + return verifier.verify(signature); + } catch (Exception e) { + throw new RuntimeException("Unable to validate JWT signature", e); + } + } + + private boolean canRecognizeAlg(String alg) { + return switch (alg) { + case "RS256", "EdDSA" -> true; + default -> false; + }; + } + + private JsonObject decodeJwtBody(String body) { + return _decodedJwtBodyByEncodedBody.computeIfAbsent(body, key -> { + // TODO: Switch to stream + String decodedBody = new String(Base64.getUrlDecoder().decode(body), StandardCharsets.UTF_8); + return gson.fromJson(decodedBody, JsonObject.class); + }); + } + + private JwtHeader decodeJwtHeader(String header) { + return _decodedJwtHeaderByEncodedHeader.computeIfAbsent(header, key -> { + Base64.Decoder base64 = Base64.getDecoder(); + String decodedHeader = new String(base64.decode(header), StandardCharsets.UTF_8); + return new JwtHeader(gson.fromJson(decodedHeader, JsonObject.class)); + }); + } + + class JwtHeader { + private final JsonObject _jsonObject; + + JwtHeader(JsonObject jsonObject) { + _jsonObject = jsonObject; + } + + String getAlgorithm() { + return getString("alg"); + } + + String getKeyId() { + return getString("kid"); + } + + String getString(String name) { + return JsonUtils.getString(_jsonObject, name); + } + } } diff --git a/src/main/java/io/curity/oauth/AuthenticatedUser.java b/src/main/java/io/curity/oauth/AuthenticatedUser.java index e37d9cb..0cd4632 100644 --- a/src/main/java/io/curity/oauth/AuthenticatedUser.java +++ b/src/main/java/io/curity/oauth/AuthenticatedUser.java @@ -16,52 +16,46 @@ package io.curity.oauth; -import javax.json.JsonObject; -import javax.json.JsonValue; import java.util.Objects; import java.util.Set; -public class AuthenticatedUser -{ - private final String _sub; - private final Set _scopes; - private final JsonData _jsonData; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; - private AuthenticatedUser(String subject, Set scopes, JsonData jsonData) - { - _sub = subject; - _scopes = scopes; - _jsonData = jsonData; - } +public class AuthenticatedUser { + private final String _sub; + private final Set _scopes; + private final JsonData _jsonData; - public String getSubject() - { - return _sub; - } + private AuthenticatedUser(String subject, Set scopes, JsonData jsonData) { + _sub = subject; + _scopes = scopes; + _jsonData = jsonData; + } - public Set getScopes() - { - return _scopes; - } + public String getSubject() { + return _sub; + } - public JsonValue getClaim(String name) - { - return _jsonData.getClaim(name); - } + public Set getScopes() { + return _scopes; + } - public JsonObject getClaims() - { - return _jsonData.getClaims(); - } + public JsonElement getClaim(String name) { + return _jsonData.getClaim(name); + } - static AuthenticatedUser from(JsonData tokenData) - { - Objects.requireNonNull(tokenData); + public JsonObject getClaims() { + return _jsonData.getClaims(); + } - String subject = tokenData.getSubject(); + static AuthenticatedUser from(JsonData tokenData) { + Objects.requireNonNull(tokenData); - Objects.requireNonNull(subject); + String subject = tokenData.getSubject(); - return new AuthenticatedUser(subject, tokenData.getScopes(), tokenData); - } + Objects.requireNonNull(subject); + + return new AuthenticatedUser(subject, tokenData.getScopes(), tokenData); + } } diff --git a/src/main/java/io/curity/oauth/AuthenticatedUserRequestWrapper.java b/src/main/java/io/curity/oauth/AuthenticatedUserRequestWrapper.java index 0174b28..5349557 100644 --- a/src/main/java/io/curity/oauth/AuthenticatedUserRequestWrapper.java +++ b/src/main/java/io/curity/oauth/AuthenticatedUserRequestWrapper.java @@ -16,12 +16,13 @@ package io.curity.oauth; +import java.io.IOException; +import java.security.Principal; + import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.security.Principal; class AuthenticatedUserRequestWrapper extends HttpServletRequestWrapper { @@ -31,7 +32,6 @@ class AuthenticatedUserRequestWrapper extends HttpServletRequestWrapper * @see RFC 6750 - The OAuth 2.0 Authorization Framework: Bearer * Token Usage */ - @SuppressWarnings("WeakerAccess") public static final String OAUTH_AUTH = "OAUTH"; private final HttpServletRequest _request; diff --git a/src/main/java/io/curity/oauth/JsonData.java b/src/main/java/io/curity/oauth/JsonData.java index 86ae62a..5f35527 100644 --- a/src/main/java/io/curity/oauth/JsonData.java +++ b/src/main/java/io/curity/oauth/JsonData.java @@ -1,70 +1,46 @@ -/* - * Copyright (C) 2017 Curity AB. - * - * 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 io.curity.oauth; -import javax.json.JsonObject; -import javax.json.JsonValue; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + import java.time.Instant; import java.util.Set; -public class JsonData implements Expirable -{ +public class JsonData implements Expirable { private final JsonObject _jsonObject; private final Set _scopes; - JsonData(JsonObject jsonObject) - { + JsonData(JsonObject jsonObject) { _jsonObject = jsonObject; _scopes = JsonUtils.getScopes(jsonObject); } - JsonObject getJsonObject() - { + JsonObject getJsonObject() { return _jsonObject; } - public String getSubject() - { + public String getSubject() { return JsonUtils.getString(_jsonObject, "sub"); } - public Set getScopes() - { + public Set getScopes() { return _scopes; } - public Set getClaimNames() - { + public Set getClaimNames() { return _jsonObject.keySet(); } - public JsonObject getClaims() - { + public JsonObject getClaims() { return _jsonObject; } - public JsonValue getClaim(String claimName) - { + public JsonElement getClaim(String claimName) { return _jsonObject.get(claimName); } @Override - public Instant getExpiresAt() - { + public Instant getExpiresAt() { return Instant.ofEpochSecond(JsonUtils.getLong(_jsonObject, "exp")); } -} +} \ No newline at end of file diff --git a/src/main/java/io/curity/oauth/JsonUtils.java b/src/main/java/io/curity/oauth/JsonUtils.java index 800bce3..2fac895 100644 --- a/src/main/java/io/curity/oauth/JsonUtils.java +++ b/src/main/java/io/curity/oauth/JsonUtils.java @@ -1,67 +1,37 @@ -/* - * Copyright (C) 2017 Curity AB. - * - * 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 io.curity.oauth; -import javax.json.JsonNumber; -import javax.json.JsonObject; -import javax.json.JsonReaderFactory; -import javax.json.JsonString; -import javax.json.JsonValue; -import javax.json.spi.JsonProvider; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Optional; import java.util.Set; -final class JsonUtils -{ +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +final class JsonUtils { private static final String[] NO_SCOPES = {}; - private JsonUtils() - { + private JsonUtils() { } - static JsonReaderFactory createDefaultReaderFactory() - { - return JsonProvider.provider().createReaderFactory(Collections.emptyMap()); - } - static Set getScopes(JsonObject jsonObject) - { + static Set getScopes(JsonObject jsonObject) { String scopesInToken = getString(jsonObject, "scope"); String[] presentedScopes = scopesInToken == null ? NO_SCOPES : scopesInToken.split("\\s+"); return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(presentedScopes))); } - static String getString(JsonObject jsonObject, String name) - { + static String getString(JsonObject jsonObject, String name) { return Optional.ofNullable(jsonObject.get(name)) - .filter(it -> it.getValueType() == JsonValue.ValueType.STRING) - .map(it -> ((JsonString) it).getString()) + .filter(JsonElement::isJsonPrimitive).map(it -> it.getAsJsonPrimitive().getAsString()) .orElse(null); } - static long getLong(JsonObject jsonObject, String name) - { + static long getLong(JsonObject jsonObject, String name) { return Optional.ofNullable(jsonObject.get(name)) - .filter(it -> it.getValueType() == JsonValue.ValueType.NUMBER) - .map(it -> ((JsonNumber) it).longValue()) + .filter(JsonElement::isJsonPrimitive).map(it -> it.getAsJsonPrimitive().getAsLong()) .orElse(Long.MIN_VALUE); } -} +} \ No newline at end of file diff --git a/src/main/java/io/curity/oauth/JsonWebKey.java b/src/main/java/io/curity/oauth/JsonWebKey.java index 7ae67ca..7778973 100644 --- a/src/main/java/io/curity/oauth/JsonWebKey.java +++ b/src/main/java/io/curity/oauth/JsonWebKey.java @@ -16,76 +16,59 @@ package io.curity.oauth; -import javax.json.JsonObject; -import javax.json.JsonString; -import javax.json.JsonValue; -import java.util.Optional; - -class JsonWebKey -{ - private final JsonObject _jsonObject; - - private JsonWebKeyType _keyType; - - JsonWebKey(JsonObject jsonObject) - { - _jsonObject = jsonObject; - - JsonValue jsonValue = jsonObject.get("kty"); - - _keyType = JsonWebKeyType.from(jsonValue); - } - - String getKeyId() - { - return getString("kid"); - } - - JsonWebKeyType getKeyType() - { - return _keyType; - } - - String getUse() - { - return getString("use"); - } - - String getXCoordinate() - { - return getString("x"); - } - - String getYCoordinate() - { - return getString("y"); - } - - String getEllipticalCurve() - { - return getString("crv"); - } - - String getModulus() - { - return getString("n"); - } - - String getExponent() - { - return getString("e"); - } - - String getAlgorithm() - { - return getString("alg"); - } - - private String getString(String name) - { - return Optional.ofNullable(_jsonObject.get(name)) - .filter(it -> it.getValueType() == JsonValue.ValueType.STRING) - .map(it -> ((JsonString) it).getString()) - .orElse(null); - } +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +class JsonWebKey { + private final JsonObject _jsonObject; + + private JsonWebKeyType _keyType; + + JsonWebKey(JsonObject jsonObject) { + _jsonObject = jsonObject; + + JsonElement jsonValue = jsonObject.get("kty"); + + _keyType = JsonWebKeyType.from(jsonValue); + } + + String getKeyId() { + return getString("kid"); + } + + JsonWebKeyType getKeyType() { + return _keyType; + } + + String getUse() { + return getString("use"); + } + + String getXCoordinate() { + return getString("x"); + } + + String getYCoordinate() { + return getString("y"); + } + + String getEllipticalCurve() { + return getString("crv"); + } + + String getModulus() { + return getString("n"); + } + + String getExponent() { + return getString("e"); + } + + String getAlgorithm() { + return getString("alg"); + } + + private String getString(String name) { + return _jsonObject.get(name).getAsString(); + } } diff --git a/src/main/java/io/curity/oauth/JsonWebKeyNotFoundException.java b/src/main/java/io/curity/oauth/JsonWebKeyNotFoundException.java index 2f4f722..a757d42 100644 --- a/src/main/java/io/curity/oauth/JsonWebKeyNotFoundException.java +++ b/src/main/java/io/curity/oauth/JsonWebKeyNotFoundException.java @@ -20,7 +20,9 @@ class JsonWebKeyNotFoundException extends IOException { - JsonWebKeyNotFoundException(String msg) + private static final long serialVersionUID = 6941054254935232366L; + + JsonWebKeyNotFoundException(String msg) { super(msg); } diff --git a/src/main/java/io/curity/oauth/JsonWebKeyType.java b/src/main/java/io/curity/oauth/JsonWebKeyType.java index 9d80050..2567a04 100644 --- a/src/main/java/io/curity/oauth/JsonWebKeyType.java +++ b/src/main/java/io/curity/oauth/JsonWebKeyType.java @@ -16,49 +16,39 @@ package io.curity.oauth; -import javax.json.JsonString; -import javax.json.JsonValue; import java.util.logging.Logger; -enum JsonWebKeyType { - EC("EC"), - OCT("oct"), - OKP("OKP"), - RSA("RSA"), - UNSPECIFIED("UNSPECIFIED"); - - private static final Logger _logger = Logger.getLogger(JsonWebKeyType.class.getName()); - String name; - - JsonWebKeyType(String name) { - this.name = name; - } - - static JsonWebKeyType from(JsonValue value) { - if (value == null || value.toString().length() == 0) { - return UNSPECIFIED; - } - - if (value.getValueType() != JsonValue.ValueType.STRING) { - _logger.warning(() -> String.format("Value '%s' is not a string, as required; it is %s", - value, value.getValueType())); - } +import com.google.gson.JsonElement; - switch (((JsonString) value).getString()) { - case "RSA": - return RSA; - case "EC": - return EC; - case "OKP": - return OKP; - case "oct": - return OCT; - default: - - _logger.warning(() -> String.format("Unknown enumeration value '%s' given.", value)); - - throw new IllegalArgumentException("value"); - } - } +enum JsonWebKeyType { + EC("EC"), OCT("oct"), OKP("OKP"), RSA("RSA"), UNSPECIFIED("UNSPECIFIED"); + + private static final Logger _logger = Logger.getLogger(JsonWebKeyType.class.getName()); + String name; + + JsonWebKeyType(String name) { + this.name = name; + } + + static JsonWebKeyType from(JsonElement value) { + if (value == null || value.toString().length() == 0) { + return UNSPECIFIED; + } + + switch (value.getAsString()) { + case "RSA": + return RSA; + case "EC": + return EC; + case "OKP": + return OKP; + case "oct": + return OCT; + default: + + _logger.warning(() -> String.format("Unknown enumeration value '%s' given.", value)); + + throw new IllegalArgumentException("value"); + } + } } - diff --git a/src/main/java/io/curity/oauth/JwkManager.java b/src/main/java/io/curity/oauth/JwkManager.java index 808a65e..0821fbb 100644 --- a/src/main/java/io/curity/oauth/JwkManager.java +++ b/src/main/java/io/curity/oauth/JwkManager.java @@ -16,12 +16,8 @@ package io.curity.oauth; -import javax.json.JsonObject; -import javax.json.JsonReader; -import javax.json.JsonReaderFactory; import java.io.Closeable; import java.io.IOException; -import java.io.StringReader; import java.time.Duration; import java.time.Instant; import java.util.Collections; @@ -33,102 +29,89 @@ import java.util.logging.Level; import java.util.logging.Logger; -final class JwkManager implements Closeable -{ - private static final Logger _logger = Logger.getLogger(JwkManager.class.getName()); - private static final String ACCEPT = "Accept"; - - private final TimeBasedCache _jsonWebKeyByKID; - private final WebKeysClient _webKeysClient; - private final ScheduledExecutorService _executor = Executors.newSingleThreadScheduledExecutor(); - private final JsonReaderFactory _jsonReaderFactory; - - JwkManager(long minKidReloadTimeInSeconds, WebKeysClient webKeysClient, JsonReaderFactory jsonReaderFactory) - { - _jsonWebKeyByKID = new TimeBasedCache<>(Duration.ofSeconds(minKidReloadTimeInSeconds), this::reload); - _webKeysClient = webKeysClient; - _jsonReaderFactory = jsonReaderFactory; - - // invalidate the cache periodically to avoid stale state - _executor.scheduleAtFixedRate(this::ensureCacheIsFresh, 5, 15, TimeUnit.MINUTES); - } - - /** - * checks if the JsonWebKey exists in the local cached, otherwise this - * method will call the JsonWebKeyService to get the new keys. - * - * @param keyId keyId - * @return JsonWebKey - */ - JsonWebKey getJsonWebKeyForKeyId(String keyId) throws JsonWebKeyNotFoundException - { - JsonWebKey key = _jsonWebKeyByKID.get(keyId); - - if (key != null) - { - return key; - } - - throw new JsonWebKeyNotFoundException("Json Web Key does not exist: keyid=" + keyId); - } - - private Map reload() - { - Map newKeys = new HashMap<>(); - - try - { - JwksResponse response = parseJwksResponse(_webKeysClient.getKeys()); - - for (JsonWebKey key : response.getKeys()) - { - newKeys.put(key.getKeyId(), key); - } - - _logger.info(() -> String.format("Fetched JsonWebKeys: %s", newKeys)); - - return Collections.unmodifiableMap(newKeys); - } - catch (IOException e) - { - _logger.log(Level.SEVERE, "Could not contact JWKS Server", e); - - return Collections.emptyMap(); - } - } - - private JwksResponse parseJwksResponse(String response) - { - JsonReader jsonReader = _jsonReaderFactory.createReader(new StringReader(response)); - JsonObject jsonObject = jsonReader.readObject(); - - return new JwksResponse(jsonObject); - } - - private void ensureCacheIsFresh() - { - _logger.info("Called ensureCacheIsFresh"); - - Instant lastLoading = _jsonWebKeyByKID.getLastReloadInstant().orElse(Instant.MIN); - boolean cacheIsNotFresh = lastLoading.isBefore(Instant.now() - .minus(_jsonWebKeyByKID.getMinTimeBetweenReloads())); - - if (cacheIsNotFresh) - { - _logger.info("Invalidating JSON WebKeyID cache"); - - _jsonWebKeyByKID.clear(); - } - } - - @Override - public void close() throws IOException - { - _executor.shutdown(); - - if (_webKeysClient instanceof Closeable) - { - ((Closeable) _webKeysClient).close(); - } - } +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +final class JwkManager implements Closeable { + private static final Logger _logger = Logger.getLogger(JwkManager.class.getName()); + + private final TimeBasedCache _jsonWebKeyByKID; + private final WebKeysClient _webKeysClient; + private final ScheduledExecutorService _executor = Executors.newSingleThreadScheduledExecutor(); + private final Gson _gson; + + JwkManager(long minKidReloadTimeInSeconds, WebKeysClient webKeysClient, Gson gson) { + _gson = gson; + _jsonWebKeyByKID = new TimeBasedCache<>(Duration.ofSeconds(minKidReloadTimeInSeconds), this::reload); + _webKeysClient = webKeysClient; + + // invalidate the cache periodically to avoid stale state + _executor.scheduleAtFixedRate(this::ensureCacheIsFresh, 5, 15, TimeUnit.MINUTES); + } + + /** + * checks if the JsonWebKey exists in the local cached, otherwise this method + * will call the JsonWebKeyService to get the new keys. + * + * @param keyId keyId + * @return JsonWebKey + */ + JsonWebKey getJsonWebKeyForKeyId(String keyId) throws JsonWebKeyNotFoundException { + JsonWebKey key = _jsonWebKeyByKID.get(keyId); + + if (key != null) { + return key; + } + + throw new JsonWebKeyNotFoundException("Json Web Key does not exist: keyid=" + keyId); + } + + private Map reload() { + Map newKeys = new HashMap<>(); + + try { + JwksResponse response = parseJwksResponse(_webKeysClient.getKeys()); + + for (JsonWebKey key : response.getKeys()) { + newKeys.put(key.getKeyId(), key); + } + + _logger.info(() -> String.format("Fetched JsonWebKeys: %s", newKeys)); + + return Collections.unmodifiableMap(newKeys); + } catch (IOException e) { + _logger.log(Level.SEVERE, "Could not contact JWKS Server", e); + + return Collections.emptyMap(); + } + } + + private JwksResponse parseJwksResponse(String response) { + JsonObject jsonObject = _gson.fromJson(response, JsonObject.class); + + return new JwksResponse(jsonObject); + } + + private void ensureCacheIsFresh() { + _logger.info("Called ensureCacheIsFresh"); + + Instant lastLoading = _jsonWebKeyByKID.getLastReloadInstant().orElse(Instant.MIN); + boolean cacheIsNotFresh = lastLoading + .isBefore(Instant.now().minus(_jsonWebKeyByKID.getMinTimeBetweenReloads())); + + if (cacheIsNotFresh) { + _logger.info("Invalidating JSON WebKeyID cache"); + + _jsonWebKeyByKID.clear(); + } + } + + @Override + public void close() throws IOException { + _executor.shutdown(); + + if (_webKeysClient instanceof Closeable) { + ((Closeable) _webKeysClient).close(); + } + } } diff --git a/src/main/java/io/curity/oauth/JwksResponse.java b/src/main/java/io/curity/oauth/JwksResponse.java index 64d1c0b..2b547eb 100644 --- a/src/main/java/io/curity/oauth/JwksResponse.java +++ b/src/main/java/io/curity/oauth/JwksResponse.java @@ -1,51 +1,28 @@ -/* - * Copyright (C) 2016 Curity AB. - * - * 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 io.curity.oauth; -import javax.json.JsonObject; -import javax.json.JsonValue; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -class JwksResponse -{ - private final List _keys; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; - JwksResponse(JsonObject jsonObject) - { - JsonValue keys = jsonObject.get("keys"); +class JwksResponse { + private final List _keys; - if (keys.getValueType() != JsonValue.ValueType.ARRAY) - { - _keys = Collections.emptyList(); - } - else - { - _keys = keys.asJsonArray().stream() - .filter(it -> it.getValueType() == JsonValue.ValueType.OBJECT) - .map(JsonValue::asJsonObject) - .map(JsonWebKey::new) - .collect(Collectors.toList()); - } - } + JwksResponse(JsonObject jsonObject) { + JsonElement keys = jsonObject.get("keys"); - List getKeys() - { - return _keys; - } + if (!keys.isJsonArray()) { + _keys = Collections.emptyList(); + } else { + _keys = keys.getAsJsonArray().asList().stream().filter(it -> it.isJsonObject()) + .map(JsonElement::getAsJsonObject).map(JsonWebKey::new).collect(Collectors.toList()); + } + } + + List getKeys() { + return _keys; + } } diff --git a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java index 35db2b8..ddfe0ee 100644 --- a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java +++ b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java @@ -16,34 +16,22 @@ package io.curity.oauth; -import javax.json.JsonReaderFactory; import java.security.PublicKey; import java.util.Map; import java.util.Optional; -import java.util.logging.Logger; -final class JwtValidatorWithCert extends AbstractJwtValidator -{ - private static final Logger _logger = Logger.getLogger(JwtValidatorWithCert.class.getName()); +final class JwtValidatorWithCert extends AbstractJwtValidator { - private final Map _keys; + private final Map _keys; - JwtValidatorWithCert(String issuer, String audience, Map publicKeys) - { - this(issuer, audience, publicKeys, JsonUtils.createDefaultReaderFactory()); - } + JwtValidatorWithCert(String issuer, String audience, Map publicKeys) { + super(issuer, audience); - JwtValidatorWithCert(String issuer, String audience, Map publicKeys, - JsonReaderFactory jsonReaderFactory) - { - super(issuer, audience, jsonReaderFactory); - - _keys = publicKeys; - } + _keys = publicKeys; + } - @Override - protected Optional getPublicKey(JwtHeader jwtHeader) - { - return Optional.ofNullable(_keys.get(jwtHeader.getString("x5t#S256"))); - } + @Override + protected Optional getPublicKey(JwtHeader jwtHeader) { + return Optional.ofNullable(_keys.get(jwtHeader.getString("x5t#S256"))); + } } diff --git a/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java b/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java index 8260a6a..12789d7 100644 --- a/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java +++ b/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java @@ -16,7 +16,6 @@ package io.curity.oauth; -import javax.json.JsonReaderFactory; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; @@ -25,68 +24,63 @@ import java.util.logging.Level; import java.util.logging.Logger; -final class JwtValidatorWithJwk extends AbstractJwtValidator -{ - private static final Logger _logger = Logger.getLogger(JwtValidatorWithJwk.class.getName()); +import com.google.gson.Gson; - private final JwkManager _jwkManager; +final class JwtValidatorWithJwk extends AbstractJwtValidator { + private static final Logger _logger = Logger.getLogger(JwtValidatorWithJwk.class.getName()); - JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, String audience, String issuer, - JsonReaderFactory jsonReaderFactory) - { - super(issuer, audience, jsonReaderFactory); - - _jwkManager = new JwkManager(minKidReloadTime, webKeysClient, jsonReaderFactory); - } + private final JwkManager _jwkManager; - @Override - protected Optional getPublicKey(JwtHeader jwtHeader) - { - Optional result = Optional.empty(); + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, String audience, String issuer, Gson gson) { + super(issuer, audience); - try - { - JsonWebKey jsonWebKeyType = _jwkManager.getJsonWebKeyForKeyId(jwtHeader.getKeyId()); + _jwkManager = new JwkManager(minKidReloadTime, webKeysClient, gson); + } - switch (jsonWebKeyType.getKeyType()) { - case RSA : - result = Optional.of(RsaPublicKeyCreator.createPublicKey(jsonWebKeyType.getModulus(), - jsonWebKeyType.getExponent())); - break; - case OKP : - if (isEdDSAKey(jsonWebKeyType)) { - result = Optional.of(EdDSAPublicKeyCreator.createPublicKey(jsonWebKeyType.getEllipticalCurve(), jsonWebKeyType.getXCoordinate())); - } else { - throw new NoSuchAlgorithmException(String.format("Unsupported curve %s for key %s", jsonWebKeyType.getEllipticalCurve(), jsonWebKeyType.getKeyId())); - } - break; - case EC : - case OCT : - default: - throw new NoSuchAlgorithmException(String.format("Unsupported key type %s for key %s", jsonWebKeyType.getKeyType(), jsonWebKeyType.getKeyId())); - } - } - catch (JsonWebKeyNotFoundException e) - { - // this is not a very exceptional occurrence, so let's not log a stack-trace - _logger.info(() -> String.format("Could not find requested JsonWebKey: %s", e)); - } - catch (NoSuchAlgorithmException | InvalidKeySpecException e) - { - _logger.log(Level.WARNING, "Could not create public key", e); - } + @Override + protected Optional getPublicKey(JwtHeader jwtHeader) { + Optional result = Optional.empty(); - return result; - } + try { + JsonWebKey jsonWebKeyType = _jwkManager.getJsonWebKeyForKeyId(jwtHeader.getKeyId()); - private boolean isEdDSAKey(JsonWebKey jsonWebKey) { - String curve = jsonWebKey.getEllipticalCurve(); - return curve != null && (curve.equals("Ed25519") || curve.equals("Ed448")); - } + switch (jsonWebKeyType.getKeyType()) { + case RSA: + result = Optional.of( + RsaPublicKeyCreator.createPublicKey(jsonWebKeyType.getModulus(), jsonWebKeyType.getExponent())); + break; + case OKP: + if (isEdDSAKey(jsonWebKeyType)) { + result = Optional.of(EdDSAPublicKeyCreator.createPublicKey(jsonWebKeyType.getEllipticalCurve(), + jsonWebKeyType.getXCoordinate())); + } else { + throw new NoSuchAlgorithmException(String.format("Unsupported curve %s for key %s", + jsonWebKeyType.getEllipticalCurve(), jsonWebKeyType.getKeyId())); + } + break; + case EC: + case OCT: + default: + throw new NoSuchAlgorithmException(String.format("Unsupported key type %s for key %s", + jsonWebKeyType.getKeyType(), jsonWebKeyType.getKeyId())); + } + } catch (JsonWebKeyNotFoundException e) { + // this is not a very exceptional occurrence, so let's not log a stack-trace + _logger.info(() -> String.format("Could not find requested JsonWebKey: %s", e)); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + _logger.log(Level.WARNING, "Could not create public key", e); + } - @Override - public void close() throws IOException - { - _jwkManager.close(); - } + return result; + } + + private boolean isEdDSAKey(JsonWebKey jsonWebKey) { + String curve = jsonWebKey.getEllipticalCurve(); + return curve != null && (curve.equals("Ed25519") || curve.equals("Ed448")); + } + + @Override + public void close() throws IOException { + _jwkManager.close(); + } } diff --git a/src/main/java/io/curity/oauth/OAuthIntrospectResponse.java b/src/main/java/io/curity/oauth/OAuthIntrospectResponse.java index effa3f7..d664935 100644 --- a/src/main/java/io/curity/oauth/OAuthIntrospectResponse.java +++ b/src/main/java/io/curity/oauth/OAuthIntrospectResponse.java @@ -16,24 +16,20 @@ package io.curity.oauth; -import javax.json.JsonObject; +import com.google.gson.JsonObject; -class OAuthIntrospectResponse -{ - private final JsonObject _jsonObject; +class OAuthIntrospectResponse { + private final JsonObject _jsonObject; - OAuthIntrospectResponse(JsonObject jsonObject) - { - _jsonObject = jsonObject; - } + OAuthIntrospectResponse(JsonObject jsonObject) { + _jsonObject = jsonObject; + } - boolean isActive() - { - return _jsonObject.getBoolean("active"); - } + boolean isActive() { + return _jsonObject.get("active").getAsBoolean(); + } - JsonObject getJsonObject() - { - return _jsonObject; - } + JsonObject getJsonObject() { + return _jsonObject; + } } diff --git a/src/main/java/io/curity/oauth/OAuthJwtFilter.java b/src/main/java/io/curity/oauth/OAuthJwtFilter.java index b574cf7..60d5817 100644 --- a/src/main/java/io/curity/oauth/OAuthJwtFilter.java +++ b/src/main/java/io/curity/oauth/OAuthJwtFilter.java @@ -16,13 +16,15 @@ package io.curity.oauth; -import javax.json.JsonReaderFactory; -import javax.json.spi.JsonProvider; +import java.util.Map; +import java.util.logging.Logger; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.UnavailableException; -import java.util.Map; -import java.util.logging.Logger; public class OAuthJwtFilter extends OAuthFilter { @@ -69,13 +71,13 @@ protected TokenValidator createTokenValidator(Map filterConfig) throw // Pass all of the filter's config to the ReaderFactory factory method. It'll ignore anything it doesn't // understand (per JSR 353). This way, clients can change the provider using the service locator and configure // the ReaderFactory using the filter's config. - JsonReaderFactory jsonReaderFactory = JsonProvider.provider().createReaderFactory(filterConfig); + Gson gson = new GsonBuilder().create(); WebKeysClient webKeysClient = HttpClientProvider.provider().createWebKeysClient(filterConfig); String audience = FilterHelper.getInitParamValue(InitParams.AUDIENCE, filterConfig); String issuer = FilterHelper.getInitParamValue(InitParams.ISSUER, filterConfig); return _jwtValidator = new JwtValidatorWithJwk(_minKidReloadTimeInSeconds, webKeysClient, audience, issuer, - jsonReaderFactory); + gson); } @Override diff --git a/src/main/java/io/curity/oauth/OAuthOpaqueFilter.java b/src/main/java/io/curity/oauth/OAuthOpaqueFilter.java index f09f724..7510f44 100644 --- a/src/main/java/io/curity/oauth/OAuthOpaqueFilter.java +++ b/src/main/java/io/curity/oauth/OAuthOpaqueFilter.java @@ -16,60 +16,51 @@ package io.curity.oauth; -import javax.json.JsonReaderFactory; -import javax.json.spi.JsonProvider; +import java.util.Map; +import java.util.logging.Logger; + +import com.google.gson.Gson; + import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.UnavailableException; -import java.util.Map; -import java.util.logging.Logger; -public class OAuthOpaqueFilter extends OAuthFilter -{ - private static final Logger _logger = Logger.getLogger(OAuthOpaqueFilter.class.getName()); +public class OAuthOpaqueFilter extends OAuthFilter { + private static final Logger _logger = Logger.getLogger(OAuthOpaqueFilter.class.getName()); - private TokenValidator _opaqueTokenValidator = null; + private TokenValidator _opaqueTokenValidator = null; - private interface InitParams - { - String SCOPE = "scope"; - } + private interface InitParams { + String SCOPE = "scope"; + } - @Override - public void init(FilterConfig filterConfig) throws ServletException - { - super.init(filterConfig); + @Override + public void init(FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); - synchronized (this) - { - if (_opaqueTokenValidator == null) - { - _opaqueTokenValidator = createTokenValidator(getFilterConfiguration()); + synchronized (this) { + if (_opaqueTokenValidator == null) { + _opaqueTokenValidator = createTokenValidator(getFilterConfiguration()); - _logger.info(() -> String.format("%s successfully initialized", OAuthFilter.class.getSimpleName())); - } - else - { - _logger.warning("Attempted to set introspect URI more than once! Ignoring further attempts."); - } - } - } + _logger.info(() -> String.format("%s successfully initialized", OAuthFilter.class.getSimpleName())); + } else { + _logger.warning("Attempted to set introspect URI more than once! Ignoring further attempts."); + } + } + } - @Override - protected TokenValidator getTokenValidator() - { - return _opaqueTokenValidator; - } + @Override + protected TokenValidator getTokenValidator() { + return _opaqueTokenValidator; + } - @Override - protected TokenValidator createTokenValidator(Map initParams) throws UnavailableException - { - // Like in the OAuthJwtFilter, we'll reuse the config of this filter + the service locator to - // get a JsonReaderFactory - JsonReaderFactory jsonReaderFactory = JsonProvider.provider().createReaderFactory(initParams); - IntrospectionClient introspectionClient = HttpClientProvider.provider() - .createIntrospectionClient(initParams); + @Override + protected TokenValidator createTokenValidator(Map initParams) throws UnavailableException { + // Like in the OAuthJwtFilter, we'll reuse the config of this filter + the + // service locator to + // get a JsonReaderFactory + IntrospectionClient introspectionClient = HttpClientProvider.provider().createIntrospectionClient(initParams); - return new OpaqueTokenValidator(introspectionClient, jsonReaderFactory); - } + return new OpaqueTokenValidator(introspectionClient, new Gson()); + } } diff --git a/src/main/java/io/curity/oauth/OpaqueTokenValidator.java b/src/main/java/io/curity/oauth/OpaqueTokenValidator.java index e50c88c..df232e0 100644 --- a/src/main/java/io/curity/oauth/OpaqueTokenValidator.java +++ b/src/main/java/io/curity/oauth/OpaqueTokenValidator.java @@ -16,86 +16,69 @@ package io.curity.oauth; -import javax.json.JsonObject; -import javax.json.JsonReader; -import javax.json.JsonReaderFactory; import java.io.Closeable; import java.io.IOException; -import java.io.StringReader; import java.time.Instant; import java.util.Optional; -public class OpaqueTokenValidator implements Closeable, TokenValidator -{ - private final IntrospectionClient _introspectionClient; - private final ExpirationBasedCache _tokenCache; - private final JsonReaderFactory _jsonReaderFactory; - - OpaqueTokenValidator(IntrospectionClient introspectionClient, JsonReaderFactory jsonReaderFactory) - { - _introspectionClient = introspectionClient; - _tokenCache = new ExpirationBasedCache<>(); - _jsonReaderFactory = jsonReaderFactory; - } +import com.google.gson.Gson; +import com.google.gson.JsonObject; - public JsonData validate(String token) throws TokenValidationException - { - Optional cachedValue = _tokenCache.get(token); +public class OpaqueTokenValidator implements Closeable, TokenValidator { + private final IntrospectionClient _introspectionClient; + private final ExpirationBasedCache _tokenCache; + private final Gson _gson; - if (cachedValue.isPresent()) - { - return cachedValue.get(); - } + OpaqueTokenValidator(IntrospectionClient introspectionClient, Gson gson) { + _introspectionClient = introspectionClient; + _tokenCache = new ExpirationBasedCache<>(); + _gson = gson; + } - String introspectJson; + public JsonData validate(String token) throws TokenValidationException { + Optional cachedValue = _tokenCache.get(token); - try - { - introspectJson = _introspectionClient.introspect(token); - } - catch (Exception e) - { - // TODO: Add logging - throw new TokenValidationException("Failed to introspect token", e); - } + if (cachedValue.isPresent()) { + return cachedValue.get(); + } - OAuthIntrospectResponse response = parseIntrospectResponse(introspectJson); + String introspectJson; - if (response.isActive()) - { - JsonData newToken = new JsonData(response.getJsonObject()); + try { + introspectJson = _introspectionClient.introspect(token); + } catch (Exception e) { + // TODO: Add logging + throw new TokenValidationException("Failed to introspect token", e); + } - if (newToken.getExpiresAt().isAfter(Instant.now())) - { - //Note: If this cache is backed by some persistent storage, the token should be hashed and not stored - // in clear text - _tokenCache.put(token, newToken); + OAuthIntrospectResponse response = parseIntrospectResponse(introspectJson); - return newToken; - } - else - { - throw new ExpiredTokenException(); - } - } - else - { - throw new RevokedTokenException(); - } - } + if (response.isActive()) { + JsonData newToken = new JsonData(response.getJsonObject()); - private OAuthIntrospectResponse parseIntrospectResponse(String introspectJson) - { - JsonReader jsonReader = _jsonReaderFactory.createReader(new StringReader(introspectJson)); - JsonObject jsonObject = jsonReader.readObject(); + if (newToken.getExpiresAt().isAfter(Instant.now())) { + // Note: If this cache is backed by some persistent storage, the token should be + // hashed and not stored + // in clear text + _tokenCache.put(token, newToken); - return new OAuthIntrospectResponse(jsonObject); - } + return newToken; + } else { + throw new ExpiredTokenException(); + } + } else { + throw new RevokedTokenException(); + } + } - @Override - public void close() throws IOException - { - _introspectionClient.close(); - _tokenCache.clear(); - } + private OAuthIntrospectResponse parseIntrospectResponse(String introspectJson) { + JsonObject jsonObject = _gson.fromJson(introspectJson, JsonObject.class); + return new OAuthIntrospectResponse(jsonObject); + } + + @Override + public void close() throws IOException { + _introspectionClient.close(); + _tokenCache.clear(); + } } diff --git a/src/main/java/io/curity/oauth/TokenValidationException.java b/src/main/java/io/curity/oauth/TokenValidationException.java index 7015fff..0327f46 100644 --- a/src/main/java/io/curity/oauth/TokenValidationException.java +++ b/src/main/java/io/curity/oauth/TokenValidationException.java @@ -1,5 +1,4 @@ /* - * Copyright (C) 2017 Curity AB. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +17,9 @@ public class TokenValidationException extends Exception { - public TokenValidationException(String msg) + private static final long serialVersionUID = 2986832328622504264L; + + public TokenValidationException(String msg) { super(msg); } diff --git a/src/test/java/io/curity/oauth/AuthenticatedUserRequestWrapperTest.java b/src/test/java/io/curity/oauth/AuthenticatedUserRequestWrapperTest.java index 3daafae..0779f3a 100644 --- a/src/test/java/io/curity/oauth/AuthenticatedUserRequestWrapperTest.java +++ b/src/test/java/io/curity/oauth/AuthenticatedUserRequestWrapperTest.java @@ -16,22 +16,23 @@ package io.curity.oauth; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collection; + import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import javax.json.Json; -import javax.json.JsonObject; +import com.google.gson.JsonObject; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.Arrays; -import java.util.Collection; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class AuthenticatedUserRequestWrapperTest { @@ -39,7 +40,8 @@ public class AuthenticatedUserRequestWrapperTest public void testCanAuthenticate() throws Exception { // GIVEN: an authenticated user - JsonObject json = Json.createObjectBuilder().add("sub", "test-user").build(); + JsonObject json = new JsonObject(); + json.addProperty("sub", "test-user"); JsonData jsonData = new JsonData(json); AuthenticatedUser user = AuthenticatedUser.from(jsonData); diff --git a/src/test/java/io/curity/oauth/JwtWithCertTest.java b/src/test/java/io/curity/oauth/JwtWithCertTest.java index 60004f2..d919396 100644 --- a/src/test/java/io/curity/oauth/JwtWithCertTest.java +++ b/src/test/java/io/curity/oauth/JwtWithCertTest.java @@ -16,16 +16,10 @@ package io.curity.oauth; -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertTrue; -import javax.json.JsonObject; -import javax.json.JsonString; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; @@ -39,9 +33,16 @@ import java.util.HashMap; import java.util.Map; -import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertNotNull; -import static junit.framework.TestCase.assertTrue; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import com.google.gson.JsonObject; @RunWith(Parameterized.class) public class JwtWithCertTest @@ -122,11 +123,11 @@ public void testValidContentInToken() throws Exception JsonObject jsonObject = result.getJsonObject(); - assertTrue(jsonObject.containsKey("sub")); - assertTrue(jsonObject.containsKey(EXTRA_CLAIM)); + assertTrue(jsonObject.keySet().contains("sub")); + assertTrue(jsonObject.keySet().contains(EXTRA_CLAIM)); - assertEquals(SUBJECT, ((JsonString) jsonObject.get("sub")).getString()); - assertEquals(EXTRA_CLAIM_VALUE, ((JsonString) jsonObject.get(EXTRA_CLAIM)).getString()); + assertEquals(SUBJECT, jsonObject.get("sub").getAsString()); + assertEquals(EXTRA_CLAIM_VALUE, jsonObject.get(EXTRA_CLAIM).getAsString()); } /** diff --git a/src/test/java/io/curity/oauth/JwtWithJwksTest.java b/src/test/java/io/curity/oauth/JwtWithJwksTest.java index 5e59676..0aadbd4 100644 --- a/src/test/java/io/curity/oauth/JwtWithJwksTest.java +++ b/src/test/java/io/curity/oauth/JwtWithJwksTest.java @@ -16,17 +16,12 @@ package io.curity.oauth; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -import javax.json.JsonObject; -import javax.json.JsonReaderFactory; -import javax.json.JsonString; -import javax.json.spi.JsonProvider; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; @@ -36,154 +31,153 @@ import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.interfaces.EdECPrivateKey; -import java.util.Collections; import java.util.HashMap; import java.util.Map; -import static junit.framework.TestCase.assertEquals; -import static junit.framework.TestCase.assertNotNull; -import static junit.framework.TestCase.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; @RunWith(Parameterized.class) -public class JwtWithJwksTest -{ - private static final Logger _logger = LogManager.getLogger(JwtWithJwksTest.class); - - private final String SUBJECT = "testsubject"; - private final String AUDIENCE = "foo:audience"; - private final String ISSUER = "test:issuer"; - private final int EXPIRATION = 200; - private final String EXTRA_CLAIM = "TEST_KEY"; - private final String EXTRA_CLAIM_VALUE = "TEST_VALUE"; - - // Used for signing tokens - private final String PATH_TO_KEY = "/Se.Curity.Test.p12"; - private final String KEY_PWD = "Password1"; - - private String _testToken; - private KeyStore _keyStore; - - @Parameterized.Parameter() - public String _keyAlias; - - @Parameterized.Parameter(1) - public String _algorithm; - - @Parameterized.Parameter(2) - public String _keyId; - - @Parameterized.Parameters - public static Object[] keysToTest() { - return new Object[][] { {"se.curity.test", "RS256", "-38074812"}, {"se.curity.test.ed25519", "EdDSA", "-1909572257"}, {"se.curity.test.ed448", "EdDSA", "1716999904"} }; - } - - @Before - public void before() throws Exception - { - loadKeyStore(); - - PrivateKey key = getPrivateKey(); - - if (!_algorithm.equals("EdDSA")) { - // Create test token on the fly - JwtTokenIssuer issuer = new JwtTokenIssuer(ISSUER, _algorithm, key, _keyId); - Map attributes = new HashMap<>(); - attributes.put(EXTRA_CLAIM, EXTRA_CLAIM_VALUE); - _testToken = issuer.issueToken(SUBJECT, AUDIENCE, EXPIRATION, attributes); - } else { - // Use hardcoded values until jose4j supports EdDSA - String curveName = ((EdECPrivateKey) key).getParams().getName(); - if ("Ed25519".equals(curveName)) { - _testToken ="eyJraWQiOiItMTkwOTU3MjI1NyIsIng1dCI6IlNIZDRIQ1VkQThISlZHTTJVV3o1Tm1JUFRHMCIsIng1dCNTMjU2IjoiS0lZVnBHXzVXSnh0ZUdOUTVLR043M0xlQWNHS0w4MmMyWFhaR0M5RUNKVSIsImFsZyI6IkVkRFNBIn0.eyJqdGkiOiJlMzE2YjBmOS1mN2JlLTQ3M2QtOGMzNi05NGMwNzRlMjMzNjEiLCJkZWxlZ2F0aW9uSWQiOiIzZDE2NzM5Ni02NGEzLTQ2MmYtOGMyZS02MzdhYzM0NzdkMTMiLCJleHAiOjE5Njg0MDkwNzMsIm5iZiI6MTY1MzA0OTA3Mywic2NvcGUiOiJyZWFkIG9wZW5pZCIsImlzcyI6InRlc3Q6aXNzdWVyIiwic3ViIjoidGVzdHN1YmplY3QiLCJhdWQiOiJmb286YXVkaWVuY2UiLCJpYXQiOjE2NTMwNDkwNzMsInB1cnBvc2UiOiJhY2Nlc3NfdG9rZW4iLCJURVNUX0tFWSI6IlRFU1RfVkFMVUUifQ.NGWCDwzCPOx50-WBJRqKFvPy2562rqFjNS3Q9zmJqNhdxtZK3s7g7JWtgI_AwnJBnaPeC1ATMYyxKjionwzQAA"; - } else { - _testToken ="eyJraWQiOiIxNzE2OTk5OTA0IiwieDV0IjoiMUlSVEJMTFFlaUwyWVpMQjFWRER2Q1RHb3pjIiwieDV0I1MyNTYiOiJTbGVDbTlwRVI5a2ZiTjBYeGlqa1g4MmdyR0hUYXhOTkNCRHNUMHR1M3lBIiwiYWxnIjoiRWREU0EifQ.eyJqdGkiOiIyNjNiNmM2OS02NTExLTQ5YjktYWVlYi0yY2JkOGMyMGE3NGUiLCJkZWxlZ2F0aW9uSWQiOiIwY2NjZmMyZi1mY2EzLTRlOGQtOTgxYy05ZjU5MzIyNmYyNTEiLCJleHAiOjE5Njg0MDg5NzUsIm5iZiI6MTY1MzA0ODk3NSwic2NvcGUiOiJyZWFkIG9wZW5pZCIsImlzcyI6InRlc3Q6aXNzdWVyIiwic3ViIjoidGVzdHN1YmplY3QiLCJhdWQiOiJmb286YXVkaWVuY2UiLCJpYXQiOjE2NTMwNDg5NzUsInB1cnBvc2UiOiJhY2Nlc3NfdG9rZW4iLCJURVNUX0tFWSI6IlRFU1RfVkFMVUUifQ.2gcRnLTFnCsdkElgcecSjxvrKA3bKAFuUf5vhVapdLqxZvx6E1BblTzjaVjqy3OT0OzdN3p1q5kApJ5EjVUT0tdjHVxZMBtkosviYM5EL2UkJO_T3tA-on7h0lfcufxnhd_TUOlM_YTkJxFGSkOtLg4A"; - } - } - } - - @Test - public void testFindAndValidateWithOneJwk() throws Exception - { - JsonReaderFactory jsonReaderFactory = JsonProvider.provider().createReaderFactory(Collections.emptyMap()); - WebKeysClient webKeysClient = mock(WebKeysClient.class); - - JwtValidatorWithJwk validator = new JwtValidatorWithJwk(0, webKeysClient, AUDIENCE, ISSUER,jsonReaderFactory); - when(webKeysClient.getKeys()).thenReturn(prepareKeyMap().get(_keyId)); - _logger.info("test token = {}", _testToken); - - JsonData validatedToken = validator.validate(_testToken); - - assertNotNull(validatedToken); - } - - @Test - public void testValidContentInToken() throws Exception - { - JsonReaderFactory jsonReaderFactory = JsonProvider.provider().createReaderFactory(Collections.emptyMap()); - WebKeysClient webKeysClient = mock(WebKeysClient.class); - - JwtValidatorWithJwk validator = new JwtValidatorWithJwk(0, webKeysClient, AUDIENCE, ISSUER,jsonReaderFactory); - when(webKeysClient.getKeys()).thenReturn(prepareKeyMap().get(_keyId)); - _logger.info("test token = {}", _testToken); - - JsonData result = validator.validate(_testToken); - - _logger.info("test token = {}", _testToken); - - assertNotNull(result); - - JsonObject jsonObject = result.getJsonObject(); - - assertTrue(jsonObject.containsKey("sub")); - assertTrue(jsonObject.containsKey(EXTRA_CLAIM)); - - assertEquals(SUBJECT, ((JsonString) jsonObject.get("sub")).getString()); - assertEquals(EXTRA_CLAIM_VALUE, ((JsonString) jsonObject.get(EXTRA_CLAIM)).getString()); - } - - /** - * Load the private keymap with the kid and the jwks - * The map only contains a single key - * @return a map with a single entry representing a JWKS that contains the key with the keyid - */ - private Map prepareKeyMap() - { - Map keys = new HashMap<>(); - - keys.put("-38074812","{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"-38074812\",\"use\":\"sig\",\"alg\":\"RS256\",\"n\":\"yMAHZiIfbAgmZJ-_4Gj-wdS8rvaKNBbnHz_krmd-kkX51bA1EsUc0CN672-xnUb_-E_-u_GoWhJzdjiBuz9XasSfQK8WyAwbc7MLkw40A7Zxl2sfsxGTod3qi1u8mjguoc9CbVqPdYe_9YPVxoK4CeJz6V8AsPcxVJxYq6os1rI9qFx_6a1JdQEhetGtkHLFvwo80UTzKXKhGXSu96WrXnkDE8Kw5TSKvh2gI_BX4QHXjE82xldJRJ8QIXGpRNbdyzGkUdjsrhmZl3ARC9IUlxmowkcEEIzjfbOKBVGrVcJ7rHb0GYNaKtMB_MlH1uAPDxl6qKeXOAZ8YEZ1r0ToPw\",\"e\":\"AQAB\",\"x5t\":\"MR-pGTa866RdZLjN6Vwrfay907g\"}]}"); - keys.put("-1909572257", "{\"keys\":[{\"kty\":\"OKP\",\"kid\":\"-1909572257\",\"use\":\"sig\",\"alg\":\"EdDSA\",\"crv\":\"Ed25519\",\"x\":\"XWxGtApfcqmKI7p0OKnF5JSEWMVoLsytFXLEP7xZ_l8\",\"x5t\":\"SHd4HCUdA8HJVGM2UWz5NmIPTG0\"}]}"); - keys.put("1716999904","{\"keys\":[{\"kty\":\"OKP\",\"kid\":\"1716999904\",\"use\":\"sig\",\"alg\":\"EdDSA\",\"crv\":\"Ed448\",\"x\":\"lDc565Rydl9MUCoOB9JpGV3pUSHm7FvuiuEMvrvRkS7PeYL41rPU6s2rMdLeHiXfSxvR1veh4C0A\",\"x5t\":\"1IRTBLLQeiL2YZLB1VDDvCTGozc\"}]}"); - return keys; - } - - private void loadKeyStore() - throws Exception - { - URL url = getClass().getResource(PATH_TO_KEY); - assert url != null; - File certFile = new File(url.getFile()); - - InputStream keyIS = new FileInputStream(certFile); - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(keyIS, KEY_PWD.toCharArray()); - - keyIS.close(); - - this._keyStore=keyStore; - } - - private PrivateKey getPrivateKey() - throws Exception - { - return (PrivateKey)this._keyStore.getKey(_keyAlias, KEY_PWD.toCharArray()); - - } - - private Certificate getCertificate() throws KeyStoreException { - //Get key by alias (found in the p12 file using: - //keytool -list -keystore test-root-ca.p12 -storepass foobar -storetype PKCS12 - return this._keyStore.getCertificate(_keyAlias); - } +public class JwtWithJwksTest { + private static final Logger _logger = LogManager.getLogger(JwtWithJwksTest.class); + + private final String SUBJECT = "testsubject"; + private final String AUDIENCE = "foo:audience"; + private final String ISSUER = "test:issuer"; + private final int EXPIRATION = 200; + private final String EXTRA_CLAIM = "TEST_KEY"; + private final String EXTRA_CLAIM_VALUE = "TEST_VALUE"; + + // Used for signing tokens + private final String PATH_TO_KEY = "/Se.Curity.Test.p12"; + private final String KEY_PWD = "Password1"; + + private String _testToken; + private KeyStore _keyStore; + + @Parameterized.Parameter() + public String _keyAlias; + + @Parameterized.Parameter(1) + public String _algorithm; + + @Parameterized.Parameter(2) + public String _keyId; + + @Parameterized.Parameters + public static Object[] keysToTest() { + return new Object[][] { { "se.curity.test", "RS256", "-38074812" }, + { "se.curity.test.ed25519", "EdDSA", "-1909572257" }, + { "se.curity.test.ed448", "EdDSA", "1716999904" } }; + } + + @Before + public void before() throws Exception { + loadKeyStore(); + + PrivateKey key = getPrivateKey(); + + if (!_algorithm.equals("EdDSA")) { + // Create test token on the fly + JwtTokenIssuer issuer = new JwtTokenIssuer(ISSUER, _algorithm, key, _keyId); + Map attributes = new HashMap<>(); + attributes.put(EXTRA_CLAIM, EXTRA_CLAIM_VALUE); + _testToken = issuer.issueToken(SUBJECT, AUDIENCE, EXPIRATION, attributes); + } else { + // Use hardcoded values until jose4j supports EdDSA + String curveName = ((EdECPrivateKey) key).getParams().getName(); + if ("Ed25519".equals(curveName)) { + _testToken = "eyJraWQiOiItMTkwOTU3MjI1NyIsIng1dCI6IlNIZDRIQ1VkQThISlZHTTJVV3o1Tm1JUFRHMCIsIng1dCNTMjU2IjoiS0lZVnBHXzVXSnh0ZUdOUTVLR043M0xlQWNHS0w4MmMyWFhaR0M5RUNKVSIsImFsZyI6IkVkRFNBIn0.eyJqdGkiOiJlMzE2YjBmOS1mN2JlLTQ3M2QtOGMzNi05NGMwNzRlMjMzNjEiLCJkZWxlZ2F0aW9uSWQiOiIzZDE2NzM5Ni02NGEzLTQ2MmYtOGMyZS02MzdhYzM0NzdkMTMiLCJleHAiOjE5Njg0MDkwNzMsIm5iZiI6MTY1MzA0OTA3Mywic2NvcGUiOiJyZWFkIG9wZW5pZCIsImlzcyI6InRlc3Q6aXNzdWVyIiwic3ViIjoidGVzdHN1YmplY3QiLCJhdWQiOiJmb286YXVkaWVuY2UiLCJpYXQiOjE2NTMwNDkwNzMsInB1cnBvc2UiOiJhY2Nlc3NfdG9rZW4iLCJURVNUX0tFWSI6IlRFU1RfVkFMVUUifQ.NGWCDwzCPOx50-WBJRqKFvPy2562rqFjNS3Q9zmJqNhdxtZK3s7g7JWtgI_AwnJBnaPeC1ATMYyxKjionwzQAA"; + } else { + _testToken = "eyJraWQiOiIxNzE2OTk5OTA0IiwieDV0IjoiMUlSVEJMTFFlaUwyWVpMQjFWRER2Q1RHb3pjIiwieDV0I1MyNTYiOiJTbGVDbTlwRVI5a2ZiTjBYeGlqa1g4MmdyR0hUYXhOTkNCRHNUMHR1M3lBIiwiYWxnIjoiRWREU0EifQ.eyJqdGkiOiIyNjNiNmM2OS02NTExLTQ5YjktYWVlYi0yY2JkOGMyMGE3NGUiLCJkZWxlZ2F0aW9uSWQiOiIwY2NjZmMyZi1mY2EzLTRlOGQtOTgxYy05ZjU5MzIyNmYyNTEiLCJleHAiOjE5Njg0MDg5NzUsIm5iZiI6MTY1MzA0ODk3NSwic2NvcGUiOiJyZWFkIG9wZW5pZCIsImlzcyI6InRlc3Q6aXNzdWVyIiwic3ViIjoidGVzdHN1YmplY3QiLCJhdWQiOiJmb286YXVkaWVuY2UiLCJpYXQiOjE2NTMwNDg5NzUsInB1cnBvc2UiOiJhY2Nlc3NfdG9rZW4iLCJURVNUX0tFWSI6IlRFU1RfVkFMVUUifQ.2gcRnLTFnCsdkElgcecSjxvrKA3bKAFuUf5vhVapdLqxZvx6E1BblTzjaVjqy3OT0OzdN3p1q5kApJ5EjVUT0tdjHVxZMBtkosviYM5EL2UkJO_T3tA-on7h0lfcufxnhd_TUOlM_YTkJxFGSkOtLg4A"; + } + } + } + + @Test + public void testFindAndValidateWithOneJwk() throws Exception { + WebKeysClient webKeysClient = mock(WebKeysClient.class); + + JwtValidatorWithJwk validator = new JwtValidatorWithJwk(0, webKeysClient, AUDIENCE, ISSUER, new Gson()); + when(webKeysClient.getKeys()).thenReturn(prepareKeyMap().get(_keyId)); + _logger.info("test token = {}", _testToken); + + JsonData validatedToken = validator.validate(_testToken); + + assertNotNull(validatedToken); + } + + @Test + public void testValidContentInToken() throws Exception { + WebKeysClient webKeysClient = mock(WebKeysClient.class); + + JwtValidatorWithJwk validator = new JwtValidatorWithJwk(0, webKeysClient, AUDIENCE, ISSUER, new Gson()); + when(webKeysClient.getKeys()).thenReturn(prepareKeyMap().get(_keyId)); + _logger.info("test token = {}", _testToken); + + JsonData result = validator.validate(_testToken); + + _logger.info("test token = {}", _testToken); + + assertNotNull(result); + + JsonObject jsonObject = result.getJsonObject(); + + assertTrue(jsonObject.keySet().contains("sub")); + assertTrue(jsonObject.keySet().contains(EXTRA_CLAIM)); + + assertEquals(SUBJECT, jsonObject.get("sub").getAsString()); + assertEquals(EXTRA_CLAIM_VALUE, jsonObject.get(EXTRA_CLAIM).getAsString()); + } + + /** + * Load the private keymap with the kid and the jwks The map only contains a + * single key + * + * @return a map with a single entry representing a JWKS that contains the key + * with the keyid + */ + private Map prepareKeyMap() { + Map keys = new HashMap<>(); + + keys.put("-38074812", + "{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"-38074812\",\"use\":\"sig\",\"alg\":\"RS256\",\"n\":\"yMAHZiIfbAgmZJ-_4Gj-wdS8rvaKNBbnHz_krmd-kkX51bA1EsUc0CN672-xnUb_-E_-u_GoWhJzdjiBuz9XasSfQK8WyAwbc7MLkw40A7Zxl2sfsxGTod3qi1u8mjguoc9CbVqPdYe_9YPVxoK4CeJz6V8AsPcxVJxYq6os1rI9qFx_6a1JdQEhetGtkHLFvwo80UTzKXKhGXSu96WrXnkDE8Kw5TSKvh2gI_BX4QHXjE82xldJRJ8QIXGpRNbdyzGkUdjsrhmZl3ARC9IUlxmowkcEEIzjfbOKBVGrVcJ7rHb0GYNaKtMB_MlH1uAPDxl6qKeXOAZ8YEZ1r0ToPw\",\"e\":\"AQAB\",\"x5t\":\"MR-pGTa866RdZLjN6Vwrfay907g\"}]}"); + keys.put("-1909572257", + "{\"keys\":[{\"kty\":\"OKP\",\"kid\":\"-1909572257\",\"use\":\"sig\",\"alg\":\"EdDSA\",\"crv\":\"Ed25519\",\"x\":\"XWxGtApfcqmKI7p0OKnF5JSEWMVoLsytFXLEP7xZ_l8\",\"x5t\":\"SHd4HCUdA8HJVGM2UWz5NmIPTG0\"}]}"); + keys.put("1716999904", + "{\"keys\":[{\"kty\":\"OKP\",\"kid\":\"1716999904\",\"use\":\"sig\",\"alg\":\"EdDSA\",\"crv\":\"Ed448\",\"x\":\"lDc565Rydl9MUCoOB9JpGV3pUSHm7FvuiuEMvrvRkS7PeYL41rPU6s2rMdLeHiXfSxvR1veh4C0A\",\"x5t\":\"1IRTBLLQeiL2YZLB1VDDvCTGozc\"}]}"); + return keys; + } + + private void loadKeyStore() throws Exception { + URL url = getClass().getResource(PATH_TO_KEY); + assert url != null; + File certFile = new File(url.getFile()); + + InputStream keyIS = new FileInputStream(certFile); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(keyIS, KEY_PWD.toCharArray()); + + keyIS.close(); + + this._keyStore = keyStore; + } + + private PrivateKey getPrivateKey() throws Exception { + return (PrivateKey) this._keyStore.getKey(_keyAlias, KEY_PWD.toCharArray()); + + } + + private Certificate getCertificate() throws KeyStoreException { + // Get key by alias (found in the p12 file using: + // keytool -list -keystore test-root-ca.p12 -storepass foobar -storetype PKCS12 + return this._keyStore.getCertificate(_keyAlias); + } } From 90a1027bc5b5243feb6c8c39eea4c121bf54023c Mon Sep 17 00:00:00 2001 From: Marco Descher Date: Mon, 10 Mar 2025 08:10:32 +0100 Subject: [PATCH 2/2] Add array aud support, osgi manifest --- pom.xml | 74 +++++++++++-------- readme.md | 8 +- .../io/curity/oauth/AbstractJwtValidator.java | 14 +++- src/main/java/io/curity/oauth/JsonUtils.java | 38 ++++++---- .../java/io/curity/oauth/JwtWithCertTest.java | 22 +++++- .../java/io/curity/oauth/JwtWithJwksTest.java | 20 ++++- 6 files changed, 122 insertions(+), 54 deletions(-) diff --git a/pom.xml b/pom.xml index 05fa15f..3b2cfab 100644 --- a/pom.xml +++ b/pom.xml @@ -92,7 +92,7 @@ authenticates and authorizes requests using OAuth access tokens of various kinds. ${project.version} - oauth-filter + io.curity.oauth-filter * ${api.package} ${project.version} @@ -100,6 +100,11 @@ <_versionpolicy> [$(version;==;$(@)),$(version;+;$(@))) <_nodefaultversion>false + =1.0.0)(!(version>=2.0.0)))";resolution:=optional, + osgi.serviceloader;filter:="(osgi.serviceloader=io.curity.oauth.HttpClientProvider)";osgi.serviceloader="io.curity.oauth.HttpClientProvider";cardinality:=multiple;resolution:=optional, + osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=17))" + ]]> @@ -170,32 +175,32 @@ - org.cyclonedx - cyclonedx-maven-plugin - 2.9.1 - - - package - - makeAggregateBom - - - - - library - 1.4 - true - true - false - true - true - false - false - true - all - CycloneDX-Sbom - - + org.cyclonedx + cyclonedx-maven-plugin + 2.9.1 + + + package + + makeAggregateBom + + + + + library + 1.4 + true + true + false + true + true + false + false + true + all + CycloneDX-Sbom + + org.apache.felix @@ -325,13 +330,24 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + 3.3.1 attach-sources jar-no-fork + + + + + ${project.groupId}.${project.artifactId}.source + ${project.version} + ${project.groupId}.${project.artifactId};version="${project.version}";roots:="." + + + + @@ -348,7 +364,7 @@ - 8 + 17 diff --git a/readme.md b/readme.md index db610e7..ac01cc3 100644 --- a/readme.md +++ b/readme.md @@ -4,7 +4,6 @@ [![Availability](https://img.shields.io/badge/availability-source-blue)](https://curity.io/resources/code-examples/status/) - This project contains a Servlet Filter that authenticates and authorizes requests using OAuth access tokens of various kinds. There are two `OAuthFilter` implementations. `OAuthJwtFilter` and `OAuthOpaqueFilter`. Both implement `jakarta.servlet.Filter`, and can be used to protect APIs built using Java. Depending on the format of the access token, these two concrete implementations can be used in the following manner: 1. If the token is a Json Web Token (JWT) then validate the token using a public key @@ -13,17 +12,18 @@ This project contains a Servlet Filter that authenticates and authorizes request An example of how to use this filter can be found in a [separate repository](https://github.com/curityio/example-java-oauth-protected-api). -## FORK INFO +# FORK INFO -This is a fork of the original repo +This is a fork of the original repo https://github.com/curityio/oauth-filter-for-java -It updates to version 5 with the following - breaking - changes +It upgrades to version 5 with the following - breaking - changes * Fully move to `jakarta.servlet` namespace * Change JSON parser to `com.google.gson` * Update some dependent libraries * Generate OSGi capable `META-INF/MANIFEST.MF` * Fix some build warnings +* The value in JWT `aud` now accepts arrays, as the configured `audience` can be one of multiple allowed ## Filter Overview diff --git a/src/main/java/io/curity/oauth/AbstractJwtValidator.java b/src/main/java/io/curity/oauth/AbstractJwtValidator.java index aa5cc26..0c4de6d 100644 --- a/src/main/java/io/curity/oauth/AbstractJwtValidator.java +++ b/src/main/java/io/curity/oauth/AbstractJwtValidator.java @@ -24,8 +24,10 @@ import java.time.Instant; import java.util.Base64; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -68,14 +70,18 @@ public final JsonData validate(String jwt) throws TokenValidationException { long exp = JsonUtils.getLong(jwtBody, "exp"); long iat = JsonUtils.getLong(jwtBody, "iat"); - String aud = JsonUtils.getString(jwtBody, "aud"); + Set aud = new HashSet(); + String _aud = JsonUtils.getString(jwtBody, "aud"); + Optional.ofNullable(_aud).ifPresent(aud::add); + aud.addAll(JsonUtils.getStringSet(jwtBody, "aud")); + String iss = JsonUtils.getString(jwtBody, "iss"); - assert aud != null && aud.length() > 0 : "aud claim is not present in JWT"; + assert !aud.isEmpty() : "aud claim is not present in JWT"; assert iss != null && iss.length() > 0 : "iss claim is not present in JWT"; - if (!aud.equals(_audience)) { - throw new InvalidAudienceException(_audience, aud); + if (!aud.contains(_audience)) { + throw new InvalidAudienceException(_audience, aud.toString()); } if (!iss.equals(_issuer)) { diff --git a/src/main/java/io/curity/oauth/JsonUtils.java b/src/main/java/io/curity/oauth/JsonUtils.java index 2fac895..69f5c7b 100644 --- a/src/main/java/io/curity/oauth/JsonUtils.java +++ b/src/main/java/io/curity/oauth/JsonUtils.java @@ -5,33 +5,41 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; final class JsonUtils { - private static final String[] NO_SCOPES = {}; + private static final String[] NO_SCOPES = {}; private JsonUtils() { - } - + } static Set getScopes(JsonObject jsonObject) { - String scopesInToken = getString(jsonObject, "scope"); - String[] presentedScopes = scopesInToken == null ? NO_SCOPES : scopesInToken.split("\\s+"); + String scopesInToken = getString(jsonObject, "scope"); + String[] presentedScopes = scopesInToken == null ? NO_SCOPES : scopesInToken.split("\\s+"); - return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(presentedScopes))); - } + return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(presentedScopes))); + } static String getString(JsonObject jsonObject, String name) { - return Optional.ofNullable(jsonObject.get(name)) - .filter(JsonElement::isJsonPrimitive).map(it -> it.getAsJsonPrimitive().getAsString()) - .orElse(null); - } + return Optional.ofNullable(jsonObject.get(name)).filter(JsonElement::isJsonPrimitive) + .map(it -> it.getAsJsonPrimitive().getAsString()).orElse(null); + } + + static Set getStringSet(JsonObject jsonObject, String name) { + JsonArray array = Optional.ofNullable(jsonObject.get(name)).filter(JsonElement::isJsonArray) + .map(it -> it.getAsJsonArray()).orElse(null); + if (array != null) { + return array.asList().stream().map(it -> it.getAsString()).collect(Collectors.toSet()); + } + return Collections.emptySet(); + } static long getLong(JsonObject jsonObject, String name) { - return Optional.ofNullable(jsonObject.get(name)) - .filter(JsonElement::isJsonPrimitive).map(it -> it.getAsJsonPrimitive().getAsLong()) - .orElse(Long.MIN_VALUE); - } + return Optional.ofNullable(jsonObject.get(name)).filter(JsonElement::isJsonPrimitive) + .map(it -> it.getAsJsonPrimitive().getAsLong()).orElse(Long.MIN_VALUE); + } } \ No newline at end of file diff --git a/src/test/java/io/curity/oauth/JwtWithCertTest.java b/src/test/java/io/curity/oauth/JwtWithCertTest.java index d919396..521ffa2 100644 --- a/src/test/java/io/curity/oauth/JwtWithCertTest.java +++ b/src/test/java/io/curity/oauth/JwtWithCertTest.java @@ -19,6 +19,8 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertNotNull; import static junit.framework.TestCase.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.File; import java.io.FileInputStream; @@ -42,6 +44,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import com.google.gson.Gson; import com.google.gson.JsonObject; @RunWith(Parameterized.class) @@ -109,7 +112,7 @@ public void testFindAndValidateWithOneCert() throws Exception assertNotNull(validatedToken); } - + @Test public void testValidContentInToken() throws Exception { @@ -129,6 +132,23 @@ public void testValidContentInToken() throws Exception assertEquals(SUBJECT, jsonObject.get("sub").getAsString()); assertEquals(EXTRA_CLAIM_VALUE, jsonObject.get(EXTRA_CLAIM).getAsString()); } + + @Test + public void testValidContentInTokenAudienceArray() throws Exception { + JwtTokenIssuer issuer = new JwtTokenIssuer(ISSUER, _algorithm, getPrivateKey(), getCertificate()); + Map attributes = new HashMap<>(); + attributes.put(EXTRA_CLAIM, EXTRA_CLAIM_VALUE); + _testToken = issuer.issueToken(SUBJECT, AUDIENCE+" bar:audience", EXPIRATION, attributes); + + JwtValidator validator = new JwtValidatorWithCert(ISSUER, AUDIENCE, prepareKeyMap()); + + _logger.info("test token = {}", _testToken); + + JsonData validatedToken = validator.validate(_testToken); + + assertNotNull(validatedToken); + + } /** * Load the private Keymap with the x5t256 thumbprint and the public key diff --git a/src/test/java/io/curity/oauth/JwtWithJwksTest.java b/src/test/java/io/curity/oauth/JwtWithJwksTest.java index 0aadbd4..a457e01 100644 --- a/src/test/java/io/curity/oauth/JwtWithJwksTest.java +++ b/src/test/java/io/curity/oauth/JwtWithJwksTest.java @@ -100,7 +100,7 @@ public void before() throws Exception { } } } - + @Test public void testFindAndValidateWithOneJwk() throws Exception { WebKeysClient webKeysClient = mock(WebKeysClient.class); @@ -137,6 +137,24 @@ public void testValidContentInToken() throws Exception { assertEquals(EXTRA_CLAIM_VALUE, jsonObject.get(EXTRA_CLAIM).getAsString()); } + @Test + public void testValidContentInTokenAudienceArray() throws Exception { + JwtTokenIssuer issuer = new JwtTokenIssuer(ISSUER, _algorithm, getPrivateKey(), _keyId); + Map attributes = new HashMap<>(); + attributes.put(EXTRA_CLAIM, EXTRA_CLAIM_VALUE); + _testToken = issuer.issueToken(SUBJECT, AUDIENCE+" bar:audience", EXPIRATION, attributes); + + WebKeysClient webKeysClient = mock(WebKeysClient.class); + + JwtValidatorWithJwk validator = new JwtValidatorWithJwk(0, webKeysClient, AUDIENCE, ISSUER, new Gson()); + when(webKeysClient.getKeys()).thenReturn(prepareKeyMap().get(_keyId)); + _logger.info("test token = {}", _testToken); + + JsonData validatedToken = validator.validate(_testToken); + + assertNotNull(validatedToken); + } + /** * Load the private keymap with the kid and the jwks The map only contains a * single key