/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.security.cas; import java.nio.charset.Charset; import java.util.Base64; import javax.security.auth.callback.CallbackHandler; import org.apache.commons.lang.StringUtils; import org.apache.cxf.sts.STSPropertiesMBean; import org.apache.cxf.sts.request.ReceivedToken; import org.apache.cxf.sts.request.ReceivedToken.STATE; import org.apache.cxf.sts.token.validator.TokenValidator; import org.apache.cxf.sts.token.validator.TokenValidatorParameters; import org.apache.cxf.sts.token.validator.TokenValidatorResponse; import org.apache.cxf.ws.security.sts.provider.model.secext.BinarySecurityTokenType; import org.apache.wss4j.common.crypto.Crypto; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.dom.engine.WSSConfig; import org.apache.wss4j.dom.handler.RequestData; import org.codice.ddf.configuration.PropertyResolver; 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; import ddf.security.encryption.EncryptionService; /** * Validates Web Single Sign-On Tokens. * * @author kcwire */ public class WebSSOTokenValidator implements TokenValidator { // The Supported SSO Token Types public static final String CAS_TYPE = "#CAS"; public static final String CAS_BST_SEP = "|"; private static final Logger LOGGER = LoggerFactory.getLogger(WebSSOTokenValidator.class); private PropertyResolver casServerUrl; private EncryptionService encryptionService; public String getCasServerUrl() { return casServerUrl.getResolvedString(); } public void setCasServerUrl(String casServerUrl) { this.casServerUrl = new PropertyResolver(casServerUrl); } public void setEncryptionService(EncryptionService encryptionService) { this.encryptionService = encryptionService; } /* * Return true if this TokenValidator implementation is capable of validating the ReceivedToken * argument. */ @Override public boolean canHandleToken(ReceivedToken validateTarget) { return canHandleToken(validateTarget, null); } /* * Return true if this TokenValidator implementation is capable of validating the ReceivedToken * argument. The realm is ignored in this token Validator. */ @Override public boolean canHandleToken(ReceivedToken validateTarget, String realm) { final Object token = validateTarget.getToken(); // Check the ValueType to see if this is a supported SSO Token. if ((token instanceof BinarySecurityTokenType)) { if (CAS_TYPE.equalsIgnoreCase(((BinarySecurityTokenType) token).getValueType())) { LOGGER.debug("Can handle token type of: " + ((BinarySecurityTokenType) token).getValueType()); return true; } LOGGER.debug("Cannot handle token type of: " + ((BinarySecurityTokenType) token).getValueType()); } return false; } /** * Validate a Token using the given TokenValidatorParameters. */ @Override public TokenValidatorResponse validateToken(TokenValidatorParameters tokenParameters) { LOGGER.debug("Validating SSO Token"); STSPropertiesMBean stsProperties = tokenParameters.getStsProperties(); Crypto sigCrypto = stsProperties.getSignatureCrypto(); CallbackHandler callbackHandler = stsProperties.getCallbackHandler(); RequestData requestData = new RequestData(); requestData.setSigVerCrypto(sigCrypto); WSSConfig wssConfig = WSSConfig.getNewInstance(); requestData.setWssConfig(wssConfig); requestData.setCallbackHandler(callbackHandler); LOGGER.debug("Setting validate state to invalid before check."); TokenValidatorResponse response = new TokenValidatorResponse(); ReceivedToken validateTarget = tokenParameters.getToken(); validateTarget.setState(STATE.INVALID); response.setToken(validateTarget); if (!validateTarget.isBinarySecurityToken()) { LOGGER.debug( "Validate target is not a binary security token, returning invalid response."); return response; } LOGGER.debug("Getting binary security token from validate target"); BinarySecurityTokenType binarySecurityToken = (BinarySecurityTokenType) validateTarget.getToken(); // // Decode the token // LOGGER.debug("Decoding binary security token."); String base64Token = binarySecurityToken.getValue(); String ticket = null; String service = null; try { byte[] token = Base64.getDecoder() .decode(base64Token); if (token == null || token.length == 0) { throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN, "Binary security token NOT successfully decoded, is empty or null."); } String decodedToken = new String(token, Charset.forName("UTF-8")); if (StringUtils.isNotBlank(decodedToken)) { LOGGER.debug("Binary security token successfully decoded: {}", decodedToken); // Token is in the format ticket|service String[] parts = StringUtils.split(decodedToken, CAS_BST_SEP); if (parts.length == 2) { ticket = parts[0]; service = parts[1]; } else { throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN, "Was not able to parse out BST propertly. Should be in ticket|service format."); } } else { throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY_TOKEN, "Binary security token NOT successfully decoded, is empty or null."); } } catch (WSSecurityException wsse) { String msg = "Unable to decode BST into ticket and service for validation to CAS."; LOGGER.info(msg, wsse); return response; } // // Do some validation of the token here // try { LOGGER.debug("Validating ticket [{}] for service [{}].", ticket, service); // validate either returns an assertion or throws an exception Assertion assertion = validate(ticket, service); AttributePrincipal principal = assertion.getPrincipal(); LOGGER.debug("User name retrieved from CAS: {}", principal.getName()); response.setPrincipal(principal); LOGGER.debug("CAS ticket successfully validated, setting state to valid."); validateTarget.setState(STATE.VALID); } catch (TicketValidationException e) { LOGGER.debug("Unable to validate CAS token.", e); } return response; } /** * Validate the CAS ticket and service * * @param ticket * @param service * @return * @throws TicketValidationException */ public Assertion validate(String ticket, String service) throws TicketValidationException { LOGGER.trace("CAS Server URL = {}", casServerUrl); Cas20ProxyTicketValidator casValidator = new Cas20ProxyTicketValidator(casServerUrl.getResolvedString()); casValidator.setAcceptAnyProxy(true); return casValidator.validate(ticket, service); } }