Skip to content

Commit d6bce8e

Browse files
authored
Validate expiry, nbg + audience for the JwtAccessTokenValidator (#3126)
1 parent be511e5 commit d6bce8e

3 files changed

Lines changed: 221 additions & 1 deletion

File tree

rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/filters/JwtAccessTokenValidator.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
3333
import org.apache.cxf.rs.security.jose.jwt.JwtConstants;
3434
import org.apache.cxf.rs.security.jose.jwt.JwtToken;
35+
import org.apache.cxf.rs.security.jose.jwt.JwtUtils;
3536
import org.apache.cxf.rs.security.oauth2.common.AccessTokenValidation;
3637
import org.apache.cxf.rs.security.oauth2.common.OAuthPermission;
3738
import org.apache.cxf.rs.security.oauth2.common.UserSubject;
@@ -46,6 +47,7 @@ public class JwtAccessTokenValidator extends JoseJwtConsumer implements AccessTo
4647
private static final String USERNAME_PROP = "username";
4748

4849
private Map<String, String> jwtAccessTokenClaimMap;
50+
private boolean validateAudience = true;
4951

5052
public List<String> getSupportedAuthorizationSchemes() {
5153
return Collections.singletonList(OAuthConstants.BEARER_AUTHORIZATION_SCHEME);
@@ -64,6 +66,15 @@ public AccessTokenValidation validateAccessToken(MessageContext mc,
6466
}
6567
}
6668

69+
@Override
70+
protected void validateToken(JwtToken jwt) {
71+
// We must have an issuer
72+
if (jwt.getClaim(JwtConstants.CLAIM_ISSUER) == null) {
73+
throw new OAuthServiceException(OAuthConstants.INVALID_GRANT);
74+
}
75+
76+
JwtUtils.validateTokenClaims(jwt.getClaims(), getTtl(), getClockOffset(), isValidateAudience());
77+
}
6778

6879
private AccessTokenValidation convertClaimsToValidation(JwtClaims claims) {
6980
AccessTokenValidation atv = new AccessTokenValidation();
@@ -134,4 +145,11 @@ public void setJwtAccessTokenClaimMap(Map<String, String> jwtAccessTokenClaimMap
134145
this.jwtAccessTokenClaimMap = jwtAccessTokenClaimMap;
135146
}
136147

148+
public boolean isValidateAudience() {
149+
return validateAudience;
150+
}
151+
152+
public void setValidateAudience(boolean validateAudience) {
153+
this.validateAudience = validateAudience;
154+
}
137155
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.cxf.rs.security.oauth2.filters;
20+
21+
import java.lang.reflect.Field;
22+
23+
import jakarta.ws.rs.core.MultivaluedHashMap;
24+
import jakarta.ws.rs.core.MultivaluedMap;
25+
import org.apache.cxf.jaxrs.ext.MessageContext;
26+
import org.apache.cxf.message.Message;
27+
import org.apache.cxf.message.MessageImpl;
28+
import org.apache.cxf.phase.PhaseInterceptorChain;
29+
import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
30+
import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureProvider;
31+
import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureVerifier;
32+
import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactProducer;
33+
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
34+
import org.apache.cxf.rs.security.jose.jwt.JwtConstants;
35+
import org.apache.cxf.rs.security.oauth2.common.AccessTokenValidation;
36+
import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
37+
38+
import org.junit.After;
39+
import org.junit.Test;
40+
41+
import static org.junit.Assert.assertEquals;
42+
import static org.junit.Assert.assertNotNull;
43+
import static org.junit.Assert.assertThrows;
44+
import static org.junit.Assert.assertTrue;
45+
import static org.mockito.Mockito.mock;
46+
47+
48+
public class JwtAccessTokenValidatorTest {
49+
50+
private static final String SIGNING_KEY = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoq2QjvA6P5f8";
51+
private static final String DIFFERENT_SIGNING_KEY = "hJtXIZ2uSN5kbQfbtTNWbg6X5U0ZSyxP6oJ6H3f3j1k";
52+
53+
@Test
54+
public void testValidateAccessTokenSignedAndSignatureVerified() {
55+
JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
56+
validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));
57+
validator.setValidateAudience(false);
58+
59+
String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600);
60+
MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
61+
62+
AccessTokenValidation result = validator.validateAccessToken(
63+
mock(MessageContext.class), "Bearer", jwt, params);
64+
65+
assertNotNull(result);
66+
assertTrue(result.isInitialValidationSuccessful());
67+
assertEquals("signed-client", result.getClientId());
68+
}
69+
70+
@Test
71+
public void testValidateAccessTokenSignedButSignatureValidationFails() {
72+
JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
73+
validator.setJwsVerifier(new HmacJwsSignatureVerifier(DIFFERENT_SIGNING_KEY, SignatureAlgorithm.HS256));
74+
validator.setValidateAudience(false);
75+
76+
String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600);
77+
MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
78+
79+
OAuthServiceException ex = assertThrows(OAuthServiceException.class, () ->
80+
validator.validateAccessToken(mock(MessageContext.class), "Bearer", jwt, params));
81+
82+
assertNotNull(ex.getCause());
83+
assertTrue(ex.getCause().getMessage().contains("Invalid Signature"));
84+
}
85+
86+
@Test
87+
public void testValidateAccessTokenExpired() {
88+
JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
89+
validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));
90+
validator.setValidateAudience(false);
91+
92+
String jwt = createSignedToken(SIGNING_KEY, "signed-client", -3600); // Expired 1 hour ago
93+
MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
94+
95+
OAuthServiceException ex = assertThrows(OAuthServiceException.class, () ->
96+
validator.validateAccessToken(mock(MessageContext.class), "Bearer", jwt, params));
97+
98+
assertNotNull(ex.getCause());
99+
assertTrue(ex.getCause().getMessage().contains("expired"));
100+
}
101+
102+
@Test
103+
public void testValidateAccessTokenNotBefore() {
104+
JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
105+
validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));
106+
validator.setValidateAudience(false);
107+
108+
// Not valid before 1 hour from now
109+
String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600, 3600, null);
110+
MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
111+
112+
OAuthServiceException ex = assertThrows(OAuthServiceException.class, () ->
113+
validator.validateAccessToken(mock(MessageContext.class), "Bearer", jwt, params));
114+
115+
assertNotNull(ex.getCause());
116+
assertTrue(ex.getCause().getMessage().contains("cannot be accepted"));
117+
}
118+
119+
@After
120+
public void clearCurrentMessage() throws Exception {
121+
setThreadLocalMessage(null);
122+
}
123+
124+
@Test
125+
public void testValidAudience() throws Exception {
126+
JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
127+
validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));
128+
129+
String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600, 0, "valid-audience");
130+
MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
131+
132+
Message message = new MessageImpl();
133+
message.put(JwtConstants.EXPECTED_CLAIM_AUDIENCE, "valid-audience");
134+
setThreadLocalMessage(message);
135+
136+
AccessTokenValidation result = validator.validateAccessToken(
137+
mock(MessageContext.class), "Bearer", jwt, params);
138+
139+
assertNotNull(result);
140+
assertTrue(result.isInitialValidationSuccessful());
141+
assertEquals("signed-client", result.getClientId());
142+
}
143+
144+
@Test
145+
public void testInvalidAudience() throws Exception {
146+
JwtAccessTokenValidator validator = new JwtAccessTokenValidator();
147+
validator.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));
148+
149+
String jwt = createSignedToken(SIGNING_KEY, "signed-client", 3600, 0, "invalid-audience");
150+
MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
151+
152+
Message message = new MessageImpl();
153+
message.put(JwtConstants.EXPECTED_CLAIM_AUDIENCE, "valid-audience");
154+
setThreadLocalMessage(message);
155+
156+
OAuthServiceException ex = assertThrows(OAuthServiceException.class, () ->
157+
validator.validateAccessToken(mock(MessageContext.class), "Bearer", jwt, params));
158+
159+
assertNotNull(ex.getCause());
160+
assertTrue(ex.getCause().getMessage().contains("Invalid audience restriction"));
161+
}
162+
163+
private static String createSignedToken(String key, String clientId, long expiresInSeconds) {
164+
return createSignedToken(key, clientId, expiresInSeconds, 0, null);
165+
}
166+
167+
private static String createSignedToken(String key, String clientId, long expiresInSeconds,
168+
long notBeforeOffsetSeconds, String audience) {
169+
long now = System.currentTimeMillis() / 1000;
170+
JwtClaims claims = new JwtClaims();
171+
claims.setIssuedAt(now);
172+
claims.setExpiryTime(now + expiresInSeconds);
173+
if (clientId != null) {
174+
claims.setClaim("client_id", clientId);
175+
}
176+
claims.setIssuer("SomeIssuer");
177+
claims.setSubject("SomeSubject");
178+
if (notBeforeOffsetSeconds != 0) {
179+
claims.setNotBefore(now + notBeforeOffsetSeconds);
180+
}
181+
if (audience != null) {
182+
claims.setAudience(audience);
183+
}
184+
185+
JwsJwtCompactProducer producer = new JwsJwtCompactProducer(claims);
186+
return producer.signWith(new HmacJwsSignatureProvider(key, SignatureAlgorithm.HS256));
187+
}
188+
189+
private static void setThreadLocalMessage(Message message) throws Exception {
190+
Field f = PhaseInterceptorChain.class.getDeclaredField("CURRENT_MESSAGE");
191+
f.setAccessible(true);
192+
@SuppressWarnings("unchecked")
193+
ThreadLocal<Message> tl = (ThreadLocal<Message>) f.get(null);
194+
if (message == null) {
195+
tl.remove();
196+
} else {
197+
tl.set(message);
198+
}
199+
}
200+
}

systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oauth2/tls/serverTls.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ under the License.
6969

7070
<bean id="oauthJson" class="org.apache.cxf.rs.security.oauth2.provider.OAuthJSONProvider"/>
7171
<bean id="dataProvider" class="org.apache.cxf.systest.jaxrs.security.oauth2.tls.OAuthDataProviderImpl"/>
72-
<bean id="dataProviderJwt" class="org.apache.cxf.systest.jaxrs.security.oauth2.tls.OAuthDataProviderImplJwt"/>
72+
<bean id="dataProviderJwt" class="org.apache.cxf.systest.jaxrs.security.oauth2.tls.OAuthDataProviderImplJwt">
73+
<property name="issuer" value="Some-Issuer"/>
74+
</bean>
7375
<bean id="rsService" class="org.apache.cxf.systest.jaxrs.security.BookStore"/>
7476

7577
<bean id="accessTokenService1" class="org.apache.cxf.rs.security.oauth2.services.AccessTokenService">

0 commit comments

Comments
 (0)