/**
* 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.clearpass;
import org.apereo.portal.security.IOpaqueCredentials;
import org.apereo.portal.security.provider.NotSoOpaqueCredentials;
import org.apereo.portal.security.provider.cas.CasAssertionSecurityContext;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.XmlUtils;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.util.Assert;
public class PasswordCachingCasAssertionSecurityContext extends CasAssertionSecurityContext {
private static final long serialVersionUID = -3816036745827152340L;
private final String clearPassUrl;
private byte[] cachedCredentials;
protected PasswordCachingCasAssertionSecurityContext(final String clearPassUrl) {
super();
Assert.notNull(clearPassUrl, "clearPassUrl cannot be null.");
this.clearPassUrl = clearPassUrl;
}
@Override
protected final void postAuthenticate(final Assertion assertion) {
retrievePasswordFromCasServer(assertion);
super.postAuthenticate(assertion);
}
@Override
public final IOpaqueCredentials getOpaqueCredentials() {
log.debug("Invoking getOpacheCredentials()");
if (this.cachedCredentials == null) {
log.debug("Have no credentials, invoking superclass");
return super.getOpaqueCredentials();
}
final NotSoOpaqueCredentials credentials = new CacheOpaqueCredentials();
credentials.setCredentials(this.cachedCredentials);
log.debug("Returning credentials");
return credentials;
}
/**
* Attempt to use the PGTIOU returned in the service ticket validation to obtain a proxy ticket
* to call CAS to get the user's password. The Proxy Granting Ticket IOU (PGTIOU) from the
* service ticket validation response has already been matched to the Proxy Granting Ticket
* (PGT) that CAS delivers to one of the uPortal servers and the PGT should be present in the
* assertion. If password retrieval fails, log messages to help the user identify the issue. It
* may be that the uPortal servers are in a cluster and the configuration is not correct so this
* uPortal server does not have the PGT.
*
* <p>NOTE: This method will update <code>cachedCredentials</code> so the caller does not need
* to.
*
* @param assertion CAS Assertion
* @return Bytes representing the user's password. Null if not available. cachedCredentials is
* also updated
*/
protected byte[] retrievePasswordFromCasServer(Assertion assertion) {
// Get the Proxy Granting Ticket from the assertion and use it to obtain the user's password.
log.debug(
"Attempting to get CAS ClearPass Proxy Ticket for user {} using PGTIOU in assertion",
assertion.getPrincipal().getName());
String proxyTicket = assertion.getPrincipal().getProxyTicketFor(this.clearPassUrl);
// If we were able to get the proxy ticket (it was delivered to this server or replicated to this server),
// invoke CAS to retrieve the password.
if (proxyTicket != null) {
log.debug(
"Attempting to get password for user {} using Proxy Ticket {}",
assertion.getPrincipal().getName(),
proxyTicket);
String password = retrievePasswordUsingProxyTicket(proxyTicket);
if (password != null) {
log.debug("Password length {} retrieved from ClearPass.", password.length());
this.cachedCredentials = password.getBytes();
}
}
if (cachedCredentials == null) {
// If the proxy ticket is not available because it was sent to another server and hasn't replicated to this
// server yet, or if the password retrieval from CAS failed for some reason we'll log a message.
log.error(
"Unable to obtain {} for {} from CAS ClearPass. Check your"
+ " uPortal and CAS configuration. See uPortal manual section on Caching and"
+ " Replaying credentials",
proxyTicket == null ? "proxy ticket" : "password",
assertion.getPrincipal().getName());
}
return cachedCredentials;
}
protected final String retrievePasswordUsingProxyTicket(final String proxyTicket) {
try {
final String url =
this.clearPassUrl
+ (this.clearPassUrl.contains("?") ? "&" : "?")
+ "ticket="
+ proxyTicket;
final String response = retrieveResponseFromServer(url, "UTF-8");
final String password = XmlUtils.getTextForElement(response, "credentials");
if (CommonUtils.isNotBlank(password)) {
return password;
}
// Response won't have password so OK to log it.
log.error(
"Unable to Retrieve Password using url {}. If you see a [403] HTTP response code returned from"
+ " CommonUtils then it most likely means the proxy configuration on the CAS server is not"
+ " correct.\n\nFull Response from ClearPass was [{}]",
url,
response);
return null;
} catch (Exception e) {
/*
* uPortal failed to obtain the user's password from the ClearPass feature. This issue commonly occurs
* in local dev environments because CAS will not share the user's credential over a non-SSL
* connection. We need to log this event, but should not fail the whole Authentication.
*/
if (log.isWarnEnabled()) {
log.warn(
"Unable to retrieve the credential from the ClearPass "
+ "service for proxy ticket {}",
proxyTicket,
e);
}
return null;
}
}
/** Exists purely for testing purposes. */
protected String retrieveResponseFromServer(final String url, final String encoding) {
return CommonUtils.getResponseFromServer(url, "UTF-8");
}
/** Copied from {@link org.apereo.portal.security.provider.CacheSecurityContext} */
private class CacheOpaqueCredentials extends ChainingOpaqueCredentials
implements NotSoOpaqueCredentials {
private static final long serialVersionUID = 1l;
public String getCredentials() {
log.debug("credentialString is {}", credentialstring != null ? "non-null" : "null");
return this.credentialstring != null ? new String(this.credentialstring) : null;
}
}
}