001/*
002 *  (C) Copyright 2000-2003 Yale University. All rights reserved.
003 *
004 *  THIS SOFTWARE IS PROVIDED "AS IS," AND ANY EXPRESS OR IMPLIED
005 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
006 *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE EXPRESSLY
007 *  DISCLAIMED. IN NO EVENT SHALL YALE UNIVERSITY OR ITS EMPLOYEES BE
008 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
009 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED, THE COSTS OF
010 *  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR
011 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
012 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
013 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
014 *  SOFTWARE, EVEN IF ADVISED IN ADVANCE OF THE POSSIBILITY OF SUCH
015 *  DAMAGE.
016 *
017 *  Redistribution and use of this software in source or binary forms,
018 *  with or without modification, are permitted, provided that the
019 *  following conditions are met:
020 *
021 *  1. Any redistribution must include the above copyright notice and
022 *  disclaimer and this list of conditions in any related documentation
023 *  and, if feasible, in the redistributed software.
024 *
025 *  2. Any redistribution must include the acknowledgment, "This product
026 *  includes software developed by Yale University," in any related
027 *  documentation and, if feasible, in the redistributed software.
028 *
029 *  3. The names "Yale" and "Yale University" must not be used to endorse
030 *  or promote products derived from this software.
031 */
032
033package edu.yale.its.tp.cas.client;
034
035import java.io.IOException;
036import java.io.StringReader;
037import java.util.HashMap;
038import java.util.Map;
039
040import javax.xml.parsers.ParserConfigurationException;
041import javax.xml.parsers.SAXParserFactory;
042
043import org.nuxeo.common.utils.URIUtils;
044import org.xml.sax.Attributes;
045import org.xml.sax.InputSource;
046import org.xml.sax.SAXException;
047import org.xml.sax.XMLReader;
048import org.xml.sax.helpers.DefaultHandler;
049
050import edu.yale.its.tp.cas.util.SecureURL;
051
052/**
053 * Validates STs and optionally retrieves PGT IOUs. Designed with a bean-like interface for simplicity and generality.
054 */
055public class ServiceTicketValidator {
056
057    // *********************************************************************
058    // Private state
059
060    protected String casValidateUrl, proxyCallbackUrl, st, service, pgtIou, user, errorCode, errorMessage,
061            entireResponse;
062
063    protected boolean renew = false;
064
065    protected boolean attemptedAuthentication;
066
067    protected boolean successfulAuthentication;
068
069    // *********************************************************************
070    // Accessors
071
072    /**
073     * Sets the CAS validation URL to use when validating tickets and retrieving PGT IOUs.
074     */
075    public void setCasValidateUrl(String x) {
076        this.casValidateUrl = x;
077    }
078
079    /**
080     * Gets the CAS validation URL to use when validating tickets and retrieving PGT IOUs.
081     */
082    public String getCasValidateUrl() {
083        return this.casValidateUrl;
084    }
085
086    /**
087     * Sets the callback URL, owned logically by the calling service, to receive the PGTid/PGTiou mapping.
088     */
089    public void setProxyCallbackUrl(String x) {
090        this.proxyCallbackUrl = x;
091    }
092
093    /**
094     * Sets the "renew" flag on authentication. When set to "true", authentication will only succeed if this was an
095     * initial login (forced by the "renew" flag being set on login).
096     */
097    public void setRenew(boolean b) {
098        this.renew = b;
099    }
100
101    /**
102     * Gets the callback URL, owned logically by the calling service, to receive the PGTid/PGTiou mapping.
103     */
104    public String getProxyCallbackUrl() {
105        return this.proxyCallbackUrl;
106    }
107
108    /**
109     * Sets the ST to validate.
110     */
111    public void setServiceTicket(String x) {
112        this.st = x;
113    }
114
115    /**
116     * Sets the service to use when validating.
117     */
118    public void setService(String x) {
119        this.service = x;
120    }
121
122    /**
123     * Returns the strongly authenticated username.
124     */
125    public String getUser() {
126        return this.user;
127    }
128
129    /**
130     * Returns the PGT IOU returned by CAS.
131     */
132    public String getPgtIou() {
133        return this.pgtIou;
134    }
135
136    /**
137     * Returns {@code true} if the most recent authentication attempted succeeded, {@code false} otherwise.
138     */
139    public boolean isAuthenticationSuccesful() {
140        return this.successfulAuthentication;
141    }
142
143    /**
144     * Returns an error message if CAS authentication failed.
145     */
146    public String getErrorMessage() {
147        return this.errorMessage;
148    }
149
150    /**
151     * Returns CAS's error code if authentication failed.
152     */
153    public String getErrorCode() {
154        return this.errorCode;
155    }
156
157    /**
158     * Retrieves CAS's entire response, if authentication was succsesful.
159     */
160    public String getResponse() {
161        return this.entireResponse;
162    }
163
164    // *********************************************************************
165    // Actuator
166
167    public void validate() throws IOException, SAXException, ParserConfigurationException {
168        if (casValidateUrl == null || st == null)
169            throw new IllegalStateException("must set validation URL and ticket");
170        clear();
171        attemptedAuthentication = true;
172
173        Map<String, String> urlParameters = new HashMap<>();
174        urlParameters.put("service", service);
175        urlParameters.put("ticket", st);
176        if (proxyCallbackUrl != null) {
177            urlParameters.put("pgtUrl", proxyCallbackUrl);
178        }
179        if (renew) {
180            urlParameters.put("renew", "true");
181        }
182
183        String url = URIUtils.addParametersToURIQuery(casValidateUrl, urlParameters);
184        String response = SecureURL.retrieve(url, false);
185        this.entireResponse = response;
186
187        // parse the response and set appropriate properties
188        if (response != null) {
189            XMLReader r = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
190            r.setFeature("http://xml.org/sax/features/namespaces", false);
191            r.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
192            r.setContentHandler(newHandler());
193            r.parse(new InputSource(new StringReader(response)));
194        }
195    }
196
197    // *********************************************************************
198    // Response parser
199
200    protected DefaultHandler newHandler() {
201        return new Handler();
202    }
203
204    protected class Handler extends DefaultHandler {
205
206        // **********************************************
207        // Constants
208
209        protected static final String AUTHENTICATION_SUCCESS = "cas:authenticationSuccess";
210
211        protected static final String AUTHENTICATION_FAILURE = "cas:authenticationFailure";
212
213        protected static final String PROXY_GRANTING_TICKET = "cas:proxyGrantingTicket";
214
215        protected static final String USER = "cas:user";
216
217        // **********************************************
218        // Parsing state
219
220        protected StringBuilder currentText = new StringBuilder();
221
222        protected boolean authenticationSuccess = false;
223
224        protected boolean authenticationFailure = false;
225
226        protected String netid, pgtIou, errorCode, errorMessage;
227
228        // **********************************************
229        // Parsing logic
230
231        @Override
232        public void startElement(String ns, String ln, String qn, Attributes a) {
233            // clear the buffer
234            currentText = new StringBuilder();
235
236            // check outer elements
237            if (qn.equals(AUTHENTICATION_SUCCESS)) {
238                authenticationSuccess = true;
239            } else if (qn.equals(AUTHENTICATION_FAILURE)) {
240                authenticationFailure = true;
241                errorCode = a.getValue("code");
242                if (errorCode != null)
243                    errorCode = errorCode.trim();
244            }
245        }
246
247        @Override
248        public void characters(char[] ch, int start, int length) {
249            // store the body, in stages if necessary
250            currentText.append(ch, start, length);
251        }
252
253        @Override
254        public void endElement(String ns, String ln, String qn) throws SAXException {
255            if (authenticationSuccess) {
256                if (qn.equals(USER))
257                    user = currentText.toString().trim();
258                if (qn.equals(PROXY_GRANTING_TICKET))
259                    pgtIou = currentText.toString().trim();
260            } else if (authenticationFailure) {
261                if (qn.equals(AUTHENTICATION_FAILURE))
262                    errorMessage = currentText.toString().trim();
263            }
264        }
265
266        @Override
267        public void endDocument() throws SAXException {
268            // save values as appropriate
269            if (authenticationSuccess) {
270                ServiceTicketValidator.this.user = user;
271                ServiceTicketValidator.this.pgtIou = pgtIou;
272                ServiceTicketValidator.this.successfulAuthentication = true;
273            } else if (authenticationFailure) {
274                ServiceTicketValidator.this.errorMessage = errorMessage;
275                ServiceTicketValidator.this.errorCode = errorCode;
276                ServiceTicketValidator.this.successfulAuthentication = false;
277            } else
278                throw new SAXException("no indication of success of failure from CAS");
279        }
280    }
281
282    // *********************************************************************
283    // Utility methods
284
285    /**
286     * Clears internally manufactured state.
287     */
288    protected void clear() {
289        user = pgtIou = errorMessage = null;
290        attemptedAuthentication = false;
291        successfulAuthentication = false;
292    }
293
294}