diff --git a/docs/modules/servers/partials/configure/imap.adoc b/docs/modules/servers/partials/configure/imap.adoc
index af6911fe434..c42b9432b3d 100644
--- a/docs/modules/servers/partials/configure/imap.adoc
+++ b/docs/modules/servers/partials/configure/imap.adoc
@@ -62,6 +62,22 @@ Whether to enable Authentication PLAIN if the connection is not encrypted via SS
| auth.oidc.scope
| An OAuth scope that is valid to access the service (RF: RFC7628). Only configure this when you want to authenticate IMAP server using a OIDC provider.
+| auth.oidc.aud
+| An OAuth audience to access the service (RF: RFC7628). Only configure this when you want to authenticate IMAP server using a OIDC provider.
+Compulsory but can be relaxed with `-Djames.sasl.oidc.validate.aud=false`
+
+| auth.oidc.introspection.url
+| Optional. An OAuth introspection token URL will be called to validate the token (RF: RFC7662).
+Note that James always verifies the signature of the token even whether this configuration is provided or not.
+
+| auth.oidc.introspection.auth
+| Optional. Provide Authorization in header request when introspecting token.
+Eg: `Basic xyz`
+
+| auth.oidc.userinfo.url
+| Optional. An Userinfo URL will be called to retrieve additional user information
+(RF: OpenId.Core https://openid.net/specs/openid-connect-core-1_0.html).
+
| timeout
| Default to 30 minutes. After this time, inactive channels that have not performed read, write, or both operation for a while
will be closed. Negative value disable this behaviour.
@@ -265,4 +281,4 @@ Example:
2min
500 MiB
-....
\ No newline at end of file
+....
diff --git a/docs/modules/servers/partials/configure/smtp.adoc b/docs/modules/servers/partials/configure/smtp.adoc
index 1a68a0094fa..788f205b7bf 100644
--- a/docs/modules/servers/partials/configure/smtp.adoc
+++ b/docs/modules/servers/partials/configure/smtp.adoc
@@ -111,9 +111,12 @@ can be used to enforce strong authentication mechanisms.
| auth.oidc.scope
| An OAuth scope that is valid to access the service (RF: RFC7628). Only configure this when you want to authenticate SMTP server using a OIDC provider.
+| auth.oidc.aud
+| An OAuth audience to access the service (RF: RFC7628). Only configure this when you want to authenticate IMAP server using a OIDC provider.
+Compulsory but can be relaxed with `-Djames.sasl.oidc.validate.aud=false`
+
| auth.oidc.introspection.url
| Optional. An OAuth introspection token URL will be called to validate the token (RF: RFC7662).
-Only configure this when you want to validate the revocation token by the OIDC provider.
Note that James always verifies the signature of the token even whether this configuration is provided or not.
| auth.oidc.introspection.auth
@@ -121,10 +124,8 @@ Note that James always verifies the signature of the token even whether this con
Eg: `Basic xyz`
| auth.oidc.userinfo.url
-| Optional. An Userinfo URL will be called to validate the token (RF: OpenId.Core https://openid.net/specs/openid-connect-core-1_0.html).
-Only configure this when you want to validate the revocation token by the OIDC provider.
-Note that James always verifies the signature of the token even whether this configuration is provided or not.
-James will ignore check token by userInfo if the `auth.oidc.introspection.url` is already configured
+| Optional. An Userinfo URL will be called to retrieve additional user information
+(RF: OpenId.Core https://openid.net/specs/openid-connect-core-1_0.html).
| authorizedAddresses
| Authorize specific addresses/networks.
diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
index ae197d8e68b..7fb8350c151 100644
--- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
+++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java
@@ -37,7 +37,6 @@
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Locator;
-import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.io.CompressionAlgorithm;
public class JwtTokenVerifier {
@@ -121,27 +120,33 @@ public Optional verifyAndExtractLogin(String token) {
}
public Optional verifyAndExtractClaim(String token, String claimName, Class returnType) {
+ return verify(token)
+ .flatMap(claims -> {
+ try {
+ return Optional.ofNullable(claims.get(claimName, returnType));
+ } catch (JwtException e) {
+ LOGGER.info("Failed Jwt verification", e);
+ return Optional.empty();
+ }
+ });
+ }
+
+ public Optional verify(String token) {
try {
// if the token contains a kid, verify only with the corresponding key (or fail)
- return verifyAndExtractClaim(token, claimName, returnType, kidJwtParser);
+ return retrieveClaims(token, kidJwtParser);
} catch (NullPointerException npe) { // our own key locator throws NPE when there is no kid
// if token does not specify kid, fallback to trying all keys
return jwtParsers.stream()
- .flatMap(parser -> verifyAndExtractClaim(token, claimName, returnType, parser).stream())
+ .flatMap(parser -> retrieveClaims(token, parser).stream())
.findFirst();
}
}
- private Optional verifyAndExtractClaim(String token, String claimName, Class returnType, JwtParser parser) {
+ private Optional retrieveClaims(String token, JwtParser parser) {
try {
Jws jws = parser.parseSignedClaims(token);
- T claim = jws
- .getPayload()
- .get(claimName, returnType);
- if (claim == null) {
- throw new MalformedJwtException("'" + claimName + "' field in token is mandatory");
- }
- return Optional.of(claim);
+ return Optional.of(jws.getPayload());
} catch (JwtException e) { // also if kid was given but our locator didn't find the corresponding key
LOGGER.info("Failed Jwt verification", e);
return Optional.empty();
diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java
index 7f87132bb7c..78dd2444305 100644
--- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java
+++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java
@@ -21,7 +21,6 @@
import java.net.URL;
import java.util.Optional;
-import java.util.function.Predicate;
import org.apache.james.core.Username;
import org.apache.james.jwt.introspection.IntrospectionEndpoint;
@@ -32,6 +31,7 @@
import com.google.common.annotations.VisibleForTesting;
+import io.jsonwebtoken.JwtException;
import reactor.core.publisher.Mono;
public class OidcJwtTokenVerifier {
@@ -72,8 +72,16 @@ private Optional validTokenWithIntrospection(String token) {
@VisibleForTesting
Optional verifySignatureAndExtractClaim(String jwtToken) {
- return new JwtTokenVerifier(JwksPublicKeyProvider.of(oidcSASLConfiguration.getJwksURL()))
- .verifyAndExtractClaim(jwtToken, oidcSASLConfiguration.getClaim(), String.class);
+ try {
+ return new JwtTokenVerifier(JwksPublicKeyProvider.of(oidcSASLConfiguration.getJwksURL()))
+ .verify(jwtToken)
+ .filter(claims -> oidcSASLConfiguration.getAud().map(expectedAud -> claims.getAudience().contains(expectedAud))
+ .orElse(true)) // true if no aud is configured
+ .flatMap(claims -> Optional.ofNullable(claims.get(oidcSASLConfiguration.getClaim(), String.class)));
+ } catch (JwtException e) {
+ LOGGER.info("Failed Jwt verification", e);
+ return Optional.empty();
+ }
}
@VisibleForTesting
@@ -82,27 +90,12 @@ Publisher verifyWithIntrospection(String jwtToken, IntrospectionEndpoint
.flatMap(optional -> optional.map(Mono::just).orElseGet(Mono::empty))
.flatMap(claimResult -> Mono.from(CHECK_TOKEN_CLIENT.introspect(introspectionEndpoint, jwtToken))
.filter(TokenIntrospectionResponse::active)
- .filter(validateAud())
.filter(tokenIntrospectionResponse -> tokenIntrospectionResponse.claimByPropertyName(oidcSASLConfiguration.getClaim())
.map(claim -> claim.equals(claimResult))
.orElse(false))
.map(activeResponse -> claimResult));
}
- private Predicate validateAud() {
- return oidcSASLConfiguration.getAud()
- .map(this::validateAud)
- .orElse(any -> true);
- }
-
- private Predicate validateAud(String expectedAud) {
- return token -> {
- boolean result = token.aud().map(expectedAud::equals).orElse(false);
- LOGGER.warn("Wrong aud. Expected {} got {}", expectedAud, token.aud());
- return result;
- };
- }
-
@VisibleForTesting
Publisher verifyWithUserinfo(String jwtToken, URL userinfoEndpoint) {
return Mono.fromCallable(() -> verifySignatureAndExtractClaim(jwtToken))
diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java
index cb59ef1811b..2084fe62feb 100644
--- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java
+++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java
@@ -139,14 +139,6 @@ public static OidcSASLConfiguration parse(HierarchicalConfiguration emailAddress = new OidcJwtTokenVerifier(
+ OidcSASLConfiguration.builder()
+ .jwksURL(getJwksURL())
+ .scope("email")
+ .oidcConfigurationURL(new URL("https://whatever.nte"))
+ .claim("email_address")
+ .aud("account")
+ .build())
+ .verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN);
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(emailAddress.isPresent()).isTrue();
+ softly.assertThat(emailAddress.get()).isEqualTo("user@domain.org");
+ });
+ }
+
+ @Test
+ void verifyAndClaimShouldRejectInvalidAud() throws Exception {
+ Optional emailAddress = new OidcJwtTokenVerifier(
+ OidcSASLConfiguration.builder()
+ .jwksURL(getJwksURL())
+ .scope("email")
+ .oidcConfigurationURL(new URL("https://whatever.nte"))
+ .claim("email_address")
+ .aud("other")
+ .build())
+ .verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN);
+
+ assertThat(emailAddress).isEmpty();
+ }
+
@Test
void verifyAndClaimShouldReturnClaimValueWhenValidTokenHasKid() {
Optional emailAddress = new OidcJwtTokenVerifier(configForClaim("email_address")).verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN);
diff --git a/server/protocols/protocols-imap4/pom.xml b/server/protocols/protocols-imap4/pom.xml
index eb9199b7a2b..f119258df33 100644
--- a/server/protocols/protocols-imap4/pom.xml
+++ b/server/protocols/protocols-imap4/pom.xml
@@ -188,7 +188,7 @@
-Djava.library.path=
-javaagent:"${settings.localRepository}"/org/jacoco/org.jacoco.agent/${jacoco-maven-plugin.version}/org.jacoco.agent-${jacoco-maven-plugin.version}-runtime.jar=destfile=${basedir}/target/jacoco.exec
- -Xms1024m -Xmx2048m -Djames.sasl.oidc.force.introspect=false -Djames.sasl.oidc.validate.aud=false
+ -Xms1024m -Xmx2048m -Djames.sasl.oidc.validate.aud=false
true
1800
diff --git a/server/protocols/protocols-lmtp/pom.xml b/server/protocols/protocols-lmtp/pom.xml
index f9a0dfca741..ff8fd671b17 100644
--- a/server/protocols/protocols-lmtp/pom.xml
+++ b/server/protocols/protocols-lmtp/pom.xml
@@ -195,7 +195,7 @@
-Djava.library.path=
-javaagent:"${settings.localRepository}"/org/jacoco/org.jacoco.agent/${jacoco-maven-plugin.version}/org.jacoco.agent-${jacoco-maven-plugin.version}-runtime.jar=destfile=${basedir}/target/jacoco.exec
- -Xms512m -Xmx1024m -Djames.sasl.oidc.force.introspect=false -Djames.sasl.oidc.validate.aud=false
+ -Xms512m -Xmx1024m -Djames.sasl.oidc.validate.aud=false
true
1800
diff --git a/server/protocols/protocols-smtp/pom.xml b/server/protocols/protocols-smtp/pom.xml
index 5cf71317544..9507cd95ec0 100644
--- a/server/protocols/protocols-smtp/pom.xml
+++ b/server/protocols/protocols-smtp/pom.xml
@@ -226,7 +226,7 @@
-Djava.library.path=
-javaagent:"${settings.localRepository}"/org/jacoco/org.jacoco.agent/${jacoco-maven-plugin.version}/org.jacoco.agent-${jacoco-maven-plugin.version}-runtime.jar=destfile=${basedir}/target/jacoco.exec
- -Xms512m -Xmx1024m -Djames.sasl.oidc.force.introspect=false -Djames.sasl.oidc.validate.aud=false
+ -Xms512m -Xmx1024m -Djames.sasl.oidc.validate.aud=false
true
1800