The Session layer in the IdP is what tracks information associated with a Subject across multiple transactions separated significantly in time. It is not involved in managing state for a particular transaction or for a web flow; that's managed by Spring as part of the SWF layer.
The core object managed by the Session layer is net.shibboleth.idp.session.IdPSession, which contains the following:
- an ID
- creation time
- last activity time
- canonical principal name of the Subject
- zero or more net.shibboleth.idp.authn.AuthenticationResult objects indexed by the result's flow ID
- zero or more net.shibboleth.idp.session.SPSession objects indexed primarily by the SP's unique name (entityID in SAML)
An IdPSession can also be bound during creation and afterward to client addresses, one per address family (e.g. IPv4, IPv6), and offers a method to check for a timeout that also updates the last activity time. This decouples use cases that care about client address or timeout checks from those that don't.
Only a single AuthenticationResult for a given flow, and only a single SPSession for a given SP are tracked in a session. Of course, multiple such objects might exist in distinct sessions with a Subject (e.g., across different devices).
The AuthenticationResult object is discussed extensively on the Authentication page, and is how the IdP "remembers" an act of authentication for Single Sign-On. Only results stored in an IdPSession are ever reused for SSO, so disabling sessions globally disables SSO.
An SPSession is a way to track the authentication interactions the IdP has been involved in during a session (in SAML terms, the SPs it has issued assertions to). There are two main reasons to do this:
- some kind of distributed logout
- user interface considerations
The former is as worthless in practice as its always been, but the design accomodates SAML logout requirements explicitly, at a massive cost in code complexity. The latter is to support scenarios in which a UI component may need to present information about the services a subject may have accessed. Obviously this overlaps with logout, but could also be useful in other cases. It might even substitute for logout by giving the subject information about what's not going to be logged out, but in turn that just gives an attacker sitting at a user's terminal more information about what he/she can access.
This aspect of the Session layer can be disabled independently of the rest, to simplify storage requirements or just save cycles. In particularly, to store sessions entirely within a cookie, this tracking must be off, for size reasons.
Within an SPSession, we track:
- unique name/ID of service
- creation time
- expiration time
- authentication flow ID used to fulfill the service's request for authentication
- an optional secondary key that may be needed to lookup the SPSession by alternative means
The last one is really for SAML; we have to store the NameID and SessionIndex issued to the SP because that's how logout works. By abstracting this behind a generic interface property, the code isn't SAML-specific except where needed.
Because the actual underlying SPSession type is going to be protocol-specific, it's left to the profile web flow to eventually create and attach it, rather than part of the authentication layer.
There are two interfaces used to interact with the Session layer, one for creating/destroying them (net.shibboleth.idp.session.SessionManager) and one for looking them up (net.shibboleth.idp.session.SessionResolver). In practice they are implemented together.
The SessionManager interface is very minimal because actual updates to an IdPSession are done via methods on the IdPSession, not through the SessionManager. This is more elegant for the caller, but generally means a particular SessionManager implementation is also supplying its own custom implementation of IdPSession to manage changes.
The SessionResolver is designed around the Resolver notion used in a lot of the code base, and lookups are done based on custom Criterion objects that provide for the use cases we have for session access, such as:
- by session ID
- by implicit session ID found in a servlet request (i.e., a cookie, though this isn't necessarily required)
- by a secondary lookup of an SP ID plus custom key (this supports the SAML logout case)
As a technical matter, the session and storage layers are distinct, but in practice a lot of what SessionManager and SessionResolver have to do depends on the way storage is handled. The concrete implementation provided for the session interfaces is built on top of the StorageService abstraction in OpenSAML.
Any storage implementation able to satisfy that contract will work transparently with the session implementation, including one already implemented that stores data in HTML Local Storage or a cookie and reads/writes it on a per-request basis.
There are a large number of unusual features implemented in the StorageService and the session layer is the reason for that. It's trying to serve two goals: providing a very customizeable storage layout for advanced use cases like this one, but limiting the impact of that complexity on the actual storage plugin, which is meant to be highly unspecialized and use very opaque storage formats and layouts.
The current implementation manages the storage of an IdPSession as a set of records under a context matching the session ID. One of those records is a "master" record with a fixed key of "_session" that contains a JSON serialization of the "core" sesson data attached to the IdPSession interface. The master record also contains a pair of arrays containing the keys to all associated AuthenticationResult or SPSession objects (which are the flow ID or SP name/ID respectively). This is done because the storage API doesn't provide a way to enumerate the keys within a context, so this provides a foreign key lookup from an IdPSession to its content.
The master record is set to expire based on the session timeout value, and the expiration slides forward on every update of the activity time. There is no actual "lifetime" bound because the session itself has no security value, so as long as a session continues to see use, it will stay alive. The size of the session is bounded by making sure the individual records that make up the session all have appropriate expirations.
Attached to the same context as the master record, each individual AuthenticationResult and SPSession added to the IdPSession is serialized and stored under the foreign key stored in the master record (the flow ID or SP name/ID).
An AuthenticationResult is serialized by the corresponding AuthenticationFlowDescriptor, which implements the StorageSerializer API and handles all the details. Similarly to the master record, the expiration is timeout-driven, based on the last activity of the result, along with an offset that prevents a result from disappearing from storage too quickly.
An SPSession is serialized using a plugin registered for the underlying SPSession class, which allows subclasses to contain extended information specific to new protocols. To make this reversible, the class name of the SPSession is prefixed to the serialized data so it can be read back to determine the right plugin to use to reconstitute the object. The expirations are set based on the actual expiration of the SPSession (as determined by the profile flow that created it) plus some "slop" that keeps knowledge of the SPSession available for logout purposes.
Collectively, these records all expire at somewhat varying times, but they are all bounded and are eventually cleaned up by the StorageService without any intervention.
An example layout:
|<session ID>||_session||<serialized form of IdPSession and subrecord keys>||session last activity + timeout + offset|
|<session ID>||authn/JAAS||<serialized AuthenticationResult>||result last activity + timeout + offset|
|<session ID>||authn/SPNEGO||<serialized AuthenticationResult>||result last activity + timeout + offset|
|<session ID>||https://sp.example.org/shibboleth||net.shibboleth.idp.saml.session.SAML2Session:<serialized SPSession>||SP session expiration + offset|
|<session ID>||net.shibboleth.idp.saml.session.SAML2Session:<serialized SPSession>||SP session expiration + offset|
A word about versioning: a big reason for manipulating the various record expirations is to be able to update and recover the last activity time without actually touching the record. This is safe because the versioning policy for such a field would be last-update-wins anyway, and it avoids needing to modify the serialized form of a record to perform the most common update.
Additionally, AuthenticationResult and SPSession records are otherwise static; they can't change in any other respects, so once serialized they never change. The master record does change any time an address is added, or a sub-record is attached (to maintain the foreign key lists), and the record versioning feature of the StorageService prevents race conditions when updates occur at the same time.
If all of that sounds horrible, the real fun is implementing a secondary index using the SPSession records to be able to track back to an IdPSession based on an SP name and a custom key. Without SQL semantics, the secondary index is implemented using a simple delimited list of session IDs in a record keyed by the SP name and whatever custom key is appropriate for the session type. This is implementation-specific, but in SAML 2 is probably going to be a truncation of the NameID value. It doesn't have to be unique because we're storing a list of sessions in the record, not just one, and we can independently validate which sessions really "match" in other ways.
The index is maintained by a read followed by a create or update to maintain the session ID list in the record. This is guarded using the versioning support, again, to prevent races and maintain coherent data.
Another "trick" is that the expiration of the record is based on the expiration of the SPSession record being indexed on. This ensures that the index record survives at least as long as every SPSession that may trigger a lookup.
The one warning is that this very low-tech solution breaks under load testing scenarios, because that tends to generate a huge number of sessions with the same SP name and custom key, whereas under normal use you never see significant numbers that are identical. With such a large number, reading and writing the list of sessions becomes inefficient. The same problem has been observed with the Shibboleth SP for the same reason. In practice it doesn't matter, one simply disables this secondary indexing when load testing.
Following along with the example above, the secondary records created might be:
|<NameID value>||<session ID>||SP session expiration + offset|
|<NameID value>||<session ID>||SP session expiration + offset|