/global_session

Rack/Rails middleware that enables large-scale distributed Web apps to share session state.

Primary LanguageRubyMIT LicenseMIT

Copyright © 2009-2015 RightScale, Inc. <support@rightscale.com>; see LICENSE for more details.

Preamble

WARNING: This RubyGem was authored in 2010 when Rails 2.1 was state of the art. Its Rails integration has not been kept up to date over time; it is untested with Rails 3, 4 and 5, and its generators are broken with Rails above 2.3.5.

We continue to support the Rack middleware and other components of this gem, and recommend using it as a plain old Rack middleware in your Rails apps. Instructions for doing so are provided in this README.

Introduction

GlobalSession enables multiple heterogeneous Web applications to share session state in a cryptographically secure way, facilitating single sign-on and enabling easier development of distributed applications that make use of architectural strategies such as sharding or separation of concerns.

In other words: it glues your Web apps together by letting them share session state. This is done by putting the session itself into a cookie and adding some crypto to protect against tampering.

Maintained by

- [RightScale Engineering](https://github.com/rightscale)

Merge to master whitelist

- @tony-spataro-rs

What Is It Not?

This gem does not provide a complete solution for identity management. In particular, it does not provide any of the following:

  • federation – aka cross-domain single sign-on – use SAML for that.

  • authentication – the application must authenticate the user.

  • authorization – the application is responsible for using the contents of the global session to make authorization decisions.

  • secrecy – global session attributes can be signed but never encrypted. Protect against third-party snooping using SSL. Group secrecy is expensive; if you don’t want your users to see their session state, put it in a database, or in an encrypted local session cookie.

  • replication – the session authorities must have some way to share information about the database of users in order to authenticate them and place identifying information into the global session.

  • single sign-out – the authorities must have some way to broadcast a notification when sessions are invalidated; they can override the default Directory implementation to do realtime revocation checking.

Examples

Make a YML configuration file

The config file format is designed to be self-documenting. The most important data are: what data can be in your global session (‘attributes`), what directory contains your `.pub` files with authorities’ public keys (‘keystore.public`), and the locatio nof `.key` private key file, if any, used by this app (`keystore.private`).

You can omit ‘keystore.private` if the app should be able to read, but not write, global sessions.

If you have asymmetrical trust (e.g. dev trusts production but not vice-versa), you can include an optional ‘trust` list. By default, every public key file is trusted.

common:
  attributes:
    signed:
    - user
    insecure:
    - favorite_color
  cookie:
    name: global_session
  keystore:
    public: config/authorities
  renew: 30
  timeout: 60
development:
  keystore:
    private: config/authorities/dev
production:
trust:
  - prod
keystore:
  private: config/authorities/prod

Make a new keypair for a GlobalSession authority

Decide on a name for your authority. The name is a short string that identifies a pair of key files on disk (one public, one private) which will be used to sign and verify sessions. If you have mutual trust between every app in your architecture, then you only need one authority and your domain name, e.g. ‘example-com`, is a fine choice of name. If you want partition trust within your architecture, then authorities could be named after environments (`staging`, `production`), regions (`us`, `eu`) or even specific apps (`frontend`, `api`) depending on where you draw the trust boundaries.

Figure out where key files live in your application. This is whatever value you set in the ‘keystore: public: …` directive in the configuration.

If you have complete, mutual trust between all components of your architecture, then something based on your organization’s domain name (e.g. ‘example-com`) is a fine choice.

Open irb or your console of choice and require the ‘global_session` gem.

# for RSA cryptosystem, do this
keypair = GlobalSession::Keystore.create_keypair(:RSA, 1024)
public_pem  = keypair.public_key.to_pem
private_pem = keypair.to_pem

# for EC cryptosystem, do this
# note missing group parameter; default is 'prime256v2'
keypair = GlobalSession::Keystore.create_keypair(:EC)
private_pem = keypair.to_pem
keypair.private_key = nil
public_pem  = keypair.to_pem

# write keys to disk
File.open('config/authorities/example-com.pub', 'w') { |f| f.write public_pem }
File.open('config/authorities/example-com.key', 'w') { |f| f.write private_pem }

Integration with Rails

Install the GlobalSession middleware in your application startup. Open ‘environment.rb` or `application.rb` (depending on your Rails version) and add a new file to `config/initializers` to configure and install the middleware:

configuration = GlobalSession::Configuration.new('config/global_session.yml', Rails.env)
directory = GlobalSession::Directory.new(configuration)

Integration with Rack

Install the GlobalSession middleware into your Rack stack; pass a config and a directory object to its initializer. For instance, in config.ru:

configuration = GlobalSession::Configuration.new('path/to/config.yml', RACK_ENV)
directory = GlobalSession::Directory.new(configuration)
use ::GlobalSession::Rack::Middleware, configuration, directory

Application.config.middleware.insert_before(Application.config.session_store, ::Rack::Cookies)
Application.config.middleware.insert_before(Application.config.session_store, ::Rack::GlobalSession, configuration, directory)

Note that the GlobalSession middleware depends on ‘Rack::Cookies`; be sure to install them both, and in the proper order.

Global Session Contents

Global session state is stored as a cookie in the user’s browser and/or sent with every request as an HTTP Authorization header. If your app uses the Authorization header, then it’s responsible for communicating new or changed header values to clients out-of-band (i.e. as part of an OAuth refresh-token operation). If your app uses the cookie, GlobalSession will take care of updating the cookie whenever session values change.

Data-wise, the session is a JSON dictionary containing the following stuff:

  • session metadata (UUID, created at, expires at, signing authority)

  • signed session attributes (e.g. the authenticated user ID)

  • insecure session attributes (e.g. the last-visited URL)

  • a cryptographic signature of the metadata and signed attributes

The global session is unserialized and its signature is verified whenever Rack receives a request. The cookie’s value is updated whenever attributes change. As an optimization, the signature is only recomputed when the metadata or signed attributes have changed; insecure attributes can change “for free.”

Because the security properties of attributes can vary, GlobalSession requires all possible attributes to be declared up-front in the config file. The ‘attributes’ section of the config file defines the schema for the global session: which attributes can be used, which can be trusted to make authorization decisions (because they are signed), and which are insecure and act only as “hints” about the session.

Since the session is serialized as JSON, only a limited range of object types can be stored in it: nil, strings, numbers, lists, hashes, booleans and other Ruby primitives.

Detailed Information

Global Session Domain

We refer to collection of all Web application instances capable of using the global session as the “domain.” The global session domain may consist of any number of distinct nodes, possibly hidden behind load balancers or proxies. The nodes within the domain may all be running the same Rails application, or they may be running different codebases that represent different parts of a distributed application. (They may also be using app frameworks other than Rails.)

The only constraint imposed by GlobalSession is that all nodes within the domain must have end-user-facing URLs within the same second-level DNS domain. This is due to limitations imposed by the HTTP cookie mechanism: for privacy reasons, cookies will only be sent to nodes within the same domain as the node that first created them.

For example, in my GlobalSession configuration file I might specify that my cookie’s domain is “example.com”. My app nodes at app1.example.com and app2.example.com would be part of the global session domain, but my business partner’s application at app3.partner.com could not participate.

If your app uses an Authorization header instead of cookies, the domain-name constraint does not apply!

Authorities and Relying Parties

A node that can create or update the global session is said to be an “authority” (because it’s trusted by other parties to make assertions about global session state). An application that can read the global session is said to be a “relying party.” In practice, every application is a relying party, but not all of them need to be authorities.

There is an RSA key pair associated with each authority. The authority’s public key is distribued to all relying parties, but the private key must remain a secret to that authority (which may consist of many individual nodes).

This system allows for significant flexibility when configuring a distributed app’s global session. There must be at least one authority, but for many apps one authority (plus an arbitrary number of relying parties, which do not need a key pair) will be sufficient.

In general, two systems should be part of the same authority if there is no trust boundary between them – that is to say, trust between the two systems is unlimited in both directions.

Here are some reasons you might consider dividing your systems into different authorities:

  • beta/staging system vs. production system

  • system hosted by a third party vs. system hosted internally

  • e-commerce app vs. storefront app vs. admin app

The Keystore

The Directory is a Ruby object that provides lookups of public and private keys. Given an authority name (as found in a session cookie), the Directory can find the corresponding RSA public key.

If the local system is an authority itself, #private_key_name will return the authority name and #private_key will return an RSA private key suitable for signing session attributes.

The Keystore implementation included with GlobalSession uses the filesystem as the backing store for its key pairs. Its #initialize method accepts a filesystem path that will be searched for files containing PEM-encoded public and private keys (the same format used by OpenSSH). This simple Directory implementation relies on the following conventions:

  • Public keys have a *.pub extension.

  • Private keys have a *.key extension.

  • If a node is an authority, then one (and only one) *.key file should exist.

  • The local node’s authority name is inferred from the name of the private key file.

When used with a Rails app, GlobalSession expects to find its keystore in config/authorities. You can use the global_session generator to create new key pairs. Remember never to check a *.key file into a public repository!! (*.pub files can be checked into source control and distributed freely.)

If you wish all of the systems to stop trusting an authority, simply delete its public key from config/authorities and re-deploy your app.

The keystore needs to be told where to find its keys. This is accomplished by setting some configuration attributes like so:

keystore:
  public:
    - config/authorities
    - config/more_authorities
  private: config/authorities/my_private.key

The filename of keys is relevant; after stripping the *.pub or *.key extension, the remainder of the file’s basename is taken to be the authority name. For instance, “production.pub” is the public key of the authority named “production” and “development.key” is the private key of the authority named “development.”

The Directory

The Directory is a Ruby object that performs session management operations, including:

  • Checking whether sessions have become invalid (e.g. after sign-out)

  • Creating new sessions

  • Unserializing existing sessions

  • Renewing sessions if they will expire

  • Updating session signatures

In GlobalSession v3, Directory also presents methods for key management, but these are all delegated to the Keystore class. In v4, the concerns of Directory and Keystore will be fully separated.

Implementing Your Own Directory Provider

To replace or enhance the built-in Directory, simply create a new class that extends Directory and put the class somewhere in your app (the lib directory is a good choice). In the GlobalSession configuration file, specify the class name of the directory under the ‘common’ section, like so:

common:
  directory:
    class: MyCoolDirectory