Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 6 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

<groupId>io.curity</groupId>
<artifactId>oauth-filter</artifactId>
<version>4.0.0</version>
<version>4.1.0</version>
<name>OAuth API Filter</name>
<description>A Servlet Filter that authenticates and authorizes requests using OAuth access tokens of various kinds.</description>
<url>https://github.com/curityio/oauth-filter-for-java</url>
Expand Down Expand Up @@ -73,10 +73,11 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
<!-- The deps require Java 11, but the use of java.security.interfaces.EdECPublicKey requires Java 15 -->
<source>15</source>
<target>15</target>
<optimize>true</optimize>
<debug>false</debug>
</configuration>
Expand Down Expand Up @@ -269,7 +270,7 @@
</execution>
</executions>
<configuration>
<source>11</source>
<source>15</source>
</configuration>
</plugin>
<plugin>
Expand Down
32 changes: 17 additions & 15 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down
48 changes: 32 additions & 16 deletions src/main/java/io/curity/oauth/AbstractJwtValidator.java
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

Expand All @@ -42,16 +43,31 @@ abstract class AbstractJwtValidator implements JwtValidator
private final Map<String, JsonObject> _decodedJwtBodyByEncodedBody = new HashMap<>(1);
private final Map<String, JwtHeader> _decodedJwtHeaderByEncodedHeader = new HashMap<>(1);
private final JsonReaderFactory _jsonReaderFactory;
private final String _audience;
private final String _issuer;
private final Set<String> _audiences;
private final Set<String> _issuers;

AbstractJwtValidator(String issuer, String audience, JsonReaderFactory jsonReaderFactory)
protected AbstractJwtValidator(Set<String> issuers, Set<String> 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<String> audiences, JsonReaderFactory jsonReaderFactory)
{
this(Set.of(issuer), audiences, jsonReaderFactory);
}

protected AbstractJwtValidator(Set<String> issuers, String audience, JsonReaderFactory jsonReaderFactory)
{
this(issuers, Set.of(audience), jsonReaderFactory);
}

public final JsonData validate(String jwt) throws TokenValidationException
{
String[] jwtParts = jwt.split("\\.");
Expand All @@ -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<String> 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();
Expand Down Expand Up @@ -230,7 +246,7 @@ private JwtHeader decodeJwtHeader(String header)
});
}

class JwtHeader
static final class JwtHeader
{
private final JsonObject _jsonObject;

Expand Down
23 changes: 17 additions & 6 deletions src/main/java/io/curity/oauth/FilterHelper.java
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -67,15 +69,24 @@ static <T> T getInitParamValue(String name, Map<String, ?> initParams,
}

static <T> Optional<T> getOptionalInitParamValue(String name, Map<String, ?> initParams,
Function<String, T> converter) throws UnavailableException
Function<String, T> converter)
throws UnavailableException
{
Optional<String> value = getSingleValue(name, initParams);

return value.flatMap(s -> Optional.ofNullable(converter.apply(s)));
}

private static Optional<String> getSingleValue(String name, Map<String, ?> initParams) throws
UnavailableException
static List<String> getWhitespaceSeparatedInitParamValues(String name, Map<String, ?> initParams)
throws UnavailableException
{
Optional<String> value = getSingleValue(name, initParams);

return value.map(v -> List.of(v.split("\\s+"))).orElseGet(List::of);
}

private static Optional<String> getSingleValue(String name, Map<String, ?> initParams)
throws UnavailableException
{
return Optional.ofNullable(initParams.get(name)).map(Object::toString);
}
Expand Down
50 changes: 49 additions & 1 deletion src/main/java/io/curity/oauth/JsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.curity.oauth;

import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonReaderFactory;
Expand All @@ -27,6 +28,7 @@
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

final class JsonUtils
{
Expand All @@ -49,19 +51,65 @@ static Set<String> 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<String> 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))
.filter(it -> it.getValueType() == JsonValue.ValueType.NUMBER)
.map(it -> ((JsonNumber) it).longValue())
.orElse(Long.MIN_VALUE);
}

private static String stringValue(JsonValue value)
{
return ((JsonString) value).getString();
}

private static Set<String> 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());
}
Comment on lines +105 to +110

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do the humans think here? I intentionally decided to throw on arrays containing a non-string element since that's almost certainly to be a broken token.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's OK. It's true for a claim like aud that it should contain only strings, and it would mean a broken token otherwise. The method is also named "getStrings" so it shouldn't be a surprise that it can't get other types ;)


// be extremely lenient and ignore the value
return Set.of();
}
}
44 changes: 38 additions & 6 deletions src/main/java/io/curity/oauth/JwtValidatorWithCert.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, PublicKey> _keys;

JwtValidatorWithCert(Set<String> issuers, Set<String> audiences, Map<String, PublicKey> publicKeys)
{
this(issuers, audiences, publicKeys, JsonUtils.createDefaultReaderFactory());
}

JwtValidatorWithCert(String issuer, Set<String> audiences, Map<String, PublicKey> publicKeys)
{
this(Set.of(issuer), audiences, publicKeys);
}

JwtValidatorWithCert(Set<String> issuers, String audience, Map<String, PublicKey> publicKeys)
{
this(issuers, Set.of(audience), publicKeys);
}

JwtValidatorWithCert(String issuer, String audience, Map<String, PublicKey> publicKeys)
{
this(issuer, audience, publicKeys, JsonUtils.createDefaultReaderFactory());
this(Set.of(issuer), audience, publicKeys);
}

JwtValidatorWithCert(String issuer, String audience, Map<String, PublicKey> publicKeys,
JwtValidatorWithCert(Set<String> issuers, Set<String> audiences, Map<String, PublicKey> publicKeys,
JsonReaderFactory jsonReaderFactory)
{
super(issuer, audience, jsonReaderFactory);
super(issuers, audiences, jsonReaderFactory);

_keys = publicKeys;
}

JwtValidatorWithCert(String issuer, Set<String> audiences, Map<String, PublicKey> publicKeys,
JsonReaderFactory jsonReaderFactory)
{
this(Set.of(issuer), audiences, publicKeys, jsonReaderFactory);
}

JwtValidatorWithCert(Set<String> issuers, String audience, Map<String, PublicKey> publicKeys,
JsonReaderFactory jsonReaderFactory)
{
this(issuers, Set.of(audience), publicKeys, jsonReaderFactory);
}

JwtValidatorWithCert(String issuer, String audience, Map<String, PublicKey> publicKeys,
JsonReaderFactory jsonReaderFactory)
{
this(Set.of(issuer), audience, publicKeys, jsonReaderFactory);
}

@Override
protected Optional<PublicKey> getPublicKey(JwtHeader jwtHeader)
{
Expand Down
Loading