lua-resty-session

Session Library for OpenResty - Flexible and Secure

$ opm get bungle/lua-resty-session

lua-resty-session

lua-resty-session is a secure, and flexible session library for OpenResty.

TL;DR;

  • Sessions are immutable (each save generates a new session), and lockless.

  • Session data is AES-256-GCM encrypted with a key derived using HKDF-SHA256.

  • Session has a fixed size header that is protected with HMAC-SHA256 MAC with a key derived using HKDF-SHA256.

  • Session data can be stored in a stateless cookie or in various backend storages.

  • A single session cookie can maintain multiple sessions across different audiences.

Note: Version 4.0.0 was a rewrite of this library with a lot of lessons learned during the years. If you still use older version, please refer old documentation.

Status

This library is considered production ready.

Synopsis

    worker_processes  1;
    
    events {
      worker_connections 1024;
    }
    
    http {
      init_by_lua_block {
        require "resty.session".init({
          remember = true,
          audience = "demo",
          secret   = "RaJKp8UQW1",
          storage  = "cookie",
        })
      }
      
      server {
        listen       8080;
        server_name  localhost;
        default_type text/html;
    
        location / {
          content_by_lua_block {
            ngx.say([[
              <html>
              <body>
                <a href=/start>Start the test</a>
              </body>
              </html>
            ]])
          }
        }
    
        location /start {
          content_by_lua_block {
            local session = require "resty.session".new()
            session:set_subject("OpenResty Fan")
            session:set("quote", "The quick brown fox jumps over the lazy dog")
            local ok, err = session:save()
           
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session started (%s)</p>
                <p><a href=/started>Check if it really was</a></p>
              </body>
              </html>
            ]], err or "no error"))
          }
        }
    
        location /started {
          content_by_lua_block {
            local session, err = require "resty.session".start()
            
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was started by %s (%s)</p>
                <p><blockquote>%s</blockquote></p>
                <p><a href=/modify>Modify the session</a></p>
              </body>
              </html>
            ]],
              session:get_subject() or "Anonymous",
              err or "no error",
              session:get("quote") or "no quote"
            ))
          }
        }
        
        location /modify {
          content_by_lua_block {
            local session, err = require "resty.session".start()
            session:set_subject("Lua Fan")
            session:set("quote", "Lorem ipsum dolor sit amet")
            local _, err_save = session:save()
            
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was modified (%s)</p>
                <p><a href=/modified>Check if it is modified</a></p>
              </body>
              </html>
            ]], err or err_save or "no error"))
          }
        }
        
        location /modified {
          content_by_lua_block {
            local session, err = require "resty.session".start()
    
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was started by %s (%s)</p>
                <p><blockquote>%s</blockquote></p>
                <p><a href=/destroy>Destroy the session</a></p>
              </body>
              </html>
            ]],
              session:get_subject() or "Anonymous",
              err or "no error",
              session:get("quote")  or "no quote"
            ))
          }
        }
        
        location /destroy {
          content_by_lua_block {
            local ok, err = require "resty.session".destroy()
    
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was destroyed (%s)</p>
                <p><a href=/destroyed>Check that it really was?</a></p>
              </body>
              </html>
            ]], err or "no error"))
          }
        }
        
        location /destroyed {
          content_by_lua_block {
            local session, err = require "resty.session".open()
    
            ngx.say(string.format([[
              <html>
              <body>
                <p>Session was really destroyed, you are known as %s (%s)</p>
                <p><a href=/>Start again</a></p>
              </body>
              </html>
            ]],
              session:get_subject() or "Anonymous",
              err or "no error"
            ))
          }
        }    
      }
    }  

Installation

Using OpenResty Package Manager (opm)

    ❯ opm get bungle/lua-resty-session

OPM repository for lua-resty-session is located at https://opm.openresty.org/package/bungle/lua-resty-session/.

Also check the dependencies for each storage (there may be additional dependencies).

Using LuaRocks

    ❯ luarocks install lua-resty-session

LuaRocks repository for lua-resty-session is located at https://luarocks.org/modules/bungle/lua-resty-session.

Also check the dependencies for each storage (there may be additional dependencies).

Configuration

The configuration can be divided to generic session configuration and the server side storage configuration.

Here is an example:

    init_by_lua_block {
      require "resty.session".init({
        remember = true,
        store_metadata = true,
        secret = "RaJKp8UQW1",
        secret_fallbacks = {
          "X88FuG1AkY",
          "fxWNymIpbb",
        },
        storage = "postgres",
        postgres = {
          username = "my-service",
          password = "kVgIXCE5Hg",
          database = "sessions",
        },
      })
    }

Session Configuration

Session configuration can be passed to initialization, constructor, and helper functions.

Here are the possible session configuration options:

