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