Java tutorial
/* *Copyright (c) 2005-2010, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * *WSO2 Inc. 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.wso2.carbon.appmgt.impl.token; import org.apache.axiom.util.base64.Base64Utils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.appmgt.api.AppManagementException; import org.wso2.carbon.appmgt.impl.AppMConstants; import org.wso2.carbon.appmgt.impl.AppManagerConfiguration; import org.wso2.carbon.appmgt.impl.dto.APIKeyValidationInfoDTO; import org.wso2.carbon.appmgt.impl.internal.ServiceReferenceHolder; import org.wso2.carbon.appmgt.impl.utils.AppManagerUtil; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.CarbonContext; import org.wso2.carbon.core.util.KeyStoreManager; import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.user.core.service.RealmService; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; import java.security.*; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * This class represents the JSON Web Token generator. * By default the following properties are encoded to each authenticated WebApp request: * subscriber, applicationName, apiContext, version, tier, and endUserName * Additional properties can be encoded by engaging the ClaimsRetrieverImplClass callback-handler. * The JWT header and body are base64 encoded separately and concatenated with a dot. * Finally the token is signed using SHA256 with RSA algorithm. */ public class JWTGenerator { private static final Log log = LogFactory.getLog(JWTGenerator.class); private static final String API_GATEWAY_ID = "wso2.org/products/am"; private static final String SHA256_WITH_RSA = "SHA256withRSA"; private static final String NONE = "NONE"; private static volatile long ttl = -1L; private ClaimsRetriever claimsRetriever; private String dialectURI = ClaimsRetriever.DEFAULT_DIALECT_URI; private String signatureAlgorithm = SHA256_WITH_RSA; private static final String SIGNATURE_ALGORITHM = "APIConsumerAuthentication.SignatureAlgorithm"; private boolean includeClaims = true; private boolean enableSigning = true; private boolean saml2Enabled = true; private boolean addClaimsSelectively = false; private static ConcurrentHashMap<Integer, Key> privateKeys = new ConcurrentHashMap<Integer, Key>(); private static ConcurrentHashMap<Integer, Certificate> publicCerts = new ConcurrentHashMap<Integer, Certificate>(); //constructor for testing purposes public JWTGenerator(boolean includeClaims, boolean enableSigning) { this.includeClaims = includeClaims; this.enableSigning = enableSigning; signatureAlgorithm = NONE; } /** * Reads the ClaimsRetrieverImplClass from app-manager.xml -> * APIConsumerAuthentication -> ClaimsRetrieverImplClass. * * @throws org.wso2.carbon.appmgt.api.AppManagementException */ public JWTGenerator() { String claimsRetrieverImplClass = ServiceReferenceHolder.getInstance().getAPIManagerConfigurationService() .getAPIManagerConfiguration().getFirstProperty(ClaimsRetriever.CLAIMS_RETRIEVER_IMPL_CLASS); dialectURI = ServiceReferenceHolder.getInstance().getAPIManagerConfigurationService() .getAPIManagerConfiguration().getFirstProperty(ClaimsRetriever.CONSUMER_DIALECT_URI); if (dialectURI == null) { dialectURI = ClaimsRetriever.DEFAULT_DIALECT_URI; } if (claimsRetrieverImplClass != null) { try { claimsRetriever = (ClaimsRetriever) Class.forName(claimsRetrieverImplClass).newInstance(); claimsRetriever.init(); } catch (ClassNotFoundException e) { log.error("Cannot find class: " + claimsRetrieverImplClass, e); } catch (InstantiationException e) { log.error("Error instantiating " + claimsRetrieverImplClass); } catch (IllegalAccessException e) { log.error("Illegal access to " + claimsRetrieverImplClass); } catch (AppManagementException e) { log.error("Error while initializing " + claimsRetrieverImplClass); } } signatureAlgorithm = ServiceReferenceHolder.getInstance().getAPIManagerConfigurationService() .getAPIManagerConfiguration().getFirstProperty(SIGNATURE_ALGORITHM); if (signatureAlgorithm == null || !(signatureAlgorithm.equals(NONE) || signatureAlgorithm.equals(SHA256_WITH_RSA))) { signatureAlgorithm = SHA256_WITH_RSA; } addClaimsSelectively = Boolean.parseBoolean(ServiceReferenceHolder.getInstance() .getAPIManagerConfigurationService().getAPIManagerConfiguration() .getFirstProperty(AppMConstants.API_CONSUMER_AUTHENTICATION_ADD_CLAIMS_SELECTIVELY)); } /** * Method that generates the JWT. * * @param keyValidationInfoDTO * @param apiContext * @param version * @param includeEndUserName * @return signed JWT token * @throws org.wso2.carbon.appmgt.api.AppManagementException */ public String generateToken(APIKeyValidationInfoDTO keyValidationInfoDTO, String apiContext, String version, boolean includeEndUserName) throws AppManagementException { //generating expiring timestamp long currentTime = Calendar.getInstance().getTimeInMillis(); long expireIn = currentTime + 1000 * 60 * getTTL(); String jwtBody; String dialect; if (claimsRetriever != null) { //jwtBody = JWT_INITIAL_BODY.replaceAll("\\[0\\]", claimsRetriever.getDialectURI(endUserName)); dialect = claimsRetriever.getDialectURI(keyValidationInfoDTO.getEndUserName()); } else { //jwtBody = JWT_INITIAL_BODY.replaceAll("\\[0\\]", dialectURI); dialect = dialectURI; } String subscriber = keyValidationInfoDTO.getSubscriber(); String applicationName = keyValidationInfoDTO.getApplicationName(); String applicationId = keyValidationInfoDTO.getApplicationId(); String tier = keyValidationInfoDTO.getTier(); String endUserName = includeEndUserName ? keyValidationInfoDTO.getEndUserName() : null; String keyType = keyValidationInfoDTO.getType(); String userType = keyValidationInfoDTO.getUserType(); String applicationTier = keyValidationInfoDTO.getApplicationTier(); String enduserTenantId = includeEndUserName ? String.valueOf(getTenantId(endUserName)) : null; //Sample JWT body //{"iss":"wso2.org/products/am","exp":1349267862304,"http://wso2.org/claims/subscriber":"nirodhasub", // "http://wso2.org/claims/applicationname":"App1","http://wso2.org/claims/apicontext":"/echo", // "http://wso2.org/claims/version":"1.2.0","http://wso2.org/claims/tier":"Gold", // "http://wso2.org/claims/enduser":"null"} StringBuilder jwtBuilder = new StringBuilder(); jwtBuilder.append("{"); jwtBuilder.append("\"iss\":\""); jwtBuilder.append(API_GATEWAY_ID); jwtBuilder.append("\","); jwtBuilder.append("\"exp\":"); jwtBuilder.append(String.valueOf(expireIn)); jwtBuilder.append(","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/subscriber\":\""); jwtBuilder.append(subscriber); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/applicationid\":\""); jwtBuilder.append(applicationId); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/applicationname\":\""); jwtBuilder.append(applicationName); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/applicationtier\":\""); jwtBuilder.append(applicationTier); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/apicontext\":\""); jwtBuilder.append(apiContext); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/version\":\""); jwtBuilder.append(version); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/tier\":\""); jwtBuilder.append(tier); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/keytype\":\""); jwtBuilder.append(keyType); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/usertype\":\""); jwtBuilder.append(userType); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/enduser\":\""); jwtBuilder.append(endUserName); jwtBuilder.append("\","); jwtBuilder.append("\""); jwtBuilder.append(dialect); jwtBuilder.append("/enduserTenantId\":\""); jwtBuilder.append(enduserTenantId); jwtBuilder.append("\""); if (claimsRetriever != null) { SortedMap<String, String> claimValues = claimsRetriever.getClaims(endUserName); Iterator<String> it = new TreeSet(claimValues.keySet()).iterator(); while (it.hasNext()) { String claimURI = it.next(); jwtBuilder.append(", \""); jwtBuilder.append(claimURI); jwtBuilder.append("\":\""); jwtBuilder.append(claimValues.get(claimURI)); jwtBuilder.append("\""); } } jwtBuilder.append("}"); jwtBody = jwtBuilder.toString(); String jwtHeader = null; //if signature algo==NONE, header without cert if (signatureAlgorithm.equals(NONE)) { jwtHeader = "{\"typ\":\"JWT\"}"; } else if (signatureAlgorithm.equals(SHA256_WITH_RSA)) { jwtHeader = addCertToHeader(endUserName); } /*//add cert thumbprint to header String headerWithCertThumb = addCertToHeader(endUserName);*/ String base64EncodedHeader = Base64Utils.encode(jwtHeader.getBytes()); String base64EncodedBody = Base64Utils.encode(jwtBody.getBytes()); if (signatureAlgorithm.equals(SHA256_WITH_RSA)) { String assertion = base64EncodedHeader + "." + base64EncodedBody; //get the assertion signed byte[] signedAssertion = signJWT(assertion, endUserName); if (log.isDebugEnabled()) { log.debug("signed assertion value : " + new String(signedAssertion)); } String base64EncodedAssertion = Base64Utils.encode(signedAssertion); return base64EncodedHeader + "." + base64EncodedBody + "." + base64EncodedAssertion; } else { return base64EncodedHeader + "." + base64EncodedBody + "."; } } /** * Method that generates the JWT token from SAML2 response * @param saml2Assertions * @param apiContext * @param version * @return * @throws org.wso2.carbon.appmgt.api.AppManagementException */ public String generateToken(Map<String, Object> saml2Assertions, String apiContext, String version) throws AppManagementException { //generating expiring timestamp long currentTime = Calendar.getInstance().getTimeInMillis(); long expireIn = currentTime + 1000 * 60 * getTTL(); String jwtBody; StringBuilder jwtBuilder = new StringBuilder(); jwtBuilder.append("{"); jwtBuilder.append("\"iss\":\""); jwtBuilder.append(API_GATEWAY_ID); jwtBuilder.append("\","); jwtBuilder.append("\"exp\":"); jwtBuilder.append(String.valueOf(expireIn)); /* Populate claims from SAML Assertion if "AddClaimsSelectively" property is set to true, else add all claims values available in user profile */ if (addClaimsSelectively) { if (saml2Assertions != null) { Iterator<String> it = new TreeSet(saml2Assertions.keySet()).iterator(); while (it.hasNext()) { String assertionAttribute = it.next(); jwtBuilder.append(",\""); jwtBuilder.append(assertionAttribute); jwtBuilder.append("\":\""); jwtBuilder.append(saml2Assertions.get(assertionAttribute)); jwtBuilder.append("\""); } } } else { Map<String, String> customClaims = populateCustomClaims(saml2Assertions); if (customClaims != null) { Iterator<String> it = new TreeSet(customClaims.keySet()).iterator(); while (it.hasNext()) { String claimAttribute = it.next(); jwtBuilder.append(",\""); jwtBuilder.append(claimAttribute); jwtBuilder.append("\":\""); jwtBuilder.append(customClaims.get(claimAttribute)); jwtBuilder.append("\""); } } } jwtBuilder.append("}"); jwtBody = jwtBuilder.toString(); String jwtHeader = null; String endUserName = (String) saml2Assertions.get("Subject"); //if signature algo==NONE, header without cert if (signatureAlgorithm.equals(NONE)) { jwtHeader = "{\"typ\":\"JWT\"}"; } else if (signatureAlgorithm.equals(SHA256_WITH_RSA)) { jwtHeader = addCertToHeader(endUserName); } /*//add cert thumbprint to header String headerWithCertThumb = addCertToHeader(endUserName);*/ String base64EncodedHeader = Base64Utils.encode(jwtHeader.getBytes()); String base64EncodedBody = Base64Utils.encode(jwtBody.getBytes()); if (signatureAlgorithm.equals(SHA256_WITH_RSA)) { String assertion = base64EncodedHeader + "." + base64EncodedBody; //get the assertion signed byte[] signedAssertion = signJWT(assertion, endUserName); if (log.isDebugEnabled()) { log.debug("signed assertion value : " + new String(signedAssertion)); } String base64EncodedAssertion = Base64Utils.encode(signedAssertion); return base64EncodedHeader + "." + base64EncodedBody + "." + base64EncodedAssertion; } else { return base64EncodedHeader + "." + base64EncodedBody + "."; } } public Map<String, String> populateCustomClaims(Map<String, Object> saml2Assertions) throws AppManagementException { Map<String, String> claims = new HashMap<String, String>(); ClaimsRetriever claimsRetriever = getClaimsRetriever(); if (claimsRetriever != null) { String userName = (String) saml2Assertions.get("Subject"); String tenantDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain(); String tenantAwareUserName = userName + "@" + tenantDomain; try { int tenantId = ServiceReferenceHolder.getInstance().getRealmService().getTenantManager() .getTenantId(tenantDomain); if (MultitenantConstants.SUPER_TENANT_ID == tenantId) { tenantAwareUserName = MultitenantUtils.getTenantAwareUsername(tenantAwareUserName); } claims.put("Subject", userName); claims.putAll(claimsRetriever.getClaims(tenantAwareUserName)); return claims; } catch (UserStoreException e) { log.error("Error while getting tenant id to populate claims ", e); throw new AppManagementException("Error while getting tenant id to populate claims ", e); } } return null; } public ClaimsRetriever getClaimsRetriever() { return claimsRetriever; } /** * Helper method to sign the JWT * * @param assertion * @param endUserName * @return signed assertion * @throws org.wso2.carbon.appmgt.api.AppManagementException */ private byte[] signJWT(String assertion, String endUserName) throws AppManagementException { try { //get tenant domain String tenantDomain = MultitenantUtils.getTenantDomain(endUserName); //get tenantId int tenantId = getTenantId(endUserName); Key privateKey = null; if (!(privateKeys.containsKey(tenantId))) { AppManagerUtil.loadTenantRegistry(tenantId); //get tenant's key store manager KeyStoreManager tenantKSM = KeyStoreManager.getInstance(tenantId); if (!tenantDomain.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) { //derive key store name String ksName = tenantDomain.trim().replace(".", "-"); String jksName = ksName + ".jks"; //obtain private key //TODO: maintain a hash map with tenants' private keys after first initialization privateKey = tenantKSM.getPrivateKey(jksName, tenantDomain); } else { try { privateKey = tenantKSM.getDefaultPrivateKey(); } catch (Exception e) { log.error("Error while obtaining private key for super tenant", e); } } if (privateKey != null) { privateKeys.put(tenantId, privateKey); } } else { privateKey = privateKeys.get(tenantId); } //initialize signature with private key and algorithm Signature signature = Signature.getInstance(signatureAlgorithm); signature.initSign((PrivateKey) privateKey); //update signature with data to be signed byte[] dataInBytes = assertion.getBytes(); signature.update(dataInBytes); //sign the assertion and return the signature byte[] signedInfo = signature.sign(); return signedInfo; } catch (NoSuchAlgorithmException e) { String error = "Signature algorithm not found."; //do not log throw new AppManagementException(error); } catch (InvalidKeyException e) { String error = "Invalid private key provided for the signature"; //do not log throw new AppManagementException(error); } catch (SignatureException e) { String error = "Error in signature"; //do not log throw new AppManagementException(error); } catch (AppManagementException e) { //do not log throw new AppManagementException(e.getMessage()); } } /** * Helper method to add public certificate to JWT_HEADER to signature verification. * * @param endUserName * @throws org.wso2.carbon.appmgt.api.AppManagementException */ private String addCertToHeader(String endUserName) throws AppManagementException { try { //get tenant domain String tenantDomain = MultitenantUtils.getTenantDomain(endUserName); //get tenantId int tenantId = getTenantId(endUserName); Certificate publicCert = null; if (!(publicCerts.containsKey(tenantId))) { //get tenant's key store manager AppManagerUtil.loadTenantRegistry(tenantId); KeyStoreManager tenantKSM = KeyStoreManager.getInstance(tenantId); KeyStore keyStore = null; if (!tenantDomain.equals(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME)) { //derive key store name String ksName = tenantDomain.trim().replace(".", "-"); String jksName = ksName + ".jks"; keyStore = tenantKSM.getKeyStore(jksName); publicCert = keyStore.getCertificate(tenantDomain); } else { keyStore = tenantKSM.getPrimaryKeyStore(); publicCert = tenantKSM.getDefaultPrimaryCertificate(); } if (publicCert != null) { publicCerts.put(tenantId, publicCert); } } else { publicCert = publicCerts.get(tenantId); } //generate the SHA-1 thumbprint of the certificate //TODO: maintain a hashmap with tenants' pubkey thumbprints after first initialization MessageDigest digestValue = MessageDigest.getInstance("SHA-1"); byte[] der = publicCert.getEncoded(); digestValue.update(der); byte[] digestInBytes = digestValue.digest(); String publicCertThumbprint = hexify(digestInBytes); String base64EncodedThumbPrint = Base64Utils.encode(publicCertThumbprint.getBytes()); //String headerWithCertThumb = JWT_HEADER.replaceAll("\\[1\\]", base64EncodedThumbPrint); //headerWithCertThumb = headerWithCertThumb.replaceAll("\\[2\\]", signatureAlgorithm); //return headerWithCertThumb; StringBuilder jwtHeader = new StringBuilder(); //Sample header //{"typ":"JWT", "alg":"SHA256withRSA", "x5t":"NmJmOGUxMzZlYjM2ZDRhNTZlYTA1YzdhZTRiOWE0NWI2M2JmOTc1ZA=="} //{"typ":"JWT", "alg":"[2]", "x5t":"[1]"} jwtHeader.append("{\"typ\":\"JWT\","); jwtHeader.append("\"alg\":\""); jwtHeader.append(signatureAlgorithm); jwtHeader.append("\","); jwtHeader.append("\"x5t\":\""); jwtHeader.append(base64EncodedThumbPrint); jwtHeader.append("\""); jwtHeader.append("}"); return jwtHeader.toString(); } catch (KeyStoreException e) { String error = "Error in obtaining tenant's keystore"; throw new AppManagementException(error); } catch (CertificateEncodingException e) { String error = "Error in generating public cert thumbprint"; throw new AppManagementException(error); } catch (NoSuchAlgorithmException e) { String error = "Error in generating public cert thumbprint"; throw new AppManagementException(error); } catch (Exception e) { String error = "Error in obtaining tenant's keystore"; throw new AppManagementException(error); } } private long getTTL() { if (ttl != -1) { return ttl; } synchronized (JWTGenerator.class) { if (ttl != -1) { return ttl; } AppManagerConfiguration config = ServiceReferenceHolder.getInstance() .getAPIManagerConfigurationService().getAPIManagerConfiguration(); String ttlValue = config.getFirstProperty(AppMConstants.API_KEY_SECURITY_CONTEXT_TTL); if (ttlValue != null) { ttl = Long.parseLong(ttlValue); } else { ttl = 15L; } return ttl; } } /** * Helper method to get tenantId from userName * * @param userName * @return tenantId * @throws org.wso2.carbon.appmgt.api.AppManagementException */ static int getTenantId(String userName) throws AppManagementException { //get tenant domain from user name String tenantDomain = MultitenantUtils.getTenantDomain(userName); RealmService realmService = ServiceReferenceHolder.getInstance().getRealmService(); if (realmService == null) { return MultitenantConstants.SUPER_TENANT_ID; } try { int tenantId = realmService.getTenantManager().getTenantId(tenantDomain); return tenantId; } catch (UserStoreException e) { String error = "Error in obtaining tenantId from Domain"; //do not log throw new AppManagementException(error); } } /** * Helper method to hexify a byte array. * TODO:need to verify the logic * * @param bytes * @return hexadecimal representation */ private String hexify(byte bytes[]) { char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; StringBuffer buf = new StringBuffer(bytes.length * 2); for (int i = 0; i < bytes.length; ++i) { buf.append(hexDigits[(bytes[i] & 0xf0) >> 4]); buf.append(hexDigits[bytes[i] & 0x0f]); } return buf.toString(); } }