Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Work in progress document on design of Sessions in the IdP.

IdPSession

What we're maybe storing:

...

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.

IdPSession

The core object managed by the Session layer is an IdPSession, which contains the following:

  • an ID
  • creation time
  • last activity time
  • canonical principal name map of of Subject
  • zero or more AuthenticationResults indexed by the result's flow ID to AuthenticationResult
  • map of SP entityID to ServiceSession

I think the secret is irrelevant. V2 uses session cookies that are MAC'd with the secret and contain V4 and V6 client addresses. This is more complex than we need, we can just replace the secret with the two addresses and use a highly random ID in the session cookie like the SP does.

A client side session probably doesn't need an ID strictly speaking but we probably want it for logging / auditing.

If we don't use a multi-map then each type of authentication configured would be tracked only once per session, so reuse of a method might replace the older copy. This seems ok, probably, and avoids the problem of garbage collection of older results.

ServiceSession

What we're maybe storing:

  • ID of service
  • maybe a creation time, do we care?
  • thinking we might track an expiration, otherwise how do we bound the number of these?
  • flow ID used for service (could be fresh, or a reuse of active result derived from flow)
  • for SAML at least, we would need the NameID and SessionIndex issued in the transaction to be able to propagate logout

The latter is a bitch, because we don't have it until very late in the flow. Thinking this means we don't want to actually update the session until very late (or at least not write it back to storage, which is what update really means in this context). Or we explicitly support this in the API instead of bundling all this together as a single session object update.

We're storing custom Principals to handle SAML-specific stuff elsewhere, maybe we create custom ServiceSession types with additional data tracked. This might help allow for heterogenous sessions with different protocols used with particular SPs. I know we didn't want hierarchies, but this feels more like data modeling to me, and it's just one deep.

Storage

Pretty well decided that we have two modes, client-side and server-side. Client-side means we don't store the ServiceSessions, but try to fit the rest. Obviously this makes logout completely impossible. Even if we work around the problem of not having access to the session when the request comes in, we will never have the data needed to propagate the logout. We discussed splitting the storage model up so that anything needed for logout would be managed by a separate StorageService instance that would have to be non-client-side.

Lookup Requirements

Main one obviously is lookup via client delivering session or client cookie with key to session, latter requiring basic index by session ID. Note: validation of a session's use is a separate issue. We are not going to overload lookup with validation, excepting that lookup of an expired session can be blocked by the storage API already.

V2 supports lookups in conjunction with queries. I'm very reluctant to support this because it's ambiguous; a non-transient NameID could map to multiple Sessions and I'm not sure that's helpful, but if we do it, I think we have to expose all of them. Also, the query lookups for a session are done based on the principal name after reversing the NameID. I think we should dump this, it's superfluous if we index by NameID. We didn't used to index by NameID in V2, so that's probably why it was done that way.

Logout requires a lookup by SP, NameID, and optional SessionIndex. With no SessionIndex, we definitely have to handle multiple Sessions coming back by design. The use case for non-indexed logout was about terminating sessions with multiple devices. With a SessionIndex, we should only get a single one back.

So we have:

Session ID -> Session (1:1)

SP + NameID -> Session (1:N)

SP + NameID + SAML SessionIndex -> Session (1:1)

...

  • zero or more SPSessions indexed primarily by the SP's name/entityID

An IdPSession can also be bound during creation and afterward to client addresses, one per address family, 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 construct is discussed extensively on the Authentication page, and is how we "remember" an act of authentication for Single Sign-On. Only results stored in an IdPSession can ever be eligible for SSO, so disabling sessions globally disables SSO.

SPSession

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 at all:

  • some kind of distributed logout
  • user interface considerations

The former is probably as worthless in practice as its always been, but we are accomodating SAML logout requirements explicitly in the session design, 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 of.

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:

  • 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.

SessionManager and SessionResolver

There are two interfaces used to interact with the Session layer, one for creating/destroying them (SessionManager) and one for looking them up (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)

Storage

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 a secure cookie and reads/writes it on a per-request basis.

The Session implementation can be configured to store SPSession data only in the case that the storage implementation is not per-request/client-side storage, because of size constraints.

Technical Details

There are 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 and 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 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.

---

With the SP, I don't try and turn the NameID, etc. into a fully unique key, just a mostly unique one. I just index by a possibly truncated version of the NameID value, ignoring the qualifiers. Then I validate the results I get back and check for an exact match using the content of the Session before I include it in the set returned.

...