Перейти к содержанию

jwt: JWT для великого nginx-module-lua

Установка

Если вы еще не настроили подписку на репозиторий RPM, зарегистрируйтесь. Затем вы можете продолжить с следующими шагами.

CentOS/RHEL 7 или Amazon Linux 2

yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-jwt

CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023

dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-jwt

Чтобы использовать эту библиотеку Lua с NGINX, убедитесь, что nginx-module-lua установлен.

Этот документ описывает lua-resty-jwt v0.1.11, выпущенную 11 июля 2017 года.


Эта библиотека требует сборку nginx с OpenSSL, ngx_lua module, LuaJIT 2.0, lua-resty-hmac и lua-resty-string.

Синопсис

    # nginx.conf:

    server {
        default_type text/plain;
        location = /verify {
            content_by_lua '
                local cjson = require "cjson"
                local jwt = require "resty.jwt"

                local jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" ..
                    ".eyJmb28iOiJiYXIifQ" ..
                    ".VAoRL1IU0nOguxURF2ZcKR0SGKE1gCbqwyh8u2MLAyY"
                local jwt_obj = jwt:verify("lua-resty-jwt", jwt_token)
                ngx.say(cjson.encode(jwt_obj))
            ';
        }
        location = /sign {
            content_by_lua '
                local cjson = require "cjson"
                local jwt = require "resty.jwt"

                local jwt_token = jwt:sign(
                    "lua-resty-jwt",
                    {
                        header={typ="JWT", alg="HS256"},
                        payload={foo="bar"}
                    }
                )
                ngx.say(jwt_token)
            ';
        }
    }

Методы

Чтобы загрузить эту библиотеку,

  1. вам нужно указать путь к этой библиотеке в директиве ngx_lua lua_package_path. Например, lua_package_path "/path/to/lua-resty-jwt/lib/?.lua;;";.
  2. вы используете require, чтобы загрузить библиотеку в локальную переменную Lua:
    local jwt = require "resty.jwt"

sign

syntax: local jwt_token = jwt:sign(key, table_of_jwt)

подписывает table_of_jwt в jwt_token.

Аргумент alg указывает, какой алгоритм хеширования использовать (HS256, HS512, RS256).

пример table_of_jwt

{
    "header": {"typ": "JWT", "alg": "HS512"},
    "payload": {"foo": "bar"}
}

verify

syntax: local jwt_obj = jwt:verify(key, jwt_token [, claim_spec [, ...]])

проверяет jwt_token и возвращает таблицу jwt_obj. key может быть заранее определенным ключом (в виде строки), или функцией, которая принимает один параметр (значение kid из заголовка) и возвращает либо заранее определенный ключ (в виде строки) для kid, либо nil, если поиск kid не удался. Этот вызов завершится неудачей, если вы попытаетесь указать функцию для key, и в заголовке не будет существовать kid.

Смотрите Verification для получения деталей о формате параметров claim_spec.

load & verify

syntax: local jwt_obj = jwt:load_jwt(jwt_token)
syntax: local verified = jwt:verify_jwt_obj(key, jwt_obj [, claim_spec [, ...]])

verify = load_jwt + verify_jwt_obj

загружает jwt, проверяет kid, затем проверяет его с правильным ключом.

пример jwt_obj

{
    "raw_header": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9",
    "raw_payload": "eyJmb28iOiJiYXIifQ",
    "signature": "wrong-signature",
    "header": {"typ": "JWT", "alg": "HS256"},
    "payload": {"foo": "bar"},
    "verified": false,
    "valid": true,
    "reason": "signature mismatched: wrong-signature"
}

sign-jwe

syntax: local jwt_token = jwt:sign(key, table_of_jwt)

подписывает table_of_jwt в jwt_token.

Аргумент alg указывает, какой алгоритм хеширования использовать для шифрования ключа (dir). Аргумент enc указывает, какой алгоритм хеширования использовать для шифрования полезной нагрузки (A128CBC-HS256, A256CBC-HS512).

пример table_of_jwt

{
    "header": {"typ": "JWE", "alg": "dir", "enc":"A128CBC-HS256"},
    "payload": {"foo": "bar"}
}

Проверка

Обе функции jwt:load и jwt:verify_jwt_obj принимают в качестве дополнительных параметров любое количество необязательных параметров claim_spec. claim_spec — это просто таблица lua с утверждениями и валидаторами. Каждый ключ в таблице claim_spec соответствует соответствующему ключу в полезной нагрузке, а validator — это функция, которая будет вызвана, чтобы определить, выполнены ли утверждения.

Подпись функции validator выглядит следующим образом:

function(val, claim, jwt_json)

Где val — это значение утверждения из проверяемого jwt_obj (или nil, если оно не существует в полезной нагрузке объекта), claim — это имя утверждения, которое проверяется, а jwt_json — это сериализованное в json представление объекта, который проверяется. Если функции не нужны параметры claim или jwt_json, их можно опустить.