| Option | Default | Description | |-----------------------------|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | secret | nil | Secret used for the key derivation. The secret is hashed with SHA-256 before using it. E.g. "RaJKp8UQW1". | | secret_fallbacks | nil | Array of secrets that can be used as alternative secrets (when doing key rotation), E.g. { "6RfrAYYzYq", "MkbTkkyF9C" }. | | ikm | (random) | Initial keying material (or ikm) can be specified directly (without using a secret) with exactly 32 bytes of data. E.g. "5ixIW4QVMk0dPtoIhn41Eh1I9enP2060" | | ikm_fallbacks | nil | Array of initial keying materials that can be used as alternative keys (when doing key rotation), E.g. { "QvPtlPKxOKdP5MCu1oI3lOEXIVuDckp7" }. | | cookie_prefix | nil | Cookie prefix, use nil, "__Host-" or "__Secure-". | | cookie_name | "session" | Session cookie name, e.g. "session". | | cookie_path | "/" | Cookie path, e.g. "/". | | cookie_http_only | true | Mark cookie HTTP only, use true or false. | | cookie_secure | nil | Mark cookie secure, use nil, true or false. | | cookie_priority | nil | Cookie priority, use nil, "Low", "Medium", or "High". | | cookie_same_site | "Lax" | Cookie same-site policy, use nil, "Lax", "Strict", "None", or "Default" | | cookie_same_party | nil | Mark cookie with same party flag, use nil, true, or false. | | cookie_partitioned | nil | Mark cookie with partitioned flag, use nil, true, or false. | | remember | false | Enable or disable persistent sessions, use nil, true, or false. | | remember_safety | "Medium" | Remember cookie key derivation complexity, use nil, "None" (fast), "Low", "Medium", "High" or "Very High" (slow). | | remember_cookie_name | "remember" | Persistent session cookie name, e.g. "remember". | | audience | "default" | Session audience, e.g. "my-application". | | subject | nil | Session subject, e.g. "john.doe@example.com". | | enforce_same_subject | false | When set to true, audiences need to share the same subject. The library removes non-subject matching audience data on save. | | stale_ttl | 10 | When session is saved a new session is created, stale ttl specifies how long the old one can still be used, e.g. 10 (in seconds). | | idling_timeout | 900 | Idling timeout specifies how long the session can be inactive until it is considered invalid, e.g. 900 (15 minutes) (in seconds), 0 disables the checks and touching. | | rolling_timeout | 3600 | Rolling timeout specifies how long the session can be used until it needs to be renewed, e.g. 3600 (an hour) (in seconds), 0 disables the checks and rolling. | | absolute_timeout | 86400 | Absolute timeout limits how long the session can be renewed, until re-authentication is required, e.g. 86400 (a day) (in seconds), 0 disables the checks. | | remember_rolling_timeout | 604800 | Remember timeout specifies how long the persistent session is considered valid, e.g. 604800 (a week) (in seconds), 0 disables the checks and rolling. | | remember_absolute_timeout | 2592000 | Remember absolute timeout limits how long the persistent session can be renewed, until re-authentication is required, e.g. 2592000 (30 days) (in seconds), 0 disables the checks. | | hash_storage_key | false | Whether to hash or not the storage key. With storage key hashed it is impossible to decrypt data on server side without having a cookie too, use nil, true or false. | | hash_subject | false | Whether to hash or not the subject when store_metadata is enabled, e.g. for PII reasons. | | store_metadata | false | Whether to also store metadata of sessions, such as collecting data of sessions for a specific audience belonging to a specific subject. | | touch_threshold | 60 | Touch threshold controls how frequently or infrequently the session:refresh touches the cookie, e.g. 60 (a minute) (in seconds) | | compression_threshold | 1024 | Compression threshold controls when the data is deflated, e.g. 1024 (a kilobyte) (in bytes), 0 disables compression. | | request_headers | nil | Set of headers to send to upstream, use id, audience, subject, timeout, idling-timeout, rolling-timeout, absolute-timeout. E.g. { "id", "timeout" } will set Session-Id and Session-Timeout request headers when set_headers is called. | | response_headers | nil | Set of headers to send to downstream, use id, audience, subject, timeout, idling-timeout, rolling-timeout, absolute-timeout. E.g. { "id", "timeout" } will set Session-Id and Session-Timeout response headers when set_headers is called. | | storage | nil | Storage is responsible of storing session data, use nil or "cookie" (data is stored in cookie), "dshm", "file", "memcached", "mysql", "postgres", "redis", or "shm", or give a name of custom module ("custom-storage"), or a table that implements session storage interface. | | dshm | nil | Configuration for dshm storage, e.g. { prefix = "sessions" } (see below) | | file | nil | Configuration for file storage, e.g. { path = "/tmp", suffix = "session" } (see below) | | memcached | nil | Configuration for memcached storage, e.g. { prefix = "sessions" } (see below) | | mysql | nil | Configuration for MySQL / MariaDB storage, e.g. { database = "sessions" } (see below) | | postgres | nil | Configuration for Postgres storage, e.g. { database = "sessions" } (see below) | | redis | nil | Configuration for Redis / Redis Sentinel / Redis Cluster storages, e.g. { prefix = "sessions" } (see below) | | shm | nil | Configuration for shared memory storage, e.g. { zone = "sessions" } | | ["custom-storage"] | nil | custom storage (loaded with require "custom-storage") configuration. |

