You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@usergrid.apache.org by mr...@apache.org on 2016/10/28 19:39:46 UTC
[4/5] usergrid git commit: Add key freshness and full set of tests
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/efa65ac6
Tree: http://git-wip-us.apache.org/repos/asf/usergrid/tree/efa65ac6
Diff: http://git-wip-us.apache.org/repos/asf/usergrid/diff/efa65ac6
Branch: refs/heads/hotfix-20160819
Commit: efa65ac6ad86c00300ccf83797aae5d8d552cf70
Parents: e0eb11c
Author: Dave Johnson <sn...@apache.org>
Authored: Tue Oct 25 17:03:18 2016 -0400
Committer: Michael Russo <mr...@apigee.com>
Committed: Fri Oct 28 12:21:25 2016 -0700
----------------------------------------------------------------------
.../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/efa65ac6/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/efa65ac6/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 (snoopdave@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;
+ }
+}
+
+