Repository: cxf Updated Branches: refs/heads/master 67ac0ab27 -> c8905fd54
Adding JWTValidator Project: http://git-wip-us.apache.org/repos/asf/cxf/repo Commit: http://git-wip-us.apache.org/repos/asf/cxf/commit/c8905fd5 Tree: http://git-wip-us.apache.org/repos/asf/cxf/tree/c8905fd5 Diff: http://git-wip-us.apache.org/repos/asf/cxf/diff/c8905fd5 Branch: refs/heads/master Commit: c8905fd544457546be73f8887e154bb72dee7c7e Parents: 67ac0ab Author: Colm O hEigeartaigh <cohei...@apache.org> Authored: Fri Nov 13 16:28:07 2015 +0000 Committer: Colm O hEigeartaigh <cohei...@apache.org> Committed: Fri Nov 13 16:28:07 2015 +0000 ---------------------------------------------------------------------- .../apache/cxf/sts/request/ReceivedToken.java | 7 +- .../token/validator/jwt/JWTTokenValidator.java | 207 ++++++++++++++++ .../token/validator/JWTTokenValidatorTest.java | 246 +++++++++++++++++++ 3 files changed, 459 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cxf/blob/c8905fd5/services/sts/sts-core/src/main/java/org/apache/cxf/sts/request/ReceivedToken.java ---------------------------------------------------------------------- diff --git a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/request/ReceivedToken.java b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/request/ReceivedToken.java index c2e1aee..252ec60 100644 --- a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/request/ReceivedToken.java +++ b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/request/ReceivedToken.java @@ -33,7 +33,7 @@ import org.apache.cxf.ws.security.sts.provider.STSException; /** * This class contains values that have been extracted from a received Token. The Token can be a - * JAXB UsernameTokenType/BinarySecurityTokenType or a DOM Element. + * JAXB UsernameTokenType/BinarySecurityTokenType, a DOM Element or a String. */ public class ReceivedToken { @@ -74,6 +74,11 @@ public class ReceivedToken { } this.token = receivedToken; isDOMElement = true; + } else if (receivedToken instanceof String) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Found ValidateTarget String"); + } + this.token = receivedToken; } else { LOG.fine("Found ValidateTarget object of unknown type"); throw new STSException( http://git-wip-us.apache.org/repos/asf/cxf/blob/c8905fd5/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java ---------------------------------------------------------------------- diff --git a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java new file mode 100644 index 0000000..837c3c1 --- /dev/null +++ b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java @@ -0,0 +1,207 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cxf.sts.token.validator.jwt; + +import java.security.KeyStore; +import java.security.Principal; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.cxf.common.logging.LogUtils; +import org.apache.cxf.common.security.SimplePrincipal; +import org.apache.cxf.rs.security.jose.common.JoseConstants; +import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer; +import org.apache.cxf.rs.security.jose.jws.JwsSignatureVerifier; +import org.apache.cxf.rs.security.jose.jws.JwsUtils; +import org.apache.cxf.rs.security.jose.jwt.JwtToken; +import org.apache.cxf.rs.security.jose.jwt.JwtUtils; +import org.apache.cxf.sts.STSPropertiesMBean; +import org.apache.cxf.sts.request.ReceivedToken; +import org.apache.cxf.sts.request.ReceivedToken.STATE; +import org.apache.cxf.sts.token.validator.TokenValidator; +import org.apache.cxf.sts.token.validator.TokenValidatorParameters; +import org.apache.cxf.sts.token.validator.TokenValidatorResponse; +import org.apache.cxf.ws.security.sts.provider.STSException; +import org.apache.wss4j.common.crypto.Crypto; +import org.apache.wss4j.common.crypto.Merlin; + +/** + * Validate a SAML Assertion. It is valid if it was issued and signed by this STS. + */ +public class JWTTokenValidator implements TokenValidator { + + private static final Logger LOG = LogUtils.getL7dLogger(JWTTokenValidator.class); + private int clockOffset; + private int ttl; + + /** + * Return true if this TokenValidator implementation is capable of validating the + * ReceivedToken argument. + */ + public boolean canHandleToken(ReceivedToken validateTarget) { + return canHandleToken(validateTarget, null); + } + + /** + * Return true if this TokenValidator implementation is capable of validating the + * ReceivedToken argument. The realm is ignored in this Validator. + */ + public boolean canHandleToken(ReceivedToken validateTarget, String realm) { + Object token = validateTarget.getToken(); + if (token instanceof String) { + try { + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer((String)token); + if (jwtConsumer.getJwtToken() != null) { + return true; + } + } catch (RuntimeException ex) { + return false; + } + } + return false; + } + + /** + * Validate a Token using the given TokenValidatorParameters. + */ + public TokenValidatorResponse validateToken(TokenValidatorParameters tokenParameters) { + LOG.fine("Validating JWT Token"); + STSPropertiesMBean stsProperties = tokenParameters.getStsProperties(); + + TokenValidatorResponse response = new TokenValidatorResponse(); + ReceivedToken validateTarget = tokenParameters.getToken(); + validateTarget.setState(STATE.INVALID); + response.setToken(validateTarget); + + String token = (String)validateTarget.getToken(); + if (token == null) { + return response; + } + + if (token.split("\\.").length != 3) { + LOG.log(Level.WARNING, "JWT Token appears not to be signed. Validation has failed"); + return response; + } + + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(token); + JwtToken jwt = jwtConsumer.getJwtToken(); + + // Verify the signature + Properties verificationProperties = new Properties(); + + Crypto signatureCrypto = stsProperties.getSignatureCrypto(); + String alias = stsProperties.getSignatureUsername(); + + if (alias != null) { + verificationProperties.put(JoseConstants.RSSEC_KEY_STORE_ALIAS, alias); + } + + if (!(signatureCrypto instanceof Merlin)) { + throw new STSException("Can't get the keystore", STSException.REQUEST_FAILED); + } + KeyStore keystore = ((Merlin)signatureCrypto).getKeyStore(); + verificationProperties.put(JoseConstants.RSSEC_KEY_STORE, keystore); + + JwsSignatureVerifier signatureVerifier = + JwsUtils.loadSignatureVerifier(verificationProperties, jwt.getJwsHeaders()); + + if (!jwtConsumer.verifySignatureWith(signatureVerifier)) { + return response; + } + + try { + validateToken(jwt); + } catch (RuntimeException ex) { + LOG.log(Level.WARNING, "JWT token validation failed", ex); + return response; + } + + /* + // Parse roles from the validated token + if (samlRoleParser != null) { + Set<Principal> roles = + samlRoleParser.parseRolesFromAssertion(principal, null, assertion); + response.setRoles(roles); + } + + // Get the realm of the SAML token + String tokenRealm = null; + if (samlRealmCodec != null) { + tokenRealm = samlRealmCodec.getRealmFromToken(assertion); + // verify the realm against the cached token + if (secToken != null) { + Map<String, Object> props = secToken.getProperties(); + if (props != null) { + String cachedRealm = (String)props.get(STSConstants.TOKEN_REALM); + if (cachedRealm != null && !tokenRealm.equals(cachedRealm)) { + return response; + } + } + } + } + response.setTokenRealm(tokenRealm); + */ + + if (isVerifiedWithAPublicKey(jwt)) { + Principal principal = new SimplePrincipal(jwt.getClaims().getSubject()); + response.setPrincipal(principal); + } + + validateTarget.setState(STATE.VALID); + LOG.fine("JWT Token successfully validated"); + + return response; + } + + private boolean isVerifiedWithAPublicKey(JwtToken jwt) { + String alg = (String)jwt.getJwsHeader(JoseConstants.HEADER_ALGORITHM); + SignatureAlgorithm sigAlg = SignatureAlgorithm.getAlgorithm(alg); + return SignatureAlgorithm.isPublicKeyAlgorithm(sigAlg); + } + + protected void validateToken(JwtToken jwt) { + // If we have no issued time then we need to have an expiry + boolean expiredRequired = jwt.getClaims().getIssuedAt() == null; + JwtUtils.validateJwtExpiry(jwt.getClaims(), clockOffset, expiredRequired); + + JwtUtils.validateJwtNotBefore(jwt.getClaims(), clockOffset, false); + + // If we have no expiry then we must have an issued at + boolean issuedAtRequired = jwt.getClaims().getExpiryTime() == null; + JwtUtils.validateJwtIssuedAt(jwt.getClaims(), ttl, clockOffset, issuedAtRequired); + } + + public int getClockOffset() { + return clockOffset; + } + + public void setClockOffset(int clockOffset) { + this.clockOffset = clockOffset; + } + + public int getTtl() { + return ttl; + } + + public void setTtl(int ttl) { + this.ttl = ttl; + } +} http://git-wip-us.apache.org/repos/asf/cxf/blob/c8905fd5/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/JWTTokenValidatorTest.java ---------------------------------------------------------------------- diff --git a/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/JWTTokenValidatorTest.java b/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/JWTTokenValidatorTest.java new file mode 100644 index 0000000..3e2bc05 --- /dev/null +++ b/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/JWTTokenValidatorTest.java @@ -0,0 +1,246 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cxf.sts.token.validator; + +import java.io.IOException; +import java.security.Principal; +import java.util.Properties; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; + +import org.apache.cxf.jaxws.context.WebServiceContextImpl; +import org.apache.cxf.jaxws.context.WrappedMessageContext; +import org.apache.cxf.message.MessageImpl; +import org.apache.cxf.sts.STSConstants; +import org.apache.cxf.sts.StaticSTSProperties; +import org.apache.cxf.sts.cache.DefaultInMemoryTokenStore; +import org.apache.cxf.sts.common.PasswordCallbackHandler; +import org.apache.cxf.sts.request.KeyRequirements; +import org.apache.cxf.sts.request.ReceivedToken; +import org.apache.cxf.sts.request.ReceivedToken.STATE; +import org.apache.cxf.sts.request.TokenRequirements; +import org.apache.cxf.sts.service.EncryptionProperties; +import org.apache.cxf.sts.token.provider.TokenProvider; +import org.apache.cxf.sts.token.provider.TokenProviderParameters; +import org.apache.cxf.sts.token.provider.TokenProviderResponse; +import org.apache.cxf.sts.token.provider.jwt.JWTTokenProvider; +import org.apache.cxf.sts.token.validator.jwt.JWTTokenValidator; +import org.apache.cxf.ws.security.tokenstore.TokenStore; +import org.apache.wss4j.common.crypto.Crypto; +import org.apache.wss4j.common.crypto.CryptoFactory; +import org.apache.wss4j.common.ext.WSPasswordCallback; +import org.apache.wss4j.common.ext.WSSecurityException; +import org.apache.wss4j.common.principal.CustomTokenPrincipal; + +/** + * Some unit tests for validating JWTTokens. + */ +public class JWTTokenValidatorTest extends org.junit.Assert { + private static TokenStore tokenStore = new DefaultInMemoryTokenStore(); + + @org.junit.Test + public void testCreateAndValidateSignedJWT() throws Exception { + // Create + TokenProvider jwtTokenProvider = new JWTTokenProvider(); + ((JWTTokenProvider)jwtTokenProvider).setSignToken(true); + + TokenProviderParameters providerParameters = createProviderParameters(); + + assertTrue(jwtTokenProvider.canHandleToken(JWTTokenProvider.JWT_TOKEN_TYPE)); + TokenProviderResponse providerResponse = jwtTokenProvider.createToken(providerParameters); + assertTrue(providerResponse != null); + assertTrue(providerResponse.getToken() != null && providerResponse.getTokenId() != null); + + String token = (String)providerResponse.getToken(); + assertNotNull(token); + assertTrue(token.split("\\.").length == 3); + + // Validate the token + TokenValidator jwtTokenValidator = new JWTTokenValidator(); + TokenValidatorParameters validatorParameters = createValidatorParameters(); + TokenRequirements tokenRequirements = validatorParameters.getTokenRequirements(); + + // Create a ValidateTarget consisting of a JWT Token + ReceivedToken validateTarget = new ReceivedToken(token); + tokenRequirements.setValidateTarget(validateTarget); + validatorParameters.setToken(validateTarget); + + assertTrue(jwtTokenValidator.canHandleToken(validateTarget)); + + TokenValidatorResponse validatorResponse = + jwtTokenValidator.validateToken(validatorParameters); + assertTrue(validatorResponse != null); + assertTrue(validatorResponse.getToken() != null); + assertTrue(validatorResponse.getToken().getState() == STATE.VALID); + + Principal principal = validatorResponse.getPrincipal(); + assertTrue(principal != null && principal.getName() != null); + } + + @org.junit.Test + public void testInvalidSignature() throws Exception { + // Create + TokenProvider jwtTokenProvider = new JWTTokenProvider(); + ((JWTTokenProvider)jwtTokenProvider).setSignToken(true); + + TokenProviderParameters providerParameters = createProviderParameters(); + Crypto crypto = CryptoFactory.getInstance(getEveCryptoProperties()); + CallbackHandler callbackHandler = new EveCallbackHandler(); + providerParameters.getStsProperties().setSignatureCrypto(crypto); + providerParameters.getStsProperties().setCallbackHandler(callbackHandler); + providerParameters.getStsProperties().setSignatureUsername("eve"); + + assertTrue(jwtTokenProvider.canHandleToken(JWTTokenProvider.JWT_TOKEN_TYPE)); + TokenProviderResponse providerResponse = jwtTokenProvider.createToken(providerParameters); + assertTrue(providerResponse != null); + assertTrue(providerResponse.getToken() != null && providerResponse.getTokenId() != null); + + String token = (String)providerResponse.getToken(); + assertNotNull(token); + assertTrue(token.split("\\.").length == 3); + + // Validate the token + TokenValidator jwtTokenValidator = new JWTTokenValidator(); + TokenValidatorParameters validatorParameters = createValidatorParameters(); + TokenRequirements tokenRequirements = validatorParameters.getTokenRequirements(); + + // Create a ValidateTarget consisting of a JWT Token + ReceivedToken validateTarget = new ReceivedToken(token); + tokenRequirements.setValidateTarget(validateTarget); + validatorParameters.setToken(validateTarget); + + assertTrue(jwtTokenValidator.canHandleToken(validateTarget)); + + TokenValidatorResponse validatorResponse = + jwtTokenValidator.validateToken(validatorParameters); + assertTrue(validatorResponse != null); + assertTrue(validatorResponse.getToken() != null); + assertTrue(validatorResponse.getToken().getState() == STATE.INVALID); + } + + private TokenProviderParameters createProviderParameters() throws WSSecurityException { + TokenProviderParameters parameters = new TokenProviderParameters(); + + TokenRequirements tokenRequirements = new TokenRequirements(); + tokenRequirements.setTokenType(JWTTokenProvider.JWT_TOKEN_TYPE); + parameters.setTokenRequirements(tokenRequirements); + + KeyRequirements keyRequirements = new KeyRequirements(); + parameters.setKeyRequirements(keyRequirements); + + parameters.setTokenStore(tokenStore); + + parameters.setPrincipal(new CustomTokenPrincipal("alice")); + // Mock up message context + MessageImpl msg = new MessageImpl(); + WrappedMessageContext msgCtx = new WrappedMessageContext(msg); + WebServiceContextImpl webServiceContext = new WebServiceContextImpl(msgCtx); + parameters.setWebServiceContext(webServiceContext); + + parameters.setAppliesToAddress("http://dummy-service.com/dummy"); + + // Add STSProperties object + StaticSTSProperties stsProperties = new StaticSTSProperties(); + Crypto crypto = CryptoFactory.getInstance(getEncryptionProperties()); + stsProperties.setSignatureCrypto(crypto); + stsProperties.setSignatureUsername("mystskey"); + stsProperties.setCallbackHandler(new PasswordCallbackHandler()); + stsProperties.setIssuer("STS"); + parameters.setStsProperties(stsProperties); + + parameters.setEncryptionProperties(new EncryptionProperties()); + stsProperties.setEncryptionCrypto(crypto); + stsProperties.setEncryptionUsername("myservicekey"); + stsProperties.setCallbackHandler(new PasswordCallbackHandler()); + + return parameters; + } + + private TokenValidatorParameters createValidatorParameters() throws WSSecurityException { + TokenValidatorParameters parameters = new TokenValidatorParameters(); + + TokenRequirements tokenRequirements = new TokenRequirements(); + tokenRequirements.setTokenType(STSConstants.STATUS); + parameters.setTokenRequirements(tokenRequirements); + + KeyRequirements keyRequirements = new KeyRequirements(); + parameters.setKeyRequirements(keyRequirements); + + parameters.setPrincipal(new CustomTokenPrincipal("alice")); + // Mock up message context + MessageImpl msg = new MessageImpl(); + WrappedMessageContext msgCtx = new WrappedMessageContext(msg); + WebServiceContextImpl webServiceContext = new WebServiceContextImpl(msgCtx); + parameters.setWebServiceContext(webServiceContext); + + // Add STSProperties object + StaticSTSProperties stsProperties = new StaticSTSProperties(); + Crypto crypto = CryptoFactory.getInstance(getEncryptionProperties()); + stsProperties.setEncryptionCrypto(crypto); + stsProperties.setSignatureCrypto(crypto); + stsProperties.setEncryptionUsername("myservicekey"); + stsProperties.setSignatureUsername("mystskey"); + stsProperties.setCallbackHandler(new PasswordCallbackHandler()); + stsProperties.setIssuer("STS"); + parameters.setStsProperties(stsProperties); + parameters.setTokenStore(tokenStore); + + return parameters; + } + + private Properties getEncryptionProperties() { + Properties properties = new Properties(); + properties.put( + "org.apache.wss4j.crypto.provider", "org.apache.wss4j.common.crypto.Merlin" + ); + properties.put("org.apache.wss4j.crypto.merlin.keystore.password", "stsspass"); + properties.put("org.apache.wss4j.crypto.merlin.keystore.file", "stsstore.jks"); + + return properties; + } + + private Properties getEveCryptoProperties() { + Properties properties = new Properties(); + properties.put( + "org.apache.wss4j.crypto.provider", "org.apache.wss4j.common.crypto.Merlin" + ); + properties.put("org.apache.wss4j.crypto.merlin.keystore.password", "evespass"); + properties.put("org.apache.wss4j.crypto.merlin.keystore.file", "eve.jks"); + + return properties; + } + + public class EveCallbackHandler implements CallbackHandler { + + public void handle(Callback[] callbacks) throws IOException, + UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof WSPasswordCallback) { // CXF + WSPasswordCallback pc = (WSPasswordCallback) callbacks[i]; + if ("eve".equals(pc.getIdentifier())) { + pc.setPassword("evekpass"); + break; + } + } + } + } + } +}