diff --git a/CHANGELOG.md b/CHANGELOG.md index 847a930..b8b7387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +## 4.1.0 (2026-07-30) + +- Support `aud` claim in JWT when it's specified as an array rather than a string. +- Allow JWT validators to configure a Set of accepted audiences and issuers, not just one. +- OAuthJwtFilter configuration now accepts whitespace-separated values for `audience` and `issuer`. +- Minimum Java version requirement changed to 15. + ## 4.0.0 (2023-11-27) - Added support for EdDSA signatures. diff --git a/pom.xml b/pom.xml index ffdd5fa..b355cbf 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.curity oauth-filter - 4.0.0 + 4.1.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 @@ -73,10 +73,11 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 + 3.8.1 - 11 - 11 + + 15 + 15 true false @@ -269,7 +270,7 @@ - 11 + 15 diff --git a/readme.md b/readme.md index cc37db4..e340908 100644 --- a/readme.md +++ b/readme.md @@ -62,24 +62,26 @@ depending on the format of the token your OAuth server is using. ### Init-params for the `OAuthJwtFilter` -Configuration Setting Name | Description ----------------------------|---------------- -oauthHost | Hostname of the OAuth server. -oauthPort | Port of the OAuth server. -jsonWebKeysPath | Path to the JWKS endpoint on the OAuth server. -scope | A space separated list of scopes required to access the API. -minKidReloadTimeInSeconds | Minimum time to reload the webKeys cache used by the Filter. +| Configuration Setting Name | Description | +|----------------------------|----------------------------------------------------------------------------------------------| +| oauthHost | Hostname of the OAuth server. | +| oauthPort | Port of the OAuth server. | +| jsonWebKeysPath | Path to the JWKS endpoint on the OAuth server. | +| scope | A space separated list of scopes required to access the API. | +| minKidReloadTimeInSeconds | Minimum time to reload the webKeys cache used by the Filter. | +| issuer | A space separated list of issuers to accept. If not specified, all issuers are accepted. | +| audience | A space separated list of audiences to accept. If not specified, all audiences are accepted. | ### Init-params for the `OAuthOpaqueFilter` -Configuration Setting Name | Description ----------------------------|---------------- -oauthHost | Hostname of the OAuth server. -oauthPort | Port of the OAuth server. -introspectionPath | Path to the introspection endpoint on the OAuth server. -scope | A space separated list of scopes required to access the API. -clientId | Your application's client id to use for introspection. -clientSecret | Your application's client secret. +| Configuration Setting Name | Description | +|----------------------------|--------------------------------------------------------------| +| oauthHost | Hostname of the OAuth server. | +| oauthPort | Port of the OAuth server. | +| introspectionPath | Path to the introspection endpoint on the OAuth server. | +| scope | A space separated list of scopes required to access the API. | +| clientId | Your application's client id to use for introspection. | +| clientSecret | Your application's client secret. | ## Providing external services (`io.curity.oauth.HttpClientProvider` and `javax.json.spi.JsonProvider`) diff --git a/src/main/java/io/curity/oauth/AbstractJwtValidator.java b/src/main/java/io/curity/oauth/AbstractJwtValidator.java index 959f7bb..80a8684 100644 --- a/src/main/java/io/curity/oauth/AbstractJwtValidator.java +++ b/src/main/java/io/curity/oauth/AbstractJwtValidator.java @@ -1,12 +1,12 @@ /* * 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. @@ -30,6 +30,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -42,16 +43,31 @@ abstract class AbstractJwtValidator implements JwtValidator 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; + private final Set _audiences; + private final Set _issuers; - AbstractJwtValidator(String issuer, String audience, JsonReaderFactory jsonReaderFactory) + protected AbstractJwtValidator(Set issuers, Set audiences, JsonReaderFactory jsonReaderFactory) { - _issuer = issuer; - _audience = audience; + _issuers = Set.copyOf(issuers); + _audiences = Set.copyOf(audiences); _jsonReaderFactory = jsonReaderFactory; } + protected AbstractJwtValidator(String issuer, String audience, JsonReaderFactory jsonReaderFactory) + { + this(Set.of(issuer), Set.of(audience), jsonReaderFactory); + } + + protected AbstractJwtValidator(String issuer, Set audiences, JsonReaderFactory jsonReaderFactory) + { + this(Set.of(issuer), audiences, jsonReaderFactory); + } + + protected AbstractJwtValidator(Set issuers, String audience, JsonReaderFactory jsonReaderFactory) + { + this(issuers, Set.of(audience), jsonReaderFactory); + } + public final JsonData validate(String jwt) throws TokenValidationException { String[] jwtParts = jwt.split("\\."); @@ -73,20 +89,20 @@ 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 = JsonUtils.getStringOrStrings(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"; + assert !aud.isEmpty() : "aud claim is not present or is invalid in JWT"; + assert iss != null && !iss.isEmpty() : "iss claim is not present in JWT"; - if (!aud.equals(_audience)) + if (!_audiences.isEmpty() && aud.stream().noneMatch(_audiences::contains)) { - throw new InvalidAudienceException(_audience, aud); + throw new InvalidAudienceException(String.join(", ", _audiences), String.join(", ", aud)); } - if (!iss.equals(_issuer)) + if (!_issuers.isEmpty() && !_issuers.contains(iss)) { - throw new InvalidIssuerException(_issuer, iss); + throw new InvalidIssuerException(String.join(", ", _issuers), iss); } Instant now = Instant.now(); @@ -230,7 +246,7 @@ private JwtHeader decodeJwtHeader(String header) }); } - class JwtHeader + static final class JwtHeader { private final JsonObject _jsonObject; diff --git a/src/main/java/io/curity/oauth/FilterHelper.java b/src/main/java/io/curity/oauth/FilterHelper.java index 0beb82a..9c7ed4c 100644 --- a/src/main/java/io/curity/oauth/FilterHelper.java +++ b/src/main/java/io/curity/oauth/FilterHelper.java @@ -1,12 +1,12 @@ /* * 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. @@ -18,8 +18,10 @@ import jakarta.servlet.FilterConfig; import jakarta.servlet.UnavailableException; + import java.util.Enumeration; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -67,15 +69,24 @@ static T getInitParamValue(String name, Map initParams, } static Optional getOptionalInitParamValue(String name, Map initParams, - Function converter) throws UnavailableException + Function converter) + throws UnavailableException { Optional value = getSingleValue(name, initParams); return value.flatMap(s -> Optional.ofNullable(converter.apply(s))); } - private static Optional getSingleValue(String name, Map initParams) throws - UnavailableException + static List getWhitespaceSeparatedInitParamValues(String name, Map initParams) + throws UnavailableException + { + Optional value = getSingleValue(name, initParams); + + return value.map(v -> List.of(v.split("\\s+"))).orElseGet(List::of); + } + + private static Optional getSingleValue(String name, Map initParams) + throws UnavailableException { return Optional.ofNullable(initParams.get(name)).map(Object::toString); } diff --git a/src/main/java/io/curity/oauth/JsonUtils.java b/src/main/java/io/curity/oauth/JsonUtils.java index 800bce3..751fc2e 100644 --- a/src/main/java/io/curity/oauth/JsonUtils.java +++ b/src/main/java/io/curity/oauth/JsonUtils.java @@ -16,6 +16,7 @@ package io.curity.oauth; +import javax.json.JsonArray; import javax.json.JsonNumber; import javax.json.JsonObject; import javax.json.JsonReaderFactory; @@ -27,6 +28,7 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; final class JsonUtils { @@ -49,14 +51,38 @@ static Set getScopes(JsonObject jsonObject) return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(presentedScopes))); } + /** + * Get the string value of a JSON property. + * + * @param jsonObject The JSON object containing the property. + * @param name The name of the property. + * @return The string value of the property, or null if not present or not a string. + */ 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()) + .map(JsonUtils::stringValue) .orElse(null); } + /** + * Get the string values of a JSON property. + * + * @param jsonObject The JSON object containing the property. + * @param name The name of the property. + * @return The string values of the property, or an empty set if not present or not a string or array. + * If the property is an array, it must only contain strings, otherwise this method will throw a + * {@link ClassCastException}. + */ + static Set getStringOrStrings(JsonObject jsonObject, String name) + { + return Optional.ofNullable(jsonObject.get(name)) + .filter(it -> it.getValueType() == JsonValue.ValueType.STRING || it.getValueType() == JsonValue.ValueType.ARRAY) + .map(JsonUtils::stringOrStringArrayValue) + .orElse(Collections.emptySet()); + } + static long getLong(JsonObject jsonObject, String name) { return Optional.ofNullable(jsonObject.get(name)) @@ -64,4 +90,26 @@ static long getLong(JsonObject jsonObject, String name) .map(it -> ((JsonNumber) it).longValue()) .orElse(Long.MIN_VALUE); } + + private static String stringValue(JsonValue value) + { + return ((JsonString) value).getString(); + } + + private static Set stringOrStringArrayValue(JsonValue value) + { + if (value.getValueType() == JsonValue.ValueType.STRING) + { + return Set.of(stringValue(value)); + } + if (value.getValueType() == JsonValue.ValueType.ARRAY) + { + return ((JsonArray) value).stream() + .map(JsonUtils::stringValue) + .collect(Collectors.toSet()); + } + + // be extremely lenient and ignore the value + return Set.of(); + } } diff --git a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java index 35db2b8..7a3a4e1 100644 --- a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java +++ b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java @@ -20,27 +20,59 @@ import java.security.PublicKey; import java.util.Map; import java.util.Optional; -import java.util.logging.Logger; +import java.util.Set; final class JwtValidatorWithCert extends AbstractJwtValidator { - private static final Logger _logger = Logger.getLogger(JwtValidatorWithCert.class.getName()); private final Map _keys; + JwtValidatorWithCert(Set issuers, Set audiences, Map publicKeys) + { + this(issuers, audiences, publicKeys, JsonUtils.createDefaultReaderFactory()); + } + + JwtValidatorWithCert(String issuer, Set audiences, Map publicKeys) + { + this(Set.of(issuer), audiences, publicKeys); + } + + JwtValidatorWithCert(Set issuers, String audience, Map publicKeys) + { + this(issuers, Set.of(audience), publicKeys); + } + JwtValidatorWithCert(String issuer, String audience, Map publicKeys) { - this(issuer, audience, publicKeys, JsonUtils.createDefaultReaderFactory()); + this(Set.of(issuer), audience, publicKeys); } - JwtValidatorWithCert(String issuer, String audience, Map publicKeys, + JwtValidatorWithCert(Set issuers, Set audiences, Map publicKeys, JsonReaderFactory jsonReaderFactory) { - super(issuer, audience, jsonReaderFactory); - + super(issuers, audiences, jsonReaderFactory); + _keys = publicKeys; } + JwtValidatorWithCert(String issuer, Set audiences, Map publicKeys, + JsonReaderFactory jsonReaderFactory) + { + this(Set.of(issuer), audiences, publicKeys, jsonReaderFactory); + } + + JwtValidatorWithCert(Set issuers, String audience, Map publicKeys, + JsonReaderFactory jsonReaderFactory) + { + this(issuers, Set.of(audience), publicKeys, jsonReaderFactory); + } + + JwtValidatorWithCert(String issuer, String audience, Map publicKeys, + JsonReaderFactory jsonReaderFactory) + { + this(Set.of(issuer), audience, publicKeys, jsonReaderFactory); + } + @Override protected Optional getPublicKey(JwtHeader jwtHeader) { diff --git a/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java b/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java index 8260a6a..ca0fbc3 100644 --- a/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java +++ b/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java @@ -22,6 +22,7 @@ import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -31,14 +32,32 @@ final class JwtValidatorWithJwk extends AbstractJwtValidator private final JwkManager _jwkManager; - JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, String audience, String issuer, + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, Set audiences, Set issuers, JsonReaderFactory jsonReaderFactory) { - super(issuer, audience, jsonReaderFactory); - + super(issuers, audiences, jsonReaderFactory); + _jwkManager = new JwkManager(minKidReloadTime, webKeysClient, jsonReaderFactory); } + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, Set audiences, String issuer, + JsonReaderFactory jsonReaderFactory) + { + this(minKidReloadTime, webKeysClient, audiences, Set.of(issuer), jsonReaderFactory); + } + + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, String audience, String issuer, + JsonReaderFactory jsonReaderFactory) + { + this(minKidReloadTime, webKeysClient, Set.of(audience), Set.of(issuer), jsonReaderFactory); + } + + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, String audience, Set issuers, + JsonReaderFactory jsonReaderFactory) + { + this(minKidReloadTime, webKeysClient, Set.of(audience), issuers, jsonReaderFactory); + } + @Override protected Optional getPublicKey(JwtHeader jwtHeader) { diff --git a/src/main/java/io/curity/oauth/OAuthFilter.java b/src/main/java/io/curity/oauth/OAuthFilter.java index c68b21f..3e786c1 100644 --- a/src/main/java/io/curity/oauth/OAuthFilter.java +++ b/src/main/java/io/curity/oauth/OAuthFilter.java @@ -1,12 +1,12 @@ /* * 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. @@ -25,8 +25,8 @@ import jakarta.servlet.UnavailableException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; @@ -35,15 +35,15 @@ public abstract class OAuthFilter implements Filter { - private static final String[] NO_SCOPES = {}; private static final Logger _logger = Logger.getLogger(OAuthFilter.class.getName()); + private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; private static final String AUTHORIZATION = "Authorization"; public static final String PRINCIPAL_ATTRIBUTE_NAME = "principal"; private Map _filterConfig; // Protected, so subclasses don't have to repeat the conversion to this private String _oauthHost = null; - private String[] _scopes = null; + private List _scopes = List.of(); private interface InitParams { @@ -58,25 +58,25 @@ public void init(FilterConfig filterConfig) throws ServletException _oauthHost = FilterHelper.getInitParamValue(InitParams.OAUTH_HOST, _filterConfig); - _scopes = FilterHelper.getOptionalInitParamValue(InitParams.SCOPE, _filterConfig, it -> it.split("\\s+")) - .orElse(NO_SCOPES); + _scopes = FilterHelper.getWhitespaceSeparatedInitParamValues(InitParams.SCOPE, _filterConfig); } /** * The doFilter is the primary filter method of a Servlet filter. It is implemented as a final method * and will call the configured filters authenticate and authorize methods. * Authorize is optional to implement as this filter implements a default scope check method. - * @param servletRequest The default servlet request + * + * @param servletRequest The default servlet request * @param servletResponse The default servlet response - * @param filterChain A filter chain to continue with after this filter is done - * @throws IOException when response fails to send an error + * @param filterChain A filter chain to continue with after this filter is done + * @throws IOException when response fails to send an error * @throws ServletException when authentication fails for some exceptional reason */ @Override public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - HttpServletResponse response = (HttpServletResponse)servletResponse; + HttpServletResponse response = (HttpServletResponse) servletResponse; Optional token = extractAccessTokenFromHeader(servletRequest); String oauthHost = getOAuthServerRealm(); @@ -111,7 +111,7 @@ public final void doFilter(ServletRequest servletRequest, ServletResponse servle if (filterChain != null) { filterChain.doFilter( - new AuthenticatedUserRequestWrapper((HttpServletRequest)servletRequest, authenticatedUser), + new AuthenticatedUserRequestWrapper((HttpServletRequest) servletRequest, authenticatedUser), servletResponse); } } @@ -163,6 +163,7 @@ protected String getOAuthServerRealm() throws UnavailableException /** * This is the authenticate method of the filter, it will take the token as string input and * must perform the appropriate operation to validate the token. + * * @param token - The token extracted from the Authorization header and stripped of the Bearer * @return An AuthenticatedUser if the token was valid, or null if not. * @throws ServletException when authentication fails for some exceptional reason @@ -184,6 +185,7 @@ protected Optional authenticate(String token) throws ServletE return Optional.ofNullable(result); } + /** * Authorizes the current request by checking that all configured scopes are included in the one presented in the * request. @@ -196,10 +198,8 @@ protected Optional authenticate(String token) throws ServletE */ protected boolean isAuthorized(AuthenticatedUser authenticatedUser) { - List requiredScopes = Arrays.asList(_scopes); - // No scopes required for authorization - return requiredScopes.isEmpty() || authenticatedUser.getScopes().containsAll(requiredScopes); + return _scopes.isEmpty() || authenticatedUser.getScopes().containsAll(_scopes); } @Override @@ -222,12 +222,13 @@ public void destroy() /** * Extracts the token from the Authorization header, removing the Bearer prefix + * * @param request The incoming request * @return the token or null if not present */ private Optional extractAccessTokenFromHeader(ServletRequest request) { - HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpServletRequest httpRequest = (HttpServletRequest) request; String authorizationHeader = httpRequest.getHeader(AUTHORIZATION); String result = null; @@ -235,7 +236,7 @@ private Optional extractAccessTokenFromHeader(ServletRequest request) { String[] tokenSplit = authorizationHeader.split("[Bb][Ee][Aa][Rr][Ee][Rr]\\s+"); - if(tokenSplit.length != 2) + if (tokenSplit.length != 2) { _logger.fine("Incoming token in Authorization header is not a Bearer token"); } diff --git a/src/main/java/io/curity/oauth/OAuthJwtFilter.java b/src/main/java/io/curity/oauth/OAuthJwtFilter.java index b574cf7..f7cc0f3 100644 --- a/src/main/java/io/curity/oauth/OAuthJwtFilter.java +++ b/src/main/java/io/curity/oauth/OAuthJwtFilter.java @@ -1,12 +1,12 @@ /* * 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. @@ -16,12 +16,14 @@ package io.curity.oauth; -import javax.json.JsonReaderFactory; -import javax.json.spi.JsonProvider; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.UnavailableException; + +import javax.json.JsonReaderFactory; +import javax.json.spi.JsonProvider; import java.util.Map; +import java.util.Set; import java.util.logging.Logger; public class OAuthJwtFilter extends OAuthFilter @@ -53,7 +55,7 @@ public void init(FilterConfig filterConfig) throws ServletException if (_jwtValidator == null) { _jwtValidator = createTokenValidator(getFilterConfiguration()); - + _logger.info(() -> String.format("%s successfully initialized", OAuthFilter.class.getSimpleName())); } else @@ -71,10 +73,10 @@ protected TokenValidator createTokenValidator(Map filterConfig) throw // the ReaderFactory using the filter's config. JsonReaderFactory jsonReaderFactory = JsonProvider.provider().createReaderFactory(filterConfig); WebKeysClient webKeysClient = HttpClientProvider.provider().createWebKeysClient(filterConfig); - String audience = FilterHelper.getInitParamValue(InitParams.AUDIENCE, filterConfig); - String issuer = FilterHelper.getInitParamValue(InitParams.ISSUER, filterConfig); + Set audiences = Set.copyOf(FilterHelper.getWhitespaceSeparatedInitParamValues(InitParams.AUDIENCE, filterConfig)); + Set issuers = Set.copyOf(FilterHelper.getWhitespaceSeparatedInitParamValues(InitParams.ISSUER, filterConfig)); - return _jwtValidator = new JwtValidatorWithJwk(_minKidReloadTimeInSeconds, webKeysClient, audience, issuer, + return _jwtValidator = new JwtValidatorWithJwk(_minKidReloadTimeInSeconds, webKeysClient, audiences, issuers, jsonReaderFactory); } diff --git a/src/main/java/io/curity/oauth/ValidationExceptions.java b/src/main/java/io/curity/oauth/ValidationExceptions.java index a406861..15ebbd2 100644 --- a/src/main/java/io/curity/oauth/ValidationExceptions.java +++ b/src/main/java/io/curity/oauth/ValidationExceptions.java @@ -43,7 +43,7 @@ class MissingAlgorithmException extends TokenValidationException class InvalidAudienceException extends TokenValidationException { - private static final String _formattedMessage = "Audience %s does not match expected one %s"; + private static final String _formattedMessage = "Audience(s) '%s' does not match expected '%s'"; InvalidAudienceException(String expectedAudience, String actualAudience) { @@ -103,7 +103,7 @@ class UnknownAlgorithmException extends InvalidTokenFormatException class InvalidIssuerException extends TokenValidationException { - private static final String _formattedMessage = "Issuer %s does not match expected one %s"; + private static final String _formattedMessage = "Issuer '%s' does not match expected one of '%s'"; InvalidIssuerException(String expectedIssuer, String actualIssuer) { diff --git a/src/test/java/io/curity/oauth/FilterHelperTest.java b/src/test/java/io/curity/oauth/FilterHelperTest.java new file mode 100644 index 0000000..8e04626 --- /dev/null +++ b/src/test/java/io/curity/oauth/FilterHelperTest.java @@ -0,0 +1,230 @@ +/* + * 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 jakarta.servlet.FilterConfig; +import jakarta.servlet.UnavailableException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +public class FilterHelperTest +{ + @Mock + private FilterConfig mockFilterConfig; + + @Before + public void setUp() + { + MockitoAnnotations.openMocks(this); + } + + @Test + public void canProvideSimpleStrings() throws UnavailableException + { + Map params = Collections.singletonMap("tokenEndpoint", "https://example.com/token"); + + String result = FilterHelper.getInitParamValue("tokenEndpoint", params); + + assertEquals("https://example.com/token", result); + } + + @Test + public void canProvideSimpleStringsWithConverter() throws UnavailableException + { + Map params = Collections.singletonMap("maxRetries", "5"); + + Integer result = FilterHelper.getInitParamValue("maxRetries", params, Integer::parseInt); + + assertEquals(Integer.valueOf(5), result); + } + + @Test + public void canProvideWhiteSeparatedStringLists() throws UnavailableException + { + Map params = Collections.singletonMap("scope", "read write delete"); + + List result = FilterHelper.getWhitespaceSeparatedInitParamValues("scope", params); + + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("read", result.get(0)); + assertEquals("write", result.get(1)); + assertEquals("delete", result.get(2)); + } + + @Test + public void canProvideWhiteSeparatedStringListsWithVariousWhitespace() throws UnavailableException + { + Map params = Collections.singletonMap("scope", "read write\tdelete\nupdate"); + + List result = FilterHelper.getWhitespaceSeparatedInitParamValues("scope", params); + + assertNotNull(result); + assertEquals(4, result.size()); + assertEquals("read", result.get(0)); + assertEquals("write", result.get(1)); + assertEquals("delete", result.get(2)); + assertEquals("update", result.get(3)); + } + + @Test + public void canProvideSingleValueInWhiteSeparatedStringList() throws UnavailableException + { + Map params = Collections.singletonMap("scope", "read"); + + List result = FilterHelper.getWhitespaceSeparatedInitParamValues("scope", params); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("read", result.get(0)); + } + + @Test(expected = UnavailableException.class) + public void failsWhenParameterIsMissing() throws UnavailableException + { + Map params = Collections.emptyMap(); + + FilterHelper.getInitParamValue("tokenEndpoint", params); + } + + @Test + public void failsWhenParameterIsMissingWithCorrectMessage() throws UnavailableException + { + Map params = Collections.emptyMap(); + + try + { + FilterHelper.getInitParamValue("tokenEndpoint", params); + fail("Should have thrown UnavailableException"); + } + catch (UnavailableException e) + { + assertTrue(e.getMessage().contains("missing required initParam [tokenEndpoint]")); + assertTrue(e.getMessage().contains("OAuthFilter")); + } + } + + @Test + public void doesNotFailWhenOptionalParameterIsMissing() throws UnavailableException + { + Map params = Collections.emptyMap(); + + Optional result = FilterHelper.getOptionalInitParamValue("audience", params, + v -> v); + + assertFalse(result.isPresent()); + } + + @Test + public void doesNotFailWhenOptionalParameterIsPresentWithConverter() throws UnavailableException + { + Map params = Collections.singletonMap("connectionTimeout", "30000"); + + Optional result = FilterHelper.getOptionalInitParamValue("connectionTimeout", params, + Long::parseLong); + + assertTrue(result.isPresent()); + assertEquals(Long.valueOf(30000), result.get()); + } + + @Test + public void doesNotFailWhenOptionalParameterIsNull() throws UnavailableException + { + Map params = Collections.singletonMap("someParam", null); + + Optional result = FilterHelper.getOptionalInitParamValue("someParam", params, + v -> v); + + assertFalse(result.isPresent()); + } + + @Test + public void doesNotFailWhenNoParametersProvidedForWhiteSpaceSeparatedStringLists() + throws UnavailableException + { + Map params = Collections.emptyMap(); + + List result = FilterHelper.getWhitespaceSeparatedInitParamValues("scope", params); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void initParamsMapFromWithNoParameters() + { + when(mockFilterConfig.getInitParameterNames()) + .thenReturn(Collections.emptyEnumeration()); + + Map result = FilterHelper.initParamsMapFrom(mockFilterConfig); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void initParamsMapFromWithSingleParameter() + { + when(mockFilterConfig.getInitParameterNames()) + .thenReturn(Collections.enumeration(Collections.singletonList("tokenEndpoint"))); + when(mockFilterConfig.getInitParameter("tokenEndpoint")) + .thenReturn("https://example.com/token"); + + Map result = FilterHelper.initParamsMapFrom(mockFilterConfig); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("https://example.com/token", result.get("tokenEndpoint")); + } + + @Test + public void initParamsMapFromWithMultipleParameters() + { + List paramNames = Arrays.asList("tokenEndpoint", "scope", "audience"); + when(mockFilterConfig.getInitParameterNames()) + .thenReturn(Collections.enumeration(paramNames)); + when(mockFilterConfig.getInitParameter("tokenEndpoint")) + .thenReturn("https://example.com/token"); + when(mockFilterConfig.getInitParameter("scope")) + .thenReturn("read write"); + when(mockFilterConfig.getInitParameter("audience")) + .thenReturn("api"); + + Map result = FilterHelper.initParamsMapFrom(mockFilterConfig); + + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("https://example.com/token", result.get("tokenEndpoint")); + assertEquals("read write", result.get("scope")); + assertEquals("api", result.get("audience")); + } +} diff --git a/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java new file mode 100644 index 0000000..264d524 --- /dev/null +++ b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java @@ -0,0 +1,143 @@ +/* + * 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 org.junit.Test; + +import java.security.PublicKey; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertThrows; + +public class JwtAudienceClaimTest +{ + private static final String ISSUER = "test:issuer"; + + @Test + public void testValidatorAcceptsOneAudienceAndTokenHasValidAudienceString() throws Exception + { + assertTokenValidation("some:audience", + keys -> new JwtValidatorWithCert(ISSUER, "some:audience", keys), true); + } + + @Test + public void testValidatorAcceptsOneAudienceAndTokenHasValidAudienceArray() throws Exception + { + assertTokenValidation(List.of("some:audience", "other:audience"), + keys -> new JwtValidatorWithCert(ISSUER, "some:audience", keys), true); + } + + @Test + public void testValidatorAcceptsOneAudienceAndTokenHasInvalidAudienceString() throws Exception + { + assertTokenValidation("some:audience", + keys -> new JwtValidatorWithCert(ISSUER, "other:audience", keys), false); + } + + @Test + public void testValidatorAcceptsOneAudienceAndTokenHasOnlyInvalidAudiencesInArray() throws Exception + { + assertTokenValidation(List.of("invalid:audience", "other:invalid:audience"), + keys -> new JwtValidatorWithCert(ISSUER, "valid:audience", keys), false); + } + + @Test + public void testValidatorAcceptsManyAudiencesAndTokenHasValidAudienceString() throws Exception + { + assertTokenValidation("valid:audience", + keys -> new JwtValidatorWithCert(ISSUER, Set.of("valid:audience", "something:else"), keys), true); + } + + @Test + public void testValidatorAcceptsManyAudiencesAndTokenHasValidAudiencesArray() throws Exception + { + assertTokenValidation(List.of("x", "y", "a"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), true); + + assertTokenValidation(List.of("x", "b", "y"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), true); + + assertTokenValidation(List.of("a", "b"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), true); + } + + @Test + public void testValidatorAcceptsManyAudiencesAndTokenHasInvalidAudienceString() throws Exception + { + assertTokenValidation("invalid:audience", + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), false); + } + + @Test + public void testValidatorAcceptsManyAudiencesAndTokenHasOnlyInvalidAudiencesArray() throws Exception + { + assertTokenValidation(List.of("invalid:audience", "other:audience"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), false); + } + + @Test + public void testValidatorAcceptsNoAudiencesAndTokenHasAnyAudience() throws Exception + { + assertTokenValidation("any:audience", + keys -> new JwtValidatorWithCert(ISSUER, Set.of(), keys), true); + + assertTokenValidation(List.of("any:audience", "another:audience"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of(), keys), true); + } + + @Test + public void testValidatorAcceptsNoIssuersAndTokenHasAnyIssuer() throws Exception + { + assertTokenValidation("some:audience", + keys -> new JwtValidatorWithCert(Set.of(), Set.of("some:audience"), keys), true); + + assertTokenValidation(List.of("some:audience", "other:audience"), + keys -> new JwtValidatorWithCert(Set.of(), Set.of("some:audience"), keys), true); + } + + private static void assertTokenValidation(String tokenAudience, + Function, JwtValidator> validatorFactory, + boolean shouldBeValid) throws Exception + { + // the token is issued with an array of audiences if there's more than one audience, + // or with a string audience if there's only one. + assertTokenValidation(List.of(tokenAudience), validatorFactory, shouldBeValid); + } + + private static void assertTokenValidation(List tokenAudiences, + Function, JwtValidator> validatorFactory, + boolean shouldBeValid) throws Exception + { + JwtTokenFixtureHelper.TokenFixture fixture = JwtTokenFixtureHelper.createTokenFixture(ISSUER, tokenAudiences); + try (JwtValidator validator = validatorFactory.apply(fixture.keys)) + { + if (shouldBeValid) + { + JsonData result = validator.validate(fixture.token); + assertNotNull(result); + } + else + { + assertThrows(TokenValidationException.class, () -> validator.validate(fixture.token)); + } + } + } +} diff --git a/src/test/java/io/curity/oauth/JwtIssuerClaimTest.java b/src/test/java/io/curity/oauth/JwtIssuerClaimTest.java new file mode 100644 index 0000000..13a8970 --- /dev/null +++ b/src/test/java/io/curity/oauth/JwtIssuerClaimTest.java @@ -0,0 +1,77 @@ +/* + * 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 org.junit.Test; + +import java.security.PublicKey; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertThrows; + +public class JwtIssuerClaimTest +{ + private static final String AUDIENCE = "test:audience"; + + @Test + public void testValidatorAcceptsOneIssuerAndTokenHasValidIssuer() throws Exception + { + assertTokenValidation("some:issuer", keys -> new JwtValidatorWithCert("some:issuer", AUDIENCE, keys), true); + } + + @Test + public void testValidatorAcceptsOneIssuerAndTokenHasInvalidIssuer() throws Exception + { + assertTokenValidation("other:issuer", keys -> new JwtValidatorWithCert("some:issuer", AUDIENCE, keys), false); + } + + @Test + public void testValidatorAcceptsManyIssuersAndTokenHasValidIssuer() throws Exception + { + assertTokenValidation("valid:issuer", + keys -> new JwtValidatorWithCert(Set.of("valid:issuer", "other:issuer"), AUDIENCE, keys), true); + } + + @Test + public void testValidatorAcceptsManyIssuersAndTokenHasInvalidIssuer() throws Exception + { + assertTokenValidation("invalid:issuer", + keys -> new JwtValidatorWithCert(Set.of("valid:issuer", "other:issuer"), AUDIENCE, keys), false); + } + + private static void assertTokenValidation(String tokenIssuer, + Function, JwtValidator> validatorFactory, + boolean shouldBeValid) throws Exception + { + JwtTokenFixtureHelper.TokenFixture fixture = JwtTokenFixtureHelper.createTokenFixture(tokenIssuer, AUDIENCE); + try (JwtValidator validator = validatorFactory.apply(fixture.keys)) + { + if (shouldBeValid) + { + JsonData result = validator.validate(fixture.token); + assertNotNull(result); + } + else + { + assertThrows(TokenValidationException.class, () -> validator.validate(fixture.token)); + } + } + } +} diff --git a/src/test/java/io/curity/oauth/JwtTokenFixtureHelper.java b/src/test/java/io/curity/oauth/JwtTokenFixtureHelper.java new file mode 100644 index 0000000..e128754 --- /dev/null +++ b/src/test/java/io/curity/oauth/JwtTokenFixtureHelper.java @@ -0,0 +1,66 @@ +/* + * 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 org.apache.commons.codec.digest.DigestUtils; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +final class JwtTokenFixtureHelper +{ + private static final String KEY_ALIAS = "se.curity.test"; + + private JwtTokenFixtureHelper() {} + + static TokenFixture createTokenFixture(String issuer, String audience) throws Exception + { + return createTokenFixture(issuer, List.of(audience)); + } + + static TokenFixture createTokenFixture(String issuer, List audiences) throws Exception + { + KeyStore keyStore = TestKeyStoreHelper.loadKeyStore(); + PrivateKey privateKey = TestKeyStoreHelper.getPrivateKey(keyStore, KEY_ALIAS); + Certificate cert = TestKeyStoreHelper.getCertificate(keyStore, KEY_ALIAS); + + JwtTokenIssuer tokenIssuer = new JwtTokenIssuer(issuer, "RS256", privateKey, cert); + String token = tokenIssuer.issueToken("testsubject", audiences, 200, Collections.emptyMap()); + + byte[] x5tS256 = DigestUtils.sha256(cert.getEncoded()); + String b64x5tS256 = org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(x5tS256); + Map keys = Map.of(b64x5tS256, cert.getPublicKey()); + return new TokenFixture(token, keys); + } + + static final class TokenFixture + { + final String token; + final Map keys; + + private TokenFixture(String token, Map keys) + { + this.token = token; + this.keys = keys; + } + } +} diff --git a/src/test/java/io/curity/oauth/JwtTokenIssuer.java b/src/test/java/io/curity/oauth/JwtTokenIssuer.java index 4b6443f..db7b76a 100644 --- a/src/test/java/io/curity/oauth/JwtTokenIssuer.java +++ b/src/test/java/io/curity/oauth/JwtTokenIssuer.java @@ -1,12 +1,12 @@ /* * 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. @@ -29,15 +29,14 @@ import javax.json.JsonArrayBuilder; import javax.json.JsonObjectBuilder; import javax.json.JsonWriter; -import java.security.PrivateKey; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; import java.io.StringWriter; import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.time.Instant; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -99,13 +98,10 @@ private JwtTokenIssuer(String issuer, int skewTolerance, String algorithm, Priva String issueToken(String subject, String audience, int lifetimeInMinutes, Map attributes) throws Exception { - String[] audiences = stringToArray(audience); - - return issueToken(subject, Arrays.asList(audiences), lifetimeInMinutes, attributes); + return issueToken(subject, List.of(audience), lifetimeInMinutes, attributes); } - private String issueToken(String subject, List audiences, int lifetimeInMinutes, Map - attributes) + String issueToken(String subject, List audiences, int lifetimeInMinutes, Map attributes) throws Exception { Map claims = new LinkedHashMap<>(); @@ -197,7 +193,7 @@ else if (this._keyId != null) _logger.trace(message); } - + _logger.trace(serializedToken); } @@ -211,20 +207,13 @@ else if (this._keyId != null) } } - private String[] stringToArray(String str) - { - String[] ret; - ret = str.split(" "); - return ret; - } - - private Object arrayOrString(List data) + private Object arrayOrString(List data) { if (data.size() == 1) { return data.get(0); } - + return data; } diff --git a/src/test/java/io/curity/oauth/JwtWithCertTest.java b/src/test/java/io/curity/oauth/JwtWithCertTest.java index 60004f2..79e1f1f 100644 --- a/src/test/java/io/curity/oauth/JwtWithCertTest.java +++ b/src/test/java/io/curity/oauth/JwtWithCertTest.java @@ -26,12 +26,7 @@ import javax.json.JsonObject; import javax.json.JsonString; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.net.URL; import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; @@ -55,9 +50,6 @@ public class JwtWithCertTest private final String EXTRA_CLAIM = "TEST_KEY"; private final String EXTRA_CLAIM_VALUE = "TEST_VALUE"; - private final String PATH_TO_KEY = "/Se.Curity.Test.p12"; - private final String KEY_PWD = "Password1"; - private String _testToken; private KeyStore _keyStore; @@ -75,10 +67,10 @@ public static Object[] keysToTest() { @Before public void before() throws Exception { - loadKeyStore(); + _keyStore = TestKeyStoreHelper.loadKeyStore(); - PrivateKey key = getPrivateKey(); - Certificate cert = getCertificate(); + PrivateKey key = TestKeyStoreHelper.getPrivateKey(_keyStore, _keyAlias); + Certificate cert = TestKeyStoreHelper.getCertificate(_keyStore, _keyAlias); if (!_algorithm.equals("EdDSA")) { // Create test token on the fly @@ -139,7 +131,7 @@ private Map prepareKeyMap() throws Exception { Map keys = new HashMap<>(); - Certificate cert = getCertificate(); + Certificate cert = TestKeyStoreHelper.getCertificate(_keyStore, _keyAlias); PublicKey key = cert.getPublicKey(); @@ -150,33 +142,4 @@ private Map prepareKeyMap() throws Exception 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); - } } diff --git a/src/test/java/io/curity/oauth/JwtWithJwksTest.java b/src/test/java/io/curity/oauth/JwtWithJwksTest.java index 5e59676..ca48cd9 100644 --- a/src/test/java/io/curity/oauth/JwtWithJwksTest.java +++ b/src/test/java/io/curity/oauth/JwtWithJwksTest.java @@ -1,12 +1,12 @@ /* * 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. @@ -27,14 +27,8 @@ 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; -import java.net.URL; import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.PrivateKey; -import java.security.cert.Certificate; import java.security.interfaces.EdECPrivateKey; import java.util.Collections; import java.util.HashMap; @@ -58,10 +52,6 @@ public class JwtWithJwksTest 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; @@ -82,9 +72,9 @@ public static Object[] keysToTest() { @Before public void before() throws Exception { - loadKeyStore(); + _keyStore = TestKeyStoreHelper.loadKeyStore(); - PrivateKey key = getPrivateKey(); + PrivateKey key = TestKeyStoreHelper.getPrivateKey(_keyStore, _keyAlias); if (!_algorithm.equals("EdDSA")) { // Create test token on the fly @@ -157,33 +147,4 @@ private Map prepareKeyMap() 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); - } } diff --git a/src/test/java/io/curity/oauth/TestKeyStoreHelper.java b/src/test/java/io/curity/oauth/TestKeyStoreHelper.java new file mode 100644 index 0000000..aefb952 --- /dev/null +++ b/src/test/java/io/curity/oauth/TestKeyStoreHelper.java @@ -0,0 +1,64 @@ +/* + * 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 java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; + +final class TestKeyStoreHelper +{ + private static final String PATH_TO_KEY = "/Se.Curity.Test.p12"; + private static final String KEY_PWD = "Password1"; + + private TestKeyStoreHelper() + { + } + + static KeyStore loadKeyStore() throws Exception + { + return loadKeyStore(TestKeyStoreHelper.class, PATH_TO_KEY); + } + + static KeyStore loadKeyStore(Class testClass, String keyPath) throws Exception + { + URL url = testClass.getResource(keyPath); + assert url != null; + File certFile = new File(url.getFile()); + + try (InputStream keyIS = new FileInputStream(certFile)) + { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(keyIS, KEY_PWD.toCharArray()); + return keyStore; + } + } + + static PrivateKey getPrivateKey(KeyStore keyStore, String keyAlias) throws Exception + { + return (PrivateKey) keyStore.getKey(keyAlias, KEY_PWD.toCharArray()); + } + + static Certificate getCertificate(KeyStore keyStore, String keyAlias) throws Exception + { + return keyStore.getCertificate(keyAlias); + } +}