When storing data to cookie, there is no additional configuration required, just set the storage to nil or "cookie".

DSHM Storage Configuration

With DHSM storage you can use the following settings (set the storage to "dshm"):

| Option | Default | Description | |---------------------|:-------------:|----------------------------------------------------------------------------------------------| | prefix | nil | The Prefix for the keys stored in DSHM. | | suffix | nil | The suffix for the keys stored in DSHM. | | host | "127.0.0.1" | The host to connect. | | port | 4321 | The port to connect. | | connect_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's connect method. | | send_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's send method. | | read_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's receive method. | | keepalive_timeout | nil | Controls the default maximal idle time of the connections in the connection pool. | | pool | nil | A custom name for the connection pool being used. | | pool_size | nil | The size of the connection pool. | | backlog | nil | A queue size to use when the connection pool is full (configured with pool_size). | | ssl | nil | Enable SSL. | | ssl_verify | nil | Verify server certificate. | | server_name | nil | The server name for the new TLS extension Server Name Indication (SNI). |

Please refer to ngx-distributed-shm to get necessary dependencies installed.

File Storage Configuration

With file storage you can use the following settings (set the storage to "file"):

| Option | Default | Description | |---------------------|:---------------:|-------------------------------------------------------------------------------------| | prefix | nil | File prefix for session file. | | suffix | nil | File suffix (or extension without .) for session file. | | pool | nil | Name of the thread pool under which file writing happens (available on Linux only). | | path | (tmp directory) | Path (or directory) under which session files are created. |

The implementation requires LuaFileSystem which you can install with LuaRocks:

    ❯ luarocks install LuaFileSystem

Memcached Storage Configuration

With file Memcached you can use the following settings (set the storage to "memcached"):

| Option | Default | Description | |---------------------|:-----------:|----------------------------------------------------------------------------------------------| | prefix | nil | Prefix for the keys stored in memcached. | | suffix | nil | Suffix for the keys stored in memcached. | | host | 127.0.0.1 | The host to connect. | | port | 11211 | The port to connect. | | socket | nil | The socket file to connect to. | | connect_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's connect method. | | send_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's send method. | | read_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's receive method. | | keepalive_timeout | nil | Controls the default maximal idle time of the connections in the connection pool. | | pool | nil | A custom name for the connection pool being used. | | pool_size | nil | The size of the connection pool. | | backlog | nil | A queue size to use when the connection pool is full (configured with pool_size). | | ssl | false | Enable SSL | | ssl_verify | nil | Verify server certificate | | server_name | nil | The server name for the new TLS extension Server Name Indication (SNI). |

MySQL / MariaDB Storage Configuration

With file MySQL / MariaDB you can use the following settings (set the storage to "mysql"):

| Option | Default | Description | |---------------------|:-----------------:|----------------------------------------------------------------------------------------------| | host | "127.0.0.1" | The host to connect. | | port | 3306 | The port to connect. | | socket | nil | The socket file to connect to. | | username | nil | The database username to authenticate. | | password | nil | Password for authentication, may be required depending on server configuration. | | charset | "ascii" | The character set used on the MySQL connection. | | database | nil | The database name to connect. | | table_name | "sessions" | Name of database table to which to store session data. | | table_name_meta | "sessions_meta" | Name of database meta data table to which to store session meta data. | | max_packet_size | 1048576 | The upper limit for the reply packets sent from the MySQL server (in bytes). | | connect_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's connect method. | | send_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's send method. | | read_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's receive method. | | keepalive_timeout | nil | Controls the default maximal idle time of the connections in the connection pool. | | pool | nil | A custom name for the connection pool being used. | | pool_size | nil | The size of the connection pool. | | backlog | nil | A queue size to use when the connection pool is full (configured with pool_size). | | ssl | false | Enable SSL. | | ssl_verify | nil | Verify server certificate. |

You also need to create following tables in your database:

`sql

-- Database table that stores session data.

CREATE TABLE IF NOT EXISTS sessions ( sid CHAR(43) PRIMARY KEY, name VARCHAR(255), data MEDIUMTEXT, exp DATETIME, INDEX (exp)

) CHARACTER SET ascii;

-- Sessions metadata table.

-- This is only needed if you want to store session metadata.

CREATE TABLE IF NOT EXISTS sessions_meta ( aud VARCHAR(255), sub VARCHAR(255), sid CHAR(43), PRIMARY KEY (aud, sub, sid), CONSTRAINT FOREIGN KEY (sid) REFERENCES sessions(sid) ON DELETE CASCADE ON UPDATE CASCADE ) CHARACTER SET ascii;

    ## Postgres Configuration
    
    With file Postgres you can use the following settings (set the `storage` to `"postgres"`):
    
    | Option              |      Default      | Description                                                                                               |
    |---------------------|:-----------------:|-----------------------------------------------------------------------------------------------------------|
    | `host`              |   `"127.0.0.1"`   | The host to connect.                                                                                      |
    | `port`              |      `5432`       | The port to connect.                                                                                      |
    | `application`       |      `5432`       | Set the name of the connection as displayed in pg_stat_activity (defaults to `"pgmoon"`).                 |
    | `username`          |   `"postgres"`    | The database username to authenticate.                                                                    |
    | `password`          |       `nil`       | Password for authentication, may be required depending on server configuration.                           |
    | `database`          |       `nil`       | The database name to connect.                                                                             |
    | `table_name`        |   `"sessions"`    | Name of database table to which to store session data (can be `database schema` prefixed).                |
    | `table_name_meta`   | `"sessions_meta"` | Name of database meta data table to which to store session meta data (can be `database schema` prefixed). |
    | `connect_timeout`   |       `nil`       | Controls the default timeout value used in TCP/unix-domain socket object's `connect` method.              |
    | `send_timeout`      |       `nil`       | Controls the default timeout value used in TCP/unix-domain socket object's `send` method.                 |
    | `read_timeout`      |       `nil`       | Controls the default timeout value used in TCP/unix-domain socket object's `receive` method.              |
    | `keepalive_timeout` |       `nil`       | Controls the default maximal idle time of the connections in the connection pool.                         |
    | `pool`              |       `nil`       | A custom name for the connection pool being used.                                                         |
    | `pool_size`         |       `nil`       | The size of the connection pool.                                                                          |
    | `backlog`           |       `nil`       | A queue size to use when the connection pool is full (configured with pool_size).                         |
    | `ssl`               |      `false`      | Enable SSL.                                                                                               |
    | `ssl_verify`        |       `nil`       | Verify server certificate.                                                                                |
    | `ssl_required`      |       `nil`       | Abort the connection if the server does not support SSL connections.                                      |
    
    You also need to create following tables in your database:
    
    ```sql
    --
    -- Database table that stores session data.
    --
    CREATE TABLE IF NOT EXISTS sessions (
      sid  TEXT PRIMARY KEY,
      name TEXT,
      data TEXT,
      exp  TIMESTAMP WITH TIME ZONE
    );
    CREATE INDEX ON sessions (exp);
    
    --
    -- Sessions metadata table.
    --
    -- This is only needed if you want to store session metadata.
    --
    CREATE TABLE IF NOT EXISTS sessions_meta (
      aud TEXT,
      sub TEXT,
      sid TEXT REFERENCES sessions (sid) ON DELETE CASCADE ON UPDATE CASCADE,
      PRIMARY KEY (aud, sub, sid)
    );

The implementation requires pgmoon which you can install with LuaRocks:

    ❯ luarocks install pgmoon

Redis Configuration

The session library supports single Redis, Redis Sentinel, and Redis Cluster connections. Common configuration settings among them all:

| Option | Default | Description | |---------------------|:-------:|----------------------------------------------------------------------------------------------| | prefix | nil | Prefix for the keys stored in Redis. | | suffix | nil | Suffix for the keys stored in Redis. | | username | nil | The database username to authenticate. | | password | nil | Password for authentication. | | connect_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's connect method. | | send_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's send method. | | read_timeout | nil | Controls the default timeout value used in TCP/unix-domain socket object's receive method. | | keepalive_timeout | nil | Controls the default maximal idle time of the connections in the connection pool. | | pool | nil | A custom name for the connection pool being used. | | pool_size | nil | The size of the connection pool. | | backlog | nil | A queue size to use when the connection pool is full (configured with pool_size). | | ssl | false | Enable SSL | | ssl_verify | nil | Verify server certificate | | server_name | nil | The server name for the new TLS extension Server Name Indication (SNI). |

The single redis implementation is selected when you don't pass either sentinels or nodes, which would lead to selecting sentinel or cluster implementation.

Single Redis Configuration

Single Redis has following additional configuration options (set the storage to "redis"):

| Option | Default | Description | |-------------|:---------------:|--------------------------------| | host | "127.0.0.1" | The host to connect. | | port | 6379 | The port to connect. | | socket | nil | The socket file to connect to. | | database | nil | The database to connect. |

Redis Sentinels Configuration

Redis Sentinel has following additional configuration options (set the storage to "redis" and configure the sentinels):

| Option | Default | Description | |---------------------|:--------:|--------------------------------| | master | nil | Name of master. | | role | nil | "master" or "slave". | | socket | nil | The socket file to connect to. | | sentinels | nil | Redis Sentinels. | | sentinel_username | nil | Optional sentinel username. | | sentinel_password | nil | Optional sentinel password. | | database | nil | The database to connect. |

