Home

Awesome

lua-resty-session

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

TL;DR;

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"
        ))
      }
    }    
  }
}  

Table of Contents

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:

OptionDefaultDescription
secretnilSecret used for the key derivation. The secret is hashed with SHA-256 before using it. E.g. "RaJKp8UQW1".
secret_fallbacksnilArray 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_fallbacksnilArray of initial keying materials that can be used as alternative keys (when doing key rotation), E.g. { "QvPtlPKxOKdP5MCu1oI3lOEXIVuDckp7" }.
cookie_prefixnilCookie prefix, use nil, "__Host-" or "__Secure-".
cookie_name"session"Session cookie name, e.g. "session".
cookie_path"/"Cookie path, e.g. "/".
cookie_domainnilCookie domain, e.g. "example.com"
cookie_http_onlytrueMark cookie HTTP only, use true or false.
cookie_securenilMark cookie secure, use nil, true or false.
cookie_prioritynilCookie priority, use nil, "Low", "Medium", or "High".
cookie_same_site"Lax"Cookie same-site policy, use nil, "Lax", "Strict", "None", or "Default"
cookie_same_partynilMark cookie with same party flag, use nil, true, or false.
cookie_partitionednilMark cookie with partitioned flag, use nil, true, or false.
rememberfalseEnable 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".
subjectnilSession subject, e.g. "john.doe@example.com".
enforce_same_subjectfalseWhen set to true, audiences need to share the same subject. The library removes non-subject matching audience data on save.
stale_ttl10When 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_timeout900Idling 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_timeout3600Rolling 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_timeout86400Absolute 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_timeout604800Remember 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_timeout2592000Remember 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_keyfalseWhether 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_subjectfalseWhether to hash or not the subject when store_metadata is enabled, e.g. for PII reasons.
store_metadatafalseWhether to also store metadata of sessions, such as collecting data of sessions for a specific audience belonging to a specific subject.
touch_threshold60Touch threshold controls how frequently or infrequently the session:refresh touches the cookie, e.g. 60 (a minute) (in seconds)
compression_threshold1024Compression threshold controls when the data is deflated, e.g. 1024 (a kilobyte) (in bytes), 0 disables compression.
request_headersnilSet 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_headersnilSet 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.
storagenilStorage 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.
dshmnilConfiguration for dshm storage, e.g. { prefix = "sessions" } (see below)
filenilConfiguration for file storage, e.g. { path = "/tmp", suffix = "session" } (see below)
memcachednilConfiguration for memcached storage, e.g. { prefix = "sessions" } (see below)
mysqlnilConfiguration for MySQL / MariaDB storage, e.g. { database = "sessions" } (see below)
postgresnilConfiguration for Postgres storage, e.g. { database = "sessions" } (see below)
redisnilConfiguration for Redis / Redis Sentinel / Redis Cluster storages, e.g. { prefix = "sessions" } (see below)
shmnilConfiguration for shared memory storage, e.g. { zone = "sessions" }
["custom-storage"]nilcustom storage (loaded with require "custom-storage") configuration.

Cookie 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"):

OptionDefaultDescription
prefixnilThe Prefix for the keys stored in DSHM.
suffixnilThe suffix for the keys stored in DSHM.
host"127.0.0.1"The host to connect.
port4321The port to connect.
connect_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's connect method.
send_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's send method.
read_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's receive method.
keepalive_timeoutnilControls the default maximal idle time of the connections in the connection pool.
poolnilA custom name for the connection pool being used.
pool_sizenilThe size of the connection pool.
backlognilA queue size to use when the connection pool is full (configured with pool_size).
sslnilEnable SSL.
ssl_verifynilVerify server certificate.
server_namenilThe 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"):

OptionDefaultDescription
prefixnilFile prefix for session file.
suffixnilFile suffix (or extension without .) for session file.
poolnilName 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"):

