Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/bungle/lua-resty-session

Session library for OpenResty – flexible and secure
https://github.com/bungle/lua-resty-session

Last synced: 3 days ago
JSON representation

Session library for OpenResty – flexible and secure

Awesome Lists containing this project

README

        

# 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](https://github.com/bungle/lua-resty-session/tree/v3.10).

## Status

This library is considered production ready.

## Synopsis

```nginx
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([[


Start the test


]])
}
}

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


Session started (%s)


Check if it really was




]], err or "no error"))
}
}

location /started {
content_by_lua_block {
local session, err = require "resty.session".start()

ngx.say(string.format([[


Session was started by %s (%s)


%s

Modify the session




]],
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([[


Session was modified (%s)


Check if it is modified




]], err or err_save or "no error"))
}
}

location /modified {
content_by_lua_block {
local session, err = require "resty.session".start()

ngx.say(string.format([[


Session was started by %s (%s)


%s

Destroy the session




]],
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([[


Session was destroyed (%s)


Check that it really was?




]], err or "no error"))
}
}

location /destroyed {
content_by_lua_block {
local session, err = require "resty.session".open()

ngx.say(string.format([[


Session was really destroyed, you are known as %s (%s)


Start again




]],
session:get_subject() or "Anonymous",
err or "no error"
))
}
}
}
}
```

# Table of Contents

