Add key freshness and full set of tests
Project: http://git-wip-us.apache.org/repos/asf/usergrid/repo Commit: http://git-wip-us.apache.org/repos/asf/usergrid/commit/f0c9d44c Tree: http://git-wip-us.apache.org/repos/asf/usergrid/tree/f0c9d44c Diff: http://git-wip-us.apache.org/repos/asf/usergrid/diff/f0c9d44c Branch: refs/heads/master Commit: f0c9d44c23559b2e9b80161770770231b16ad2df Parents: d7c14bb Author: Dave Johnson <snoopd...@apache.org> Authored: Tue Oct 25 17:03:18 2016 -0400 Committer: Dave Johnson <snoopd...@apache.org> Committed: Tue Oct 25 17:03:18 2016 -0400 ---------------------------------------------------------------------- .../security/sso/ApigeeSSO2Provider.java | 86 ++++-- .../usergrid/security/ApigeeSSO2ProviderIT.java | 297 +++++++++++++++++++ 2 files changed, 354 insertions(+), 29 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/usergrid/blob/f0c9d44c/stack/services/src/main/java/org/apache/usergrid/security/sso/ApigeeSSO2Provider.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/sso/ApigeeSSO2Provider.java b/stack/services/src/main/java/org/apache/usergrid/security/sso/ApigeeSSO2Provider.java index 27843b5..8e3c463 100644 --- a/stack/services/src/main/java/org/apache/usergrid/security/sso/ApigeeSSO2Provider.java +++ b/stack/services/src/main/java/org/apache/usergrid/security/sso/ApigeeSSO2Provider.java @@ -45,9 +45,7 @@ import java.util.Properties; import static org.apache.commons.codec.binary.Base64.decodeBase64; -/** - * Created by ayeshadastagiri on 6/22/16. - */ + public class ApigeeSSO2Provider implements ExternalSSOProvider { private static final Logger logger = LoggerFactory.getLogger(ApigeeSSO2Provider.class); @@ -56,9 +54,16 @@ public class ApigeeSSO2Provider implements ExternalSSOProvider { protected ManagementService management; protected Client client; protected PublicKey publicKey; + protected long freshnessTime = 3000L; + + public long lastPublicKeyFetch = 0L; + public static final String USERGRID_EXTERNAL_PUBLICKEY_URL = "usergrid.external.sso.url"; + public static final String USERGRID_EXTERMAL_PUBLICKEY_FRESHNESS = "usergrid.external.sso.public-key-freshness"; + + public ApigeeSSO2Provider() { ClientConfig clientConfig = new ClientConfig(); clientConfig.register(new JacksonFeature()); @@ -67,17 +72,18 @@ public class ApigeeSSO2Provider implements ExternalSSOProvider { public PublicKey getPublicKey(String keyUrl) { - if(keyUrl != null && !keyUrl.isEmpty()) { + if ( keyUrl != null && !keyUrl.isEmpty()) { try { Map<String, Object> publicKey = client.target(keyUrl).request().get(Map.class); - String ssoPublicKey = publicKey.get(RESPONSE_PUBLICKEY_VALUE).toString().split("----\n")[1].split("\n---")[0]; + String ssoPublicKey = publicKey.get(RESPONSE_PUBLICKEY_VALUE) + .toString().split("----\n")[1].split("\n---")[0]; byte[] publicBytes = decodeBase64(ssoPublicKey); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PublicKey pubKey = keyFactory.generatePublic(keySpec); return pubKey; } - catch(Exception e){ + catch (Exception e) { throw new IllegalArgumentException("error getting public key"); } } @@ -91,14 +97,13 @@ public class ApigeeSSO2Provider implements ExternalSSOProvider { UserInfo userInfo = validateAndReturnUserInfo(token, ttl); - if(userInfo == null){ - throw new ExternalSSOProviderAdminUserNotFoundException("Unable to load user from token: "+token); + if (userInfo == null) { + throw new ExternalSSOProviderAdminUserNotFoundException("Unable to load user from token: " + token); } return new TokenInfo(UUIDUtils.newTimeUUID(), "access", 1, 1, 1, ttl, new AuthPrincipalInfo(AuthPrincipalType.ADMIN_USER, userInfo.getUuid(), CpNamingUtils.MANAGEMENT_APPLICATION_ID), null); - } @Override @@ -134,7 +139,7 @@ public class ApigeeSSO2Provider implements ExternalSSOProvider { @Override public Map<String, Object> getAllTokenDetails(String token, String keyUrl) throws Exception { - Jws<Claims> claims = getClaimsForKeyUrl(token,getPublicKey(keyUrl)); + Jws<Claims> claims = getClaimsForKeyUrl( token ); return JsonUtils.toJsonMap(claims.getBody()); } @@ -144,50 +149,64 @@ public class ApigeeSSO2Provider implements ExternalSSOProvider { return properties.getProperty(USERGRID_EXTERNAL_PUBLICKEY_URL); } - public Jws<Claims> getClaimsForKeyUrl(String token, PublicKey ssoPublicKey) throws BadTokenException { + public Jws<Claims> getClaimsForKeyUrl( String token ) throws BadTokenException { Jws<Claims> claims = null; - if (ssoPublicKey == null) { - throw new IllegalArgumentException( "Public key must be provided with Apigee JWT " + - "token in order to verify signature." ); - } + Exception lastException = null; int tries = 0; int maxTries = 2; while ( claims == null && tries++ < maxTries ) { try { - claims = Jwts.parser().setSigningKey( ssoPublicKey ).parseClaimsJws( token ); + claims = Jwts.parser().setSigningKey( publicKey ).parseClaimsJws( token ); } catch (SignatureException se) { - logger.warn( "Signature was invalid for Apigee JWT token: {} and key: {}", token, ssoPublicKey ); + // bad signature, need to get latest publicKey and try again + // logger.debug( "Signature was invalid for Apigee JWT token: {}", token ); + lastException = se; + + } catch (ArrayIndexOutOfBoundsException aio) { + // unknown error, need to get latest publicKey and try again + logger.debug("Error parsing JWT token", aio); + throw new BadTokenException( "Unknown error processing JWT", aio ); } catch (ExpiredJwtException e) { final long expiry = Long.valueOf( e.getClaims().get( "exp" ).toString() ); final long expirationDelta = ((System.currentTimeMillis() / 1000) - expiry) * 1000; - logger.info(String.format("Apigee JWT Token expired %d milliseconds ago.", expirationDelta)); + logger.debug(String.format("Apigee JWT Token expired %d milliseconds ago.", expirationDelta)); + + // token is expired + throw new BadTokenException( "Expired JWT", e ); } catch (MalformedJwtException me) { - logger.error("Malformed JWT token", me); - throw new BadTokenException( "Malformed Apigee JWT token", me ); + logger.debug( "Malformed JWT", me ); - } catch (ArrayIndexOutOfBoundsException aio) { - logger.error("Error parsing JWT token", aio); - throw new BadTokenException( "Error parsing Apigee JWT token", aio ); + // token is malformed + throw new BadTokenException( "Malformed JWT", me ); } - if ( claims == null ) { - this.publicKey = getPublicKey( getExternalSSOUrl() ); + long keyFreshness = System.currentTimeMillis() - lastPublicKeyFetch; + if ( claims == null && keyFreshness > this.freshnessTime ) { + logger.debug("Failed to get claims for token {}... fetching new public key", token); + publicKey = getPublicKey( getExternalSSOUrl() ); + lastPublicKeyFetch = System.currentTimeMillis(); + logger.info("New public key is {}", publicKey); } } + if ( claims == null ) { + logger.error("Error getting Apigee JWT claims", lastException); + throw new BadTokenException( "Error getting Apigee JWT claims", lastException ); + } else { + logger.debug( "Success! Got claims for token {} key {}", token, publicKey.toString() ); + } + return claims; } public Jws<Claims> getClaims(String token) throws Exception{ - - return getClaimsForKeyUrl(token,publicKey); - + return getClaimsForKeyUrl(token); } private void validateClaims (final Jws<Claims> claims) throws ExpiredTokenException { @@ -196,7 +215,7 @@ public class ApigeeSSO2Provider implements ExternalSSOProvider { final long expiry = Long.valueOf(body.get("exp").toString()); - if(expiry - (System.currentTimeMillis()/1000) < 0 ){ + if (expiry - (System.currentTimeMillis()/1000) < 0 ){ final long expirationDelta = ((System.currentTimeMillis()/1000) - expiry)*1000; @@ -219,5 +238,14 @@ public class ApigeeSSO2Provider implements ExternalSSOProvider { public void setProperties(Properties properties) { this.properties = properties; this.publicKey = getPublicKey(getExternalSSOUrl()); + + lastPublicKeyFetch = System.currentTimeMillis(); + + String freshnessString = (String)properties.get( USERGRID_EXTERMAL_PUBLICKEY_FRESHNESS ); + try { + freshnessTime = Long.parseLong( freshnessString ); + } catch ( Exception e ) { + logger.error("Ignoring invalid setting for " + USERGRID_EXTERMAL_PUBLICKEY_FRESHNESS ); + } } } http://git-wip-us.apache.org/repos/asf/usergrid/blob/f0c9d44c/stack/services/src/test/java/org/apache/usergrid/security/ApigeeSSO2ProviderIT.java ---------------------------------------------------------------------- diff --git a/stack/services/src/test/java/org/apache/usergrid/security/ApigeeSSO2ProviderIT.java b/stack/services/src/test/java/org/apache/usergrid/security/ApigeeSSO2ProviderIT.java new file mode 100644 index 0000000..8dc13cf --- /dev/null +++ b/stack/services/src/test/java/org/apache/usergrid/security/ApigeeSSO2ProviderIT.java @@ -0,0 +1,297 @@ +/* + * 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.usergrid.security; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.impl.crypto.RsaProvider; +import org.apache.commons.collections4.map.HashedMap; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.usergrid.NewOrgAppAdminRule; +import org.apache.usergrid.ServiceITSetup; +import org.apache.usergrid.ServiceITSetupImpl; +import org.apache.usergrid.cassandra.ClearShiroSubject; +import org.apache.usergrid.management.ManagementService; +import org.apache.usergrid.management.UserInfo; +import org.apache.usergrid.persistence.Entity; +import org.apache.usergrid.persistence.EntityManager; +import org.apache.usergrid.persistence.SimpleEntityRef; +import org.apache.usergrid.persistence.entities.User; +import org.apache.usergrid.security.sso.ApigeeSSO2Provider; +import org.apache.usergrid.security.tokens.TokenInfo; +import org.apache.usergrid.security.tokens.exceptions.BadTokenException; +import org.junit.*; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.*; +import java.security.spec.X509EncodedKeySpec; +import java.util.*; + +import static org.apache.commons.codec.binary.Base64.decodeBase64; + + +/** + * Created by Dave Johnson (snoopd...@apache.org) on 10/25/16. + */ +public class ApigeeSSO2ProviderIT { + private static final Logger logger = LoggerFactory.getLogger(ApigeeSSO2ProviderIT.class); + + @ClassRule + public static final ServiceITSetup setup = new ServiceITSetupImpl(); + + + @Test + public void testBasicOperation() throws Exception { + + // create keypair + KeyPair kp = RsaProvider.generateKeyPair(1024); + PublicKey publicKey = kp.getPublic(); + PrivateKey privateKey = kp.getPrivate(); + + // create provider with private key + ApigeeSSO2Provider provider = new MockApigeeSSO2Provider(); + provider.setManagement( setup.getMgmtSvc() ); + provider.setPublicKey( publicKey ); + + // create user, claims and a token for those things + User user = createUser(); + long exp = System.currentTimeMillis() + 10000; + Map<String, Object> claims = createClaims( user.getUsername(), user.getEmail(), exp ); + String token = Jwts.builder().setClaims(claims).signWith( SignatureAlgorithm.RS256, privateKey).compact(); + + // test that provider can validate the token, get user, return token info + TokenInfo tokenInfo = provider.validateAndReturnTokenInfo( token, 86400L ); + Assert.assertNotNull( tokenInfo ); + } + + + @Test + public void testExpiredToken() throws Exception { + + // create keypair + KeyPair kp = RsaProvider.generateKeyPair(1024); + PublicKey publicKey = kp.getPublic(); + PrivateKey privateKey = kp.getPrivate(); + + // create provider with private key + ApigeeSSO2Provider provider = new MockApigeeSSO2Provider(); + provider.setManagement( setup.getMgmtSvc() ); + provider.setPublicKey( publicKey ); + + // create user, claims and a token for those things + User user = createUser(); + long exp = System.currentTimeMillis() - 1500; + Map<String, Object> claims = createClaims( user.getUsername(), user.getEmail(), exp ); + String token = Jwts.builder() + .setClaims(claims) + .setExpiration( new Date() ) + .signWith( SignatureAlgorithm.RS256, privateKey) + .compact(); + + Thread.sleep(500); // wait for claims to timeout + + // test that token is expired + try { + provider.validateAndReturnTokenInfo( token, 86400L ); + Assert.fail("Should have failed due to expired token"); + + } catch ( BadTokenException e ) { + Assert.assertTrue( e.getCause() instanceof ExpiredJwtException ); + } + } + + + @Test + public void testMalformedToken() throws Exception { + + // create keypair + KeyPair kp = RsaProvider.generateKeyPair(1024); + PublicKey publicKey = kp.getPublic(); + + // create provider with private key + ApigeeSSO2Provider provider = new MockApigeeSSO2Provider(); + provider.setManagement( setup.getMgmtSvc() ); + provider.setPublicKey( publicKey ); + + // test that token is malformed + try { + provider.getClaims( "{;aklsjd;fkajsd;fkjasd;lfkj}" ); + Assert.fail("Should have failed due to malformed token"); + + } catch ( BadTokenException e ) { + Assert.assertTrue( e.getCause() instanceof MalformedJwtException ); + } + } + + @Test + public void testNewPublicKeyFetch() throws Exception { + + // create old keypair + KeyPair kp = RsaProvider.generateKeyPair(1024); + PublicKey publicKey = kp.getPublic(); + PrivateKey privateKey = kp.getPrivate(); + + // create new keypair + KeyPair kpNew = RsaProvider.generateKeyPair(1024); + PublicKey publicKeyNew = kpNew.getPublic(); + PrivateKey privateKeyNew = kpNew.getPrivate(); + + // create mock provider with old and old key + MockApigeeSSO2ProviderNewKey provider = new MockApigeeSSO2ProviderNewKey( publicKey, publicKeyNew ); + provider.setManagement( setup.getMgmtSvc() ); + + // create user, claims and a token for those things. Sign with new public key + User user = createUser(); + long exp = System.currentTimeMillis() + 10000; + Map<String, Object> claims = createClaims( user.getUsername(), user.getEmail(), exp ); + String token = Jwts.builder().setClaims(claims).signWith( SignatureAlgorithm.RS256, privateKeyNew).compact(); + + // test that provider can validate the token, get user, return token info + TokenInfo tokenInfo = provider.validateAndReturnTokenInfo( token, 86400L ); + Assert.assertNotNull( tokenInfo ); + + // assert that provider called for new key + Assert.assertTrue( provider.isGetPublicKeyCalled() ); + + + // try it again, but this time it should fail due to freshness value + + provider.setPublicKey( publicKey ); // set old key + + // test that signature exception thrown + try { + provider.validateAndReturnTokenInfo( token, 86400L ); + Assert.fail("Should have failed due to bad signature"); + + } catch ( BadTokenException e ) { + Assert.assertTrue( e.getCause() instanceof SignatureException ); + } + + } + + + @Test + public void testBadSignature() throws Exception { + + // create old keypair + KeyPair kp = RsaProvider.generateKeyPair(1024); + PublicKey publicKey = kp.getPublic(); + PrivateKey privateKey = kp.getPrivate(); + + // create new keypair + KeyPair kpNew = RsaProvider.generateKeyPair(1024); + PrivateKey privateKeyNew = kpNew.getPrivate(); + + // create mock provider with old public key + ApigeeSSO2Provider provider = new MockApigeeSSO2ProviderNewKey( publicKey, publicKey ); + provider.setManagement( setup.getMgmtSvc() ); + + // create user, claims and a token for those things. Sign with new public key + User user = createUser(); + long exp = System.currentTimeMillis() + 10000; + Map<String, Object> claims = createClaims( user.getUsername(), user.getEmail(), exp ); + String token = Jwts.builder().setClaims(claims).signWith( SignatureAlgorithm.RS256, privateKeyNew).compact(); + + // test that signature exception thrown + try { + provider.validateAndReturnTokenInfo( token, 86400L ); + Assert.fail("Should have failed due to bad signature"); + + } catch ( BadTokenException e ) { + Assert.assertTrue( e.getCause() instanceof SignatureException ); + } + + } + + private User createUser() throws Exception { + String rando = RandomStringUtils.randomAlphanumeric( 10 ); + String username = "user_" + rando; + String email = username + "@example.com"; + Map<String, Object> properties = new HashMap<String, Object>() {{ + put( "username", username ); + put( "email", email ); + }}; + EntityManager em = setup.getEmf().getEntityManager( setup.getEmf().getManagementAppId() ); + Entity entity = em.create( "user", properties ); + + return em.get( new SimpleEntityRef( User.ENTITY_TYPE, entity.getUuid() ), User.class ); + } + + + private Map<String, Object> createClaims(final String username, final String email, long exp ) { + return new HashedMap<String, Object>() {{ + put("jti","c7df0339-3847-450b-a925-628ef237953a"); + put("sub","b6d62259-217b-4e96-8f49-e00c366e4fed"); + put("scope","size = 5"); + put("client_id", "dummy1"); + put("azp","dummy2"); + put("grant_type" ,"password"); + put("user_id","b6d62259-217b-4e96-8f49-e00c366e4fed"); + put("origin","usergrid"); + put("user_name", username ); + put("email", email); + put("rev_sig","dfe5d0d3"); + put("exp", exp); + put("iat", System.currentTimeMillis()); + put("iss", "https://jwt.example.com/token"); + put("zid","uaa"); + put("aud"," size = 6"); + }}; + } +} + +class MockApigeeSSO2Provider extends ApigeeSSO2Provider { + private static final Logger logger = LoggerFactory.getLogger(MockApigeeSSO2Provider.class); + + @Override + public PublicKey getPublicKey(String keyUrl ) { + return publicKey; + } + + @Override + public void setPublicKey( PublicKey publicKey ) { + this.publicKey = publicKey; + } +} + + +class MockApigeeSSO2ProviderNewKey extends ApigeeSSO2Provider { + private static final Logger logger = LoggerFactory.getLogger(MockApigeeSSO2Provider.class); + + private PublicKey newKey; + private boolean getPublicKeyCalled = false; + + public MockApigeeSSO2ProviderNewKey( PublicKey oldKey, PublicKey newKey ) { + this.publicKey = oldKey; + this.newKey = newKey; + this.properties = new Properties(); + } + + @Override + public PublicKey getPublicKey( String keyUrl ) { + getPublicKeyCalled = true; + return newKey; + } + + public boolean isGetPublicKeyCalled() { + return getPublicKeyCalled; + } +} + +