/**
* Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright ownership. Apereo
* 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 the
* following location:
*
* <p>http://www.apache.org/licenses/LICENSE-2.0
*
* <p>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.apereo.portal.security.provider.cas;
import java.io.File;
import java.nio.file.Files;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.Cipher;
import javax.xml.bind.DatatypeConverter;
import org.apereo.portal.security.PortalSecurityException;
import org.apereo.portal.security.provider.ChainingSecurityContext;
import org.apereo.portal.spring.locator.ApplicationContextLocator;
import org.jasig.cas.client.util.AssertionHolder;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.services.persondir.support.IAdditionalDescriptors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
/**
* Implementation of the {@link org.apereo.portal.security.provider.cas.ICasSecurityContext} that
* reads the Assertion from the ThreadLocal. The Assertion stored in a ThreadLocal is an artifact of
* the Jasig CAS Client for Java 3.x library.
*
* @since 3.2
*/
public class CasAssertionSecurityContext extends ChainingSecurityContext
implements ICasSecurityContext {
private static final String SESSION_ADDITIONAL_DESCRIPTORS_BEAN =
"sessionScopeAdditionalDescriptors";
private static final String CAS_COPY_ASSERT_ATTR_TO_USER_ATTR_BEAN =
"casCopyAssertionAttributesToUserAttributes";
private static final String DECRYPT_CRED_TO_PWD = "decryptCredentialToPassword";
private static final String DECRYPT_CRED_TO_PWD_KEY = "decryptCredentialToPasswordPrivateKey";
private static final String DECRYPT_CRED_TO_PWD_ALG = "decryptCredentialToPasswordAlgorithm";
private static final String CREDENTIAL_KEY =
"credential"; // encrypted password attribute from CAS
private static final String PASSWORD_KEY = "password"; // user attribute expected by portlets
protected final Logger log = LoggerFactory.getLogger(getClass());
// UP-4212 Transient because security contexts are serialized into HTTP Session (and webflow).
private transient ApplicationContext applicationContext;
private Assertion assertion;
private boolean copyAssertionAttributesToUserAttributes = false;
private boolean decryptCredentialToPassword = false;
private static PrivateKey key = null;
private static Cipher cipher = null;
private static String algorithm;
public CasAssertionSecurityContext() {
applicationContext = ApplicationContextLocator.getApplicationContext();
String propertyVal =
applicationContext.getBean(CAS_COPY_ASSERT_ATTR_TO_USER_ATTR_BEAN, String.class);
copyAssertionAttributesToUserAttributes = Boolean.valueOf(propertyVal);
propertyVal = applicationContext.getBean(DECRYPT_CRED_TO_PWD, String.class);
decryptCredentialToPassword = Boolean.valueOf(propertyVal);
if (decryptCredentialToPassword) {
String decryptCredentialToPasswordPrivateKey =
applicationContext.getBean(DECRYPT_CRED_TO_PWD_KEY, String.class);
if (key == null) {
try {
key = getPrivateKeyFromFile(decryptCredentialToPasswordPrivateKey);
} catch (Exception e) {
log.error(
"Cannot load key from file: {}",
decryptCredentialToPasswordPrivateKey,
e);
}
}
if (cipher == null) {
try {
cipher = Cipher.getInstance(key.getAlgorithm());
} catch (Exception e) {
log.error(
"Cannot create cipher for key from file: {}",
decryptCredentialToPasswordPrivateKey,
e);
}
}
algorithm = applicationContext.getBean(DECRYPT_CRED_TO_PWD_ALG, String.class);
}
}
public int getAuthType() {
return CAS_AUTHTYPE;
}
/**
* Exposes a template post-authentication method for subclasses to implement their custom logic
* in.
*
* <p>NOTE: This is called BEFORE super.authenticate();
*
* @param assertion the Assertion that was retrieved from the ThreadLocal. CANNOT be NULL.
*/
protected void postAuthenticate(final Assertion assertion) {
copyAssertionAttributesToUserAttributes(assertion);
}
@Override
public final void authenticate() throws PortalSecurityException {
if (log.isTraceEnabled()) {
log.trace("Authenticating user via CAS.");
}
this.isauth = false;
this.assertion = AssertionHolder.getAssertion();
if (this.assertion != null) {
final String usernameFromCas = assertion.getPrincipal().getName();
if (null == usernameFromCas) {
throw new IllegalStateException(
"Non-null CAS assertion unexpectedly had null principal name.");
}
this.myPrincipal.setUID(usernameFromCas);
// verify that the principal UID was successfully set
final String uidAsSetInThePrincipal = this.myPrincipal.getUID();
if (!usernameFromCas.equals(uidAsSetInThePrincipal)) {
final String logMessage =
"Attempted to set portal principal username to ["
+ usernameFromCas
+ "] as read from the CAS assertion, but uid as set in the principal is instead ["
+ uidAsSetInThePrincipal
+ "]. This may be an attempt to exploit CVE-2014-5059 / UP-4192 .";
log.error(logMessage);
throw new IllegalStateException(logMessage);
}
this.isauth = true;
log.debug(
"CASContext authenticated ["
+ this.myPrincipal.getUID()
+ "] using assertion ["
+ this.assertion
+ "]");
postAuthenticate(assertion);
}
this.myAdditionalDescriptor = null; //no additional descriptor from CAS
super.authenticate();
if (log.isTraceEnabled()) {
log.trace("Finished CAS Authentication");
}
}
public final String getCasServiceToken(final String target)
throws CasProxyTicketAcquisitionException {
if (log.isTraceEnabled()) {
log.trace(
"Attempting to retrieve proxy ticket for target ["
+ target
+ "] by using CAS Assertion ["
+ assertion
+ "]");
}
if (this.assertion == null) {
if (log.isDebugEnabled()) {
log.debug("No Assertion found for user. Returning null Proxy Ticket.");
}
return null;
}
final String proxyTicket = this.assertion.getPrincipal().getProxyTicketFor(target);
if (proxyTicket == null) {
log.error(
"Failed to retrieve proxy ticket for assertion ["
+ assertion
+ "]. Is the PGT still valid?");
throw new CasProxyTicketAcquisitionException(target, assertion.getPrincipal());
}
if (log.isTraceEnabled()) {
log.trace("Returning from Proxy Ticket Request with ticket [" + proxyTicket + "]");
}
return proxyTicket;
}
public String toString() {
return this.getClass().getName() + " assertion:" + this.assertion;
}
/**
* If enabled, convert CAS assertion person attributes into uPortal user attributes.
*
* @param assertion the Assertion that was retrieved from the ThreadLocal. CANNOT be NULL.
*/
private void copyAssertionAttributesToUserAttributes(Assertion assertion) {
if (!copyAssertionAttributesToUserAttributes) {
return;
}
// skip this if there are no attributes or if the attribute set is empty.
if (assertion.getPrincipal().getAttributes() == null
|| assertion.getPrincipal().getAttributes().isEmpty()) {
return;
}
Map<String, List<Object>> attributes = new HashMap<>();
// loop over the set of person attributes from CAS...
for (Map.Entry<String, Object> attrEntry :
assertion.getPrincipal().getAttributes().entrySet()) {
log.debug(
"Adding attribute '{}' from Assertion with value '{}'; runtime type of value is {}",
attrEntry.getKey(),
attrEntry.getValue(),
attrEntry.getValue().getClass().getName());
// Check for credential
if (decryptCredentialToPassword
&& key != null
&& cipher != null
&& attrEntry.getKey().equals(CREDENTIAL_KEY)) {
try {
final String encPwd =
(String)
(attrEntry.getValue() instanceof List
? ((List) attrEntry.getValue()).get(0)
: attrEntry.getValue());
byte[] cred64 = DatatypeConverter.parseBase64Binary(encPwd);
cipher.init(Cipher.DECRYPT_MODE, key);
final byte[] cipherData = cipher.doFinal(cred64);
final Object pwd = new String(cipherData);
attributes.put(PASSWORD_KEY, Collections.singletonList(pwd));
} catch (Exception e) {
log.warn("Cannot decipher credential", e);
}
}
// convert each attribute to a list, if necessary...
List<Object> valueList;
if (attrEntry.getValue() instanceof List) {
valueList = (List<Object>) attrEntry.getValue();
} else {
valueList = Collections.singletonList(attrEntry.getValue());
}
// add the attribute...
attributes.put(attrEntry.getKey(), valueList);
}
// get the attribute descriptor from Spring...
IAdditionalDescriptors additionalDescriptors =
(IAdditionalDescriptors)
applicationContext.getBean(SESSION_ADDITIONAL_DESCRIPTORS_BEAN);
// add the new properties...
additionalDescriptors.addAttributes(attributes);
}
private static PrivateKey getPrivateKeyFromFile(String filename) throws Exception {
byte[] keyBytes = Files.readAllBytes(new File(filename).toPath());
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance(algorithm);
return kf.generatePrivate(spec);
}
}