The sentinels is an array of Sentinel records:

| Option | Default | Description | |--------|:-------:|----------------------| | host | nil | The host to connect. | | port | nil | The port to connect. |

The sentinel implementation is selected when you pass sentinels as part of redis configuration (and do not pass nodes, which would select cluster implementation).

The implementation requires lua-resty-redis-connector which you can install with LuaRocks:

    ❯ luarocks install lua-resty-redis-connector

Redis Cluster Configuration

Redis Cluster has following additional configuration options (set the storage to "redis" and configure the nodes):

| Option | Default | Description | |---------------------------|:-------:|--------------------------------------------------------| | name | nil | Redis cluster name. | | nodes | nil | Redis cluster nodes. | | lock_zone | nil | Shared dictionary name for locks. | | lock_prefix | nil | Shared dictionary name prefix for lock. | | max_redirections | nil | Maximum retry attempts for redirection. | | max_connection_attempts | nil | Maximum retry attempts for connection. | | max_connection_timeout | nil | Maximum connection timeout in total among the retries. |

The nodes is an array of Cluster node records:

| Option | Default | Description | |--------|:-------------:|----------------------------| | ip | "127.0.0.1" | The IP address to connect. | | port | 6379 | The port to connect. |

The cluster implementation is selected when you pass nodes as part of redis configuration.

For cluster to work properly, you need to configure lock_zone, so also add this to your Nginx configuration:

    lua_shared_dict redis_cluster_locks 100k;

And set the lock_zone to "redis_cluster_locks"

The implementation requires resty-redis-cluster or kong-redis-cluster which you can install with LuaRocks:

    ❯ luarocks install resty-redis-cluster
    # or
    ❯ luarocks install kong-redis-cluster

SHM Configuration

With SHM storage you can use the following settings (set the storage to "shm"):

| Option | Default | Description | |----------|:------------:|------------------------------------| | prefix | nil | Prefix for the keys stored in SHM. | | suffix | nil | Suffix for the keys stored in SHM. | | zone | "sessions" | A name of shared memory zone. |

You will also need to create a shared dictionary zone in Nginx:

    lua_shared_dict sessions 10m;

Note: you may need to adjust the size of shared memory zone according to your needs.

API

LDoc generated API docs can also be viewed at bungle.github.io/lua-resty-session.

Initialization

session.init

syntax: session.init(configuration)

Initialize the session library.

This function can be called on init or init_worker phases on OpenResty to set global default configuration to all session instances created by this library.

    require "resty.session".init({
      audience = "my-application",
      storage = "redis",
      redis = {
        username = "session",
        password = "storage",
      },
    })

See configuration for possible configuration settings.

Constructors

session.new

syntax: session = session.new(configuration)

Creates a new session instance.

    local session = require "resty.session".new()
    -- OR
    local session = require "resty.session".new({
      audience = "my-application",
    })

See configuration for possible configuration settings.

Helpers

session.open

syntax: session, err, exists = session.open(configuration)

This can be used to open a session, and it will either return an existing session or a new session. The exists (a boolean) return parameters tells whether it was existing or new session that was returned. The err (a string) contains a message of why opening might have failed (the function will still return session too).

    local session = require "resty.session".open()
    -- OR
    local session, err, exists = require "resty.session".open({
      audience = "my-application",
    })

See configuration for possible configuration settings.

session.start

syntax: session, err, exists, refreshed = session.start(configuration)

This can be used to start a session, and it will either return an existing session or a new session. In case there is an existing session, the session will be refreshed as well (as needed). The exists (a boolean) return parameters tells whether it was existing or new session that was returned. The refreshed (a boolean) tells whether the call to refresh was succesful. The err (a string) contains a message of why opening or refreshing might have failed (the function will still return session too).

    local session = require "resty.session".start()
    -- OR
    local session, err, exists, refreshed = require "resty.session".start({
      audience = "my-application",
    })

See configuration for possible configuration settings.

session.logout

syntax: ok, err, exists, logged_out = session.logout(configuration)

It logouts from a specific audience.

A single session cookie may be shared between multiple audiences (or applications), thus there is a need to be able to logout from just a single audience while keeping the session for the other audiences. The exists (a boolean) return parameters tells whether session existed. The logged_out (a boolean) return parameter signals if the session existed and was also logged out. The err (a string) contains a reason why session didn't exists or why the logout failed. The ok (truthy) will be true when session existed and was successfully logged out.

When there is only a single audience, then this can be considered equal to session.destroy.

When the last audience is logged out, the cookie will be destroyed as well and invalidated on a client.

    require "resty.session".logout()
    -- OR
    local ok, err, exists, logged_out = require "resty.session".logout({
      audience = "my-application",
    })

See configuration for possible configuration settings.

session.destroy

syntax: ok, err, exists, destroyed = session.destroy(configuration)

It destroys the whole session and clears the cookies.

