package org.apereo.cas.authentication.support; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apereo.cas.CasViewConstants; import org.apereo.cas.CipherExecutor; import org.apereo.cas.services.RegisteredService; import org.apereo.cas.services.RegisteredServiceCipherExecutor; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.util.EncodingUtils; import org.apereo.cas.util.services.DefaultRegisteredServiceCipherExecutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * The default implementation of the attribute * encoder that will use a per-service key-pair * to encrypt the credential password and PGT * when available. All other attributes remain in * place. * * @author Misagh Moayyed * @since 4.1 */ public class DefaultCasProtocolAttributeEncoder extends AbstractProtocolAttributeEncoder { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultCasProtocolAttributeEncoder.class); private final CipherExecutor<String, String> cacheCredentialCipherExecutor; /** * Instantiates a new Default cas attribute encoder. * * @param servicesManager the services manager * @param cacheCredentialCipherExecutor the cache credential cipher executor */ public DefaultCasProtocolAttributeEncoder(final ServicesManager servicesManager, final CipherExecutor cacheCredentialCipherExecutor) { this(servicesManager, new DefaultRegisteredServiceCipherExecutor(), cacheCredentialCipherExecutor); } /** * Instantiates a new Default cas attribute encoder. * * @param servicesManager the services manager * @param cipherExecutor the cipher executor * @param cacheCredentialCipherExecutor the cache credential cipher executor */ public DefaultCasProtocolAttributeEncoder(final ServicesManager servicesManager, final RegisteredServiceCipherExecutor cipherExecutor, final CipherExecutor cacheCredentialCipherExecutor) { super(servicesManager, cipherExecutor); this.cacheCredentialCipherExecutor = cacheCredentialCipherExecutor; } /** * Encode and encrypt credential password using the public key * supplied by the service. The result is base64 encoded * and put into the attributes collection again, overwriting * the previous value. * * @param attributes the attributes * @param cachedAttributesToEncode the cached attributes to encode * @param cipher the cipher * @param registeredService the registered service */ protected void encodeAndEncryptCredentialPassword(final Map<String, Object> attributes, final Map<String, String> cachedAttributesToEncode, final RegisteredServiceCipherExecutor cipher, final RegisteredService registeredService) { if (cachedAttributesToEncode.containsKey(CasViewConstants.MODEL_ATTRIBUTE_NAME_PRINCIPAL_CREDENTIAL)) { final String value = cachedAttributesToEncode.get(CasViewConstants.MODEL_ATTRIBUTE_NAME_PRINCIPAL_CREDENTIAL); final String decodedValue = this.cacheCredentialCipherExecutor.decode(value); cachedAttributesToEncode.remove(CasViewConstants.MODEL_ATTRIBUTE_NAME_PRINCIPAL_CREDENTIAL); if (StringUtils.isNotBlank(decodedValue)) { cachedAttributesToEncode.put(CasViewConstants.MODEL_ATTRIBUTE_NAME_PRINCIPAL_CREDENTIAL, decodedValue); } } encryptAndEncodeAndPutIntoAttributesMap(attributes, cachedAttributesToEncode, CasViewConstants.MODEL_ATTRIBUTE_NAME_PRINCIPAL_CREDENTIAL, cipher, registeredService); } /** * Encode and encrypt pgt. * * @param attributes the attributes * @param cachedAttributesToEncode the cached attributes to encode * @param cipher the cipher * @param registeredService the registered service */ protected void encodeAndEncryptProxyGrantingTicket(final Map<String, Object> attributes, final Map<String, String> cachedAttributesToEncode, final RegisteredServiceCipherExecutor cipher, final RegisteredService registeredService) { encryptAndEncodeAndPutIntoAttributesMap(attributes, cachedAttributesToEncode, CasViewConstants.MODEL_ATTRIBUTE_NAME_PROXY_GRANTING_TICKET, cipher, registeredService); } /** * Encrypt, encode and put the attribute into attributes map. * * @param attributes the attributes * @param cachedAttributesToEncode the cached attributes to encode * @param cachedAttributeName the cached attribute name * @param cipher the cipher * @param registeredService the registered service */ protected void encryptAndEncodeAndPutIntoAttributesMap(final Map<String, Object> attributes, final Map<String, String> cachedAttributesToEncode, final String cachedAttributeName, final RegisteredServiceCipherExecutor cipher, final RegisteredService registeredService) { final String cachedAttribute = cachedAttributesToEncode.remove(cachedAttributeName); if (StringUtils.isNotBlank(cachedAttribute)) { LOGGER.debug("Retrieved [{}] as a cached model attribute...", cachedAttributeName); final String encodedValue = cipher.encode(cachedAttribute, registeredService); if (StringUtils.isNotBlank(encodedValue)) { attributes.put(cachedAttributeName, encodedValue); LOGGER.debug("Encrypted and encoded [{}] as an attribute to [{}].", cachedAttributeName, encodedValue); } else { LOGGER.warn("Attribute [{}] cannot be encoded and is removed from the collection of attributes", cachedAttributeName); } } else { LOGGER.debug("[{}] is not available as a cached model attribute to encrypt...", cachedAttributeName); } } @Override protected void encodeAttributesInternal(final Map<String, Object> attributes, final Map<String, String> cachedAttributesToEncode, final RegisteredServiceCipherExecutor cipher, final RegisteredService registeredService) { encodeAndEncryptCredentialPassword(attributes, cachedAttributesToEncode, cipher, registeredService); encodeAndEncryptProxyGrantingTicket(attributes, cachedAttributesToEncode, cipher, registeredService); sanitizeAndTransformAttributeNames(attributes, registeredService); } private static void sanitizeAndTransformAttributeNames(final Map<String, Object> attributes, final RegisteredService registeredService) { LOGGER.debug("Sanitizing attribute names in preparation of the final validation response"); final Set<Pair<String, Object>> attrs = attributes.keySet().stream() .filter(s -> s.contains(":")) .map(s -> Pair.of(EncodingUtils.hexEncode(s.getBytes(StandardCharsets.UTF_8)), attributes.get(s))) .collect(Collectors.toSet()); if (!attrs.isEmpty()) { LOGGER.warn("Found [{}] attribute(s) that need to be sanitized/encoded.", attrs); attributes.entrySet().removeIf(s -> s.getKey().contains(":")); attrs.forEach(p -> { LOGGER.debug("Sanitized attribute name to be [{}]", p.getKey()); attributes.put(p.getKey(), p.getValue()); }); } } }