| /* |
| * 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.guacamole.auth.cas.ticket; |
| |
| import com.google.common.io.BaseEncoding; |
| import com.google.inject.Inject; |
| import java.net.URI; |
| import java.security.InvalidKeyException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PrivateKey; |
| import javax.crypto.BadPaddingException; |
| import javax.crypto.Cipher; |
| import javax.crypto.IllegalBlockSizeException; |
| import javax.crypto.NoSuchPaddingException; |
| import java.nio.charset.Charset; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import org.apache.guacamole.GuacamoleException; |
| import org.apache.guacamole.GuacamoleSecurityException; |
| import org.apache.guacamole.GuacamoleServerException; |
| import org.apache.guacamole.auth.cas.conf.ConfigurationService; |
| import org.apache.guacamole.net.auth.Credentials; |
| import org.apache.guacamole.token.TokenName; |
| import org.jasig.cas.client.authentication.AttributePrincipal; |
| import org.jasig.cas.client.validation.Assertion; |
| import org.jasig.cas.client.validation.Cas20ProxyTicketValidator; |
| import org.jasig.cas.client.validation.TicketValidationException; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Service for validating ID tickets forwarded to us by the client, verifying |
| * that they did indeed come from the CAS service. |
| */ |
| public class TicketValidationService { |
| |
| /** |
| * Logger for this class. |
| */ |
| private static final Logger logger = LoggerFactory.getLogger(TicketValidationService.class); |
| |
| /** |
| * The prefix to use when generating token names. |
| */ |
| public static final String CAS_ATTRIBUTE_TOKEN_PREFIX = "CAS_"; |
| |
| /** |
| * Service for retrieving CAS configuration information. |
| */ |
| @Inject |
| private ConfigurationService confService; |
| |
| /** |
| * Validates and parses the given ID ticket, returning a map of all |
| * available tokens for the given user based on attributes provided by the |
| * CAS server. If the ticket is invalid an exception is thrown. |
| * |
| * @param ticket |
| * The ID ticket to validate and parse. |
| * |
| * @param credentials |
| * The Credentials object to store retrieved username and |
| * password values in. |
| * |
| * @return |
| * A Map all of tokens for the user parsed from attributes returned |
| * by the CAS server. |
| * |
| * @throws GuacamoleException |
| * If the ID ticket is not valid or guacamole.properties could |
| * not be parsed. |
| */ |
| public Map<String, String> validateTicket(String ticket, |
| Credentials credentials) throws GuacamoleException { |
| |
| // Retrieve the configured CAS URL, establish a ticket validator, |
| // and then attempt to validate the supplied ticket. If that succeeds, |
| // grab the principal returned by the validator. |
| URI casServerUrl = confService.getAuthorizationEndpoint(); |
| Cas20ProxyTicketValidator validator = new Cas20ProxyTicketValidator(casServerUrl.toString()); |
| validator.setAcceptAnyProxy(true); |
| validator.setEncoding("UTF-8"); |
| try { |
| Map<String, String> tokens = new HashMap<>(); |
| URI confRedirectURI = confService.getRedirectURI(); |
| Assertion a = validator.validate(ticket, confRedirectURI.toString()); |
| AttributePrincipal principal = a.getPrincipal(); |
| Map<String, Object> ticketAttrs = |
| new HashMap<>(principal.getAttributes()); |
| |
| // Retrieve username and set the credentials. |
| String username = principal.getName(); |
| if (username == null) |
| throw new GuacamoleSecurityException("No username provided by CAS."); |
| |
| credentials.setUsername(username); |
| |
| // Retrieve password, attempt decryption, and set credentials. |
| Object credObj = ticketAttrs.remove("credential"); |
| if (credObj != null) { |
| String clearPass = decryptPassword(credObj.toString()); |
| if (clearPass != null && !clearPass.isEmpty()) |
| credentials.setPassword(clearPass); |
| } |
| |
| // Convert remaining attributes that have values to Strings |
| for (Entry <String, Object> attr : ticketAttrs.entrySet()) { |
| String tokenName = TokenName.canonicalize(attr.getKey(), |
| CAS_ATTRIBUTE_TOKEN_PREFIX); |
| Object value = attr.getValue(); |
| if (value != null) |
| tokens.put(tokenName, value.toString()); |
| } |
| |
| return tokens; |
| |
| } |
| catch (TicketValidationException e) { |
| throw new GuacamoleException("Ticket validation failed.", e); |
| } |
| |
| } |
| |
| /** |
| * Takes an encrypted string representing a password provided by |
| * the CAS ClearPass service and decrypts it using the private |
| * key configured for this extension. Returns null if it is |
| * unable to decrypt the password. |
| * |
| * @param encryptedPassword |
| * A string with the encrypted password provided by the |
| * CAS service. |
| * |
| * @return |
| * The decrypted password, or null if it is unable to |
| * decrypt the password. |
| * |
| * @throws GuacamoleException |
| * If unable to get Guacamole configuration data |
| */ |
| private final String decryptPassword(String encryptedPassword) |
| throws GuacamoleException { |
| |
| // If we get nothing, we return nothing. |
| if (encryptedPassword == null || encryptedPassword.isEmpty()) { |
| logger.warn("No or empty encrypted password, no password will be available."); |
| return null; |
| } |
| |
| final PrivateKey clearpassKey = confService.getClearpassKey(); |
| if (clearpassKey == null) { |
| logger.debug("No private key available to decrypt password."); |
| return null; |
| } |
| |
| try { |
| |
| final Cipher cipher = Cipher.getInstance(clearpassKey.getAlgorithm()); |
| |
| if (cipher == null) |
| throw new GuacamoleServerException("Failed to initialize cipher object with private key."); |
| |
| // Initialize the Cipher in decrypt mode. |
| cipher.init(Cipher.DECRYPT_MODE, clearpassKey); |
| |
| // Decode and decrypt, and return a new string. |
| final byte[] pass64 = BaseEncoding.base64().decode(encryptedPassword); |
| final byte[] cipherData = cipher.doFinal(pass64); |
| return new String(cipherData, Charset.forName("UTF-8")); |
| |
| } |
| catch (BadPaddingException e) { |
| throw new GuacamoleServerException("Bad padding when decrypting cipher data.", e); |
| } |
| catch (IllegalBlockSizeException e) { |
| throw new GuacamoleServerException("Illegal block size while opening private key.", e); |
| } |
| catch (InvalidKeyException e) { |
| throw new GuacamoleServerException("Specified private key for ClearPass decryption is invalid.", e); |
| } |
| catch (NoSuchAlgorithmException e) { |
| throw new GuacamoleServerException("Unexpected algorithm for the private key.", e); |
| } |
| catch (NoSuchPaddingException e) { |
| throw new GuacamoleServerException("No such padding trying to initialize cipher with private key.", e); |
| } |
| |
| } |
| |
| } |