A single session cookie may be shared between multiple audiences (or applications), thus there is a need to be able to logout from just a single audience while keeping the session for the other audiences. The exists (a boolean) return parameters tells whether session existed. The destroyed (a boolean) return parameter signals if the session existed and was also destroyed out. The err (a string) contains a reason why session didn't exists or why the logout failed. The ok (truthy) will be true when session existed and was successfully logged out.

    require "resty.session".destroy()
    -- OR
    local ok, err, exists, destroyed = require "resty.session".destroy({
      cookie_name = "auth",
    })

See configuration for possible configuration settings.

Instance Methods

session:open

syntax: ok, err = session:open()

This can be used to open a session. It returns true when session was opened and validated. Otherwise, it returns nil and an error message.

    local session = require "resty.session".new()
    local ok, err = session:open()
    if ok then
      -- session exists
      
    else
      -- session did not exists or was invalid
    end

session:save

syntax: ok, err = session:save()

Saves the session data and issues a new session cookie with a new session id. When remember is enabled, it will also issue a new persistent cookie and possibly save the data in backend store. It returns true when session was saved. Otherwise, it returns nil and an error message.

    local session = require "resty.session".new()
    session:set_subject("john")
    local ok, err = session:save()
    if not ok then
      -- error when saving session
    end

session:touch

syntax: ok, err = session:touch()

Updates idling offset of the session by sending an updated session cookie. It only sends the client cookie and never calls any backend session store APIs. Normally the session:refresh is used to call this indirectly. In error case it returns nil and an error message, otherwise true.

    local session, err, exists = require "resty.session".open()
    if exists then
      ok, err = session:touch()
    end

session:refresh

syntax: ok, err = session:refresh()

Either saves the session (creating a new session id) or touches the session depending on whether the rolling timeout is getting closer, which means by default when 3/4 of rolling timeout is spent, that is 45 minutes with default rolling timeout of an hour. The touch has a threshold, by default one minute, so it may be skipped in some cases (you can call session:touch() to force it). In error case it returns nil and an error message, otherwise true.

    local session, err, exists = require "resty.session".open()
    if exists then
      local ok, err = session:refresh()
    end

The above code looks a bit like session.start() helper.

session:logout

syntax: ok, err = session:logout()

Logout either destroys the session or just clears the data for the current audience, and saves it (logging out from the current audience). In error case it returns nil and an error message, otherwise true.

    local session, err, exists = require "resty.session".open()
    if exists then
      local ok, err = session:logout()
    end

session:destroy

syntax: ok, err = session:destroy()

Destroy the session and clear the cookies. In error case it returns nil and an error message, otherwise true.

    local session, err, exists = require "resty.session".open()
    if exists then
      local ok, err = session:destroy()
    end

session:close

syntax: session:close()

Just closes the session instance so that it cannot be used anymore.

    local session = require "resty.session".new()
    session:set_subject("john")
    local ok, err = session:save()
    if not ok then
      -- error when saving session
    end
    session:close()

session:set_data

syntax: session:set_data(data)

Set session data. The data needs to be a table.

    local session, err, exists = require "resty.session".open()
    if not exists then
       session:set_data({
         cart = {},
       })
      session:save()
    end

session:get_data

syntax: data = session:get_data()

Get session data.

    local session, err, exists = require "resty.session".open()
    if exists then
      local data = session:get_data()
      ngx.req.set_header("Authorization", "Bearer " .. data.access_token)
    end

session:set

syntax: session:set(key, value)

Set a value in session.

    local session, err, exists = require "resty.session".open()
    if not exists then
      session:set("access-token", "eyJ...")
      session:save()
    end

session:get

syntax: value = session:get(key)

Get a value from session.

    local session, err, exists = require "resty.session".open()
    if exists then
      local access_token = session:get("access-token")
      ngx.req.set_header("Authorization", "Bearer " .. access_token)
    end

session:set_audience

syntax: session:set_audience(audience)

Set session audience.

    local session = require "resty.session".new()
    session.set_audience("my-service")

session:get_audience

syntax: audience = session:get_audience()

Set session subject.

session:set_subject

syntax: session:set_subject(subject)

Set session audience.

    local session = require "resty.session".new()
    session.set_subject("john@doe.com")

session:get_subject

syntax: subject = session:get_subject()

Get session subject.

    local session, err, exists = require "resty.session".open()
    if exists then
      local subject = session.get_subject()
    end

session:get_property

syntax: value = session:get_property(name)

