Code Monkey home page Code Monkey logo

lua-resty-session's Introduction

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

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:

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. "[email protected]".
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.

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

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:

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

--
-- 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("[email protected]")

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

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:

  • 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 in a three 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:

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

lua-resty-session's People

Contributors

adigerber avatar altexy avatar andreibastun avatar bodewig avatar bungle avatar danielcaro avatar grrolland avatar jharriman avatar junhanamaki avatar kipras avatar lordjabez avatar mikefero avatar samugi avatar thorstenfleischmann avatar tieske avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

lua-resty-session's Issues

Init cookie issue.

local session = require "resty.session".start{ secret = "623q4hR325t36VsCD3g567922IC0073T" }

local session = require "resty.session".new()
session.secret = "623q4hR325t36VsCD3g567922IC0073T"

I use thie first line to init session is correct, but use the second two lines get an error:
session.lua:86: attempt to concatenate local 'i' (a nil value)

Is this a bug?

lua_code_cache off;

"lua_code_cache off" effect session. I think it is may simular like session off.
So if you check it as same as i say , it could mention on Readme.md. :)

Cookies are always not "secure"

Hi,

I've got some problems (maybe related to my setup, not to the plugin), but even when using server with ssl, session cookies don't have "Secure" flag (ngx.var.ssl_session_id is always nil).

I've tried Nginx both 1.6.2 and 1.7.7 with lua-nginx-module 0.9.12 and 0.9.13 installed. ngx_devel_kit is latest 0.2.19.

Signing and Encryption Strategy

Originally this library was implemented according to Secure Cookie Protocol. This is quite adequate still, but it seems security community has now settled on slightly different approach. The main difference is that people mostly promote these days approach called Encrypt-Then-MAC or a cipher that supports a thing called AEAD aka Authenticated Encryption using Associated Data (using different keys for signing and encryption). We may want to modify this library to support different strategies.

Session Example is not working

I have been trying a lot to figure out why my cookies were not working. I followed the documentation and still something seems to be going wrong when I 'open' a cookie before starting it. It would fail every time. I decided to test your code in the example and it is not working either. I just get "Anonymous" every time. Why is that? Thanks.

cannot resolve symbol 'RAND_bytes'

Hi! i'm try to use this library with the Nginx Openresty For Windows and show me this error.

2016/08/30 15:36:29 [error] 9084#3740: *1 lua entry thread aborted: runtime error: C:\Users\pablo\Desktop\nginx-1.11.3//resty\session.lua:30: cannot resolve symbol 'RAND_bytes': No se encontr󠥬 proceso especificado.

Lockless means sometimes decryption fails?

For this great project i have some question,

  1. Now, i am worried about the impact of using locks on performance, so i want to use it in lockless
    and i suspect that the following scenario will cause the decryption to fail:
    There are two concurrent requests and the cookie is same now.
  2. req1 was processed first : req1 updated and saved the session.data and the cookie(hmac) will also be updated
  3. req2 was processed later : now the cookie for req2 is outdate, means the condition would return false:
    image

Sorry for my poor English.

Maybe session.secret should be 'required'

Hi Bungle, thanks a lot for all your work.
I ran into a problem and it took me a long time to figure it out, so I just want to document it in case somebody sees something similar.

Basically, the problem is that sessions don't work unless I explicitely set the 'secret' , even though I don't use lua_code_cache off;

In my setup, I have various nginx locations with content_by_lua_file <whatever.lua> and on those lua files I have:
local SESS = require "resty.session"
so I can use the session functionality in my scripts.
On a login page, I set up a session:

local session = SESS.start({ name = "mysession" })
session.data.type = 'gui'
session.data.tenant_id = tenant_id
session:save()

On another script (actually in an access_by_lua_file) , I do check for the existence of a session, like so:

local SESS = require ('resty.session')
local session, present = SESS.open({ name = "mysession" })
if not session.present then
  return ngx.exit(ngx.HTTP_FORBIDDEN)
end

And this worked erratically... sometimes it worked (got the session established previously) but most of the time, the session was not detected (even though the cookie was there).
I debugged and I found that the 'problem' was in this line :
if d and hmac(k, concat{ i, e, d, self.key }) == h then

The second condition was always false. Even using
cipher = "none"
the problem was there.

I discovered that the secret was being generated on every request. This problem is explained in the documentation here , but since my app is NOT turning off the cache, I thought this didn't apply to me.