OptionDefaultDescription
prefixnilPrefix for the keys stored in memcached.
suffixnilSuffix for the keys stored in memcached.
host127.0.0.1The host to connect.
port11211The port to connect.
socketnilThe socket file to connect to.
connect_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's connect method.
send_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's send method.
read_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's receive method.
keepalive_timeoutnilControls the default maximal idle time of the connections in the connection pool.
poolnilA custom name for the connection pool being used.
pool_sizenilThe size of the connection pool.
backlognilA queue size to use when the connection pool is full (configured with pool_size).
sslfalseEnable SSL
ssl_verifynilVerify server certificate
server_namenilThe 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"):

OptionDefaultDescription
host"127.0.0.1"The host to connect.
port3306The port to connect.
socketnilThe socket file to connect to.
usernamenilThe database username to authenticate.
passwordnilPassword for authentication, may be required depending on server configuration.
charset"ascii"The character set used on the MySQL connection.
databasenilThe 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_size1048576The upper limit for the reply packets sent from the MySQL server (in bytes).
connect_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's connect method.
send_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's send method.
read_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's receive method.
keepalive_timeoutnilControls the default maximal idle time of the connections in the connection pool.
poolnilA custom name for the connection pool being used.
pool_sizenilThe size of the connection pool.
backlognilA queue size to use when the connection pool is full (configured with pool_size).
sslfalseEnable SSL.
ssl_verifynilVerify server certificate.

You also need to create following tables in your database:

--
-- 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"):

OptionDefaultDescription
host"127.0.0.1"The host to connect.
port5432The port to connect.
application5432Set the name of the connection as displayed in pg_stat_activity (defaults to "pgmoon").
username"postgres"The database username to authenticate.
passwordnilPassword for authentication, may be required depending on server configuration.
databasenilThe 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_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's connect method.
send_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's send method.
read_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's receive method.
keepalive_timeoutnilControls the default maximal idle time of the connections in the connection pool.
poolnilA custom name for the connection pool being used.
pool_sizenilThe size of the connection pool.
backlognilA queue size to use when the connection pool is full (configured with pool_size).
sslfalseEnable SSL.
ssl_verifynilVerify server certificate.
ssl_requirednilAbort the connection if the server does not support SSL connections.

You also need to create following tables in your database:

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

OptionDefaultDescription
prefixnilPrefix for the keys stored in Redis.
suffixnilSuffix for the keys stored in Redis.
usernamenilThe database username to authenticate.
passwordnilPassword for authentication.
connect_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's connect method.
send_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's send method.
read_timeoutnilControls the default timeout value used in TCP/unix-domain socket object's receive method.
keepalive_timeoutnilControls the default maximal idle time of the connections in the connection pool.
poolnilA custom name for the connection pool being used.
pool_sizenilThe size of the connection pool.
backlognilA queue size to use when the connection pool is full (configured with pool_size).
sslfalseEnable SSL
ssl_verifynilVerify server certificate
server_namenilThe 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"):

OptionDefaultDescription
host"127.0.0.1"The host to connect.
port6379The port to connect.
socketnilThe socket file to connect to.
databasenilThe database to connect.

Redis Sentinels Configuration

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

OptionDefaultDescription
masternilName of master.
rolenil"master" or "slave".
socketnilThe socket file to connect to.
sentinelsnilRedis Sentinels.
sentinel_usernamenilOptional sentinel username.
sentinel_passwordnilOptional sentinel password.
databasenilThe database to connect.

The sentinels is an array of Sentinel records:

OptionDefaultDescription
hostnilThe host to connect.
portnilThe 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):

OptionDefaultDescription
namenilRedis cluster name.
nodesnilRedis cluster nodes.
lock_zonenilShared dictionary name for locks.
lock_prefixnilShared dictionary name prefix for lock.
max_redirectionsnilMaximum retry attempts for redirection.
max_connection_attemptsnilMaximum retry attempts for connection.
max_connection_timeoutnilMaximum connection timeout in total among the retries.

The nodes is an array of Cluster node records:

OptionDefaultDescription
ip"127.0.0.1"The IP address to connect.
port6379The 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"):

OptionDefaultDescription
prefixnilPrefix for the keys stored in SHM.
suffixnilSuffix 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:

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

session:clear_request_cookie

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:

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:

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:

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