Get session property. Possible property names:

  • "id": 43 bytes session id (same as nonce, but base64 url-encoded)

  • "nonce": 32 bytes nonce (same as session id but in raw bytes)

  • "audience": Current session audience

  • "subject": Current session subject

  • "timeout": Closest timeout (in seconds) (what's left of it)

  • "idling-timeout"`: Session idling timeout (in seconds) (what's left of it)

  • "rolling-timeout"`: Session rolling timeout (in seconds) (what's left of it)

  • "absolute-timeout"`: Session absolute timeout (in seconds) (what's left of it)

Note: the returned value may be nil.

    local session, err, exists = require "resty.session".open()
    if exists then
      local timeout = session.get_property("timeout")
    end

session:set_remember

syntax: session:set_remember(value)

Set persistent sessions on/off.

In many login forms user is given an option for "remember me". You can call this function based on what user selected.

    local session = require "resty.session".new()
    if ngx.var.args.remember then
      session:set_remember(true)
    end
    session:set_subject(ngx.var.args.username)
    session:save()

session:get_remember

syntax: remember = session:get_remember()

Get state of persistent sessions.

    local session, err, exists = require "resty.session".open()
    if exists then
      local remember = session.get_remember()
    end

syntax: session:clear_request_cookie()

Modifies the request headers by removing the session related cookies. This is useful when you use the session library on a proxy server and don't want the session cookies to be forwarded to the upstream service.

    local session, err, exists = require "resty.session".open()
    if exists then
      session:clear_request_cookie()
    end

session:set_headers

syntax: session:set_headers(arg1, arg2, ...)

Sets request and response headers based on configuration.

    local session, err, exists = require "resty.session".open({
      request_headers = { "audience", "subject", "id" },
      response_headers = { "timeout", "idling-timeout", "rolling-timeout", "absolute-timeout" },
    })
    if exists then
      session:set_headers()
    end

When called without arguments it will set request headers configured with request_headers and response headers configured with response_headers.

See configuration for possible header names.

session:set_request_headers

syntax: session:set_request_headers(arg1, arg2, ...)

Set request headers.

    local session, err, exists = require "resty.session".open()
    if exists then
      session:set_request_headers("audience", "subject", "id")
    end

When called without arguments it will set request headers configured with request_headers.

See configuration for possible header names.

session:set_response_headers

syntax: session:set_response_headers(arg1, arg2, ...)

Set request headers.

    local session, err, exists = require "resty.session".open()
    if exists then
      session:set_response_headers("timeout", "idling-timeout", "rolling-timeout", "absolute-timeout")
    end

When called without arguments it will set request headers configured with response_headers.

See configuration for possible header names.

session.info:set

syntax: session.info:set(key, value)

Set a value in session information store. Session information store may be used in scenarios when you want to store data on server side storage, but do not want to create a new session and send a new session cookie. The information store data is not considered when checking authentication tag or message authentication code. Thus if you want to use this for data that needs to be encrypted, you need to encrypt value before passing it to thus function.

    local session, err, exists = require "resty.session".open()
    if exists then
      session.info:set("last-access", ngx.now())
      session.info:save()
    end

With cookie storage this still works, but it is then almost the same as session:set.

session.info:get

syntax: value = session.info:get(key)

Get a value from session information store.

    local session, err, exists = require "resty.session".open()
    if exists then
      local last_access = session.info:get("last-access")
    end

session.info:save

syntax: value = session.info:save()

Save information. Only updates backend storage. Does not send a new cookie (except with cookie storage).

    local session = require "resty.session".new()
    session.info:set("last-access", ngx.now())
    local ok, err = session.info:save()

Cookie Format

    [ HEADER -------------------------------------------------------------------------------------]
    [ Type || Flags || SID || Created at || Rolling Offset || Size || Tag || Idling Offset || Mac ]
    [ 1B   || 2B    || 32B || 5B         || 4B             || 3B   || 16B || 3B            || 16B ]

and

    [ PAYLOAD --]
    [ Data  *B  ]   

Both the HEADER and PAYLOAD are base64 url-encoded before putting in a Set-Cookie header. When using a server side storage, the PAYLOAD is not put in the cookie. With cookie storage the base64 url-encoded header is concatenated with base64 url-encoded payload.

The HEADER is fixed size 82 bytes binary or 110 bytes in base64 url-encoded form.

Header fields explained:

  • Type: number 1 binary packed in a single little endian byte (currently the only supported type).

  • Flags: binary packed flags (short) in a two byte little endian form.

  • SID: 32 bytes of crypto random data (Session ID).

  • Created at: binary packed secs from epoch in a little endian form, truncated to 5 bytes.

  • Rolling Offset: binary packed secs from creation time in a little endian form (integer).

  • Size: binary packed data size (short) in a two byte little endian form.

  • Tag: 16 bytes of authentication tag from AES-256-GCM encryption of the data.

  • Idling Offset: binary packed secs from creation time + rolling offset in a little endian form, truncated to 3 bytes.

  • Mac: 16 bytes message authentication code of the header.

Data Encryption

  1. Initial keying material (IKM):

    1. derive IKM from secret by hashing secret with SHA-256, or

    2. use 32 byte IKM when passed to library with ikm

  2. Generate 32 bytes of crypto random session id (sid)

  3. Derive 32 byte encryption key and 12 byte initialization vector with HKDF using SHA-256 (on FIPS-mode it uses PBKDF2 with SHA-256 instead)

    1. Use HKDF extract to derive a new key from ikm to get key (this step can be done just once per ikm):

      • output length: 32

      • digest: "sha256"

      • key: <ikm>

      • mode: extract only

      • info: ""

      • salt: ""

    2. Use HKDF expand to derive 44 bytes of output:

      • output length: 44

      • digest: "sha256"

      • key: <key>

      • mode: expand only

      • info: "encryption:<sid>"

      • salt: ""

    3. The first 32 bytes of output are the encryption key (aes-key), and the last 12 bytes are the initialization vector (iv)

  4. Encrypt plaintext (JSON encoded and optionally deflated) using AES-256-GCM to get ciphertext and tag

    1. cipher: "aes-256-gcm"

    2. key: <aes-key>

    3. iv: <iv>

    4. plaintext: <plaintext>

    5. aad: use the first 47 bytes of header as aad, that includes:

      1. Type

      2. Flags

      3. Session ID

      4. Creation Time

      5. Rolling Offset

      6. Data Size

There is a variation for remember cookies on step 3, where we may use PBKDF2 instead of HKDF, depending on remember_safety setting (we also use it in FIPS-mode). The PBKDF2 settings:

  • output length: 44

  • digest: "sha256"

  • password: <key>

  • salt: "encryption:<sid>"

  • iterations: <1000|10000|100000|1000000>

Iteration counts are based on remember_safety setting ("Low", "Medium", "High", "Very High"), if remember_safety is set to "None", we will use the HDKF as above.

Cookie Header Authentication

  1. Derive 32 byte authentication key (mac_key) with HKDF using SHA-256 (on FIPS-mode it uses PBKDF2 with SHA-256 instead):

    1. Use HKDF extract to derive a new key from ikm to get key (this step can be done just once per ikm and reused with encryption key generation):

      • output length: 32

      • digest: "sha256"

      • key: <ikm>

      • mode: extract only

      • info: ""

      • salt: ""

    2. Use HKDF expand to derive 32 bytes of mac-key:

      • output length: 32

      • digest: "sha256"

      • key: <key>

      • mode: expand only

      • info: "authentication:<sid>"

      • salt: ""

  2. Calculate message authentication code using HMAC-SHA256:

    • digest: "sha256"

    • key: <mac-key>

    • message: use the first 66 bytes of header, that includes:

      1. Type

      2. Flags

      3. Session ID

      4. Creation Time

      5. Rolling Offset

      6. Data Size

      7. Tag

      8. Idling Offset

Custom Storage Interface

If you want to implement custom storage, you need to implement following interface:

`lua

-- <custom> backend for session library

-- @module <custom>

-- Storage -- @section instance

local metatable = {}

metatable.__index = metatable

function metatable.__newindex() error("attempt to update a read-only table", 2)

end

-- Store session data.

-- @function instance:set -- @tparam string name cookie name -- @tparam string key session key -- @tparam string value session value -- @tparam number ttl session ttl -- @tparam number current_time current time -- @tparam[opt] string old_key old session id -- @tparam string stale_ttl stale ttl -- @tparam[opt] table metadata table of metadata -- @tparam boolean remember whether storing persistent session or not -- @treturn true|nil ok -- @treturn string error message function metatable:set(name, key, value, ttl, current_time, old_key, stale_ttl, metadata, remember) -- NYI

end

-- Retrieve session data.

-- @function instance:get -- @tparam string name cookie name -- @tparam string key session key -- @treturn string|nil session data -- @treturn string error message function metatable:get(name, key) -- NYI

end

-- Delete session data.

-- @function instance:delete -- @tparam string name cookie name -- @tparam string key session key -- @tparam[opt] table metadata session meta data -- @treturn boolean|nil session data -- @treturn string error message function metatable:delete(name, key, current_time, metadata) -- NYI end

local storage = {}

-- Constructors

-- @section constructors

-- Configuration

-- @section configuration

-- <custom> storage backend configuration -- @field <field-name> TBD

-- @table configuration

-- Create a <custom> storage.

-- This creates a new shared memory storage instance.

-- @function module.new -- @tparam[opt] table configuration <custom> storage @{configuration} -- @treturn table <custom> storage instance function storage.new(configuration) -- NYI -- return setmetatable({}, metatable) end

return storage

    Please check the existing implementations for the defails. And please
    make a pull-request so that we can integrate it directly to library
    for other users as well.
    
    
    # License
    
    `lua-resty-session` uses two clause BSD license.
    

Copyright (c) 2014 – 2023 Aapo Talvensaari, 2022 – 2023 Samuele Illuminati All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

Authors

Aapo Talvensaari (@bungle),Samuele Illuminati (@samugi)

License

2bsd

Dependencies

hamishforbes/lua-ffi-zlib >= 0.5, fffonion/lua-resty-openssl >= 0.8.0, luajit, nginx, ngx_http_lua

Versions