As soon as I created my sessions with a specific secret, and opened them with that secret, everything worked fine
SESS.open({ name = "mysession" , secret = "mysecretwhateverblah" })

I'm not really sure why the secret was being re-generated each time... even with cache "on" ... but my suggestion is to change the documentation to say that setting the 'secret' is not only a good idea... but that it should always be done... more like a requirement.

Question about $session_cookie_renew

Hello,

I am not sure about the situation so I thought about asking the author.

I am testing your library with Redis storage. I understand that $session_cookie_lifetime translates into Redis TTL expiry for the key (and this is what I want).

I am not sure where the $session_cookie_renew fits in this. Reading the manual it mentions about renewing the TTL of the cookie.

From what I see, on each "open" request, the Redis TTL of the session is updated to my $session_cookie_lifetime (as expected also since if the user has come back, I want his session to extend).

Since I'm using session cookies (ie the default of the library) with a lifetime of browser being open, is renew really used for the cookie? Or only the Redis TTL is honored (which is what I need actually)? I'm a little confused whether my understanding is correct in this particular scenario.

What I understand for my specific scenario is: the browser cookie does not expire so $session_cookie_renew is not applicable. The session expires when the key is removed from Redis (on TTL or destroy session). The browser cookie will be removed when browser is closed. If session is removed from Redis then the cookie will remain but session is invalid - ie I have to trap it in my code and do "start/save" for a new session.

Can you please clarify? Probably I'm reading things and the code wrong so please verify or correct my assumptions and my question.

Thanks

lose ngx.var[cookie_name]

Hi, I met a issue. I found cookie will lose when nginx redirect . For example , GET / HTTP/1.1 just redirect to /index.html. cookie in var[concat(n)] (session.lua function getcookie) will lose, this drives me crazy. Can you help me.

session:regenerate(true) failing after starting session

I am checking for valid sessions and in some situations new credentials are provided. In this situation I am choosing to regenerate the session instead of destroying and creating a new one (my understanding was that regenerate already did that). It is failing to regenerate though.

Here is the stack trace:

2015/10/15 21:15:14 [error] 25755#0: *980 lua entry thread aborted: runtime error: /usr/share/luajit/share/lua/5.1/resty/session.lua:101: attempt to call field 'destroy' (a nil value)
stack traceback:
coroutine 0:
        /usr/share/luajit/share/lua/5.1/resty/session.lua: in function 'regenerate'
        /usr/share/luajit/share/lua/5.1/resty/session.lua:282: in function 'regenerate'
        /etc/nginx/scripts/combined.lua:61: in function </etc/nginx/scripts/combined.lua:1>, client: 10.129.253.131, server: , request: "GET /?accesskey=dd37a772-a0b3-49b9-bd7f-2ce217427f15 HTTP/1.1", host: "10.129.22.47", referrer: "http://localhost:8080/"

Any help would be appreciated. Thanks.

about checking whether the sended session is right

hello, i am new bb to web serivce , i am having problems using lua-resty-session, i am trying to verify the session that the browser sended is origina from the nginx resty part or it is not .I have been searching for some time I cant find a good example to follow. here are my codes:

	local session = require "resty.session".start
	{
	   secret = "623q4hR325t36VsCD3g567922IC0073T",
	   cookie = { persistent = true, lifetime = 280 }
	}
	session.open()
	if session.present then
		-- ngx.print(session.data.name)
		args.sessionids=session.data.name
		args.testsession=1
		args.datasession=session.data
		local ret=rpccall(red, '  LogininfofromSession', 100, args)
	end
	local ret=rpccall(red, 'LoigininfofromPasswordandUsername', 100, args)
	if(ret=='yes')then
		local session = require "resty.session".start
		{
		secret = "623q4hR325t36VsCD3g567922IC0073T",      
		cookie = { persistent = true, lifetime = 280 }
		}
		session.open()	
		session.data.name = "OpenResty Fan"	    
		session:save()
	end

i try to debug by step to find when the sentence session.present wil be l steped into。 And i find everytime even it is not original from resty session it will step into 。what i want is simple.When the session sended is original from openresty calling LogininfofromSession" function.When then session sended is not from orignal just skip this part ,and it call LoigininfofromPasswordandUsername function;And additionally how can I get session id from javascript?Can anyone show me an easy example ,thank you so much !

Consider not checking TLS session ID by default

I just lost a couple hours to debugging why my sessions would suddenly fail to decrypt, sometimes a few seconds after starting the server, sometimes 10 minutes later, with no logging output about why. I had to disable $session_check_ssi.

