Repository: usergrid Updated Branches: refs/heads/apigee-sso-provider b21e47780 -> 7cf07b4ed
Reconfigure using factory pattern for external SSO providers. Implement JWT token validation ( expiry for example). Project: http://git-wip-us.apache.org/repos/asf/usergrid/repo Commit: http://git-wip-us.apache.org/repos/asf/usergrid/commit/7cf07b4e Tree: http://git-wip-us.apache.org/repos/asf/usergrid/tree/7cf07b4e Diff: http://git-wip-us.apache.org/repos/asf/usergrid/diff/7cf07b4e Branch: refs/heads/apigee-sso-provider Commit: 7cf07b4ede4ac97df9c7d993755a36e6082d1d08 Parents: b21e477 Author: Michael Russo <[email protected]> Authored: Fri Jun 24 16:57:33 2016 -0700 Committer: Michael Russo <[email protected]> Committed: Fri Jun 24 16:57:33 2016 -0700 ---------------------------------------------------------------------- .../main/resources/usergrid-default.properties | 8 +- .../rest/management/ManagementResource.java | 56 +++- .../security/sso/ApigeeSSO2Provider.java | 167 ++++++++++++ .../security/sso/ExternalSSOProvider.java | 22 ++ .../security/sso/SSOProviderFactory.java | 57 ++++ .../security/sso/UsergridExternalProvider.java | 263 +++++++++++++++++++ .../tokens/cassandra/TokenServiceImpl.java | 74 ++++-- .../externalProviders/ApigeeSSO2Provider.java | 108 -------- .../resources/usergrid-services-context.xml | 13 +- 9 files changed, 623 insertions(+), 145 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/usergrid/blob/7cf07b4e/stack/config/src/main/resources/usergrid-default.properties ---------------------------------------------------------------------- diff --git a/stack/config/src/main/resources/usergrid-default.properties b/stack/config/src/main/resources/usergrid-default.properties index 2783e0e..fe739b9 100644 --- a/stack/config/src/main/resources/usergrid-default.properties +++ b/stack/config/src/main/resources/usergrid-default.properties @@ -492,11 +492,11 @@ usergrid.central.read.timeout=10000 -############################### Usergrid SSO2 ############################# -usergrid.external.sso.enabled= -usergrid.external.sso.provider=apigee +#################### Usergrid External SSO Configuration ##################### +usergrid.external.sso.enabled=false +usergrid.external.sso.provider= usergrid.external.sso.url= -usergrid.external.sso.publicKeyUrl=https://login.apigee.com/token_key +usergrid.external.sso.publicKeyUrl= ############################### Usergrid Assets ############################# # http://git-wip-us.apache.org/repos/asf/usergrid/blob/7cf07b4e/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java ---------------------------------------------------------------------- diff --git a/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java b/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java index d0f3efc..3d794d6 100644 --- a/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java +++ b/stack/rest/src/main/java/org/apache/usergrid/rest/management/ManagementResource.java @@ -35,6 +35,8 @@ import org.apache.usergrid.rest.management.organizations.OrganizationsResource; import org.apache.usergrid.rest.management.users.UsersResource; import org.apache.usergrid.security.oauth.AccessInfo; import org.apache.usergrid.security.shiro.utils.SubjectUtils; +import org.apache.usergrid.security.sso.ExternalSSOProvider; +import org.apache.usergrid.security.sso.SSOProviderFactory; import org.glassfish.jersey.server.mvc.Viewable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,6 +90,9 @@ public class ManagementResource extends AbstractContextResource { @Autowired private ApplicationCreator applicationCreator; + @Autowired + private SSOProviderFactory ssoProviderFactory; + // usergrid configuration property names needed public static final String USERGRID_SYSADMIN_LOGIN_NAME = "usergrid.sysadmin.login.name"; @@ -157,8 +162,47 @@ public class ManagementResource extends AbstractContextResource { @QueryParam( "access_token" ) String access_token, @QueryParam( "callback" ) @DefaultValue( "" ) String callback ) throws Exception { - return getAccessTokenInternal( ui, authorization, grant_type, username, password, client_id, client_secret, ttl, - callback, false, true ); + + + final UserInfo user = SubjectUtils.getUser(); + + // if user is null ( meaning no token was provided and previously validated in OAuth2AccessTokenSecurityFilter) + // then assume it's a token request + if( user == null) { + return getAccessTokenInternal(ui, authorization, grant_type, username, password, client_id, client_secret, ttl, + callback, false, true); + } + + + + // if it's not a token request and we have a user, extract details from the token + + final long passwordChanged = management.getLastAdminPasswordChange( user.getUuid() ); + final boolean ssoEnabled = Boolean.parseBoolean(properties.getProperty(USERGRID_EXTERNAL_SSO_ENABLED)); + long tokenTtl; + + if(ssoEnabled){ + + ExternalSSOProvider provider = ssoProviderFactory.getProvider(); + + tokenTtl = + Long.valueOf(provider.getDecodedTokenDetails(access_token).get("expiry")) - System.currentTimeMillis()/1000; + + }else{ + + tokenTtl = tokens.getTokenInfo(access_token).getDuration(); + } + + + final AccessInfo access_info = new AccessInfo().withExpiresIn( tokenTtl ).withAccessToken( access_token ) + .withPasswordChanged( passwordChanged ); + + access_info.setProperty( "user", management.getAdminUserOrganizationData( user, true ) ); + + return Response.status( SC_OK ).type( jsonMediaType( callback ) ) + .entity( wrapWithCallback( access_info, callback ) ).build(); + + } @@ -181,6 +225,8 @@ public class ManagementResource extends AbstractContextResource { String callback, boolean adminData, boolean me) throws Exception { + + UserInfo user = null; try { @@ -196,12 +242,14 @@ public class ManagementResource extends AbstractContextResource { if ( user == null ) { - if ( !me ) { // if not lightweight-auth, i.e. /management/me then... + + + //if ( !me ) { // if not lightweight-auth, i.e. /management/me then... // make sure authentication is allowed considering // external token validation configuration (UG Central SSO) ensureAuthenticationAllowed( username, grant_type ); - } + // } if ( authorization != null ) { String type = stringOrSubstringBeforeFirst( authorization, ' ' ).toUpperCase(); http://git-wip-us.apache.org/repos/asf/usergrid/blob/7cf07b4e/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 new file mode 100644 index 0000000..5400212 --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/sso/ApigeeSSO2Provider.java @@ -0,0 +1,167 @@ +package org.apache.usergrid.security.sso; + +import io.jsonwebtoken.*; +import org.apache.usergrid.corepersistence.util.CpNamingUtils; +import org.apache.usergrid.management.ManagementService; +import org.apache.usergrid.management.UserInfo; +import org.apache.usergrid.security.AuthPrincipalInfo; +import org.apache.usergrid.security.AuthPrincipalType; +import org.apache.usergrid.security.tokens.TokenInfo; +import org.apache.usergrid.security.tokens.exceptions.BadTokenException; +import org.apache.usergrid.security.tokens.exceptions.ExpiredTokenException; +import org.apache.usergrid.utils.UUIDUtils; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.Map; +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); + private static final String RESPONSE_PUBLICKEY_VALUE = "value"; + protected Properties properties; + protected ManagementService management; + protected Client client; + protected String publicKey; + + public static final String USERGRID_EXTERNAL_PUBLICKEY_URL = "usergrid.external.sso.url"; + + public ApigeeSSO2Provider() { + ClientConfig clientConfig = new ClientConfig(); + clientConfig.register(new JacksonFeature()); + client = ClientBuilder.newClient(clientConfig); + } + + private String getPublicKey() { + Map<String, Object> publicKey = client.target(properties.getProperty(USERGRID_EXTERNAL_PUBLICKEY_URL)).request().get(Map.class); + return publicKey.get(RESPONSE_PUBLICKEY_VALUE).toString().split("----\n")[1].split("\n---")[0]; + } + + @Override + public TokenInfo validateAndReturnTokenInfo(String token, long ttl) throws Exception { + + + UserInfo userInfo = validateAndReturnUserInfo(token, ttl); + + if(userInfo == null){ + throw new RuntimeException("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 + public UserInfo validateAndReturnUserInfo(String token, long ttl) throws Exception { + + Jws<Claims> payload = getClaims(token); + + // this step super important to ensure the token is a valid token + validateClaims(payload); + + UserInfo userInfo = management.getAdminUserByEmail(payload.getBody().get("email").toString()); + + return userInfo; + } + + @Override + public Map<String, String> getDecodedTokenDetails(String token) throws Exception { + + Jws<Claims> jws = getClaims(token); + + Claims claims = jws.getBody(); + Map<String, String> tokenDetails = new HashMap<>(); + + tokenDetails.put("username", (String)claims.get("user_name")); + tokenDetails.put("email", (String)claims.get("email")); + tokenDetails.put("expiry", claims.get("exp").toString()); + + + return tokenDetails; + + } + + + private Jws<Claims> getClaims(String token) throws Exception{ + + Jws<Claims> claims = null; + + if(publicKey == null){ + throw new IllegalArgumentException("Public key must be provided with Apigee " + + "token in order to verify signature."); + } + + + byte[] publicBytes = decodeBase64(publicKey); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey pubKey = keyFactory.generatePublic(keySpec); + + try { + claims = Jwts.parser().setSigningKey(pubKey).parseClaimsJws(token); + } catch (SignatureException se) { + if(logger.isDebugEnabled()) { + logger.debug("Signature was invalid for Apigee JWT: {} and key: {}", token, publicKey); + } + throw new BadTokenException("Invalid Apigee SSO token signature"); + } catch (MalformedJwtException me){ + if(logger.isDebugEnabled()) { + logger.debug("Beginning JSON object section of Apigee JWT invalid for token: {}", token); + } + throw new BadTokenException("Malformed Apigee JWT"); + } catch (ArrayIndexOutOfBoundsException aio){ + if(logger.isDebugEnabled()) { + logger.debug("Signature section of Apigee JWT invalid for token: {}", token); + } + throw new BadTokenException("Malformed Apigee JWT"); + } + + + return claims; + } + + private void validateClaims (final Jws<Claims> claims) throws ExpiredTokenException { + + final Claims body = claims.getBody(); + + final long expiry = Long.valueOf(body.get("exp").toString()); + + if(expiry - (System.currentTimeMillis()/1000) < 0 ){ + + final long expirationDelta = ((System.currentTimeMillis()/1000) - expiry)*1000; + + throw new ExpiredTokenException(String.format("Token expired %d millisecons ago.", expirationDelta )); + } + + + } + + + @Autowired + public void setManagement(ManagementService management) { + this.management = management; + } + + @Autowired + public void setProperties(Properties properties) { + this.properties = properties; + this.publicKey = getPublicKey(); + } +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/7cf07b4e/stack/services/src/main/java/org/apache/usergrid/security/sso/ExternalSSOProvider.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/sso/ExternalSSOProvider.java b/stack/services/src/main/java/org/apache/usergrid/security/sso/ExternalSSOProvider.java new file mode 100644 index 0000000..eee46c6 --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/sso/ExternalSSOProvider.java @@ -0,0 +1,22 @@ +package org.apache.usergrid.security.sso; + +import org.apache.usergrid.management.UserInfo; +import org.apache.usergrid.security.tokens.TokenInfo; + +import java.util.Map; + +/** + * Created by ayeshadastagiri on 6/22/16. + */ +public interface ExternalSSOProvider { + + /** Authenticate a userId and external token against this provider */ + TokenInfo validateAndReturnTokenInfo(String token, long ttl) throws Exception; + + /** Authenticate a userId and external token against this provider */ + UserInfo validateAndReturnUserInfo(String token, long ttl) throws Exception; + + /** Decode the token, if supported, and return any information encoded with the token */ + Map<String, String> getDecodedTokenDetails(String token) throws Exception; + +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/7cf07b4e/stack/services/src/main/java/org/apache/usergrid/security/sso/SSOProviderFactory.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/sso/SSOProviderFactory.java b/stack/services/src/main/java/org/apache/usergrid/security/sso/SSOProviderFactory.java new file mode 100644 index 0000000..fae0f7f --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/sso/SSOProviderFactory.java @@ -0,0 +1,57 @@ +package org.apache.usergrid.security.sso; + +import org.apache.usergrid.corepersistence.CpEntityManagerFactory; +import org.apache.usergrid.management.ManagementService; +import org.apache.usergrid.persistence.EntityManagerFactory; +import org.apache.usergrid.security.tokens.cassandra.TokenServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Properties; + +/** + * Created by russo on 6/24/16. + */ +public class SSOProviderFactory { + + enum Provider { + APIGEE, USERGRID + } + + private EntityManagerFactory emf; + protected Properties properties; + + + public ExternalSSOProvider getProvider(){ + + final Provider configuredProvider; + try{ + configuredProvider = + Provider.valueOf(properties.getProperty(TokenServiceImpl.USERGRID_EXTERNAL_PROVIDER).toUpperCase()); + }catch (IllegalArgumentException e){ + throw new RuntimeException("Property usergrid.external.sso.provider must " + + "be configured when external SSO is enabled"); + } + + switch (configuredProvider){ + + case APIGEE: + return ((CpEntityManagerFactory)emf).getApplicationContext().getBean( ApigeeSSO2Provider.class ); + case USERGRID: + return ((CpEntityManagerFactory)emf).getApplicationContext().getBean( UsergridExternalProvider.class ); + default: + throw new RuntimeException("Unknown SSO provider"); + } + + } + + @Autowired + public void setEntityManagerFactory( EntityManagerFactory emf ) { + this.emf = emf; + } + + + @Autowired + public void setProperties(Properties properties) { + this.properties = properties; + } +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/7cf07b4e/stack/services/src/main/java/org/apache/usergrid/security/sso/UsergridExternalProvider.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/sso/UsergridExternalProvider.java b/stack/services/src/main/java/org/apache/usergrid/security/sso/UsergridExternalProvider.java new file mode 100644 index 0000000..a8d250d --- /dev/null +++ b/stack/services/src/main/java/org/apache/usergrid/security/sso/UsergridExternalProvider.java @@ -0,0 +1,263 @@ +package org.apache.usergrid.security.sso; + +import com.codahale.metrics.Counter; +import com.google.inject.Injector; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.http.impl.conn.PoolingClientConnectionManager; +import org.apache.usergrid.management.*; +import org.apache.usergrid.persistence.core.metrics.MetricsFactory; +import org.apache.usergrid.persistence.exceptions.EntityNotFoundException; +import org.apache.usergrid.security.tokens.TokenInfo; +import org.codehaus.jackson.JsonNode; +import org.glassfish.jersey.apache.connector.ApacheClientProperties; +import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import java.util.*; + +/** + * Created by ayeshadastagiri on 6/23/16. + */ +public class UsergridExternalProvider implements ExternalSSOProvider { + private static final Logger logger = LoggerFactory.getLogger(ApigeeSSO2Provider.class); + + private static final String SSO_PROCESSING_TIME = "sso.processing_time"; + private static final String SSO_TOKENS_REJECTED = "sso.tokens_rejected"; + private static final String SSO_TOKENS_VALIDATED = "sso.tokens_validated"; + public static final String USERGRID_CENTRAL_URL = "usergrid.external.sso.publicKeyUrl"; + public static final String CENTRAL_CONNECTION_POOL_SIZE = "usergrid.central.connection.pool.size"; + public static final String CENTRAL_CONNECTION_TIMEOUT = "usergrid.central.connection.timeout"; + public static final String CENTRAL_READ_TIMEOUT = "usergrid.central.read.timeout"; + private static final String SSO_CREATED_LOCAL_ADMINS = "sso.created_local_admins"; + + protected ManagementService management; + protected MetricsFactory metricsFactory; + protected Properties properties; + + private static Client jerseyClient = null; + + @Autowired + private Injector injector; + + @Autowired + private ApplicationCreator applicationCreator; + + @Autowired + public void setManagement(ManagementService management) { + this.management = management; + } + + @Autowired + public void setProperties(Properties properties) { + this.properties = properties; + } + + @Autowired + public void setMetricFactory() { + this.metricsFactory = injector.getInstance(MetricsFactory.class); + } + + MetricsFactory getMetricsFactory() { + return metricsFactory; + } + + @Override + public TokenInfo validateAndReturnTokenInfo(String token, long ttl) throws Exception { + throw new UnsupportedOperationException("Returning user info not supported from external Usergrid SSO tokens"); + } + + @Override + public UserInfo validateAndReturnUserInfo(String token, long ttl) throws Exception { + if (token == null) { + throw new IllegalArgumentException("ext_access_token must be specified"); + } + if (ttl == -1) { + throw new IllegalArgumentException("ttl must be specified"); + } + + com.codahale.metrics.Timer processingTimer = getMetricsFactory().getTimer( + UsergridExternalProvider.class, SSO_PROCESSING_TIME); + + com.codahale.metrics.Timer.Context timerContext = processingTimer.time(); + + try { + // look up user via UG Central's /management/me endpoint. + + JsonNode accessInfoNode = getMeFromUgCentral(token); + + JsonNode userNode = accessInfoNode.get("user"); + + String username = userNode.get("username").asText(); + + // if user does not exist locally then we need to fix that + + UserInfo userInfo = management.getAdminUserByUsername(username); + UUID userId = userInfo == null ? null : userInfo.getUuid(); + + if (userId == null) { + + // create local user and and organizations they have on the central Usergrid instance + logger.info("User {} does not exist locally, creating", username); + + String name = userNode.get("name").asText(); + String email = userNode.get("email").asText(); + String dummyPassword = RandomStringUtils.randomAlphanumeric(40); + + JsonNode orgsNode = userNode.get("organizations"); + Iterator<String> fieldNames = orgsNode.getFieldNames(); + + if (!fieldNames.hasNext()) { + // no organizations for user exist in response from central Usergrid SSO + // so create user's personal organization and use username as organization name + fieldNames = Collections.singletonList(username).iterator(); + } + + // create user and any organizations that user is supposed to have + + while (fieldNames.hasNext()) { + + String orgName = fieldNames.next(); + + if (userId == null) { +// + // haven't created user yet so do that now + OrganizationOwnerInfo ownerOrgInfo = management.createOwnerAndOrganization( + orgName, username, name, email, dummyPassword, true, false); + + applicationCreator.createSampleFor(ownerOrgInfo.getOrganization()); + + userId = ownerOrgInfo.getOwner().getUuid(); + userInfo = ownerOrgInfo.getOwner(); + + Counter createdAdminsCounter = getMetricsFactory().getCounter( + UsergridExternalProvider.class, SSO_CREATED_LOCAL_ADMINS); + createdAdminsCounter.inc(); + + logger.info("Created user {} and org {}", username, orgName); + + } else { + + // already created user, so just create an org + final OrganizationInfo organization = + management.createOrganization(orgName, userInfo, true); + + applicationCreator.createSampleFor(organization); + + logger.info("Created user {}'s other org {}", username, orgName); + } + } + } + + return userInfo; + } catch (Exception e) { + timerContext.stop(); + logger.debug("Error validating external token", e); + throw e; + } + + } + + @Override + public Map<String, String> getDecodedTokenDetails(String token) { + + throw new UnsupportedOperationException("Not currently supported with Usergrid external tokens"); + + } + + /** + * Look up Admin User via UG Central's /management/me endpoint. + * + * @param extAccessToken Access token issued by UG Central of Admin User + * @return JsonNode representation of AccessInfo object for Admin User + * @throws EntityNotFoundException if access_token is not valid. + */ + private JsonNode getMeFromUgCentral(String extAccessToken) throws EntityNotFoundException { + + // prepare to count tokens validated and rejected + + Counter tokensRejectedCounter = getMetricsFactory().getCounter( + UsergridExternalProvider.class, SSO_TOKENS_REJECTED); + Counter tokensValidatedCounter = getMetricsFactory().getCounter( + UsergridExternalProvider.class, SSO_TOKENS_VALIDATED); + + // create URL of central Usergrid's /management/me endpoint + + String externalUrl = properties.getProperty(USERGRID_CENTRAL_URL).trim(); + + // be lenient about trailing slash + externalUrl = !externalUrl.endsWith("/") ? externalUrl + "/" : externalUrl; + String me = externalUrl + "management/me?access_token=" + extAccessToken; + + // use our favorite HTTP client to GET /management/me + + Client client = getJerseyClient(); + final org.codehaus.jackson.JsonNode accessInfoNode; + try { + accessInfoNode = client.target(me).request() + .accept(MediaType.APPLICATION_JSON_TYPE) + .get(org.codehaus.jackson.JsonNode.class); + + tokensValidatedCounter.inc(); + + } catch (Exception e) { + // user not found 404 + tokensRejectedCounter.inc(); + String msg = "Cannot find Admin User associated with " + extAccessToken; + throw new EntityNotFoundException(msg, e); + } + + return accessInfoNode; + } + + private Client getJerseyClient() { + + if (jerseyClient == null) { + + synchronized (this) { + + // create HTTPClient and with configured connection pool + + int poolSize = 100; // connections + final String poolSizeStr = properties.getProperty(CENTRAL_CONNECTION_POOL_SIZE); + if (poolSizeStr != null) { + poolSize = Integer.parseInt(poolSizeStr); + } + + PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager(); + connectionManager.setMaxTotal(poolSize); + + int timeout = 20000; // ms + final String timeoutStr = properties.getProperty(CENTRAL_CONNECTION_TIMEOUT); + if (timeoutStr != null) { + timeout = Integer.parseInt(timeoutStr); + } + + int readTimeout = 20000; // ms + final String readTimeoutStr = properties.getProperty(CENTRAL_READ_TIMEOUT); + if (readTimeoutStr != null) { + readTimeout = Integer.parseInt(readTimeoutStr); + } + + ClientConfig clientConfig = new ClientConfig(); + clientConfig.register(new JacksonFeature()); + clientConfig.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager); + clientConfig.connectorProvider(new ApacheConnectorProvider()); + + jerseyClient = ClientBuilder.newClient(clientConfig); + jerseyClient.property(ClientProperties.CONNECT_TIMEOUT, timeout); + jerseyClient.property(ClientProperties.READ_TIMEOUT, readTimeout); + } + } + + return jerseyClient; + + } +} http://git-wip-us.apache.org/repos/asf/usergrid/blob/7cf07b4e/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java b/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java index eef7a1c..7beeb3b 100644 --- a/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java +++ b/stack/services/src/main/java/org/apache/usergrid/security/tokens/cassandra/TokenServiceImpl.java @@ -32,14 +32,14 @@ import org.apache.usergrid.persistence.core.metrics.MetricsFactory; import org.apache.usergrid.persistence.entities.Application; import org.apache.usergrid.security.AuthPrincipalInfo; import org.apache.usergrid.security.AuthPrincipalType; +import org.apache.usergrid.security.sso.SSOProviderFactory; import org.apache.usergrid.security.tokens.TokenCategory; import org.apache.usergrid.security.tokens.TokenInfo; import org.apache.usergrid.security.tokens.TokenService; import org.apache.usergrid.security.tokens.exceptions.BadTokenException; import org.apache.usergrid.security.tokens.exceptions.ExpiredTokenException; import org.apache.usergrid.security.tokens.exceptions.InvalidTokenException; -import org.apache.usergrid.security.tokens.externalProviders.ApigeeSSO2Provider; -import org.apache.usergrid.security.tokens.externalProviders.UsergridCentral; +import org.apache.usergrid.security.sso.ExternalSSOProvider; import org.apache.usergrid.utils.ConversionUtils; import org.apache.usergrid.utils.JsonUtils; import org.apache.usergrid.utils.UUIDUtils; @@ -316,29 +316,43 @@ public class TokenServiceImpl implements TokenService { @Override public TokenInfo getTokenInfo( String token, boolean updateAccessTime ) throws Exception { - UUID uuid = null; + UUID uuid; + + + /** Pre-validation of the token string based on Usergrid's encoding scheme. + * + * If the token does not parse out a UUID, then it's not a Usergrid token. Check if External SSO provider + * is configured, which is not Usergrid and immediately try to validate the token based on this parsing + * information. + */ try{ uuid = getUUIDForToken( token ); } catch(Exception e){ - try{ - return validateExternalToken(token,1,properties.getProperty(USERGRID_EXTERNAL_PROVIDER)); - } - catch (Exception exception){ - logger.debug("invalid request"); - throw new IllegalArgumentException("Invalid token in the request."); + + // If the token doesn't parse as a Usergrid token, see if an external provider other than Usergrid is + // enabled. If so, just validate the external token. + if( isExternalSSOProviderEnabled() && !getExternalSSOProvider().equalsIgnoreCase("usergrid")) { + return validateExternalToken(token, 1, getExternalSSOProvider()); + }else{ + throw e; // re-throw the error } + } - long ssoTtl = 1000000L; // TODO: property for this + final TokenInfo tokenInfo; - TokenInfo tokenInfo; + /** + * Now try actual Usergrid token validations. First try locally. If that fails and SSO is enabled with + * Usergrid being a provider, validate the external token. + */ try { tokenInfo = getTokenInfo( uuid ); } catch (InvalidTokenException e){ - // now try from central sso - if ( isExternalSSOProviderEnabled() ){ - return validateExternalToken( token, maxPersistenceTokenAge,"" ); + // Try the request from Usergrid, conditions are specific so we don't incur perf hits for unncessary + // token validations that are known to not + if ( isExternalSSOProviderEnabled() && getExternalSSOProvider().equalsIgnoreCase("usergrid") ){ + return validateExternalToken( token, maxPersistenceTokenAge, getExternalSSOProvider() ); }else{ throw e; // re-throw the error } @@ -767,6 +781,9 @@ public class TokenServiceImpl implements TokenService { @Autowired protected ManagementService management; + @Autowired + private SSOProviderFactory ssoProviderFactory; + MetricsFactory getMetricsFactory() { return metricsFactory; } @@ -776,6 +793,10 @@ public class TokenServiceImpl implements TokenService { return Boolean.valueOf(properties.getProperty( USERGRID_EXTERNAL_SSO_ENABLED )); } + private String getExternalSSOProvider(){ + return properties.getProperty(USERGRID_EXTERNAL_PROVIDER); + } + /** * <p> * Validates access token from other or "external" Usergrid system. @@ -794,24 +815,25 @@ public class TokenServiceImpl implements TokenService { * @param ttl Time to live for token. */ public TokenInfo validateExternalToken(String extAccessToken, long ttl, String provider) throws Exception { - TokenInfo tokenInfo = null; - //todo: based on provider call the appropriate method. - if(provider.equalsIgnoreCase("apigee")){ - ApigeeSSO2Provider apigeeProvider = ((CpEntityManagerFactory)emf).getApplicationContext().getBean( ApigeeSSO2Provider.class ); - return apigeeProvider.validateAndReturnTokenInfo(extAccessToken,ttl); - } - else if(provider.equalsIgnoreCase("usergridCentral")){ - UsergridCentral ugCentralProvider = ((CpEntityManagerFactory)emf).getApplicationContext().getBean( UsergridCentral.class ); - UserInfo userinfo = ugCentralProvider.validateAndReturnUserInfo(extAccessToken,ttl); - // store the external access_token as if it were one of our own + + ExternalSSOProvider ssoProvider = ssoProviderFactory.getProvider(); + + if(provider.equalsIgnoreCase("usergrid")){ + + UserInfo userinfo = ssoProvider.validateAndReturnUserInfo(extAccessToken,ttl); + + // Store the external Usergrid access_token as if it were one of our own so we don't have to make the + // external HTTP validation call on subsequent requests importToken( extAccessToken, TokenCategory.ACCESS, null, new AuthPrincipalInfo( ADMIN_USER, userinfo.getUuid(), CpNamingUtils.MANAGEMENT_APPLICATION_ID), null, ttl ); return getTokenInfo( extAccessToken ); + }else{ + + return ssoProvider.validateAndReturnTokenInfo(extAccessToken,ttl); } - //todo : what if the token info is null ? - return tokenInfo; + } } http://git-wip-us.apache.org/repos/asf/usergrid/blob/7cf07b4e/stack/services/src/main/java/org/apache/usergrid/security/tokens/externalProviders/ApigeeSSO2Provider.java ---------------------------------------------------------------------- diff --git a/stack/services/src/main/java/org/apache/usergrid/security/tokens/externalProviders/ApigeeSSO2Provider.java b/stack/services/src/main/java/org/apache/usergrid/security/tokens/externalProviders/ApigeeSSO2Provider.java deleted file mode 100644 index 08ec781..0000000 --- a/stack/services/src/main/java/org/apache/usergrid/security/tokens/externalProviders/ApigeeSSO2Provider.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.apache.usergrid.security.tokens.externalProviders; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureException; -import org.apache.usergrid.corepersistence.util.CpNamingUtils; -import org.apache.usergrid.management.ManagementService; -import org.apache.usergrid.management.UserInfo; -import org.apache.usergrid.security.AuthPrincipalInfo; -import org.apache.usergrid.security.AuthPrincipalType; -import org.apache.usergrid.security.tokens.TokenInfo; -import org.apache.usergrid.utils.UUIDUtils; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.jackson.JacksonFeature; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import java.security.KeyFactory; -import java.security.PublicKey; -import java.security.spec.X509EncodedKeySpec; -import java.util.Map; -import java.util.Properties; - -import static org.apache.commons.codec.binary.Base64.decodeBase64; - -/** - * Created by ayeshadastagiri on 6/22/16. - */ -public class ApigeeSSO2Provider implements ExternalTokenProvider { - - private static final Logger logger = LoggerFactory.getLogger(ApigeeSSO2Provider.class); - private static final String RESPONSE_PUBLICKEY_VALUE = "value"; - protected Properties properties; - protected ManagementService management; - protected Client client; - protected String publicKey; - - public static final String USERGRID_EXTERNAL_PUBLICKEY_URL = "usergrid.external.sso.publicKeyUrl"; - - public ApigeeSSO2Provider() { - ClientConfig clientConfig = new ClientConfig(); - clientConfig.register(new JacksonFeature()); - client = ClientBuilder.newClient(clientConfig); - } - - private String getPublicKey() { - Map<String, Object> publicKey = client.target(properties.getProperty(USERGRID_EXTERNAL_PUBLICKEY_URL)).request().get(Map.class); - return publicKey.get(RESPONSE_PUBLICKEY_VALUE).toString().split("----\n")[1].split("\n---")[0]; - } - - @Override - public TokenInfo validateAndReturnTokenInfo(String token, long ttl) throws Exception { - - try { - UserInfo userInfo = validateAndReturnUserInfo(token, ttl); - TokenInfo tokeninfo = new TokenInfo(UUIDUtils.newTimeUUID(), "access", 1, 1, 1, ttl, - new AuthPrincipalInfo(AuthPrincipalType.ADMIN_USER, userInfo.getUuid(), - CpNamingUtils.MANAGEMENT_APPLICATION_ID), null); - return tokeninfo; - } - catch(Exception e){ - logger.debug("Error construcing token info from userinfo"); - e.printStackTrace(); - } - return null; - } - - @Override - public UserInfo validateAndReturnUserInfo(String token, long ttl) throws Exception { - - byte[] publicBytes = decodeBase64(publicKey); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PublicKey pubKey = keyFactory.generatePublic(keySpec); - Jws<Claims> payload = null; - try { - payload = Jwts.parser().setSigningKey(pubKey).parseClaimsJws(token); - UserInfo userInfo = management.getAdminUserByEmail(payload.getBody().get("email").toString()); - if (userInfo == null) { - throw new IllegalArgumentException("user " + payload.getBody().get("email").toString() + " doesnt exist"); - } - return userInfo; - } catch (SignatureException se) { - logger.debug("Signature did not match."); - throw new IllegalArgumentException("Signature did not match for the token."); - } catch (Exception e) { - logger.debug("Error validating Apigee SSO2 token."); - e.printStackTrace(); - } - return null; - } - - - @Autowired - public void setManagement(ManagementService management) { - this.management = management; - } - - @Autowired - public void setProperties(Properties properties) { - this.properties = properties; - this.publicKey = getPublicKey(); - } -} http://git-wip-us.apache.org/repos/asf/usergrid/blob/7cf07b4e/stack/services/src/main/resources/usergrid-services-context.xml ---------------------------------------------------------------------- diff --git a/stack/services/src/main/resources/usergrid-services-context.xml b/stack/services/src/main/resources/usergrid-services-context.xml index c80139c..a91524b 100644 --- a/stack/services/src/main/resources/usergrid-services-context.xml +++ b/stack/services/src/main/resources/usergrid-services-context.xml @@ -77,12 +77,12 @@ <bean id="saltProvider" class="org.apache.usergrid.security.salt.NoOpSaltProvider"/> - - <bean id="apigeeSSO2Provider" class="org.apache.usergrid.security.tokens.externalProviders.ApigeeSSO2Provider"> + <!-- singletons for custom SSO providers if they are used --> + <bean id="apigeeSSO2Provider" class="org.apache.usergrid.security.sso.ApigeeSSO2Provider"> <property name="management" ref="managementService" /> </bean> - <bean id="usergridCentral" class="org.apache.usergrid.security.tokens.externalProviders.UsergridCentral"> + <bean id="usergridExternalProvider" class="org.apache.usergrid.security.sso.UsergridExternalProvider"> <property name="management" ref="managementService" /> </bean> @@ -100,11 +100,18 @@ <constructor-arg ref="managementService"/> </bean> + <!-- sign in providers are for application users to have integration with FB,Ping,etc. --> <bean id="signInProviderFactory" class="org.apache.usergrid.security.providers.SignInProviderFactory"> <property name="entityManagerFactory" ref="entityManagerFactory"/> <property name="managementService" ref="managementService"/> </bean> + <!-- sso providers are for admin user accounts integrated with SSO systems like Apigee or central UG --> + <bean id="ssoProviderFactory" class="org.apache.usergrid.security.sso.SSOProviderFactory"> + <property name="entityManagerFactory" ref="entityManagerFactory"/> + <property name="properties" ref="properties"/> + </bean> + <bean id="exportService" class="org.apache.usergrid.management.export.ExportServiceImpl"> <property name="managementService" ref="managementService"/> <property name="emf" ref="entityManagerFactory"/>
