View Javadoc

1   package edu.internet2.middleware.shibboleth.wayf.plugins.provider;
2   
3   import java.io.UnsupportedEncodingException;
4   import java.net.URLDecoder;
5   import java.net.URLEncoder;
6   import java.util.ArrayList;
7   import java.util.Collection;
8   import java.util.Iterator;
9   import java.util.List;
10  import java.util.Map;
11  
12  import javax.servlet.http.Cookie;
13  import javax.servlet.http.HttpServletRequest;
14  import javax.servlet.http.HttpServletResponse;
15  
16  import org.apache.log4j.Logger;
17  import org.opensaml.saml2.metadata.provider.MetadataProvider;
18  import org.opensaml.xml.util.Base64;
19  import org.w3c.dom.Element;
20  
21  import edu.internet2.middleware.shibboleth.wayf.DiscoveryServiceHandler;
22  import edu.internet2.middleware.shibboleth.wayf.IdPSite;
23  import edu.internet2.middleware.shibboleth.wayf.WayfException;
24  import edu.internet2.middleware.shibboleth.wayf.plugins.Plugin;
25  import edu.internet2.middleware.shibboleth.wayf.plugins.PluginContext;
26  import edu.internet2.middleware.shibboleth.wayf.plugins.PluginMetadataParameter;
27  import edu.internet2.middleware.shibboleth.wayf.plugins.WayfRequestHandled;
28  
29  /**
30   * This is a test implementation of the saml cookie lookup stuff to 
31   * see whether it fits the plugin architecture.
32   * 
33   * @author Rod Widdowson
34   *
35   */
36  public class SamlCookiePlugin implements Plugin {
37          
38      /**
39       * The parameter which controls the cache.
40       */
41      private static final String PARAMETER_NAME = "cache";
42  
43      /**
44       * Parameter to say make it last a long time.
45       */
46      private static final String PARAMETER_PERM = "perm";
47  
48      /**
49       * Parameter to say just keep this as long as the brower is open.
50       */
51      private static final String PARAMETER_SESSION = "session";
52      
53      /**
54       * Handle for logging. 
55       */
56      private static Logger log = Logger.getLogger(SamlCookiePlugin.class.getName());
57  
58      /**
59       * As specified in the SAML2 profiles specification.
60       */
61      private static final String COOKIE_NAME = "_saml_idp";
62  
63      /**
64       * By default we keep the cookie around for a week.
65       */
66      private static final int DEFAULT_CACHE_EXPIRATION = 6048000;
67      
68      /**
69       * Do we always go where the cookie tells us, or do we just provide the cookie as a hint.
70       */
71      private boolean alwaysFollow;
72  
73      /**
74       * Is our job to clean up the cookie. 
75       */
76      private boolean deleteCookie;
77      
78      /**
79       * Lipservice towards having a common domain cookie. 
80       */
81      private String cacheDomain; 
82      
83      /**
84       * How long the cookie our will be active? 
85       */
86      private int cacheExpiration;
87      
88      /**
89       * This constructor is called during wayf initialization with it's
90       * own little bit of XML config.
91       * 
92       * @param element - further information to be gleaned from the DOM.
93       */
94      public SamlCookiePlugin(Element element) {
95          /*
96           * <Plugin idenfifier="WayfCookiePlugin" 
97           *         type="edu.internet2.middleware.shibboleth.wayf.plugins.provider.SamlCookiePlugin"
98           *         alwaysFollow = "FALSE"
99           *         deleteCookie = "FALSE"
100          *         cacheExpiration = "number" 
101          *         cacheDomain = "string"/> 
102          */
103         log.info("New plugin");
104         String s;
105 
106         s = element.getAttribute("alwaysFollow");
107         if (s != null && !s.equals("") ) {
108             alwaysFollow = Boolean.valueOf(s).booleanValue();
109         } else {
110             alwaysFollow = true;
111         }
112             
113         s = element.getAttribute("deleteCookie");
114         if (s != null && !s.equals("")) {
115             deleteCookie = Boolean.valueOf(s).booleanValue();
116         } else {
117             deleteCookie = false;
118         }
119             
120         s = element.getAttribute("cacheDomain");
121         if ((s != null) && !s.equals("")) {
122             cacheDomain = s;
123         } else {
124             cacheDomain = "";
125         }
126         
127         s  = element.getAttribute("cacheExpiration");
128         if ((s != null) && !s.equals("")) {
129             
130             try {
131 
132                 cacheExpiration = Integer.parseInt(s);
133             } catch (NumberFormatException ex) {
134                     
135                 log.error("Invalid CacheExpiration value - " + s);
136                 cacheExpiration = DEFAULT_CACHE_EXPIRATION;                       
137             }
138         } else {
139             cacheExpiration = DEFAULT_CACHE_EXPIRATION;
140         }
141     }
142     
143     /**
144      * Create a plugin with the hard-wired default settings.
145      */
146     private SamlCookiePlugin() {
147         alwaysFollow = false;
148         deleteCookie = false;
149         cacheExpiration = DEFAULT_CACHE_EXPIRATION;
150     }
151 
152     /**
153      * This is the 'hook' in the lookup part of Discovery Service processing. 
154      * 
155      * @param req - Describes the current request.  Used to find any appropriate cookies 
156      * @param res - Describes the current response.  Used to redirect the request. 
157      * @param parameter - Describes the metadata.
158      * @param context - Any processing context returned from a previous call. We set this on first call and
159      *                  use non null to indicate that we don't go there again.
160      * @param validIdps The list of IdPs which is currently views as possibly matches for the pattern. 
161      *                  The Key is the EntityId for the IdP and the value the object which describes 
162      *                  the Idp 
163      * @param idpList The set of Idps which are currently considered as potential hints.    
164      * @return a context to hand to subsequent calls
165      * @throws WayfRequestHandled if the plugin has handled the request.
166      * issues a redirect)
167      * 
168      * @see edu.internet2.middleware.shibboleth.wayf.plugins.Plugin#lookup
169      */
170     public PluginContext lookup(HttpServletRequest req,
171                                 HttpServletResponse res,  
172                                 PluginMetadataParameter parameter, 
173                                 Map<String, IdPSite> validIdps,
174                                 PluginContext context,
175                                 List <IdPSite> idpList) throws WayfRequestHandled {
176             
177         if (context != null) {
178             //
179             // We only need to be called once
180             //
181             return context;
182         }
183             
184         if (deleteCookie) {
185             deleteCookie(req, res);
186             //
187             // Only need to be called once - so set up a parameter
188             //
189             return new Context() ;
190         } 
191         List <String> idps = getIdPCookie(req, res, cacheDomain).getIdPList();
192             
193         for (String idpName : idps) {
194             IdPSite idp = validIdps.get(idpName);
195             if (idp != null) {
196                 if (alwaysFollow) {
197                     try {
198                         DiscoveryServiceHandler.forwardRequest(req, res, idp);
199                     } catch (WayfException e) {
200                         // Do nothing we are going to throw anyway
201                         ;
202                     }
203                     throw new WayfRequestHandled();
204                 }
205                 //
206                 // This IDP is ok 
207                 //
208                 idpList.add(idp);
209             }
210         } 
211             
212         return null;
213     }
214 
215     /**
216      * Plugin point which is called when the data is refreshed.
217      * @param metadata - where to get the data from.
218      * @return the value which will be provided as input to subsequent calls
219      * @see edu.internet2.middleware.shibboleth.wayf.plugins.Plugin#refreshMetadata
220      */
221     public PluginMetadataParameter refreshMetadata(MetadataProvider metadata) {
222         //
223         // We don't care about metadata - we are given all that we need
224         //
225         return null;
226     }
227 
228     /**
229      * Plgin point for searching.
230      * 
231      * @throws WayfRequestHandled 
232      * @param req Describes the current request. 
233      * @param res Describes the current response.
234      * @param parameter Describes the metadata.
235      * @param pattern What we are searchign for. 
236      * @param validIdps The list of IdPs which is currently views as possibly matches for the pattern. 
237      *                  The Key is the EntityId for the IdP and the value the object which describes 
238      *                  the Idp 
239      * @param context Any processing context returned from a previous call. We set this on first call and
240      *                use non null to indicate that we don't go there again.
241      * @param searchResult What the search yielded. 
242      * @param idpList The set of Idps which are currently considered as potential hints.    
243      * @return a context to hand to subsequent calls.
244      * @see edu.internet2.middleware.shibboleth.wayf.plugins.Plugin#search
245      * @throws WayfRequestHandled if the plugin has handled the request.
246      * 
247      */
248     public PluginContext search(HttpServletRequest req,
249                                 HttpServletResponse res, 
250                                 PluginMetadataParameter parameter, 
251                                 String pattern,
252                                 Map<String, IdPSite> validIdps,
253                                 PluginContext context,
254                                 Collection<IdPSite> searchResult,
255                                 List<IdPSite> idpList) throws WayfRequestHandled {
256         //
257         // Don't distinguish between lookup and search
258         //
259         return lookup(req, res, parameter, validIdps, context, idpList);
260     }
261 
262     /**
263      * Plugin point for selection.
264      * 
265      * @see edu.internet2.middleware.shibboleth.wayf.plugins.Plugin#selected(javax.servlet.http.HttpServletRequest.
266      *  javax.servlet.http.HttpServletResponse, 
267      *  edu.internet2.middleware.shibboleth.wayf.plugins.PluginMetadataParameter, 
268      *  java.lang.String)
269      * @param req Describes the current request. 
270      * @param res Describes the current response.
271      * @param parameter Describes the metadata.
272      * @param idP Describes the idp.
273      * 
274      */
275     public void selected(HttpServletRequest req, HttpServletResponse res,
276                          PluginMetadataParameter parameter, String idP) {
277             
278         SamlIdPCookie cookie = getIdPCookie(req, res, cacheDomain);
279         String param = req.getParameter(PARAMETER_NAME);
280         
281         if (null == param || param.equals("")) {
282             return;
283         } else if (param.equalsIgnoreCase(PARAMETER_SESSION)) {
284             cookie.addIdPName(idP, -1);
285         } else if (param.equalsIgnoreCase(PARAMETER_PERM)) {
286             cookie.addIdPName(idP, cacheExpiration);
287         }
288     }
289     
290     //
291     // Private classes for internal use
292     //
293     
294     /**
295      * This is just a marker tag.
296      */
297     private static class Context implements PluginContext {}
298     
299     /** 
300      * Class to abstract away the saml cookie for us.
301      */
302     public final class SamlIdPCookie  {
303 
304             
305         /**
306          * The associated request.
307          */
308         private final HttpServletRequest req;
309         /**
310          * The associated response.
311          */
312         private final HttpServletResponse res;
313         /**
314          * The associated domain.
315          */
316         private final String domain;
317         /**
318          * The IdPs.
319          */
320         private final List <String> idPList = new ArrayList<String>();
321             
322         /**
323          * Constructs a <code>SamlIdPCookie</code> from the provided string (which is the raw data. 
324          * 
325          * @param codedData
326          *            the information read from the cookie
327          * @param request Describes the current request. 
328          * @param response Describes the current response.
329          * @param domainName - if non null the domain for any *created* cookie.
330          */
331         private SamlIdPCookie(String codedData, 
332                               HttpServletRequest request, 
333                               HttpServletResponse response, 
334                               String domainName) {
335                     
336             this.req = request;
337             this.res = response;
338             this.domain = domainName;
339                     
340             int start;
341             int end;
342                     
343             if (codedData == null || codedData.equals(""))  {
344                 log.info("Empty cookie");
345                 return;
346             }
347             //
348             // An earlier version saved the cookie without URL encoding it, hence there may be 
349             // spaces which in turn means we may be quoted.  Strip any quotes.
350             //
351             if (codedData.charAt(0) == '"' && codedData.charAt(codedData.length()-1) == '"') {
352                 codedData = codedData.substring(1,codedData.length()-1);
353             }
354                     
355             try {
356                 codedData = URLDecoder.decode(codedData, "UTF-8");
357             } catch (UnsupportedEncodingException e) {
358                 log.error("could not decode cookie");
359                 return;
360             }
361                     
362             start = 0;
363             end = codedData.indexOf(' ', start);
364             while (end > 0) {
365                 String value = codedData.substring(start, end);
366                 start = end + 1;
367                 end = codedData.indexOf(' ', start);
368                 if (!value.equals("")) {
369                     idPList.add(new String(Base64.decode(value)));
370                 }
371             }
372             if (start < codedData.length()) {
373                 String value = codedData.substring(start);
374                 if (!value.equals("")) {
375                     idPList.add(new String(Base64.decode(value)));
376                 }
377             }
378         }
379         /**
380          * Create a SamlCookie with no data inside.
381          * @param domainName - if non null, the domain of the new cookie 
382          * @param request Describes the current request. 
383          * @param response Describes the current response.
384          *
385          */
386         private SamlIdPCookie(HttpServletRequest request, HttpServletResponse response, String domainName) {
387             this.req = request;
388             this.res = response;
389             this.domain = domainName;
390         }
391 
392         /**
393          * Add the specified Shibboleth IdP Name to the cookie list or move to 
394          * the front and then write it back.
395          * 
396          * We always add to the front (and remove from wherever it was)
397          * 
398          * @param idPName    - The name to be added
399          * @param expiration - The expiration of the cookie or zero if it is to be unchanged
400          */
401         private void addIdPName(String idPName, int expiration) {
402 
403             idPList.remove(idPName);
404             idPList.add(0, idPName);
405 
406             writeCookie(expiration);
407         }
408             
409         /**
410          * Delete the <b>entire<\b> cookie contents
411          */
412 
413 
414         /**
415          * Remove origin from the cachedata and write it back.
416          * 
417          * @param origin what to remove.
418          * @param expiration How long it will live.
419          */
420             
421         public void deleteIdPName(String origin, int expiration) {
422             idPList.remove(origin);
423             writeCookie(expiration);
424         }
425 
426         /**
427          * Write back the cookie.
428          * 
429          * @param expiration How long it will live
430          */
431         private void writeCookie(int expiration) {
432             Cookie cookie = getCookie(req);
433                     
434             if (idPList.size() == 0) {
435                 //
436                 // Nothing to write, so delete the cookie
437                 //
438                 cookie.setPath("/");
439                 cookie.setMaxAge(0);
440                 res.addCookie(cookie);
441                 return;
442             }
443 
444             //
445             // Otherwise encode up the cookie
446             //
447             StringBuffer buffer = new StringBuffer();
448             Iterator <String> it = idPList.iterator();
449                     
450             while (it.hasNext()) {
451                 String next = it.next();
452                 String what = new String(Base64.encodeBytes(next.getBytes()));
453                 buffer.append(what).append(' ');
454             }
455                     
456             String value;
457             try {
458                 value = URLEncoder.encode(buffer.toString(), "UTF-8");
459             } catch (UnsupportedEncodingException e) {
460                 log.error("Could not encode cookie");
461                 return;
462             }
463                     
464             if (cookie == null) { 
465                 cookie = new Cookie(COOKIE_NAME, value);
466             } else {
467                 cookie.setValue(value);
468             }
469             cookie.setComment("Used to cache selection of a user's Shibboleth IdP");
470             cookie.setPath("/");
471 
472 
473             cookie.setMaxAge(expiration);
474                     
475             if (domain != null && domain != "") {
476                 cookie.setDomain(domain);
477             }
478             res.addCookie(cookie);
479             
480         }
481     
482         /**
483          * Return the list of Idps for this cookie.
484          * @return The list.
485          */
486         public List <String> getIdPList() {
487             return idPList;
488         }
489     }
490 
491     /**
492      * Extract the cookie from a request.
493      * @param req the request.
494      * @return the cookie.
495      */
496     private static Cookie getCookie(HttpServletRequest req) {
497             
498         Cookie[] cookies = req.getCookies();
499         if (cookies != null) {
500             for (int i = 0; i < cookies.length; i++) {
501                 if (cookies[i].getName().equals(COOKIE_NAME)) { 
502                     return cookies[i];
503                 }
504             }
505         }
506         return null;
507     }
508 
509     /**
510      * Delete the cookie from the response.
511      * @param req The request.
512      * @param res The response.
513      */
514     private static void deleteCookie(HttpServletRequest req, HttpServletResponse res) {
515         Cookie cookie = getCookie(req);
516             
517         if (cookie == null) { 
518             return; 
519         }
520             
521         cookie.setPath("/");
522         cookie.setMaxAge(0);
523         res.addCookie(cookie);
524     }
525     /**
526      * Load up the cookie and convert it into a SamlIdPCookie.  If there is no
527      * underlying cookie return a null one.
528      * @param req The request.
529      * @param res The response.
530      * @param domain - if this is set then any <b>created</b> cookies are set to this domain
531      * @return the new object. 
532      */
533     
534     private SamlIdPCookie getIdPCookie(HttpServletRequest req, HttpServletResponse res, String domain) {
535         Cookie cookie = getCookie(req);
536             
537         if (cookie == null) {
538             return new SamlIdPCookie(req, res, domain);
539         } else {
540             return new SamlIdPCookie(cookie.getValue(), req, res, domain);
541         }
542     }
543 }
544