Функция validator возвращает либо true, либо false. Любой validator МОЖЕТ вызвать ошибку, и проверка будет считаться неудачной, а ошибка, которая была вызвана, будет помещена в поле причины результирующего объекта. Если validator ничего не возвращает (т.е. nil), то функция считается успешной — при условии, что она вызвала бы ошибку, если бы не удалась.

Специальное утверждение с именем __jwt может быть использовано так, что если для него существует функция validator, то validator будет вызвана с глубоким клоном всего разобранного jwt объекта в качестве значения val. Это позволяет вам писать проверки для всего объекта, которые могут зависеть от одного или нескольких утверждений.

Несколько таблиц claim_spec могут быть указаны для jwt:load и jwt:verify_jwt_obj — и они будут выполняться по порядку. Нет гарантии порядка выполнения отдельных validators внутри одной claim_spec. Если claim_spec не удается, то любые последующие claim_specs НЕ будут выполнены.

пример claim_spec

{
    sub = function(val) return string.match("^[a-z]+$", val) end,
    iss = function(val)
        for _, value in pairs({ "first", "second" }) do
            if value == val then return true end
        end
        return false
    end,
    __jwt = function(val, claim, jwt_json)
        if val.payload.foo == nil or val.payload.bar == nil then
            error("Need to specify either 'foo' or 'bar'")
        end
    end
}

Валидаторы JWT

Существует библиотека полезных функций validator в resty.jwt-validators. Вы можете использовать эту библиотеку, включив:

local validators = require "resty.jwt-validators"

В настоящее время в библиотеке валидаторов определены следующие функции. Те, которые помечены "(opt)", означают, что существует такая же функция с именем opt_<name>, которая принимает те же параметры. "opt" версия функции вернет true, если ключ не существует в полезной нагрузке проверяемого jwt_object, в то время как "non-opt" версия функции вернет false, если ключ не существует в полезной нагрузке проверяемого jwt_object.

validators.chain(...)

Возвращает валидатор, который объединяет данные функции вместе, одну за другой - пока они продолжают проходить свои проверки.

validators.required(chain_function)

Возвращает валидатор, который возвращает false, если значение не существует. Если значение существует и указана chain_function, то будет возвращено значение chain_function(val, claim, jwt_json), в противном случае будет возвращено true. Это позволяет указать, что значение является обязательным и должно соответствовать некоторой дополнительной проверке.

validators.require_one_of(claim_keys)

Возвращает валидатор, который выдает ошибку с сообщением, если НИ ОДНО из указанных ключей утверждений не существует. Ожидается, что эта функция будет использоваться для полного jwt объекта. claim_keys должны быть непустой таблицей строк.

validators.check(check_val, check_function, name, check_type) (opt)

Возвращает валидатор, который проверяет, возвращает ли результат вызова данной check_function для проверяемого значения и check_val true. Значение check_val и check_function не могут быть nil. Необязательное name используется для сообщений об ошибках и по умолчанию равно "check_value". Необязательный check_type используется для того, чтобы убедиться, что тип проверки совпадает и по умолчанию равен type(check_val). Первый параметр, переданный в check_function, никогда не будет nil. Если check_function вызывает ошибку, она будет добавлена к сообщению об ошибке.

validators.equals(check_val) (opt)

Возвращает валидатор, который проверяет, равно ли значение точно (с использованием ==) данному check_value. Значение check_val не может быть nil.

validators.matches(pattern) (opt)

Возвращает валидатор, который проверяет, соответствует ли значение данному шаблону (с использованием string.match). Значение pattern должно быть строкой.

validators.any_of(check_values, check_function, name, check_type, table_type) (opt)

Возвращает валидатор, который вызывает данную check_function для каждого из указанных check_values и проверяемого значения. Если любой из этих вызовов возвращает true, то эта функция возвращает true. Значение check_values должно быть непустой таблицей со всеми одинаковыми типами, а значение check_function не должно быть nil. Необязательное name используется для сообщений об ошибках и по умолчанию равно "check_values". Необязательный check_type используется для того, чтобы убедиться, что тип проверки совпадает и по умолчанию равен type(check_values[1]) - тип таблицы.

validators.equals_any_of(check_values) (opt)

Возвращает валидатор, который проверяет, равно ли значение точно любому из указанных check_values.

validators.matches_any_of(patterns) (opt)

Возвращает валидатор, который проверяет, соответствует ли значение любому из указанных patterns.

validators.contains_any_of(check_values,name) (opt)

Возвращает валидатор, который проверяет, существует ли значение ожидаемого типа string в любом из указанных check_values. Значение check_values должно быть непустой таблицей со всеми одинаковыми типами. Необязательное name используется для сообщений об ошибках и по умолчанию равно check_values.

validators.greater_than(check_val) (opt)

Возвращает валидатор, который проверяет, как значение сравнивается (численно, с использованием >) с данным check_value. Значение check_val не может быть nil и должно быть числом.

validators.greater_than_or_equal(check_val) (opt)

Возвращает валидатор, который проверяет, как значение сравнивается (численно, с использованием >=) с данным check_value. Значение check_val не может быть nil и должно быть числом.

validators.less_than(check_val) (opt)

