| /* |
| * 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.tokens.cassandra; |
| |
| |
| import com.google.inject.Injector; |
| import me.prettyprint.hector.api.Keyspace; |
| import me.prettyprint.hector.api.beans.HColumn; |
| import me.prettyprint.hector.api.mutation.Mutator; |
| import org.apache.usergrid.corepersistence.CpEntityManagerFactory; |
| import org.apache.usergrid.corepersistence.util.CpNamingUtils; |
| import org.apache.usergrid.management.ApplicationCreator; |
| import org.apache.usergrid.management.ManagementService; |
| import org.apache.usergrid.management.UserInfo; |
| import org.apache.usergrid.persistence.EntityManagerFactory; |
| import org.apache.usergrid.persistence.cassandra.CassandraService; |
| 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.sso.ExternalSSOProvider; |
| import org.apache.usergrid.utils.ConversionUtils; |
| import org.apache.usergrid.utils.JsonUtils; |
| import org.apache.usergrid.utils.UUIDUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.beans.factory.annotation.Qualifier; |
| import org.springframework.util.Assert; |
| |
| import javax.ws.rs.client.Client; |
| import java.nio.ByteBuffer; |
| import java.util.*; |
| |
| import static java.lang.System.currentTimeMillis; |
| import static me.prettyprint.hector.api.factory.HFactory.createColumn; |
| import static me.prettyprint.hector.api.factory.HFactory.createMutator; |
| import static org.apache.commons.codec.binary.Base64.decodeBase64; |
| import static org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString; |
| import static org.apache.commons.codec.digest.DigestUtils.sha; |
| import static org.apache.usergrid.persistence.cassandra.CassandraPersistenceUtils.getColumnMap; |
| import static org.apache.usergrid.persistence.cassandra.CassandraService.PRINCIPAL_TOKEN_CF; |
| import static org.apache.usergrid.persistence.cassandra.CassandraService.TOKENS_CF; |
| import static org.apache.usergrid.persistence.cassandra.Serializers.*; |
| import static org.apache.usergrid.security.AuthPrincipalType.ADMIN_USER; |
| import static org.apache.usergrid.security.tokens.TokenCategory.*; |
| import static org.apache.usergrid.utils.ConversionUtils.*; |
| import static org.apache.usergrid.utils.MapUtils.hasKeys; |
| import static org.apache.usergrid.utils.MapUtils.hashMap; |
| import static org.apache.usergrid.utils.UUIDUtils.getTimestampInMillis; |
| |
| |
| public class TokenServiceImpl implements TokenService { |
| |
| private static final Logger logger = LoggerFactory.getLogger( TokenServiceImpl.class ); |
| |
| public static final String PROPERTIES_AUTH_TOKEN_SECRET_SALT = "usergrid.auth.token_secret_salt"; |
| public static final String PROPERTIES_AUTH_TOKEN_EXPIRES_FROM_LAST_USE = |
| "usergrid.auth.token_expires_from_last_use"; |
| public static final String PROPERTIES_AUTH_TOKEN_REFRESH_REUSES_ID = "usergrid.auth.token_refresh_reuses_id"; |
| |
| private static final String TOKEN_UUID = "uuid"; |
| private static final String TOKEN_TYPE = "type"; |
| private static final String TOKEN_CREATED = "created"; |
| private static final String TOKEN_ACCESSED = "accessed"; |
| private static final String TOKEN_INACTIVE = "inactive"; |
| private static final String TOKEN_DURATION = "duration"; |
| private static final String TOKEN_PRINCIPAL_TYPE = "principal"; |
| private static final String TOKEN_ENTITY = "entity"; |
| private static final String TOKEN_APPLICATION = "application"; |
| private static final String TOKEN_STATE = "state"; |
| private static final String TOKEN_WORKFLOW_ORG_ID = "workflowOrgId"; |
| |
| |
| private static final String TOKEN_TYPE_ACCESS = "access"; |
| |
| |
| private static final Set<String> TOKEN_PROPERTIES; |
| |
| |
| static { |
| HashSet<String> set = new HashSet<String>(); |
| set.add( TOKEN_UUID ); |
| set.add( TOKEN_TYPE ); |
| set.add( TOKEN_CREATED ); |
| set.add( TOKEN_ACCESSED ); |
| set.add( TOKEN_INACTIVE ); |
| set.add( TOKEN_PRINCIPAL_TYPE ); |
| set.add( TOKEN_ENTITY ); |
| set.add( TOKEN_APPLICATION ); |
| set.add( TOKEN_STATE ); |
| set.add( TOKEN_DURATION ); |
| set.add( TOKEN_WORKFLOW_ORG_ID ); |
| TOKEN_PROPERTIES = Collections.unmodifiableSet(set); |
| } |
| |
| |
| private static final HashSet<String> REQUIRED_TOKEN_PROPERTIES = new HashSet<String>(); |
| |
| |
| static { |
| REQUIRED_TOKEN_PROPERTIES.add( TOKEN_UUID ); |
| REQUIRED_TOKEN_PROPERTIES.add( TOKEN_TYPE ); |
| REQUIRED_TOKEN_PROPERTIES.add( TOKEN_CREATED ); |
| REQUIRED_TOKEN_PROPERTIES.add( TOKEN_ACCESSED ); |
| REQUIRED_TOKEN_PROPERTIES.add( TOKEN_INACTIVE ); |
| REQUIRED_TOKEN_PROPERTIES.add( TOKEN_DURATION ); |
| } |
| |
| |
| public static final String TOKEN_SECRET_SALT = "super secret token value"; |
| |
| // Short-lived token is good for 24 hours |
| public static final long SHORT_TOKEN_AGE = 24 * 60 * 60 * 1000; |
| |
| // Long-lived token is good for 7 days |
| public static final long LONG_TOKEN_AGE = 7 * 24 * 60 * 60 * 1000; |
| |
| String tokenSecretSalt = TOKEN_SECRET_SALT; |
| |
| long maxPersistenceTokenAge = LONG_TOKEN_AGE; |
| |
| Map<TokenCategory, Long> tokenExpirations = |
| hashMap( ACCESS, LONG_TOKEN_AGE ).map( REFRESH, LONG_TOKEN_AGE ).map( EMAIL, LONG_TOKEN_AGE ) |
| .map( OFFLINE, LONG_TOKEN_AGE ); |
| |
| long maxAccessTokenAge = SHORT_TOKEN_AGE; |
| long maxRefreshTokenAge = LONG_TOKEN_AGE; |
| long maxEmailTokenAge = LONG_TOKEN_AGE; |
| long maxOfflineTokenAge = LONG_TOKEN_AGE; |
| |
| protected CassandraService cassandra; |
| |
| protected Properties properties; |
| |
| protected EntityManagerFactory emf; |
| |
| protected MetricsFactory metricsFactory; |
| |
| |
| public TokenServiceImpl() { |
| } |
| |
| |
| long getExpirationProperty( String name, long default_expiration ) { |
| long expires = Long.parseLong( |
| properties.getProperty( "usergrid.auth.token." + name + ".expires", "" + default_expiration ) ); |
| return expires > 0 ? expires : default_expiration; |
| } |
| |
| |
| long getExpirationForTokenType( TokenCategory tokenCategory ) { |
| Long l = tokenExpirations.get( tokenCategory ); |
| if ( l != null ) { |
| return l; |
| } |
| return SHORT_TOKEN_AGE; |
| } |
| |
| |
| void setExpirationFromProperties( String name ) { |
| TokenCategory tokenCategory = TokenCategory.valueOf( name.toUpperCase() ); |
| long expires = Long.parseLong( properties.getProperty( "usergrid.auth.token." + name + ".expires", |
| "" + getExpirationForTokenType( tokenCategory ) ) ); |
| if ( expires > 0 ) { |
| tokenExpirations.put( tokenCategory, expires ); |
| } |
| logger.info( "{} token expires after {} seconds", name, getExpirationForTokenType( tokenCategory ) / 1000 ); |
| } |
| |
| |
| @Autowired |
| public void setProperties( Properties properties ) { |
| this.properties = properties; |
| |
| if ( properties != null ) { |
| maxPersistenceTokenAge = getExpirationProperty( "persistence", maxPersistenceTokenAge ); |
| |
| setExpirationFromProperties( "access" ); |
| setExpirationFromProperties( "refresh" ); |
| setExpirationFromProperties( "email" ); |
| setExpirationFromProperties( "offline" ); |
| |
| tokenSecretSalt = properties.getProperty( PROPERTIES_AUTH_TOKEN_SECRET_SALT, TOKEN_SECRET_SALT ); |
| } |
| } |
| |
| |
| @Override |
| public String createToken( TokenCategory tokenCategory, String type, AuthPrincipalInfo principal, |
| Map<String, Object> state, long duration ) throws Exception { |
| return createToken( tokenCategory, type, principal, state, duration, null, System.currentTimeMillis() ); |
| } |
| |
| |
| @Override |
| public String createToken( TokenCategory tokenCategory, String type, AuthPrincipalInfo principal, |
| Map<String, Object> state, long duration, UUID workflowOrgId ) throws Exception { |
| return createToken( tokenCategory, type, principal, state, duration, workflowOrgId, System.currentTimeMillis() ); |
| } |
| |
| |
| /** Exposed for testing purposes. The interface does not allow creation timestamp checking */ |
| public String createToken( TokenCategory tokenCategory, String type, AuthPrincipalInfo principal, |
| Map<String, Object> state, long duration, UUID workflowOrgId, |
| long creationTimestamp ) throws Exception { |
| |
| long maxTokenTtl = getMaxTtl( tokenCategory, principal ); |
| |
| if ( duration > maxTokenTtl ) { |
| throw new IllegalArgumentException( |
| String.format( "Your token age cannot be more than the maximum age of %d milliseconds", |
| maxTokenTtl ) ); |
| } |
| |
| if ( duration == 0 ) { |
| duration = maxTokenTtl; |
| } |
| |
| if ( principal != null ) { |
| Assert.notNull( principal.getType() ); |
| Assert.notNull( principal.getApplicationId() ); |
| Assert.notNull( principal.getUuid() ); |
| } |
| |
| // create UUID that we will use to store token info in our database |
| UUID uuid = UUIDUtils.newTimeUUID( creationTimestamp ); |
| |
| long timestamp = getTimestampInMillis( uuid ); |
| if ( type == null ) { |
| type = TOKEN_TYPE_ACCESS; |
| } |
| TokenInfo tokenInfo = new TokenInfo( uuid, type, timestamp, timestamp, 0, duration, principal, |
| state, workflowOrgId ); |
| putTokenInfo( tokenInfo ); |
| |
| // generate token from the UUID that we created |
| return getTokenForUUID(tokenInfo, tokenCategory, uuid); |
| } |
| |
| |
| @Override |
| public void importToken(String token, TokenCategory tokenCategory, String type, AuthPrincipalInfo principal, |
| Map<String, Object> state, long duration) throws Exception { |
| |
| importToken(token, tokenCategory, type, principal, state, duration, null); |
| } |
| |
| |
| @Override |
| public void importToken(String token, TokenCategory tokenCategory, String type, AuthPrincipalInfo principal, |
| Map<String, Object> state, long duration, UUID workflowOrgId) throws Exception { |
| |
| // same logic as create token |
| |
| long maxTokenTtl = getMaxTtl( tokenCategory, principal ); |
| |
| if ( duration > maxTokenTtl ) { |
| throw new IllegalArgumentException( |
| String.format( "Your token age cannot be more than the maximum age of %d milliseconds", |
| maxTokenTtl ) ); |
| } |
| |
| if ( duration == 0 ) { |
| duration = maxTokenTtl; |
| } |
| |
| if ( principal != null ) { |
| Assert.notNull( principal.getType() ); |
| Assert.notNull( principal.getApplicationId() ); |
| Assert.notNull( principal.getUuid() ); |
| } |
| |
| // except that we generate the UUID based on the token |
| |
| UUID uuid = getUUIDForToken(token); |
| |
| long timestamp = getTimestampInMillis( uuid ); |
| if ( type == null ) { |
| type = TOKEN_TYPE_ACCESS; |
| } |
| |
| TokenInfo tokenInfo = new TokenInfo( uuid, type, timestamp, timestamp, 0, duration, principal, |
| state, workflowOrgId ); |
| putTokenInfo( tokenInfo ); |
| } |
| |
| |
| @Override |
| public TokenInfo getTokenInfo( String token ) throws Exception { |
| return getTokenInfo(token, true); |
| } |
| |
| |
| @Override |
| public TokenInfo getTokenInfo( String token, boolean updateAccessTime ) throws Exception { |
| |
| 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 (ExpiredTokenException expiredTokenException){ |
| throw new ExpiredTokenException(expiredTokenException.getMessage()); |
| } |
| catch(Exception e){ |
| |
| // 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. |
| try{ |
| if( isExternalSSOProviderEnabled() && !getExternalSSOProvider().equalsIgnoreCase("usergrid")) { |
| return validateExternalToken(token, 1, getExternalSSOProvider()); |
| }else{ |
| throw new IllegalArgumentException("invalid external provider : " + getExternalSSOProvider()); // re-throw the error |
| } |
| } |
| catch (NullPointerException npe){ |
| throw new IllegalArgumentException("The SSO provider in the config is empty."); |
| } |
| |
| } |
| |
| final 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){ |
| // 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 |
| } |
| } |
| |
| if (updateAccessTime) { |
| //update the token |
| long now = currentTimeMillis(); |
| |
| long maxTokenTtl = getMaxTtl(TokenCategory.getFromBase64String(token), tokenInfo.getPrincipal()); |
| |
| Mutator<UUID> batch = createMutator(cassandra.getUsergridApplicationKeyspace(), ue); |
| |
| HColumn<String, Long> col = |
| createColumn(TOKEN_ACCESSED, now, calcTokenTime(tokenInfo.getExpiration(maxTokenTtl)), |
| se, le); |
| batch.addInsertion(uuid, TOKENS_CF, col); |
| |
| long inactive = now - tokenInfo.getAccessed(); |
| if (inactive > tokenInfo.getInactive()) { |
| col = createColumn(TOKEN_INACTIVE, inactive, calcTokenTime(tokenInfo.getExpiration(maxTokenTtl)), |
| se, le); |
| batch.addInsertion(uuid, TOKENS_CF, col); |
| tokenInfo.setInactive(inactive); |
| } |
| |
| batch.execute(); |
| } |
| |
| return tokenInfo; |
| } |
| |
| |
| /** Get the max ttl per app. This is null safe,and will return the default in the case of missing data */ |
| private long getMaxTtl( TokenCategory tokenCategory, AuthPrincipalInfo principal ) throws Exception { |
| |
| if ( principal == null ) { |
| return maxPersistenceTokenAge; |
| } |
| long defaultMaxTtlForTokenType = getExpirationForTokenType( tokenCategory ); |
| |
| Application application = emf.getEntityManager( principal.getApplicationId() ) |
| .get( principal.getApplicationId(), Application.class ); |
| |
| if ( application == null ) { |
| return defaultMaxTtlForTokenType; |
| } |
| |
| // set the max to the default |
| long maxTokenTtl = defaultMaxTtlForTokenType; |
| |
| // it's been defined on the expiration, override it |
| if ( application.getAccesstokenttl() != null ) { |
| maxTokenTtl = application.getAccesstokenttl(); |
| |
| // it's set to 0 which equals infinity, set our expiration to |
| // LONG.MAX |
| if ( maxTokenTtl == 0 ) { |
| maxTokenTtl = Long.MAX_VALUE; |
| } |
| } |
| |
| return maxTokenTtl; |
| } |
| |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see |
| * org.apache.usergrid.security.tokens.TokenService#removeTokens(org.apache.usergrid.security |
| * .AuthPrincipalInfo) |
| */ |
| @Override |
| public void removeTokens( AuthPrincipalInfo principal ) throws Exception { |
| List<UUID> tokenIds = getTokenUUIDS( principal ); |
| |
| Mutator<ByteBuffer> batch = createMutator( cassandra.getUsergridApplicationKeyspace(), be ); |
| |
| for ( UUID tokenId : tokenIds ) { |
| batch.addDeletion( bytebuffer( tokenId ), TOKENS_CF ); |
| } |
| |
| batch.addDeletion( principalKey( principal ), PRINCIPAL_TOKEN_CF ); |
| |
| batch.execute(); |
| } |
| |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see |
| * org.apache.usergrid.security.tokens.TokenService#revokeToken(java.lang.String) |
| */ |
| @Override |
| public void revokeToken( String token ) { |
| |
| TokenInfo info; |
| |
| try { |
| info = getTokenInfo( token ); |
| } |
| catch ( Exception e ) { |
| logger.error( "Unable to find token with the specified value ignoring request. Value : {}", token ); |
| return; |
| } |
| |
| UUID tokenId = info.getUuid(); |
| |
| Mutator<ByteBuffer> batch = createMutator( cassandra.getUsergridApplicationKeyspace(), be ); |
| |
| // clean up the link in the principal -> token index if the principal is |
| // on the token |
| if ( info.getPrincipal() != null ) { |
| batch.addDeletion( principalKey( info.getPrincipal() ), PRINCIPAL_TOKEN_CF, bytebuffer( tokenId ), |
| be ); |
| } |
| |
| // remove the token from the tokens cf |
| batch.addDeletion( bytebuffer( tokenId ), TOKENS_CF ); |
| |
| batch.execute(); |
| } |
| |
| |
| private TokenInfo getTokenInfo( UUID uuid ) throws Exception { |
| if ( uuid == null ) { |
| throw new InvalidTokenException( "No token specified" ); |
| } |
| Map<String, ByteBuffer> columns = getColumnMap( cassandra |
| .getColumns( cassandra.getUsergridApplicationKeyspace(), TOKENS_CF, uuid, TOKEN_PROPERTIES, se, |
| be ) ); |
| if ( !hasKeys( columns, REQUIRED_TOKEN_PROPERTIES ) ) { |
| throw new InvalidTokenException( "Token not found in database" ); |
| } |
| String type = string( columns.get( TOKEN_TYPE ) ); |
| long created = getLong( columns.get( TOKEN_CREATED ) ); |
| long accessed = getLong( columns.get( TOKEN_ACCESSED ) ); |
| long inactive = getLong( columns.get( TOKEN_INACTIVE ) ); |
| long duration = getLong( columns.get( TOKEN_DURATION ) ); |
| String principalTypeStr = string( columns.get( TOKEN_PRINCIPAL_TYPE ) ); |
| AuthPrincipalType principalType = null; |
| if ( principalTypeStr != null ) { |
| try { |
| principalType = AuthPrincipalType.valueOf( principalTypeStr.toUpperCase() ); |
| } |
| catch ( IllegalArgumentException e ) { |
| } |
| } |
| AuthPrincipalInfo principal = null; |
| if ( principalType != null ) { |
| UUID entityId = uuid( columns.get( TOKEN_ENTITY ) ); |
| UUID appId = uuid( columns.get( TOKEN_APPLICATION ) ); |
| principal = new AuthPrincipalInfo( principalType, entityId, appId ); |
| } |
| @SuppressWarnings("unchecked") Map<String, Object> state = |
| ( Map<String, Object> ) JsonUtils.fromByteBuffer( columns.get( TOKEN_STATE ) ); |
| |
| UUID workflowOrgId = null; |
| if (columns.containsKey(TOKEN_WORKFLOW_ORG_ID)) { |
| workflowOrgId = ConversionUtils.uuid(columns.get(TOKEN_WORKFLOW_ORG_ID)); |
| } |
| |
| return new TokenInfo( uuid, type, created, accessed, inactive, duration, principal, state, workflowOrgId ); |
| } |
| |
| |
| private void putTokenInfo( TokenInfo tokenInfo ) throws Exception { |
| |
| ByteBuffer tokenUUID = bytebuffer( tokenInfo.getUuid() ); |
| |
| Keyspace ko = cassandra.getUsergridApplicationKeyspace(); |
| |
| Mutator<ByteBuffer> m = createMutator( ko, be ); |
| |
| int ttl = calcTokenTime( tokenInfo.getDuration() ); |
| |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_UUID, bytebuffer( tokenInfo.getUuid() ), ttl, se, be ) ); |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_TYPE, bytebuffer( tokenInfo.getType() ), ttl, se, be ) ); |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_CREATED, bytebuffer( tokenInfo.getCreated() ), ttl, se, be ) ); |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_ACCESSED, bytebuffer( tokenInfo.getAccessed() ), ttl, se, be ) ); |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_INACTIVE, bytebuffer( tokenInfo.getInactive() ), ttl, se, be ) ); |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_DURATION, bytebuffer( tokenInfo.getDuration() ), ttl, se, be ) ); |
| |
| if ( tokenInfo.getPrincipal() != null ) { |
| |
| AuthPrincipalInfo principalInfo = tokenInfo.getPrincipal(); |
| |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_PRINCIPAL_TYPE, bytebuffer( principalInfo.getType().toString().toLowerCase() ), |
| ttl, se, be ) ); |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_ENTITY, bytebuffer( principalInfo.getUuid() ), ttl, se, be ) ); |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_APPLICATION, bytebuffer( principalInfo.getApplicationId() ), ttl, se, |
| be ) ); |
| |
| /* |
| * write to the PRINCIPAL+TOKEN The format is as follow |
| * |
| * appid+principalId+principalType :{ tokenuuid: 0x00} |
| */ |
| |
| ByteBuffer rowKey = principalKey( principalInfo ); |
| m.addInsertion( rowKey, PRINCIPAL_TOKEN_CF, createColumn( tokenUUID, HOLDER, ttl, be, be ) ); |
| } |
| |
| if ( tokenInfo.getState() != null ) { |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_STATE, JsonUtils.toByteBuffer( tokenInfo.getState() ), ttl, se, |
| be ) ); |
| } |
| |
| if ( tokenInfo.getWorkflowOrgId() != null ) { |
| m.addInsertion( tokenUUID, TOKENS_CF, |
| createColumn( TOKEN_WORKFLOW_ORG_ID, bytebuffer( tokenInfo.getWorkflowOrgId() ), ttl, se, be ) ); |
| } |
| |
| m.execute(); |
| } |
| |
| |
| /** Load all the token uuids for a principal info */ |
| private List<UUID> getTokenUUIDS( AuthPrincipalInfo principal ) throws Exception { |
| |
| ByteBuffer rowKey = principalKey( principal ); |
| |
| List<HColumn<ByteBuffer, ByteBuffer>> cols = cassandra |
| .getColumns( cassandra.getUsergridApplicationKeyspace(), PRINCIPAL_TOKEN_CF, rowKey, null, null, Integer.MAX_VALUE, |
| false ); |
| |
| List<UUID> results = new ArrayList<UUID>( cols.size() ); |
| |
| for ( HColumn<ByteBuffer, ByteBuffer> col : cols ) { |
| results.add( uuid( col.getName() ) ); |
| } |
| |
| return results; |
| } |
| |
| |
| private ByteBuffer principalKey( AuthPrincipalInfo principalInfo ) { |
| // 66 bytes, 2 UUIDS + 2 chars for prefix |
| ByteBuffer buff = ByteBuffer.allocate( 32 * 2 + 2 ); |
| buff.put( bytes( principalInfo.getApplicationId() ) ); |
| buff.put( bytes( principalInfo.getUuid() ) ); |
| buff.put( bytes( principalInfo.getType().getPrefix() ) ); |
| buff.rewind(); |
| |
| return buff; |
| } |
| |
| |
| private UUID getUUIDForToken(String token ) throws ExpiredTokenException, BadTokenException { |
| TokenCategory tokenCategory = TokenCategory.getFromBase64String( token ); |
| byte[] bytes = decodeBase64( token.substring( TokenCategory.BASE64_PREFIX_LENGTH ) ); |
| UUID uuid = uuid( bytes ); |
| int i = 16; |
| long expires = Long.MAX_VALUE; |
| if ( tokenCategory.getExpires() ) { |
| expires = ByteBuffer.wrap( bytes, i, 8 ).getLong(); |
| i = 24; |
| } |
| ByteBuffer expected = ByteBuffer.allocate( 20 ); |
| expected.put( sha( tokenCategory.getPrefix() + uuid + tokenSecretSalt + expires ) ); |
| expected.rewind(); |
| ByteBuffer signature = ByteBuffer.wrap( bytes, i, 20 ); |
| |
| |
| if ( !signature.equals( expected ) ) { |
| throw new BadTokenException( "Invalid token signature" ); |
| } |
| |
| |
| long expirationDelta = System.currentTimeMillis() - expires; |
| |
| if ( expires != Long.MAX_VALUE && expirationDelta > 0 ) { |
| throw new ExpiredTokenException( String.format( "Token expired %d milliseconds ago.", expirationDelta ) ); |
| } |
| return uuid; |
| } |
| |
| |
| @Override |
| public long getMaxTokenAge( String token ) { |
| TokenCategory tokenCategory = TokenCategory.getFromBase64String( token ); |
| byte[] bytes = decodeBase64( token.substring( TokenCategory.BASE64_PREFIX_LENGTH ) ); |
| UUID uuid = uuid( bytes ); |
| long timestamp = getTimestampInMillis( uuid ); |
| int i = 16; |
| if ( tokenCategory.getExpires() ) { |
| long expires = ByteBuffer.wrap( bytes, i, 8 ).getLong(); |
| return expires - timestamp; |
| } |
| return Long.MAX_VALUE; |
| } |
| |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see |
| * org.apache.usergrid.security.tokens.TokenService#getMaxTokenAgeInSeconds(java. |
| * lang.String) |
| */ |
| @Override |
| public long getMaxTokenAgeInSeconds( String token ) { |
| return getMaxTokenAge( token ) / 1000; |
| } |
| |
| |
| /** |
| * The maximum age a token can be saved for |
| * |
| * @return the maxPersistenceTokenAge |
| */ |
| public long getMaxPersistenceTokenAge() { |
| return maxPersistenceTokenAge; |
| } |
| |
| |
| @Autowired |
| @Qualifier("cassandraService") |
| public void setCassandraService( CassandraService cassandra ) { |
| this.cassandra = cassandra; |
| } |
| |
| |
| @Autowired |
| public void setEntityManagerFactory( EntityManagerFactory emf ) { |
| this.emf = emf; |
| final Injector injector = ((CpEntityManagerFactory)emf).getApplicationContext().getBean( Injector.class ); |
| metricsFactory = injector.getInstance(MetricsFactory.class); |
| } |
| |
| |
| private String getTokenForUUID( TokenInfo tokenInfo, TokenCategory tokenCategory, UUID uuid ) { |
| int l = 36; |
| if ( tokenCategory.getExpires() ) { |
| l += 8; |
| } |
| ByteBuffer bytes = ByteBuffer.allocate( l ); |
| bytes.put( bytes( uuid ) ); |
| long expires = Long.MAX_VALUE; |
| if ( tokenCategory.getExpires() ) { |
| expires = ( tokenInfo.getDuration() > 0 ) ? |
| UUIDUtils.getTimestampInMillis( uuid ) + ( tokenInfo.getDuration() ) : |
| UUIDUtils.getTimestampInMillis( uuid ) + getExpirationForTokenType( tokenCategory ); |
| bytes.putLong( expires ); |
| } |
| bytes.put( sha( tokenCategory.getPrefix() + uuid + tokenSecretSalt + expires ) ); |
| return tokenCategory.getBase64Prefix() + encodeBase64URLSafeString( bytes.array() ); |
| } |
| |
| |
| /** Calculate the column lifetime and account for long truncation to seconds */ |
| private int calcTokenTime( long time ) { |
| |
| long secondsDuration = time / 1000; |
| |
| int ttl = ( int ) secondsDuration; |
| |
| // we've had a ttl that's longer than Integer.MAX value |
| if ( ttl != secondsDuration ) { |
| // Something is up with cassandra... Setting ttl to integer.max |
| // makes the cols disappear..... |
| |
| // this should be the line below once this issue is fixed. |
| // https://issues.apache.org/jira/browse/CASSANDRA-4771 |
| // ttl = Integer.MAX_VALUE |
| |
| // take the max value of an int, and substract the system time off |
| // (in seconds) ,then arbitrarily remove another 120 seconds for good |
| // measure. |
| // Cass calcs the expiration time as |
| // "(System.currentTimeMillis() / 1000) + timeToLive);", so we need |
| // to play nice otherwise it blows up on persist |
| ttl = Integer.MAX_VALUE - ( int ) ( System.currentTimeMillis() / 1000 ) - 120; |
| } |
| // hard cap at the max in o.a.c.db.IColumn |
| if ( ttl > MAX_TTL ) { |
| ttl = MAX_TTL; |
| } |
| |
| return ttl; |
| } |
| |
| |
| private static final int MAX_TTL = 20 * 365 * 24 * 60 * 60; |
| |
| |
| //------------------------------------------------------------------------------------------------------- |
| // |
| // Central SSO implementation |
| |
| 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"; |
| |
| |
| // names for metrics to be collected |
| private static final String SSO_TOKENS_REJECTED = "sso.tokens_rejected"; |
| private static final String SSO_TOKENS_VALIDATED = "sso.tokens_validated"; |
| private static final String SSO_CREATED_LOCAL_ADMINS = "sso.created_local_admins"; |
| private static final String SSO_PROCESSING_TIME = "sso.processing_time"; |
| |
| //SSO2 implementation |
| public static final String USERGRID_EXTERNAL_SSO_ENABLED = "usergrid.external.sso.enabled"; |
| public static final String USERGRID_EXTERNAL_SSO_PROVIDER = "usergrid.external.sso.provider"; |
| public static final String USERGRID_EXTERNAL_SSO_PROVIDER_URL = "usergrid.external.sso.url"; |
| public static final String USERGRID_EXTERNAL_SSO_PROVIDER_USER_PROVISION_URL |
| = "usergrid.external.sso.userprovision.url"; |
| |
| |
| private static Client jerseyClient = null; |
| |
| @Autowired |
| private ApplicationCreator applicationCreator; |
| |
| @Autowired |
| protected ManagementService management; |
| |
| @Autowired |
| private SSOProviderFactory ssoProviderFactory; |
| |
| MetricsFactory getMetricsFactory() { |
| return metricsFactory; |
| } |
| |
| |
| public boolean isExternalSSOProviderEnabled() { |
| return Boolean.valueOf(properties.getProperty( USERGRID_EXTERNAL_SSO_ENABLED )); |
| } |
| |
| private String getExternalSSOProvider(){ |
| return properties.getProperty(USERGRID_EXTERNAL_SSO_PROVIDER); |
| } |
| |
| /** |
| * <p> |
| * Validates access token from other or "external" Usergrid system. |
| * Calls other system's /management/me endpoint to get the User |
| * associated with the access token. If user does not exist locally, |
| * then user and organizations will be created. If no user is returned |
| * from the other cluster, then return null. |
| * </p> |
| * <p/> |
| * <p> Part of Usergrid Central SSO feature. |
| * See <a href="https://issues.apache.org/jira/browse/USERGRID-567">USERGRID-567</a> |
| * for details about Usergrid Central SSO. |
| * </p> |
| * |
| * @param extAccessToken Access token from external Usergrid system. |
| * @param ttl Time to live for token. |
| */ |
| public TokenInfo validateExternalToken(String extAccessToken, long ttl, String provider) throws Exception { |
| |
| |
| 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); |
| } |
| |
| } |
| |
| } |