I think there's a strong case to be made for that not to be the default. Some of the other checks are suspicious, too, like the remote_addr one (clients do rehome), but $session_check_ssi was definitely a big burden until I realized what was going on.

Can you consider logging when $session_check_ssi fails, or default to not doing it at all? Thanks!

Pluggable server-side storage engines for session data

I'd very much like to see server side support for session storage as listed in the roadmap. Any chance that this will move forward in the near future? FWIW: I'd be willing to contribute to the backends themselves (shm, files, memcached, redis...) but I don't feel comfortable to make the changes to the core infrastructure to start supporting those.

How config redis in Lua?

in https://github.com/bungle/lua-resty-session/blob/master/lib/resty/session.lua#L141-L197
no place set self.redis, but in redis.new(conf) need it. Can fix it like this?

diff --git a/lib/resty/session.lua b/lib/resty/session.lua
index 1691ac5..95fe486 100644
--- a/lib/resty/session.lua
+++ b/lib/resty/session.lua
@@ -151,7 +151,8 @@ function session.new(opts)
     if not o then
         g = require "resty.session.identifiers.random"
     end
-    local o, h = pcall(require, "resty.session.storage." .. (y.storage or z.storage))
+    local s = y.storage or z.storage or "cookie"
+    local o, h = pcall(require, "resty.session.storage." .. s)
     if not o then
         h = require "resty.session.storage.cookie"
     end
@@ -189,7 +190,8 @@ function session.new(opts)
             ua         = c.ua         or d.ua,
             scheme     = c.scheme     or d.scheme,
             addr       = c.addr       or d.addr
-        }
+        },
+        [s] = y[s]
     }
     self.storage = h.new(self)
     self.cipher = k.new(self)
@@ -310,4 +312,4 @@ function session:destroy()
     return setcookie(self, "", true)
 end

Advice: is there a way to display content of a encrypted session cookies for debugging?

Hi,

I am trying to figure out an issue with my web application. I think the issue is that the wrong data is stored in the session so I was wondering if there is an easy way that I could inspect the content of my (encrypted) session cookie. I mean something like a command line that would receive as arguments the cookie(s) content and the session secret and display the content of the session?

Cheers,

Tom

Improve variable naming

This is not a bug report, but wishful thinking :)

I had extreme problems reading the code. A short comment for the most important methods would be useful. More importantly variable names could be a lot more helpful.
A lot of variable names are reused for different meanings (d in the example) which makes it even harder to grasp the code.

Example:

local i, e, d, h = self.storage:open(cookie, self.cookie.lifetime)
if i and e and e > time() and d and h then
local k = hmac(self.secret, i .. e)
d = self.cipher:decrypt(d, k, i, self.key)
if d and hmac(k, concat{ i, e, d, self.key }) == h then
d = self.serializer.deserialize(d)
self.id = i

What is the reason for the current naming? Performance improvements?

Race condition when setting session state

We're using lua-resty-openidc in a clustered environment (multiple ingress servers), using a single redis as a session store.

We're experiencing a problem where a race condition occurs when multiple requests are made simultaneously after the access token has expired: both requests will trigger a refresh, obtain a new access token, and store the token to the redis session. Storing the session updates the hmac section of the session cookie, and sets a new session cookie on the response.

The problem seems to be that the request which is last to set the session access-token is not always the last to return the response with the new session cookie, which means that the hmac section in the client side cookie does not correspond with the cookie from the redis session store on the following requests, causing 401 errors.

Is there a good way to resolve this?

Relevant code-section (decrypt returns null):

d = self.cipher:decrypt(d, k, i, self.key)

Environment
  • lua-resty-session: 2.22-1
  • lua: 5.1.5
Configuration
 set $session_secret <secret>;

  set $session_cookie_renew 900;
  set $session_check_ssi         off;

  set $session_storage redis;

  set $session_redis_prefix        sessions;
  set $session_redis_host          redis-master.${OPENSHIFT_NAMESPACE}.svc.cluster.local;
  set $session_redis_port          6379;
  set $session_redis_auth          ${REDIS_PASSWORD};
  set $session_redis_uselocking    on;
  set $session_redis_spinlockwait  10000;
  set $session_redis_maxlockwait   30;
  set $session_redis_pool_timeout  45;
  set $session_redis_pool_size     10;

  set $session_cookie_samesite off;

Hello World example fails on Debian 9