Возвращает валидатор, который проверяет, как значение сравнивается (численно, с использованием <) с данным check_value. Значение check_val не может быть nil и должно быть числом.

validators.less_than_or_equal(check_val) (opt)

Возвращает валидатор, который проверяет, как значение сравнивается (численно, с использованием <=) с данным check_value. Значение check_val не может быть nil и должно быть числом.

validators.is_not_before() (opt)

Возвращает валидатор, который проверяет, что текущее время не раньше проверяемого значения в пределах допустимого отклонения системы. Это означает, что:

val <= (system_clock() + system_leeway).

validators.is_not_expired() (opt)

Возвращает валидатор, который проверяет, что текущее время не равно или не позже проверяемого значения в пределах допустимого отклонения системы. Это означает, что:

val > (system_clock() - system_leeway).

validators.is_at() (opt)

Возвращает валидатор, который проверяет, что текущее время совпадает с проверяемым значением в пределах допустимого отклонения системы. Это означает, что:

val >= (system_clock() - system_leeway) and val <= (system_clock() + system_leeway).

validators.set_system_leeway(leeway)

Функция для установки допустимого отклонения (в секундах), используемого для is_not_before и is_not_expired. По умолчанию используется 0 секунд.

validators.set_system_clock(clock)

Функция для установки системных часов, используемых для is_not_before и is_not_expired. По умолчанию используется ngx.now.

пример claim_spec, использующего валидаторы

local validators = require "resty.jwt-validators"
local claim_spec = {
    sub = validators.opt_matches("^[a-z]+$"),
    iss = validators.equals_any_of({ "first", "second" }),
    __jwt = validators.require_one_of({ "foo", "bar" })
}

Параметры устаревшей версии/временные рамки

Чтобы поддерживать код, который использовал предыдущие версии этой библиотеки, а также для упрощения указания claim_specs, основанных на временных рамках, вы можете использовать вместо любого одного параметра claim_spec таблицу validation_options. Параметр должен быть представлен в виде таблицы ключ/значение. Каждый ключ таблицы должен быть выбран из следующего списка.

При использовании устаревших validation_options вы ДОЛЖНЫ УКАЖИТЬ ТОЛЬКО эти параметры. То есть, вы не можете смешивать устаревшие validation_options с другими валидаторами claim_spec. Чтобы достичь этого, вы должны указать несколько параметров для функций jwt:load/jwt:verify_jwt_obj.

  • lifetime_grace_period: Определите допустимое отклонение в секундах для учета расхождения часов между сервером, который сгенерировал jwt, и сервером, который его проверяет. Значение должно быть нулевым (0) или положительным целым числом.

    • Когда этот параметр проверки указан, процесс будет гарантировать, что jwt содержит хотя бы одно из двух утверждений nbf или exp и сравнивать текущее время с этими границами. Если jwt будет признан истекшим или недействительным, проверка завершится неудачей.

    • Когда ни одно из утверждений nbf и exp не может быть найдено, проверка завершится неудачей.

    • Ожидается, что утверждения nbf и exp будут выражены в jwt в виде числовых значений. Если это не так, проверка завершится неудачей.

    • Указание этого параметра эквивалентно вызову:

      validators.set_system_leeway(leeway)
      

    и указанию в качестве claim_spec:

    {
      __jwt = validators.require_one_of({ "nbf", "exp" }),
      nbf = validators.opt_is_not_before(),
      exp = validators.opt_is_not_expired()
    }
    

  • require_nbf_claim: Укажите, является ли утверждение nbf необязательным или нет. Значение должно быть булевым.

    • Когда этот параметр проверки установлен в true и не указан lifetime_grace_period, подразумевается нулевое (0) отклонение.

    • Указание этого параметра эквивалентно указанию в качестве claim_spec:

      {
        nbf = validators.is_not_before(),
      }
      

  • require_exp_claim: Укажите, является ли утверждение exp необязательным или нет. Значение должно быть булевым.

    • Когда этот параметр проверки установлен в true и не указан lifetime_grace_period, подразумевается нулевое (0) отклонение.

    • Указание этого параметра эквивалентно указанию в качестве claim_spec:

      {
        exp = validators.is_not_expired(),
      }
      

  • valid_issuers: Белый список проверенных эмитентов jwt. Значение должно быть массивом строк.

    • Когда этот параметр проверки указан, процесс будет сравнивать утверждение jwt iss со списком действительных эмитентов. Сравнение выполняется с учетом регистра. Если эмитент jwt не найден в белом списке, проверка завершится неудачей.

    • Ожидается, что утверждение iss будет выражено в jwt в виде строки. Если это не так, проверка завершится неудачей.

    • Указание этого параметра эквивалентно указанию в качестве claim_spec:

      {
        iss = validators.equals_any_of(valid_issuers),
      }
      

пример использования validation_options

local jwt_obj = jwt:verify(key, jwt_token,
    {
        lifetime_grace_period = 120,
        require_exp_claim = true,
        valid_issuers = { "my-trusted-issuer", "my-other-trusteed-issuer" }
    }
)

Примеры

Тестирование с Docker

./ci script

См. также

GitHub

Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-jwt.