Skip to content

Commit 64f8929

Browse files
authored
Switch AbstractJwtHandler to require that the subject = the client Id by default (#3291)
1 parent c2c5619 commit 64f8929

2 files changed

Lines changed: 141 additions & 0 deletions

File tree

rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/jwt/AbstractJwtHandler.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ protected void validateSubject(Client client, String subject) {
8282
if (subject == null) {
8383
throw new OAuthServiceException(OAuthConstants.INVALID_GRANT);
8484
}
85+
// Strict-by-default: require the assertion subject to match the authenticated client id.
86+
// Subclasses can override this method to support a different authorization model
87+
// (for example, trusted delegation with an explicit client-to-subject policy).
88+
if (client != null && client.getClientId() != null
89+
&& !client.getClientId().equals(subject)) {
90+
throw new OAuthServiceException(OAuthConstants.INVALID_GRANT);
91+
}
8592
}
8693
public void setSupportedIssuers(Set<String> supportedIssuers) {
8794
this.supportedIssuers = supportedIssuers;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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.grants.jwt;
20+
21+
import java.lang.reflect.Field;
22+
23+
import jakarta.ws.rs.core.MultivaluedMap;
24+
import org.apache.cxf.jaxrs.impl.MetadataMap;
25+
import org.apache.cxf.message.Message;
26+
import org.apache.cxf.message.MessageImpl;
27+
import org.apache.cxf.phase.PhaseInterceptorChain;
28+
import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
29+
import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureProvider;
30+
import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureVerifier;
31+
import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactProducer;
32+
import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
33+
import org.apache.cxf.rs.security.oauth2.common.AccessTokenRegistration;
34+
import org.apache.cxf.rs.security.oauth2.common.Client;
35+
import org.apache.cxf.rs.security.oauth2.common.ServerAccessToken;
36+
import org.apache.cxf.rs.security.oauth2.grants.OAuthDataProviderImpl;
37+
import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
38+
import org.apache.cxf.rs.security.oauth2.tokens.bearer.BearerAccessToken;
39+
40+
import org.junit.After;
41+
import org.junit.Before;
42+
import org.junit.Test;
43+
44+
import static org.junit.Assert.assertEquals;
45+
import static org.junit.Assert.assertNotNull;
46+
import static org.junit.Assert.fail;
47+
48+
public class JwtBearerGrantHandlerTest {
49+
50+
private static final String SIGNING_KEY = "jwt-grant-test-signing-key";
51+
52+
@Before
53+
public void setUp() throws Exception {
54+
setThreadLocalMessage(new MessageImpl());
55+
}
56+
57+
@After
58+
public void tearDown() throws Exception {
59+
setThreadLocalMessage(null);
60+
}
61+
62+
@Test
63+
public void testMismatchedClientAndSubjectRejected() {
64+
JwtBearerGrantHandler handler = new JwtBearerGrantHandler();
65+
handler.setDataProvider(new SubjectAwareDataProvider());
66+
handler.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));
67+
68+
Client client = new Client("fuzz-client", "secret", true);
69+
String assertion = createSignedAssertion("trusted-issuer", "victim-user");
70+
71+
MultivaluedMap<String, String> params = new MetadataMap<>();
72+
params.putSingle(Constants.CLIENT_GRANT_ASSERTION_PARAM, assertion);
73+
74+
try {
75+
handler.createAccessToken(client, params);
76+
fail("OAuthServiceException expected");
77+
} catch (OAuthServiceException expected) {
78+
assertEquals("invalid_grant", expected.getMessage());
79+
}
80+
}
81+
82+
@Test
83+
public void testMatchingClientAndSubjectAccepted() {
84+
JwtBearerGrantHandler handler = new JwtBearerGrantHandler();
85+
handler.setDataProvider(new SubjectAwareDataProvider());
86+
handler.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));
87+
88+
Client client = new Client("fuzz-client", "secret", true);
89+
String assertion = createSignedAssertion("trusted-issuer", client.getClientId());
90+
91+
MultivaluedMap<String, String> params = new MetadataMap<>();
92+
params.putSingle(Constants.CLIENT_GRANT_ASSERTION_PARAM, assertion);
93+
94+
ServerAccessToken token = handler.createAccessToken(client, params);
95+
96+
assertNotNull(token);
97+
assertNotNull(token.getSubject());
98+
assertEquals(client.getClientId(), token.getSubject().getLogin());
99+
}
100+
101+
private static String createSignedAssertion(String issuer, String subject) {
102+
long now = System.currentTimeMillis() / 1000;
103+
JwtClaims claims = new JwtClaims();
104+
claims.setIssuer(issuer);
105+
claims.setSubject(subject);
106+
claims.setIssuedAt(now);
107+
claims.setExpiryTime(now + 300);
108+
109+
JwsJwtCompactProducer producer = new JwsJwtCompactProducer(claims);
110+
return producer.signWith(new HmacJwsSignatureProvider(SIGNING_KEY, SignatureAlgorithm.HS256));
111+
}
112+
113+
private static final class SubjectAwareDataProvider extends OAuthDataProviderImpl {
114+
@Override
115+
public ServerAccessToken createAccessToken(AccessTokenRegistration accessToken) {
116+
BearerAccessToken token = new BearerAccessToken(accessToken.getClient(), 3600);
117+
token.setSubject(accessToken.getSubject());
118+
token.setGrantType(accessToken.getGrantType());
119+
return token;
120+
}
121+
}
122+
123+
private static void setThreadLocalMessage(Message message) throws Exception {
124+
Field f = PhaseInterceptorChain.class.getDeclaredField("CURRENT_MESSAGE");
125+
f.setAccessible(true);
126+
@SuppressWarnings("unchecked")
127+
ThreadLocal<Message> tl = (ThreadLocal<Message>) f.get(null);
128+
if (message == null) {
129+
tl.remove();
130+
} else {
131+
tl.set(message);
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)