Custom CAS Service Ticket Validator
Service Ticket Validator is used by the Application(Spring here) for validating the ticket as provided by CAS to the application during login (Cross Verification).
For my own understanding and to implement Custom features as applicable I have implemented custom Ticket Validator.
Every Ticket Validator must implement TicketValidator interface, which contains a single validate method and it returns a Assertion.
In the validate() method, we contruct the validation URL from the ticket(to be validated by the application) as returned by the CAS server during login and the CASService properties.
Now using the Valdation URL as constructed above and the ticket(to be validated by the application) as returned by the CAS server during login, response is retrieved from the server. The response is in XML format.
The response is then parsed to create an object of type Assertion, which is returned from the validate() method. The custom attributes if configured through Attribute Repository(as discussed in the previous blog) will be present in the response from server and hence can be parsed according to the need.
Now finally an object of type Assertion (AssertionImpl implemention) which contains AttributePrincipalImpl object can be constructed ansd hence can be returned.
The code which I have used I am attaching for the reference:
public class CustomCas20ServiceTicketValidator implements TicketValidator {
private Logger logger = LogManager.getLogger(this.getClass().getName());
private boolean renew;
private Map<String, String> customParameters;
private final String casServerUrlPrefix;
private ProxyGrantingTicketStorage proxyGrantingTicketStorage;
private HostnameVerifier hostnameVerifier;
private ProxyRetriever proxyRetriever;
private String encoding;
public CustomCas20ServiceTicketValidator(String casServerUrlPrefix)
{
this.casServerUrlPrefix = casServerUrlPrefix;
CommonUtils.assertNotNull(this.casServerUrlPrefix, "casServerUrlPrefix cannot be null.");
this.proxyRetriever = new Cas20ProxyRetriever(casServerUrlPrefix, getEncoding());
}
@Override
public Assertion validate(String ticket, String service)
throws TicketValidationException
{
try
{
logger.debug(" *** In validate() method of CustomCas20ServiceTicketValidator *** ");
String validationUrl = constructValidationUrl(ticket, service);
logger.debug("#### The Validation URL is:"+validationUrl);
String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);
logger.debug("#### The Server Response is123:"+serverResponse);
if (serverResponse == null) {
throw new TicketValidationException("The CAS server returned no response.");
}
return parseResponseFromServer(serverResponse);
} catch (MalformedURLException e) {
logger.equals(e);
throw new TicketValidationException(e);
}
}
private String constructValidationUrl(String ticket, String serviceUrl)
{
Map<String,String> urlParameters = new HashMap<String,String>();
urlParameters.put("ticket", ticket);
urlParameters.put("service", encodeUrl(serviceUrl));
if (this.renew) {
urlParameters.put("renew", "true");
}
logger.debug("Calling template URL attribute map.");
populateUrlAttributeMap(urlParameters);
logger.debug("Loading custom parameters from configuration.");
if (this.customParameters != null) {
urlParameters.putAll(this.customParameters);
}
String suffix = getUrlSuffix();
StringBuilder buffer = new StringBuilder(urlParameters.size() * 10 + this.casServerUrlPrefix.length() + suffix.length() + 1);
int i = 0;
buffer.append(this.casServerUrlPrefix);
if (!this.casServerUrlPrefix.endsWith("/")) {
buffer.append("/");
}
buffer.append(suffix);
for (Map.Entry entry : urlParameters.entrySet()) {
String key = (String)entry.getKey();
String value = (String)entry.getValue();
if (value != null) {
buffer.append(i++ == 0 ? "?" : "&");
buffer.append(key);
buffer.append("=");
buffer.append(value);
}
}
return buffer.toString();
}
private Assertion parseResponseFromServer(String response) throws TicketValidationException {
String error = Utility.getTextForElement(response, "authenticationFailure");
if (CommonUtils.isNotBlank(error)) {
throw new TicketValidationException(error);
}
String principal = Utility.getTextForElement(response, "user");
String proxyGrantingTicketIou = Utility.getTextForElement(response, "proxyGrantingTicket");
String proxyGrantingTicket = this.proxyGrantingTicketStorage != null ? this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou) : null;
if (CommonUtils.isEmpty(principal)) {
throw new TicketValidationException("No principal was found in the response from the CAS server.");
}
Map attributes = extractCustomAttributes(response);
Assertion assertion;
if (CommonUtils.isNotBlank(proxyGrantingTicket)) {
AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes, proxyGrantingTicket, this.proxyRetriever);
assertion = new AssertionImpl(attributePrincipal);
} else {
logger.debug(" *** The Attributes from extractCustomAttributes() is:"+attributes+" before set in AssertionImpl");
assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
}
//customParseResponse(response, assertion);
return assertion;
}
private String retrieveResponseFromServer(URL validationUrl, String ticket)
{
logger.debug(" ### In retrieveResponseFromServer() method ### ");
logger.debug(" ### The HostnameVerifier is:"+this.hostnameVerifier);
if (this.hostnameVerifier != null) {
return Utility.getResponseFromServer(validationUrl, this.hostnameVerifier, getEncoding());
}
return Utility.getResponseFromServer(validationUrl, getEncoding());
}
private String encodeUrl(String url)
{
if (url == null) {
return null;
}
try
{
return URLEncoder.encode(url, "UTF-8"); } catch (UnsupportedEncodingException e) {
}
return url;
}
private void populateUrlAttributeMap(Map<String, String> urlParameters)
{
//This method can be used for populating urlParameters Map with Proxy call back values
}
protected String getUrlSuffix() {
return "serviceValidate";
//return "samlValidate";
}
private Map<String, Object> extractCustomAttributes(String xml)
{
int pos1 = xml.indexOf("<cas:attr>");
int pos2 = xml.indexOf("</cas:attr>");
if (pos1 == -1) {
return Collections.emptyMap();
}
Map attributes = new HashMap();
String attributesText = (xml.substring(pos1 + 10, pos2));
attributesText = attributesText.substring(attributesText.indexOf("{")+1,attributesText.indexOf("}"));
String[] attributesTextData = attributesText.split(",");
for(String str:attributesTextData)
{
String data[] = str.split("=");
attributes.put(data[0], data[1]);
}
/*BufferedReader br = new BufferedReader(new StringReader(attributesText));
List<String> attributeNames = new ArrayList<String>();
try
{
String line;
while ((line = br.readLine()) != null) {
String trimmedLine = line.trim();
if (trimmedLine.length() > 0) {
int leftPos = trimmedLine.indexOf(":");
int rightPos = trimmedLine.indexOf(">");
attributeNames.add(trimmedLine.substring(leftPos + 1, rightPos));
}
}
br.close();
}
catch (IOException e)
{
}
for (String name : attributeNames) {
List values = XmlUtils.getTextForElements(xml, name);
if (values.size() == 1)
attributes.put(name, values.get(0));
else {
attributes.put(name, values);
}
String values = Utility.getTextForElement(xml, name);
if(values != null || values.trim().length() != 0)
{
attributes.put(name, values);
}
}*/
return attributes;
}
public String getEncoding() {
return encoding;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
public void setRenew(boolean renew) {
this.renew = renew;
}
public void setCustomParameters(Map<String, String> customParameters) {
this.customParameters = customParameters;
}
public void setProxyGrantingTicketStorage(
ProxyGrantingTicketStorage proxyGrantingTicketStorage) {
this.proxyGrantingTicketStorage = proxyGrantingTicketStorage;
}
public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
this.hostnameVerifier = hostnameVerifier;
}
}
This ticket validator is used in AutheticationProvider of Spring Secrity Context (not in CAS Authentication). This AutheticationProvider is used by the CAS filter for generating the authentication token.(as observed with any other AutheticationProvider principal of Spring Security).
The Assertion as obtained from the Ticket Validator is used in AutheticationProvider to fetch the userDetails from the database and hence form the CasAuthenticationToken accordingly.
(At the end of CAS topic I will provide the normal flow of Spring Security by citing a custom example, which will help to co-relate this topic)
Like Cas20ServiceTicketValidator, SAML11TicketValidator can also be used, only the difference here is that, the response from the server will be in SAML format and hence parsing is required to be done accordingly to construct the object of type Assertion.
The mechanism of SAML11TicketValidator is same as that of Cas20ServiceTicketValidator in the constructValidationUrl phase.
While retrieving the response from the server in SAML format, proper request headers are to set in retrieveResponseFromServer() method.
Now in parseResponseFromServer() for parsing the SAML respnse to form the Assertion to be returned from the validate() method are basically the same, if any thing more is required then it can be configured in the the custom saml11 ticket validator.
(Note for parsing in both the abobe mentioned Ticket Validators, I have used STAX Parser.
I am not attaching the the code for Saml11TicketValidator, it required by anyone feel free to ask I will post it.
Now for configuring the Custom Ticket validator in Spring please go through the following steps:
<bean id="castTicketValidator" class="com.cas.customticketvalidator.CustomCas20ServiceTicketValidator">
<constructor-arg value="https://localhost:8443/cas/"></constructor-arg>
<property name="hostnameVerifier" ref="customHostnameVerifier"></property>
</bean>
<bean id="customHostnameVerifier" class="com.springRest.util.CustomHostNameVerifier"></bean>
This castTicketValidator is used in the Authentication Provider of Spring Security Context:
<bean id="customCASAutheticationProvider" class="com.springRest.customProvider.CustomCasAuthenticationProvider">
<property name="ticketValidator" ref="castTicketValidator"></property>
<property name="serviceProperties" ref="casService"></property>
<property name="key" value="SpringREST"></property>
<property name="authenticationUserDetailsService" ref="casAuthenticationUserDetailsService"></property>
</bean>
This Authentication Provider is used in the Authetication Manger of Spring Security Context:
<authentication-manager alias="authenticationManager">
<authentication-provider ref="customCASAutheticationProvider"></authentication-provider>
</authentication-manager>
In the next post, I will provide the STAX tutorial
For my own understanding and to implement Custom features as applicable I have implemented custom Ticket Validator.
Every Ticket Validator must implement TicketValidator interface, which contains a single validate method and it returns a Assertion.
In the validate() method, we contruct the validation URL from the ticket(to be validated by the application) as returned by the CAS server during login and the CASService properties.
Now using the Valdation URL as constructed above and the ticket(to be validated by the application) as returned by the CAS server during login, response is retrieved from the server. The response is in XML format.
The response is then parsed to create an object of type Assertion, which is returned from the validate() method. The custom attributes if configured through Attribute Repository(as discussed in the previous blog) will be present in the response from server and hence can be parsed according to the need.
Now finally an object of type Assertion (AssertionImpl implemention) which contains AttributePrincipalImpl object can be constructed ansd hence can be returned.
The code which I have used I am attaching for the reference:
public class CustomCas20ServiceTicketValidator implements TicketValidator {
private Logger logger = LogManager.getLogger(this.getClass().getName());
private boolean renew;
private Map<String, String> customParameters;
private final String casServerUrlPrefix;
private ProxyGrantingTicketStorage proxyGrantingTicketStorage;
private HostnameVerifier hostnameVerifier;
private ProxyRetriever proxyRetriever;
private String encoding;
public CustomCas20ServiceTicketValidator(String casServerUrlPrefix)
{
this.casServerUrlPrefix = casServerUrlPrefix;
CommonUtils.assertNotNull(this.casServerUrlPrefix, "casServerUrlPrefix cannot be null.");
this.proxyRetriever = new Cas20ProxyRetriever(casServerUrlPrefix, getEncoding());
}
@Override
public Assertion validate(String ticket, String service)
throws TicketValidationException
{
try
{
logger.debug(" *** In validate() method of CustomCas20ServiceTicketValidator *** ");
String validationUrl = constructValidationUrl(ticket, service);
logger.debug("#### The Validation URL is:"+validationUrl);
String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);
logger.debug("#### The Server Response is123:"+serverResponse);
if (serverResponse == null) {
throw new TicketValidationException("The CAS server returned no response.");
}
return parseResponseFromServer(serverResponse);
} catch (MalformedURLException e) {
logger.equals(e);
throw new TicketValidationException(e);
}
}
private String constructValidationUrl(String ticket, String serviceUrl)
{
Map<String,String> urlParameters = new HashMap<String,String>();
urlParameters.put("ticket", ticket);
urlParameters.put("service", encodeUrl(serviceUrl));
if (this.renew) {
urlParameters.put("renew", "true");
}
logger.debug("Calling template URL attribute map.");
populateUrlAttributeMap(urlParameters);
logger.debug("Loading custom parameters from configuration.");
if (this.customParameters != null) {
urlParameters.putAll(this.customParameters);
}
String suffix = getUrlSuffix();
StringBuilder buffer = new StringBuilder(urlParameters.size() * 10 + this.casServerUrlPrefix.length() + suffix.length() + 1);
int i = 0;
buffer.append(this.casServerUrlPrefix);
if (!this.casServerUrlPrefix.endsWith("/")) {
buffer.append("/");
}
buffer.append(suffix);
for (Map.Entry entry : urlParameters.entrySet()) {
String key = (String)entry.getKey();
String value = (String)entry.getValue();
if (value != null) {
buffer.append(i++ == 0 ? "?" : "&");
buffer.append(key);
buffer.append("=");
buffer.append(value);
}
}
return buffer.toString();
}
private Assertion parseResponseFromServer(String response) throws TicketValidationException {
String error = Utility.getTextForElement(response, "authenticationFailure");
if (CommonUtils.isNotBlank(error)) {
throw new TicketValidationException(error);
}
String principal = Utility.getTextForElement(response, "user");
String proxyGrantingTicketIou = Utility.getTextForElement(response, "proxyGrantingTicket");
String proxyGrantingTicket = this.proxyGrantingTicketStorage != null ? this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou) : null;
if (CommonUtils.isEmpty(principal)) {
throw new TicketValidationException("No principal was found in the response from the CAS server.");
}
Map attributes = extractCustomAttributes(response);
Assertion assertion;
if (CommonUtils.isNotBlank(proxyGrantingTicket)) {
AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes, proxyGrantingTicket, this.proxyRetriever);
assertion = new AssertionImpl(attributePrincipal);
} else {
logger.debug(" *** The Attributes from extractCustomAttributes() is:"+attributes+" before set in AssertionImpl");
assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
}
//customParseResponse(response, assertion);
return assertion;
}
private String retrieveResponseFromServer(URL validationUrl, String ticket)
{
logger.debug(" ### In retrieveResponseFromServer() method ### ");
logger.debug(" ### The HostnameVerifier is:"+this.hostnameVerifier);
if (this.hostnameVerifier != null) {
return Utility.getResponseFromServer(validationUrl, this.hostnameVerifier, getEncoding());
}
return Utility.getResponseFromServer(validationUrl, getEncoding());
}
private String encodeUrl(String url)
{
if (url == null) {
return null;
}
try
{
return URLEncoder.encode(url, "UTF-8"); } catch (UnsupportedEncodingException e) {
}
return url;
}
private void populateUrlAttributeMap(Map<String, String> urlParameters)
{
//This method can be used for populating urlParameters Map with Proxy call back values
}
protected String getUrlSuffix() {
return "serviceValidate";
//return "samlValidate";
}
private Map<String, Object> extractCustomAttributes(String xml)
{
int pos1 = xml.indexOf("<cas:attr>");
int pos2 = xml.indexOf("</cas:attr>");
if (pos1 == -1) {
return Collections.emptyMap();
}
Map attributes = new HashMap();
String attributesText = (xml.substring(pos1 + 10, pos2));
attributesText = attributesText.substring(attributesText.indexOf("{")+1,attributesText.indexOf("}"));
String[] attributesTextData = attributesText.split(",");
for(String str:attributesTextData)
{
String data[] = str.split("=");
attributes.put(data[0], data[1]);
}
/*BufferedReader br = new BufferedReader(new StringReader(attributesText));
List<String> attributeNames = new ArrayList<String>();
try
{
String line;
while ((line = br.readLine()) != null) {
String trimmedLine = line.trim();
if (trimmedLine.length() > 0) {
int leftPos = trimmedLine.indexOf(":");
int rightPos = trimmedLine.indexOf(">");
attributeNames.add(trimmedLine.substring(leftPos + 1, rightPos));
}
}
br.close();
}
catch (IOException e)
{
}
for (String name : attributeNames) {
List values = XmlUtils.getTextForElements(xml, name);
if (values.size() == 1)
attributes.put(name, values.get(0));
else {
attributes.put(name, values);
}
String values = Utility.getTextForElement(xml, name);
if(values != null || values.trim().length() != 0)
{
attributes.put(name, values);
}
}*/
return attributes;
}
public String getEncoding() {
return encoding;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
public void setRenew(boolean renew) {
this.renew = renew;
}
public void setCustomParameters(Map<String, String> customParameters) {
this.customParameters = customParameters;
}
public void setProxyGrantingTicketStorage(
ProxyGrantingTicketStorage proxyGrantingTicketStorage) {
this.proxyGrantingTicketStorage = proxyGrantingTicketStorage;
}
public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
this.hostnameVerifier = hostnameVerifier;
}
}
This ticket validator is used in AutheticationProvider of Spring Secrity Context (not in CAS Authentication). This AutheticationProvider is used by the CAS filter for generating the authentication token.(as observed with any other AutheticationProvider principal of Spring Security).
The Assertion as obtained from the Ticket Validator is used in AutheticationProvider to fetch the userDetails from the database and hence form the CasAuthenticationToken accordingly.
(At the end of CAS topic I will provide the normal flow of Spring Security by citing a custom example, which will help to co-relate this topic)
Like Cas20ServiceTicketValidator, SAML11TicketValidator can also be used, only the difference here is that, the response from the server will be in SAML format and hence parsing is required to be done accordingly to construct the object of type Assertion.
The mechanism of SAML11TicketValidator is same as that of Cas20ServiceTicketValidator in the constructValidationUrl phase.
While retrieving the response from the server in SAML format, proper request headers are to set in retrieveResponseFromServer() method.
Now in parseResponseFromServer() for parsing the SAML respnse to form the Assertion to be returned from the validate() method are basically the same, if any thing more is required then it can be configured in the the custom saml11 ticket validator.
(Note for parsing in both the abobe mentioned Ticket Validators, I have used STAX Parser.
I am not attaching the the code for Saml11TicketValidator, it required by anyone feel free to ask I will post it.
Now for configuring the Custom Ticket validator in Spring please go through the following steps:
<bean id="castTicketValidator" class="com.cas.customticketvalidator.CustomCas20ServiceTicketValidator">
<constructor-arg value="https://localhost:8443/cas/"></constructor-arg>
<property name="hostnameVerifier" ref="customHostnameVerifier"></property>
</bean>
<bean id="customHostnameVerifier" class="com.springRest.util.CustomHostNameVerifier"></bean>
This castTicketValidator is used in the Authentication Provider of Spring Security Context:
<bean id="customCASAutheticationProvider" class="com.springRest.customProvider.CustomCasAuthenticationProvider">
<property name="ticketValidator" ref="castTicketValidator"></property>
<property name="serviceProperties" ref="casService"></property>
<property name="key" value="SpringREST"></property>
<property name="authenticationUserDetailsService" ref="casAuthenticationUserDetailsService"></property>
</bean>
This Authentication Provider is used in the Authetication Manger of Spring Security Context:
<authentication-manager alias="authenticationManager">
<authentication-provider ref="customCASAutheticationProvider"></authentication-provider>
</authentication-manager>
In the next post, I will provide the STAX tutorial
Helped a lot. But I've an issue: how can I validate the ticket after login and can we validate a single ticket multiple times? Can you provide some work around? if yes it will be more helpful. Thanks,
ReplyDelete