This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/master by this push: new d3de21c4 CXF-6727 - Make Claim based access control interceptors/annotations usable with JWT tokens d3de21c4 is described below commit d3de21c45a8f6c74996b5edecfe2e03f8b6a2bd3 Author: Colm O hEigeartaigh <cohei...@apache.org> AuthorDate: Thu Sep 13 12:24:25 2018 +0100 CXF-6727 - Make Claim based access control interceptors/annotations usable with JWT tokens --- .../jose/jaxrs/JwtTokenSecurityContext.java | 26 ++++- .../ClaimsAuthorizingFilter.java | 2 +- .../interceptor/ClaimsAuthorizingInterceptor.java | 11 +- .../ClaimsAuthorizingInterceptorTest.java | 71 ++++++++++-- .../jaxrs/security/jose/jwt/BookStoreAuthn.java | 15 +++ .../jaxrs/security/jose/jwt/JWTAuthnAuthzTest.java | 121 +++++++++++++++++++++ .../jaxrs/security/jose/jwt/authn-authz-server.xml | 6 + .../systest/jaxrs/security/saml/secureServer.xml | 2 +- 8 files changed, 235 insertions(+), 19 deletions(-) diff --git a/rt/rs/security/jose-parent/jose-jaxrs/src/main/java/org/apache/cxf/rs/security/jose/jaxrs/JwtTokenSecurityContext.java b/rt/rs/security/jose-parent/jose-jaxrs/src/main/java/org/apache/cxf/rs/security/jose/jaxrs/JwtTokenSecurityContext.java index a72b902..0630d10 100644 --- a/rt/rs/security/jose-parent/jose-jaxrs/src/main/java/org/apache/cxf/rs/security/jose/jaxrs/JwtTokenSecurityContext.java +++ b/rt/rs/security/jose-parent/jose-jaxrs/src/main/java/org/apache/cxf/rs/security/jose/jaxrs/JwtTokenSecurityContext.java @@ -21,19 +21,24 @@ package org.apache.cxf.rs.security.jose.jaxrs; import java.security.Principal; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.security.auth.Subject; import org.apache.cxf.common.security.SimpleGroup; import org.apache.cxf.common.security.SimplePrincipal; +import org.apache.cxf.helpers.CastUtils; import org.apache.cxf.rs.security.jose.jwt.JwtToken; -import org.apache.cxf.security.LoginSecurityContext; +import org.apache.cxf.rt.security.claims.Claim; +import org.apache.cxf.rt.security.claims.ClaimCollection; +import org.apache.cxf.rt.security.claims.ClaimsSecurityContext; -public class JwtTokenSecurityContext implements LoginSecurityContext { +public class JwtTokenSecurityContext implements ClaimsSecurityContext { private final JwtToken token; private final Principal principal; private final Set<Principal> roles; + private final ClaimCollection claims = new ClaimCollection(); public JwtTokenSecurityContext(JwtToken jwt, String roleClaim) { principal = new SimplePrincipal(jwt.getClaims().getSubject()); @@ -47,6 +52,18 @@ public class JwtTokenSecurityContext implements LoginSecurityContext { } else { roles = Collections.emptySet(); } + + // Parse JwtToken into ClaimCollection + jwt.getClaims().asMap().forEach((String name, Object values) -> { + Claim claim = new Claim(); + claim.setClaimType(name); + if (values instanceof List<?>) { + claim.setValues(CastUtils.cast((List<?>)values)); + } else { + claim.setValues(Collections.singletonList(values)); + } + claims.add(claim); + }); } public JwtToken getToken() { @@ -78,4 +95,9 @@ public class JwtTokenSecurityContext implements LoginSecurityContext { return false; } + @Override + public ClaimCollection getClaims() { + return claims; + } + } diff --git a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/authorization/ClaimsAuthorizingFilter.java b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/claims/ClaimsAuthorizingFilter.java similarity index 97% rename from rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/authorization/ClaimsAuthorizingFilter.java rename to rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/claims/ClaimsAuthorizingFilter.java index 45112e4..9d5dfcd 100644 --- a/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/saml/authorization/ClaimsAuthorizingFilter.java +++ b/rt/rs/security/xml/src/main/java/org/apache/cxf/rs/security/claims/ClaimsAuthorizingFilter.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.cxf.rs.security.saml.authorization; +package org.apache.cxf.rs.security.claims; import java.util.List; import java.util.Map; diff --git a/rt/security/src/main/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptor.java b/rt/security/src/main/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptor.java index 8f254be..5bcb3c0 100644 --- a/rt/security/src/main/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptor.java +++ b/rt/security/src/main/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptor.java @@ -107,9 +107,14 @@ public class ClaimsAuthorizingInterceptor extends AbstractPhaseInterceptor<Messa for (ClaimBean claimBean : list) { org.apache.cxf.rt.security.claims.Claim matchingClaim = null; for (org.apache.cxf.rt.security.claims.Claim cl : actualClaims) { - if (cl instanceof SAMLClaim - && ((SAMLClaim)cl).getName().equals(claimBean.getClaim().getClaimType()) - && ((SAMLClaim)cl).getNameFormat().equals(claimBean.getClaimFormat())) { + if (cl instanceof SAMLClaim) { + // If it's a SAMLClaim the name + nameformat must match what's configured + if (((SAMLClaim)cl).getName().equals(claimBean.getClaim().getClaimType()) + && ((SAMLClaim)cl).getNameFormat().equals(claimBean.getClaimFormat())) { + matchingClaim = cl; + break; + } + } else if (cl.getClaimType().equals(claimBean.getClaim().getClaimType())) { matchingClaim = cl; break; } diff --git a/rt/security/src/test/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptorTest.java b/rt/security/src/test/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptorTest.java index aadc168..c2ae08c 100644 --- a/rt/security/src/test/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptorTest.java +++ b/rt/security/src/test/java/org/apache/cxf/rt/security/claims/interceptor/ClaimsAuthorizingInterceptorTest.java @@ -26,6 +26,7 @@ import java.security.Principal; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.security.auth.Subject; @@ -69,12 +70,34 @@ public class ClaimsAuthorizingInterceptorTest extends Assert { @Test public void testClaimDefaultNameAndFormat() throws Exception { doTestClaims("claimWithDefaultNameAndFormat", - createDefaultClaim("admin", "user"), - createClaim("http://authentication", "http://claims", "password")); + createDefaultClaim("admin", "user"), + createClaim("http://authentication", "http://claims", "password")); try { doTestClaims("claimWithDefaultNameAndFormat", - createDefaultClaim("user"), - createClaim("http://authentication", "http://claims", "password")); + createDefaultClaim("user"), + createClaim("http://authentication", "http://claims", "password")); + fail("AccessDeniedException expected"); + } catch (AccessDeniedException ex) { + // expected + } + } + + @Test + public void testNonSAMLClaimDefaultNameAndFormat() throws Exception { + org.apache.cxf.rt.security.claims.Claim claim1 = new org.apache.cxf.rt.security.claims.Claim(); + claim1.setClaimType("role"); + claim1.setValues(Arrays.asList("admin", "user")); + org.apache.cxf.rt.security.claims.Claim claim2 = new org.apache.cxf.rt.security.claims.Claim(); + claim2.setClaimType("http://authentication"); + claim2.setValues(Arrays.asList("password")); + + Message m = prepareMessage(TestService.class, "claimWithSpecificName", "role", claim1, claim2); + interceptor.handleMessage(m); + + try { + claim1.setValues(Arrays.asList("user")); + m = prepareMessage(TestService.class, "claimWithSpecificName", "role", claim1, claim2); + interceptor.handleMessage(m); fail("AccessDeniedException expected"); } catch (AccessDeniedException ex) { // expected @@ -202,7 +225,6 @@ public class ClaimsAuthorizingInterceptorTest extends Assert { } } - private void doTestClaims(String methodName, org.apache.cxf.rt.security.claims.Claim... claim) throws Exception { @@ -214,11 +236,19 @@ public class ClaimsAuthorizingInterceptorTest extends Assert { String methodName, org.apache.cxf.rt.security.claims.Claim... claim) throws Exception { + return prepareMessage(cls, methodName, SAMLClaim.SAML_ROLE_ATTRIBUTENAME_DEFAULT, claim); + } + + private Message prepareMessage(Class<?> cls, + String methodName, + String roleName, + org.apache.cxf.rt.security.claims.Claim... claim) + throws Exception { ClaimCollection claims = new ClaimCollection(); claims.addAll(Arrays.asList(claim)); Set<Principal> roles = - parseRolesFromClaims(claims, SAMLClaim.SAML_ROLE_ATTRIBUTENAME_DEFAULT, + parseRolesFromClaims(claims, roleName, "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"); ClaimsSecurityContext sc = new ClaimsSecurityContext() { @@ -299,6 +329,12 @@ public class ClaimsAuthorizingInterceptorTest extends Assert { } + // explicit name + @Claim(name = "role", value = {"admin", "manager" }) + public void claimWithSpecificName() { + + } + @Claim(name = "http://authentication", format = "http://claims", value = "password", mode = ClaimMode.LAX) public void claimLaxMode() { @@ -348,14 +384,17 @@ public class ClaimsAuthorizingInterceptorTest extends Assert { Set<Principal> roles = new HashSet<>(); for (org.apache.cxf.rt.security.claims.Claim claim : claims) { - if (claim instanceof SAMLClaim && ((SAMLClaim)claim).getName().equals(name) - && (nameFormat == null - || nameFormat.equals(((SAMLClaim)claim).getNameFormat()))) { - for (Object claimValue : claim.getValues()) { - if (claimValue instanceof String) { - roles.add(new SimpleGroup((String)claimValue)); + if (claim instanceof SAMLClaim) { + if (((SAMLClaim)claim).getName().equals(roleAttributeName) + && (nameFormat == null || nameFormat.equals(((SAMLClaim)claim).getNameFormat()))) { + addValues(roles, claim.getValues()); + if (claim.getValues().size() > 1) { + // Don't search for other attributes with the same name if > 1 claim value + break; } } + } else if (claim.getClaimType().equals(roleAttributeName)) { + addValues(roles, claim.getValues()); if (claim.getValues().size() > 1) { // Don't search for other attributes with the same name if > 1 claim value break; @@ -365,4 +404,12 @@ public class ClaimsAuthorizingInterceptorTest extends Assert { return roles; } + + private static void addValues(Set<Principal> roles, List<Object> values) { + for (Object claimValue : values) { + if (claimValue instanceof String) { + roles.add(new SimpleGroup((String)claimValue)); + } + } + } } diff --git a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/jose/jwt/BookStoreAuthn.java b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/jose/jwt/BookStoreAuthn.java index 29cb330..6954c5c 100644 --- a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/jose/jwt/BookStoreAuthn.java +++ b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/jose/jwt/BookStoreAuthn.java @@ -27,6 +27,8 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import org.apache.cxf.jaxrs.ext.MessageContext; +import org.apache.cxf.security.claims.authorization.Claim; +import org.apache.cxf.security.claims.authorization.Claims; import org.apache.cxf.systest.jaxrs.security.Book; import org.junit.Assert; @@ -67,6 +69,19 @@ public class BookStoreAuthn { return book; } + @POST + @Path("/booksclaims") + @Produces("application/json") + @Consumes("application/json") + @Claims({ + @Claim(name = "http://claims/authentication", + value = {"fingertip", "smartcard" }) + }) + public Book echoBook3(Book book) { + checkAuthentication(); + return book; + } + private void checkAuthentication() { // Check that we have an authenticated principal Assert.assertNotNull(jaxrsContext.getSecurityContext().getUserPrincipal()); diff --git a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/jose/jwt/JWTAuthnAuthzTest.java b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/jose/jwt/JWTAuthnAuthzTest.java index 50367e7..3a2531f 100644 --- a/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/jose/jwt/JWTAuthnAuthzTest.java +++ b/systests/rs-security/src/test/java/org/apache/cxf/systest/jaxrs/security/jose/jwt/JWTAuthnAuthzTest.java @@ -245,6 +245,127 @@ public class JWTAuthnAuthzTest extends AbstractBusClientServerTestBase { assertNotEquals(response.getStatus(), 200); } + @org.junit.Test + public void testClaimsAuthorization() throws Exception { + + URL busFile = JWTAuthnAuthzTest.class.getResource("client.xml"); + + List<Object> providers = new ArrayList<>(); + providers.add(new JacksonJsonProvider()); + providers.add(new JwtAuthenticationClientFilter()); + + String address = "https://localhost:" + PORT + "/signedjwtauthz/bookstore/booksclaims"; + WebClient client = + WebClient.create(address, providers, busFile.toString()); + client.type("application/json").accept("application/json"); + + // Create the JWT Token + JwtClaims claims = new JwtClaims(); + claims.setSubject("alice"); + claims.setIssuer("DoubleItSTSIssuer"); + claims.setIssuedAt(Instant.now().getEpochSecond()); + claims.setAudiences(toList(address)); + // The endpoint requires a role of "boss" + claims.setProperty("role", "boss"); + // We also require a "smartcard" claim + claims.setProperty("http://claims/authentication", "smartcard"); + + JwtToken token = new JwtToken(claims); + + Map<String, Object> properties = new HashMap<>(); + properties.put("rs.security.keystore.type", "jwk"); + properties.put("rs.security.keystore.alias", "2011-04-29"); + properties.put("rs.security.keystore.file", + "org/apache/cxf/systest/jaxrs/security/certs/jwkPrivateSet.txt"); + properties.put("rs.security.signature.algorithm", "RS256"); + properties.put(JwtConstants.JWT_TOKEN, token); + WebClient.getConfig(client).getRequestContext().putAll(properties); + + Response response = client.post(new Book("book", 123L)); + assertEquals(response.getStatus(), 200); + + Book returnedBook = response.readEntity(Book.class); + assertEquals(returnedBook.getName(), "book"); + assertEquals(returnedBook.getId(), 123L); + } + + @org.junit.Test + public void testClaimsAuthorizationWeakClaims() throws Exception { + + URL busFile = JWTAuthnAuthzTest.class.getResource("client.xml"); + + List<Object> providers = new ArrayList<>(); + providers.add(new JacksonJsonProvider()); + providers.add(new JwtAuthenticationClientFilter()); + + String address = "https://localhost:" + PORT + "/signedjwtauthz/bookstore/booksclaims"; + WebClient client = + WebClient.create(address, providers, busFile.toString()); + client.type("application/json").accept("application/json"); + + // Create the JWT Token + JwtClaims claims = new JwtClaims(); + claims.setSubject("alice"); + claims.setIssuer("DoubleItSTSIssuer"); + claims.setIssuedAt(Instant.now().getEpochSecond()); + claims.setAudiences(toList(address)); + // The endpoint requires a role of "boss" + claims.setProperty("role", "boss"); + claims.setProperty("http://claims/authentication", "password"); + + JwtToken token = new JwtToken(claims); + + Map<String, Object> properties = new HashMap<>(); + properties.put("rs.security.keystore.type", "jwk"); + properties.put("rs.security.keystore.alias", "2011-04-29"); + properties.put("rs.security.keystore.file", + "org/apache/cxf/systest/jaxrs/security/certs/jwkPrivateSet.txt"); + properties.put("rs.security.signature.algorithm", "RS256"); + properties.put(JwtConstants.JWT_TOKEN, token); + WebClient.getConfig(client).getRequestContext().putAll(properties); + + Response response = client.post(new Book("book", 123L)); + assertEquals(response.getStatus(), 403); + } + + @org.junit.Test + public void testClaimsAuthorizationNoClaims() throws Exception { + + URL busFile = JWTAuthnAuthzTest.class.getResource("client.xml"); + + List<Object> providers = new ArrayList<>(); + providers.add(new JacksonJsonProvider()); + providers.add(new JwtAuthenticationClientFilter()); + + String address = "https://localhost:" + PORT + "/signedjwtauthz/bookstore/booksclaims"; + WebClient client = + WebClient.create(address, providers, busFile.toString()); + client.type("application/json").accept("application/json"); + + // Create the JWT Token + JwtClaims claims = new JwtClaims(); + claims.setSubject("alice"); + claims.setIssuer("DoubleItSTSIssuer"); + claims.setIssuedAt(Instant.now().getEpochSecond()); + claims.setAudiences(toList(address)); + // The endpoint requires a role of "boss" + claims.setProperty("role", "boss"); + + JwtToken token = new JwtToken(claims); + + Map<String, Object> properties = new HashMap<>(); + properties.put("rs.security.keystore.type", "jwk"); + properties.put("rs.security.keystore.alias", "2011-04-29"); + properties.put("rs.security.keystore.file", + "org/apache/cxf/systest/jaxrs/security/certs/jwkPrivateSet.txt"); + properties.put("rs.security.signature.algorithm", "RS256"); + properties.put(JwtConstants.JWT_TOKEN, token); + WebClient.getConfig(client).getRequestContext().putAll(properties); + + Response response = client.post(new Book("book", 123L)); + assertEquals(response.getStatus(), 403); + } + private List<String> toList(String address) { return Collections.singletonList(address); } diff --git a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/jose/jwt/authn-authz-server.xml b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/jose/jwt/authn-authz-server.xml index 14cfa47..3c6a049 100644 --- a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/jose/jwt/authn-authz-server.xml +++ b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/jose/jwt/authn-authz-server.xml @@ -65,17 +65,23 @@ under the License. <map> <entry key="echoBook" value="boss"/> <entry key="echoBook2" value="boss"/> + <entry key="echoBook3" value="boss"/> <entry key="echoText" value="boss"/> </map> </property> </bean> + <bean id="claimsHandler" class="org.apache.cxf.rs.security.claims.ClaimsAuthorizingFilter"> + <property name="securedObject" ref="serviceBean"/> + </bean> + <jaxrs:server address="https://localhost:${testutil.ports.jaxrs-jwt-authn-authz}/signedjwtauthz"> <jaxrs:serviceBeans> <ref bean="serviceBean"/> </jaxrs:serviceBeans> <jaxrs:providers> <ref bean="jwtAuthzFilter"/> + <ref bean="claimsHandler"/> </jaxrs:providers> <jaxrs:inInterceptors> <ref bean="authorizationInterceptor"/> diff --git a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/saml/secureServer.xml b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/saml/secureServer.xml index ebc44c5..fd9620d 100644 --- a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/saml/secureServer.xml +++ b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/saml/secureServer.xml @@ -40,7 +40,7 @@ under the License. <bean id="serviceBean" class="org.apache.cxf.systest.jaxrs.security.saml.SecureBookStore"/> <bean id="serviceBeanClaims" class="org.apache.cxf.systest.jaxrs.security.saml.SecureClaimBookStore"/> <bean id="samlEnvHandler" class="org.apache.cxf.rs.security.saml.SamlEnvelopedInHandler"/> - <bean id="claimsHandler" class="org.apache.cxf.rs.security.saml.authorization.ClaimsAuthorizingFilter"> + <bean id="claimsHandler" class="org.apache.cxf.rs.security.claims.ClaimsAuthorizingFilter"> <property name="securedObject" ref="serviceBeanClaims"/> </bean> <bean id="authorizationInterceptor" class="org.apache.cxf.interceptor.security.SecureAnnotationsInterceptor">