/*
* 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.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.management.exceptions.ExternalSSOProviderAdminUserNotFoundException;
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.JsonUtils;
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;
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 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_EXTERNAL_PUBLICKEY_FRESHNESS = "usergrid.external.sso.public-key-freshness";
public ApigeeSSO2Provider() {
ClientConfig clientConfig = new ClientConfig();
clientConfig.register(new JacksonFeature());
client = ClientBuilder.newClient(clientConfig);
}
public PublicKey getPublicKey(String keyUrl) {
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];
byte[] publicBytes = decodeBase64(ssoPublicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
return pubKey;
}
catch (Exception e) {
throw new IllegalArgumentException("error getting public key");
}
}
return null;
}
@Override
public TokenInfo validateAndReturnTokenInfo(String token, long ttl) throws Exception {
UserInfo userInfo = validateAndReturnUserInfo(token, ttl);
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
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());
tokenDetails.put("user_id", claims.get("user_id").toString());
return tokenDetails;
}
@Override
public Map<String, Object> getAllTokenDetails(String token, String keyUrl) throws Exception {
Jws<Claims> claims = getClaimsForKeyUrl( token );
return JsonUtils.toJsonMap(claims.getBody());
}
@Override
public String getExternalSSOUrl() {
return properties.getProperty(USERGRID_EXTERNAL_PUBLICKEY_URL);
}
public Jws<Claims> getClaimsForKeyUrl( String token ) throws BadTokenException {
Jws<Claims> claims = null;
Exception lastException = null;
int tries = 0;
int maxTries = 2;
while ( claims == null && tries++ < maxTries ) {
try {
claims = Jwts.parser().setSigningKey( publicKey ).parseClaimsJws( token );
} catch (SignatureException se) {
// 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.debug(String.format("Apigee JWT Token expired %d milliseconds ago.", expirationDelta));
// token is expired
throw new BadTokenException( "Expired JWT", e );
} catch (MalformedJwtException me) {
logger.debug( "Malformed JWT", me );
// token is malformed
throw new BadTokenException( "Malformed JWT", me );
}
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);
}
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 milliseconds ago.", expirationDelta ));
}
}
public void setPublicKey( PublicKey publicKeyArg){
this.publicKey = publicKeyArg;
}
@Autowired
public void setManagement(ManagementService management) {
this.management = management;
}
@Autowired
public void setProperties(Properties properties) {
this.properties = properties;
this.publicKey = getPublicKey(getExternalSSOUrl());
lastPublicKeyFetch = System.currentTimeMillis();
String freshnessString = (String)properties.get( USERGRID_EXTERNAL_PUBLICKEY_FRESHNESS );
try {
freshnessTime = Long.parseLong( freshnessString );
} catch ( Exception e ) {
logger.error("Ignoring invalid setting for " + USERGRID_EXTERNAL_PUBLICKEY_FRESHNESS );
}
}
}