* [Installation](#installation)
* [Using OpenResty Package Manager (opm)](#using-openresty-package-manager-opm)
* [Using LuaRocks](#using-luarocks)
* [Configuration](#configuration)
* [Session Configuration](#session-configuration)
* [Cookie Storage Configuration](#cookie-storage-configuration)
* [DSHM Storage Configuration](#dshm-storage-configuration)
* [File Storage Configuration](#file-storage-configuration)
* [Memcached Storage Configuration](#memcached-storage-configuration)
* [MySQL / MariaDB Storage Configuration](#mysql--mariadb-storage-configuration)
* [Postgres Configuration](#postgres-configuration)
* [Redis Configuration](#redis-configuration)
* [Single Redis Configuration](#single-redis-configuration)
* [Redis Sentinels Configuration](#redis-sentinels-configuration)
* [Redis Cluster Configuration](#redis-cluster-configuration)
* [SHM Configuration](#shm-configuration)
* [API](#api)
* [Initialization](#initialization)
* [session.init](#sessioninit)
* [Constructors](#constructors)
* [session.new](#sessionnew)
* [Helpers](#helpers)
* [session.open](#sessionopen)
* [session.start](#sessionstart)
* [session.logout](#sessionlogout)
* [session.destroy](#sessiondestroy)
* [Instance Methods](#instance-methods)
* [session:open](#sessionopen-1)
* [session:save](#sessionsave)
* [session:touch](#sessiontouch)
* [session:refresh](#sessionrefresh)
* [session:logout](#sessionlogout-1)
* [session:destroy](#sessiondestroy-1)
* [session:close](#sessionclose)
* [session:set_data](#sessionset_data)
* [session:get_data](#sessionget_data)
* [session:set](#sessionset)
* [session:get](#sessionget)
* [session:set_audience](#sessionset_audience)
* [session:get_audience](#sessionget_audience)
* [session:set_subject](#sessionset_subject)
* [session:get_subject](#sessionget_subject)
* [session:get_property](#sessionget_property)
* [session:set_remember](#sessionset_remember)
* [session:get_remember](#sessionget_remember)
* [session:clear_request_cookie](#sessionclear_request_cookie)
* [session:set_headers](#sessionset_headers)
* [session:set_request_headers](#sessionset_request_headers)
* [session:set_response_headers](#sessionset_response_headers)
* [session.info:set](#sessioninfoset)
* [session.info:get](#sessioninfoget)
* [session.info:save](#sessioninfosave)
* [Cookie Format](#cookie-format)
* [Data Encryption](#data-encryption)
* [Cookie Header Authentication](#cookie-header-authentication)
* [Custom Storage Interface](#custom-storage-interface)
* [License](#license)

# Installation

## Using OpenResty Package Manager (opm)

```bash
❯ 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

```bash
❯ 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:

```lua
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](#initialization), [constructor](#constructors),
and [helper](#helpers) 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_domain` | `nil` | Cookie domain, e.g. `"example.com"` |
| `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](https://github.com/grrolland/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:
```sh
❯ luarocks install LuaFileSystem
```

## Memcached Storage Configuration

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

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

## MySQL / MariaDB Storage Configuration

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

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

You also need to create following tables in your database:

```sql
--
-- Database table that stores session data.
--
CREATE TABLE IF NOT EXISTS sessions (
sid CHAR(43) PRIMARY KEY,
name VARCHAR(255),
data MEDIUMTEXT,
exp DATETIME,
INDEX (exp)
) CHARACTER SET ascii;

--
-- Sessions metadata table.
--
-- This is only needed if you want to store session metadata.
--
CREATE TABLE IF NOT EXISTS sessions_meta (
aud VARCHAR(255),
sub VARCHAR(255),
sid CHAR(43),
PRIMARY KEY (aud, sub, sid),
CONSTRAINT FOREIGN KEY (sid) REFERENCES sessions(sid) ON DELETE CASCADE ON UPDATE CASCADE
) CHARACTER SET ascii;
```

## Postgres Configuration

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

| Option | Default | Description |
|---------------------|:-----------------:|-----------------------------------------------------------------------------------------------------------|
| `host` | `"127.0.0.1"` | The host to connect. |
| `port` | `5432` | The port to connect. |
| `application` | `5432` | Set the name of the connection as displayed in pg_stat_activity (defaults to `"pgmoon"`). |
| `username` | `"postgres"` | The database username to authenticate. |
| `password` | `nil` | Password for authentication, may be required depending on server configuration. |
| `database` | `nil` | The database name to connect. |
| `table_name` | `"sessions"` | Name of database table to which to store session data (can be `database schema` prefixed). |
| `table_name_meta` | `"sessions_meta"` | Name of database meta data table to which to store session meta data (can be `database schema` prefixed). |
| `connect_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `connect` method. |
| `send_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `send` method. |
| `read_timeout` | `nil` | Controls the default timeout value used in TCP/unix-domain socket object's `receive` method. |
| `keepalive_timeout` | `nil` | Controls the default maximal idle time of the connections in the connection pool. |
| `pool` | `nil` | A custom name for the connection pool being used. |
| `pool_size` | `nil` | The size of the connection pool. |
| `backlog` | `nil` | A queue size to use when the connection pool is full (configured with pool_size). |
| `ssl` | `false` | Enable SSL. |
| `ssl_verify` | `nil` | Verify server certificate. |
| `ssl_required` | `nil` | Abort the connection if the server does not support SSL connections. |

You also need to create following tables in your database:

```sql
--
-- Database table that stores session data.
--
CREATE TABLE IF NOT EXISTS sessions (
sid TEXT PRIMARY KEY,
name TEXT,
data TEXT,
exp TIMESTAMP WITH TIME ZONE
);
CREATE INDEX ON sessions (exp);

--
-- Sessions metadata table.
--
-- This is only needed if you want to store session metadata.
--
CREATE TABLE IF NOT EXISTS sessions_meta (
aud TEXT,
sub TEXT,
sid TEXT REFERENCES sessions (sid) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (aud, sub, sid)
);
```

The implementation requires `pgmoon` which you can install with LuaRocks:
```sh
❯ 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:
```sh
❯ 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:

```nginx
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:
```sh
❯ 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:

```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](https://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.

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

See [configuration](#configuration) for possible configuration settings.

## Constructors

### session.new

**syntax:** *session = session.new(configuration)*

Creates a new session instance.

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

See [configuration](#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).

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

See [configuration](#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).

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

See [configuration](#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.

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

See [configuration](#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.

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

See [configuration](#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.

```lua
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.

```lua
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`.

```lua
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`.

```lua
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`.

```lua
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`.

```lua
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.

```lua
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`.

```lua
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.

```lua
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.

```lua
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.

```lua
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.

```lua
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.

```lua
local session = require "resty.session".new()
session.set_subject("[email protected]")
```

### session:get_subject

**syntax:** *subject = session:get_subject()*

Get session subject.

```lua
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`.

```lua
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.

```lua
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.

```lua
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.

```lua
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.

```lua
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](#configuration) for possible header names.

### session:set_request_headers

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

Set request headers.

```lua
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](#configuration) for possible header names.

### session:set_response_headers

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

Set request headers.

```lua
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](#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.

```lua
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.

```lua
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).

```lua
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: ``
- mode: `extract only`
- info: `""`
- salt: `""`
2. Use HKDF expand to derive `44` bytes of `output`:
- output length: `44`
- digest: `"sha256"`
- key: ``
- mode: `expand only`
- info: `"encryption:"`
- 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: ``
3. iv: ``
4. 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: ``
- salt: `"encryption:"`
- 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: ``
- mode: `extract only`
- info: `""`
- salt: `""`
2. Use HKDF expand to derive `32` bytes of `mac-key`:
- output length: `32`
- digest: `"sha256"`
- key: ``
- mode: `expand only`
- info: `"authentication:"`
- salt: `""`
2. Calculate message authentication code using HMAC-SHA256:
- digest: `"sha256"`
- key: ``
- message: use the first 66 bytes of `header`, that includes:
1. Type
2. Flags
3. Session ID
4. Creation Time
5. Rolling Offset
6. Data Size
7. Tag
8. Idling Offset

# Custom Storage Interface

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

```lua
---
-- backend for session library
--
-- @module

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

---
-- storage backend configuration
-- @field TBD
-- @table configuration

---
-- Create a storage.
--
-- This creates a new shared memory storage instance.
--
-- @function module.new
-- @tparam[opt] table configuration storage @{configuration}
-- @treturn table 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
```