Cleanup Session management and timeout
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / serviceprovider / SessionManager.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /*
18  * SessionManager creates, maintains, and caches Session objects.
19  * 
20  * The SessionManager is a singleton object.
21  * A reference to the unique SessionManger object can always be obtained
22  * from the ServiceProviderContext.getSessionManager() method.
23  * 
24  * Sessions should only be created, modified, and deleted through methods
25  * of this class so that the in-memory collection and any disk Cache can
26  * also be changed. Disk cache implementations are referenced through the
27  * SessionCache interface. 
28  */
29 package edu.internet2.middleware.shibboleth.serviceprovider;
30
31 import java.security.SecureRandom;
32 import java.util.ArrayList;
33 import java.util.HashMap;
34 import java.util.Iterator;
35 import java.util.Map;
36 import java.util.TreeMap;
37
38 import org.apache.log4j.Logger;
39 import org.opensaml.SAMLAssertion;
40 import org.opensaml.SAMLAttribute;
41 import org.opensaml.SAMLAttributeStatement;
42 import org.opensaml.SAMLAuthenticationStatement;
43 import org.opensaml.SAMLResponse;
44 import org.opensaml.SAMLStatement;
45
46 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig.ApplicationInfo;
47
48 /**
49  * <p>SessionManager manages the memory and disk Cache of Session objects.</p>
50  * 
51  * <p>setSessionCache(SessionCache s) is an "IOC" wiring point. Pass it
52  * an implementation of the SessionCache interface.</p> 
53  * 
54  * @author Howard Gilbert
55  */
56 public class SessionManager {
57         
58         /*
59          * Sessions can be saved using any Persistance Framework. If a Cache
60          * is created, the following pointer is filled in and we start to 
61          * use it.
62          */
63         private static Logger log = Logger.getLogger(SessionManager.class.getName());
64         
65         private SessionCache cache = null; // By default, use memory cache only
66         
67         private TreeMap sessions = new TreeMap(); // The memory cache of Sessions
68         
69         private static ServiceProviderContext context = ServiceProviderContext.getInstance();
70     
71     
72     
73     /**
74      * Generate a 16 byte random ASCII string using 
75      * cryptgraphically strong Java random generator.
76      * 
77      * @return generated string
78      */
79         public String generateKey() {
80             byte[] trash = new byte[16];
81             char[] ctrash = new char[16];
82                 String key;
83             do {
84                 rand.nextBytes(trash);
85                 for (int i=0;i<16;i++) {
86                     trash[i]&=0x3f;
87                     ctrash[i]=(char)table.charAt(trash[i]);
88                 }
89                         key=new String(ctrash);
90             } while (null!=sessions.get(key));
91             return key;
92         }
93     private static SecureRandom rand = new SecureRandom();
94     private static final String table = "0123456789" +
95         "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
96         "abcdefgjikjlmnopqrstuvwxyz"+
97         "$@";
98         
99         
100         /**
101      * Find a Session object given its sessionID key. 
102      * 
103      * <p>Will not match uninitialized (reserved) Sessions.</p>
104      * 
105      * @param sessionId ID and key of session
106      * @param applicationId Sanity check, must match session contents
107      * @return null if not found, else Session
108          */
109         public synchronized Session findSession(String sessionId, String applicationId ) {
110                 if (sessionId==null || applicationId==null)
111                         throw new IllegalArgumentException();
112                 Session s = (Session) sessions.get(sessionId);
113                 if (s==null) {
114                         log.warn("Session not found with ID "+sessionId);
115                         return null;
116                 }
117                 if (null==s.getAuthenticationAssertion()) {
118                         log.warn("Uninitialized (reserved) Session has ID "+sessionId);
119                     return null;
120                 }
121                 if (!applicationId.equals(s.getApplicationId())) {
122                         log.error("Session ID "+sessionId+" doesn't match application "+applicationId);
123                         return null;
124                 }
125                 if (s.isExpired()) {
126                         log.error("Session ID "+sessionId+" has expired.");
127                         return null;
128                 }
129                 s.renew();
130                 return s;
131         }
132
133     /**
134      * Locate an empty Session block reserved by a call from the RM.
135      * This is a test on the validity of a claimed reserved SessionID.
136      * 
137      * @param sessionId
138      * @return Session block (uninitialized).
139      */
140         private synchronized Session findEmptySession(String sessionId) {
141                 if (sessionId==null)
142                         throw new IllegalArgumentException();
143                 Session s = (Session) sessions.get(sessionId);
144                 if (s==null) {
145                         log.warn("Session not found with ID "+sessionId);
146                         return null;
147                 }
148                 if (null!=s.getAuthenticationAssertion()){
149                         log.error("Active Session found when looking for reserved ID:"+sessionId);
150                     return null;
151                 }
152                 s.renew();
153                 return s;
154         }
155         
156         
157     /**
158      * Called internally to add a Session block to the cache.
159      * @param s Session
160      */
161         protected synchronized void add(Session s) {
162                 if (s==null)
163                         throw new IllegalArgumentException();
164                 log.debug("Session added: "+s.getKey());
165                 sessions.put(s.getKey(), s);
166                 if (cache!=null)
167                         cache.add(s);
168         }
169         
170     /**
171      * Called internally to replace a Session block in the cache, 
172      * or more commonly to replace a block with itself and then just
173      * notify the cache that it has been refreshed.
174      * 
175      * @param s Session
176      */
177         protected synchronized void update(Session s) {
178                 if (s==null)
179                         throw new IllegalArgumentException();
180                 s.renew();
181                 log.debug("Session updated: "+s.getKey());
182                 sessions.put(s.getKey(), s);
183                 if (cache!=null)
184                         cache.update(s);
185         }
186         
187     /**
188      * Called internally to remove a Session from the cache. Since
189      * there is no logout, this is called when a Session expires.
190      * 
191      * @param s Session
192      */
193         protected synchronized void remove(Session s) {
194                 if (s==null)
195                         throw new IllegalArgumentException();
196                 log.debug("Session removed: "+s.getKey());
197                 sessions.remove(s.getKey());
198                 if (cache!=null)
199                         cache.remove(s);
200         }
201         
202     /**
203      * Called by a timer driven process to check for expired
204      * Sessions.
205      *
206      */
207         protected synchronized void expireSessions() {
208                 Iterator iterator = sessions.entrySet().iterator();
209                 while (iterator.hasNext()) {
210                         Map.Entry entry = (Map.Entry) iterator.next();
211                         Session session = (Session) entry.getValue();
212                         if (session.isExpired()) {
213                                 log.info("Session " + session.getKey() + " has expired.");
214                                 iterator.remove();
215                         }
216                 }
217         }
218         
219
220         
221         /**
222          * Store Principal information identified by generated UUID.<br>
223          * Called from Authentication Assertion Consumer [SHIRE]
224          * 
225          * @param applicationId The application for this session
226          * @param ipaddr The client's remote IP address from HTTP
227          * @param entityId The Entity of the AA issuing the authentication
228          * @param assertion Assertion in case one needs more data
229          * @param authentication subset of assertion with handle
230          * @return String (UUID) to go in the browser cookie
231          */
232         public 
233                         String 
234         newSession(
235                         String applicationId, // not null
236                         String ipaddr,        // may be null
237                         String entityId,      // not null
238                         SAMLAssertion assertion, //not null
239                         SAMLAuthenticationStatement authenticationStatement, // may be null
240                         String emptySessionId // may be null
241                         ){
242                 
243         if (applicationId==null)
244             throw new IllegalArgumentException("applicationId null");
245         if (entityId==null)
246             throw new IllegalArgumentException("entityId null");
247         if (assertion==null)
248             throw new IllegalArgumentException("assertion null");
249         
250                 ServiceProviderConfig config = context.getServiceProviderConfig();
251                 ApplicationInfo appinfo = config.getApplication(applicationId);
252                 String sessionId = null;
253                 boolean isUpdate = false; // Assume new object
254                 
255         /*
256          * If the Id of a reserved, empty session is provided, then find
257          * that object and fill it in. Otherwise, create a new object.
258          */
259                 Session session = null;
260                 if (emptySessionId!=null) {
261                     session = findEmptySession(emptySessionId);
262                 }
263                 if (session==null) {
264                     session = new Session(generateKey(),
265                             appinfo.getMaxSessionLife(), 
266                             appinfo.getUnusedSessionTimeout(), 
267                             config.getDefaultAttributeLifetime());
268                 } else {
269                     isUpdate=true; // mark so object is updated, not added
270                 }
271                 session.setApplicationId(applicationId);
272                 session.setIpaddr(ipaddr); // may be null
273                 session.setEntityId(entityId);
274                 
275                 session.setAuthenticationAssertion(assertion);
276                 session.setAuthenticationStatement(authenticationStatement); // may be null
277                 
278                 sessionId = session.getKey();
279
280                 if (isUpdate)
281                         update(session);  
282                 else
283                         add(session);
284                 
285             log.debug("New Session created "+sessionId);
286
287                 return sessionId;
288         }
289     
290     /**
291      * Create an empty Session object. Reserves a SessionId for the RM
292      * that can later be filled in with data from the SSO Assertion.
293      * 
294      * @param applicationId The <Application> associated with the Session.
295      * @return
296      */
297         public 
298         String 
299 reserveSession(
300         String applicationId 
301         ){
302         if (applicationId==null)
303             throw new IllegalArgumentException("applicationId null");
304             
305         ServiceProviderConfig config = context.getServiceProviderConfig();
306         ApplicationInfo appinfo = config.getApplication(applicationId);
307             String sessionId = null;
308             Session session= new Session(generateKey(),
309                 appinfo.getMaxSessionLife(), 
310                 appinfo.getUnusedSessionTimeout(), 
311                 config.getDefaultAttributeLifetime());
312             session.setApplicationId(applicationId);
313             
314             sessionId = session.getKey();
315             
316             add(session);
317             
318             log.debug("SessionId reserved "+sessionId);
319             
320             return sessionId;
321 }
322         /**
323          * <p>IOC wiring point to plug in an external SessionCache implementation.
324          * </p>
325          * 
326          * @param cache Plugin object implementing the SessionCache interface
327          */
328         public synchronized void 
329         setCache(
330                         SessionCache cache) {
331                 
332                 if (cache==null)
333                         throw new IllegalArgumentException("Session cache is null");
334             log.info("Enabling Session Cache");
335                 /*
336                  * The following code supports dynamic switching from
337                  * one cache to another if, for example, you decide
338                  * to change databases without restarting Shibboleth.
339                  * Whether this is useful or not is a matter of dispute.
340                  */
341                 if (this.cache!=null) { // replacing an old cache
342                         this.cache.close(); // close it and leave it for GC
343                         return;
344                 }
345                 
346                 this.cache = cache; 
347                 
348                 /*
349                  * Make sure the Cache knows about in memory sessions
350                  * 
351                  * Note: The cache should probably be wired prior to letting
352                  * the Web server process requests, so in almost all cases this
353                  * block will not be neeed. However, we may allow the configuration
354                  * to change dynamically from uncached to cached in the middle
355                  * of a Shibboleth run, and this allows for that possiblity.
356                  */
357                 if (sessions.size()!=0) {
358                         for (Iterator i=sessions.values().iterator();i.hasNext();) {
359                                 Session s = (Session) i.next();
360                                 cache.add(s);
361                         }
362                 }
363                 
364                 /*
365                  * Now load any Sessions in the cache that are not in memory
366                  * (typically after a reboot).
367                  */
368                 cache.reload(sessions);
369         }
370         
371     /**
372      * Diagnostic routine not used during normal processing.
373      * 
374      * @param session
375      * @return
376      */
377         public static StringBuffer dumpAttributes(Session session) {
378             StringBuffer sb = new StringBuffer();
379         SAMLResponse attributeResponse = session.getAttributeResponse();
380         Iterator assertions = attributeResponse.getAssertions();
381         while (assertions.hasNext()) {
382             SAMLAssertion assertion = (SAMLAssertion) assertions.next();
383             Iterator statements = assertion.getStatements();
384             while (statements.hasNext()) {
385                 SAMLStatement statement = (SAMLStatement) statements.next();
386                 if (statement instanceof SAMLAttributeStatement) {
387                     SAMLAttributeStatement attributeStatement = 
388                         (SAMLAttributeStatement) statement;
389                     
390                     // Foreach Attribute in the AttributeStatement
391                     Iterator attributes = attributeStatement.getAttributes();
392                     while (attributes.hasNext()) {
393                         SAMLAttribute attribute = 
394                             (SAMLAttribute) attributes.next();
395                         String name = attribute.getName();
396                         String namespace = attribute.getNamespace();
397                         Iterator values = attribute.getValues();
398                         while (values.hasNext()){
399                             String val = (String) values.next();
400                             sb.append(name+" "+namespace+" "+val);
401                         }
402                     }
403                 }
404             }
405         }
406             
407             return sb;
408         }
409
410     /**
411      * Extract all attributes from the Session in a simpler format than
412      * the complete SAML structure.
413      * 
414      * @param session
415      * @return
416      */
417         public static Map /*<String,String>*/
418         mapAttributes(Session session) {
419             Map /*<String,String>*/attributeMap = new HashMap/*<String,String>*/();
420             SAMLResponse attributeResponse = session.getAttributeResponse();
421                 if (attributeResponse==null)
422                         return attributeMap;
423         Iterator assertions = attributeResponse.getAssertions();
424         while (assertions.hasNext()) {
425             SAMLAssertion assertion = (SAMLAssertion) assertions.next();
426             Iterator statements = assertion.getStatements();
427             while (statements.hasNext()) {
428                 SAMLStatement statement = (SAMLStatement) statements.next();
429                 if (statement instanceof SAMLAttributeStatement) {
430                     SAMLAttributeStatement attributeStatement = 
431                         (SAMLAttributeStatement) statement;
432                     
433                     // Foreach Attribute in the AttributeStatement
434                     Iterator attributes = attributeStatement.getAttributes();
435                     while (attributes.hasNext()) {
436                         SAMLAttribute attribute = 
437                             (SAMLAttribute) attributes.next();
438                         String name = attribute.getName();
439                         String namespace = attribute.getNamespace();
440                         ArrayList list = new ArrayList();
441                         Iterator values = attribute.getValues();
442                         String val="";
443                         while (values.hasNext()){
444                             val = (String) values.next();
445                             list.add(val);
446                         }
447                         if (list.size()==1)
448                             attributeMap.put(name,val);
449                         else
450                             attributeMap.put(name,list);
451                     }
452                 }
453             }
454         }
455             
456             return attributeMap;
457         }
458         
459 }