I have installed nginx on a clean Debian 9 machine and got initially the following error:

2017/07/05 20:59:03 [error] 7838#7838: *1 lua entry thread aborted: runtime error: /usr/local/share/lua/5.1/resty/session.lua:20: module 'resty.random' not found:
no field package.preload['resty.random']
no file './resty/random.lua'
no file '/usr/share/luajit-2.0.4/resty/random.lua'
no file '/usr/local/share/lua/5.1/resty/random.lua'
no file '/usr/local/share/lua/5.1/resty/random/init.lua'
no file '/usr/share/lua/5.1/resty/random.lua'
no file '/usr/share/lua/5.1/resty/random/init.lua'
no file './resty/random.so'
no file '/usr/local/lib/lua/5.1/resty/random.so'
no file '/usr/lib/x86_64-linux-gnu/lua/5.1/resty/random.so'
no file '/usr/local/lib/lua/5.1/loadall.so'
no file './resty.so'
no file '/usr/local/lib/lua/5.1/resty.so'
no file '/usr/lib/x86_64-linux-gnu/lua/5.1/resty.so'
no file '/usr/local/lib/lua/5.1/loadall.so'
stack traceback:
coroutine 0:
[C]: in function 'require'
content_by_lua(default:17):2: in function <content_by_lua(default:17):1>, client: 127.0.0.1, server: localhost, request: "GET /start HTTP/1.1", host: "localhost:8080", referrer: "http://localhost:8080/"

Steps to reproduce:

  1. Clean Debian 9 machine

  2. sudo apt-get install nginx-extras

  3. sudo apt-get install luarocks

  4. sudo luarocks install lua-resty-session

  5. replace the file /etc/nginx/sites-available/default by the content of the "Hello World" example (only the "server"-content)

  6. sudo systemctl restart nginx-extras

  7. browse to http://localhost:8080

  8. clicking on "Start the test!"

  9. 500 Internal Server Error appears

  10. /var/log/nginx/error.log shows the error message shown above.

By installing:

  1. sudo luarocks install lua-resty-string
  2. sudo luarocks install lua-cjson

this error can be resolved.

But now the same trial delivers the following error in /var/log/nginx/error.log:

2017/07/05 21:15:20 [error] 12961#12961: *1 lua entry thread aborted: runtime error: /usr/local/share/lua/5.1/resty/aes.lua:170: /usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2: undefined symbol: EVP_CIPHER_CTX_init
stack traceback:
coroutine 0:
[C]: in function '__index'
/usr/local/share/lua/5.1/resty/aes.lua:170: in function 'new'
/usr/local/share/lua/5.1/resty/session/ciphers/aes.lua:46: in function 'encrypt'
/usr/local/share/lua/5.1/resty/session.lua:156: in function 'save'
/usr/local/share/lua/5.1/resty/session.lua:344: in function 'start'
content_by_lua(default:17):2: in function <content_by_lua(default:17):1>, client: 127.0.0.1, server: localhost, request: "GET /start HTTP/1.1", host: "localhost:8080", referrer: "http://localhost:8080/"

Obviously this is a linker error for the libssl-dev. Several trials, for example installing

sudo apt-get install libssl-dev

did not help.

So it appears that the documentation (or the package itself) are missing important dependencies and some linking to openssl is not working out of the box on Debian 9?

Setting session expiration when using Redis?

I see there are these variables when using cookie storage

set $session_cookie_renew      600;
set $session_cookie_lifetime   3600;

Are there analogous ones when using storage like Redis? How can I set the number of seconds after which an inactive session expires? and how long is the session life extended when session is accessed?

Thanks.

feature request: add a declarative "hook" to populate new sessions with data

Use case: When a session is create I would like to load session data from storage such as mysql. This data may include user profile data such as first and last name.

Currently I can do this using Lua at the time when a session is created. However I want to treat session creation as "non-functional" and generic, while still giving each application an ability to populate the session with "functional" information.

One thought I have is to configure an internal location block, e.g. set $session.loader _my_session_loader;, and then at some point in the session lifecycle, we can make a subrequest to the loader. The result of the loader will be stored in the session.

Thoughts?

Session lost upon redirection

Hi there,

I am facing a problem right now. Herer is how I set my session:
local session = require "resty.session".start()
session.data.name = credentials.username \n
session:save()

When I try to grab it from another page upon ngx.redirect I have a nil value for session.data.name

Here is how I try to grab it:

local session = require "resty.session".open()
ngx.say("<p> session.data.name </p>")
Thanks in advance.

Question about session cookie settings

Here are the settings I have:

lua_shared_dict sessions 10m;
ssl_session_timeout 24h;
ssl_session_cache shared:SSL:20m;
set $session_cookie_renew 12h;
set $session_cookie_lifetime 24h;

I want the session to expire ONLY in one of the following cases:

  1. User leaves browser tab open for > 12h with no activity
  2. User closes browser tab (so not persistent)

Are my settings correct/optimal for this use case? Thanks.

session.id use ngx.say or ngx.log print ,but result garbled

Hi,sorry to bother ;
i want to get the session id, but the result i can't recognize, like this '󿾥l 3霫Ն', in nginx.conf i have already add 'charset utf8' ,my openResty version is '1.11.2.1'.
An question:can i use the session.id to identification the same user from explorer?
If you reply and give my question an answer ,i will grateful for this.

how to open session by specialed id?

as you know, <iframe> maybe caused generate a new session even if the domain not changed. So I need the id of parent frame, then page in <iframe> can open the same session. How to get such a "id"?

Does session save only once per request?

I write an func:

function UtilityService:setSession(key, value)
    local session = require "resty.session".start{ secret = "623q4hR325t36VsCD3g567922IC0073T" }
    session.data[key] = value
    session:save()
end

function UtilityService:getSession(key)
    local session = require "resty.session".open{ secret = "623q4hR325t36VsCD3g567922IC0073T" }
    if key == nil then
        return session.data
    else
        return session.data[key]
    end
end

In my page:

    utilityService:setSession("username", "admin")
    utilityService:setSession("password", "password")
    utilityService:setSession("is_admin", true)
    utilityService:setSession("login_time", os.time())

    local cjson = require "cjson"
    ngx.say(cjson.encode(utilityService:getSession()))

Output:
First time: {}
Second time and later: {"login_time":1460522907}

Why?

[requesting how to proceed] saving on start

Hi.

I've been using your library and I've noticed that when I do session.start() for the first time it will set a Set-Cookie in header, because it calls method regenerate. This is not desired when I just want to read and check if session exists. I could do this by first checking if a cookie named session is present, but I think it would be best to delegate this to resty-session, especially since we could change the name of the key and so on, so for now I removed the save instruct on regenerate (since I don't use it directly and I end up calling save after setting some data anyway). What do you think is the best way to proceed?

Thanks for the awesome library btw.

Untrusted X-Forwarded-Proto header

If session.check.scheme is enabled, it is expected that the scheme is checked.
Currently the code looks at the header X-Forwarded-Proto to determine the scheme

local scheme = header["X-Forwarded-Proto"]

If lua-resty-session is not behind a reverse proxy the X-Forwarded-Proto header will not come from a trusted source but may instead be user-supplied or injected by MitM. This way the determined scheme might be different from the actual scheme between user and the server.

It is not clear to me if this might lead to the server sending out confidential payload over http in edge cases.

I would propose that the reverse proxy headers should only be trusted, if a corresponding opt-in flag is set.

Lib doesn't work: error "expecting one argument"

Hi,
I'm trying to use lua-resty-session, but it doesn't work. I'm having these errors in my error.log (I'm using Openresty):

[error] 2542#0: 12456 failed to run header_filter_by_lua: /path/to/openresty/libs/resty/session.lua:134: expecting one argument

Are expired sessions enforced?

From a quick look at the code it wasn't clear if session expiration is enforced on the server side or if it entirely relies on the client cookie expiration.

This can pose issues when an expired cookie is still recoverable on disk when using persistent sessions (through either disk forensics, or because the browser has not been started since the cookie expired).

I imagine this would affect the stateless cookie backend more so than others.

non-persistent cookies seem to be invalid after cookie lifetime elapses

My understanding from the documentation was that, in cases where session.cookie.persistent = off, that $session_cookie_lifetime is ignored for that cookie. Indeed when I start a session with session.cookie.persistent = off andset $session_cookie_lifetime 600; the browser shows that the cookie is a Session cookie and does not show an expiry time/date.

However, in testing, resty-session seems to consider that cookie invalid after 600 seconds.

Have I misread the documentation, or is this an issue in resty-session?

persistent cookies possible?

Hi,

I want my clients to stay logged in next time they open the browser, but I see that you specifically say: no persistent cookies. Why, what's the downside?

Thanks,
Cosmin.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.