Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add verification of arbitrary claims #27

Merged
merged 1 commit into from
Apr 27, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add verification of arbitrary claims
See #26 and #23 for details of this feature.
  • Loading branch information
toonetown committed Apr 26, 2016
commit eae603f44574c07c88bbca8c33c658094f1aaf5d
202 changes: 178 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Table of Contents
* [verify](#verify)
* [load and verify](#load--verify)
* [sign JWE](#sign-jwe)
* [Verification](#verification)
* [JWT Validators](#jwt-validators)
* [Legacy/Timeframe options](#legacy-timeframe-options)
* [Example](#examples)
* [Installation](#installation)
* [Testing With Docker](#testing-with-docker)
Expand Down Expand Up @@ -120,16 +123,18 @@ The `alg` argument specifies which hashing algorithm to use (`HS256`, `HS512`, `

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

verify a jwt_token and returns a jwt_obj table. `key` can be a pre-shared key (as a string), *or* a function which takes a single parameter (the value of `kid` from the header) and returns either the pre-shared key (as a string) for the `kid` or `nil` if the `kid` lookup failed. This call will fail if you try to specify a function for `key` and there is no `kid` existing in the header.

See [Verification](#verification) for details on the format of `claim_spec` parameters.


load & verify
-------------
```
syntax: local jwt_obj = jwt:load_jwt(jwt_token)
syntax: local verified = jwt:verify_jwt_obj(key, jwt_obj, [, validation_options])
syntax: local verified = jwt:verify_jwt_obj(key, jwt_obj [, claim_spec [, ...]])
```


Expand Down Expand Up @@ -172,63 +177,212 @@ The `enc` argument specifies which hashing algorithm to use for encrypting paylo

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

verify a jwt_token and returns a jwt_obj table
[Back to TOC](#table-of-contents)


validation_options parameter
----------------------------
Verification
============

Both the `jwt:load` and `jwt:verify_jwt_obj` functions take, as additional parameters, any number of optional `claim_spec` parameters. A `claim_spec` is simply a lua table of claims and validators. Each key in the `claim_spec` table corresponds to a matching key in the payload, and the `validator` is a function that will be called to determine if the claims are met.

The signature of a `validator` function is:

```
function(val, claim, jwt_json)
```

Where `val` is the value of the claim from the `jwt_obj` being tested (or nil if it doesn't exist in the object's payload), `claim` is the name of the claim that is being verified, and `jwt_json` is a json-serialized representation of the object that is being verified. If the function has no need of the `claim` or `jwt_json`, parameters, they may be left off.

A `validator` function returns either `true` or `false`. Any `validator` *MAY* raise an error, and the validation will be treated as a failure, and the error that was raised will be put into the reason field of the resulting object. If a `validator` returns nothing (i.e. `nil`), then the function is treated to have succeeded - under the assumption that it would have raised an error if it would have failed.

A special claim named `__jwt` can be used such that if a `validator` function exists for it, then the `validator` will be called with a deep clone of the entire parsed jwt object as the value of `val`. This is so that you can write verifications for an entire object that may depend on one or more claims.

Multiple `claim_spec` tables can be specified to the `jwt:load` and `jwt:verify_jwt_obj` - and they will be executed in order. There is no guarantee of the execution order of individual `validators` within a single `claim_spec`. If a `claim_spec` fails, then any following `claim_specs` will *NOT* be executed.


### sample `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 Validators
--------------

A library of helpful `validator` functions exists at `resty.jwt-validators`. You can use this library by including:
```
local validators = require "resty.jwt-validators"
```

The following functions are currently defined in the validator library. Those marked with "(opt)" means that the same function exists named `opt_<name>` which takes the same parameters. The "opt" version of the function will return `true` if the key does not exist in the payload of the jwt_object being verified, while the "non-opt" version of the function will return false if the key does not exist in the payload of the jwt_object being verified.

#### `validators.chain(...)` ####
Returns a validator that chains the given functions together, one after another - as long as they keep passing their checks.

#### `validators.required(chain_function)` ####
Returns a validator that returns `false` if a value doesn't exist. If the value exists and a `chain_function` is specified, then the value of `chain_function(val, claim, jwt_json)` will be returned, otherwise, `true` will be returned. This allows for specifying that a value is both required *and* it must match some additional check.

#### `validators.require_one_of(claim_keys)` ####
Returns a validator which errors with a message if *NONE* of the given claim keys exist. It is expected that this function is used against a full jwt object. The claim_keys must be a non-empty table of strings.

#### `validators.check(check_val, check_function, name, check_type)` (opt) ####
Returns a validator that checks if the result of calling the given `check_function` for the tested value and `check_val` returns true. The value of `check_val` and `check_function` cannot be nil. The optional `name` is used for error messages and defaults to "check_value". The optional `check_type` is used to make sure that the check type matches and defaults to `type(check_val)`. The first parameter passed to check_function will *never* be nil. If the `check_function` raises an error, that will be appended to the error message.

#### `validators.equals(check_val)` (opt) ####
Returns a validator that checks if a value exactly equals (using `==`) the given check_value. The value of `check_val` cannot be nil.

#### `validators.matches(pattern)` (opt) ####
Returns a validator that checks if a value matches the given pattern (using `string.match`). The value of `pattern` must be a string.

#### `validators.any_of(check_values, check_function, name, check_type, table_type)` (opt) ####
Returns a validator which calls the given `check_function` for each of the given `check_values` and the tested value. If any of these calls return `true`, then this function returns `true`. The value of `check_values` must be a non-empty table with all the same types, and the value of `check_function` must not be `nil`. The optional `name` is used for error messages and defaults to "check_values". The optional `check_type` is used to make sure that the check type matches and defaults to `type(check_values[1])` - the table type.

#### `validators.equals_any_of(check_values)` (opt) ####
Returns a validator that checks if a value exactly equals any of the given `check_values`.

This parameter allows one to fine tune the verification process by specifying the additional validations that should be applied to the jwt.
#### `validators.matches_any_of(patterns)` (opt) ####
Returns a validator that checks if a value matches any of the given `patterns`.

The parameter should be expressed as a key/value table. Each key of the table should be picked from the following list.
#### `validators.greater_than(check_val)` (opt) ####
Returns a validator that checks how a value compares (numerically, using `>`) to a given `check_value`. The value of `check_val` cannot be `nil` and must be a number.

#### `validators.greater_than_or_equal(check_val)` (opt) ####
Returns a validator that checks how a value compares (numerically, using `>=`) to a given `check_value`. The value of `check_val` cannot be `nil` and must be a number.

#### `validators.less_than(check_val)` (opt) ####
Returns a validator that checks how a value compares (numerically, using `<`) to a given `check_value`. The value of `check_val` cannot be `nil` and must be a number.

#### `validators.less_than_or_equal(check_val)` (opt) ####
Returns a validator that checks how a value compares (numerically, using `<=`) to a given `check_value`. The value of `check_val` cannot be `nil` and must be a number.

#### `validators.is_not_before()` (opt) ####
Returns a validator that checks if the current time is not before the tested value within the system's leeway. This means that:
```
val <= (system_clock() + system_leeway).
```

#### `validators.is_not_expired()` (opt) ####
Returns a validator that checks if the current time is not equal to or after the tested value within the system's leeway. This means that:
```
val > (system_clock() - system_leeway).
```

#### `validators.is_at()` (opt) ####
Returns a validator that checks if the current time is the same as the tested value within the system's leeway. This means that:
```
val >= (system_clock() - system_leeway) and val <= (system_clock() + system_leeway).
```

#### `validators.set_system_leeway(leeway)` ####
A function to set the leeway (in seconds) used for `is_not_before` and `is_not_expired`. The default is to use `0` seconds

#### `validators.set_system_clock(clock)` ####
A function to set the system clock used for `is_not_before` and `is_not_expired`. The default is to use `ngx.now`

### sample `claim_spec` using validators ###
```
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" })
}
```


Legacy/Timeframe options
------------------------

In order to support code which used previous versions of this library, as well as to simplify specifying timeframe-based `claim_specs`, you may use in place of any single `claim_spec` parameter a table of `validation_options`. The parameter should be expressed as a key/value table. Each key of the table should be picked from the following list.

When using legacy `validation_options`, you *MUST ONLY* specify these options. That is, you cannot mix legacy `validation_options` with other `claim_spec` validators. In order to achieve that, you must specify multiple options to the `jwt:load`/`jwt:verify_jwt_obj` functions.

* `lifetime_grace_period`: Define the leeway in seconds to account for clock skew between the server that generated the jwt and the server validating it. Value should be zero (`0`) or a positive integer.

* When this validation option is specified, the process will ensure that the jwt contains at least one of the two `nbf` or `exp` claim and compare the current clock time against those boundaries. Would the jwt be deemed as expired or not valid yet, verification will fail.
* When this validation option is specified, the process will ensure that the jwt contains at least one of the two `nbf` or `exp` claim and compare the current clock time against those boundaries. Would the jwt be deemed as expired or not valid yet, verification will fail.

* When none of the `nbf` and `exp` claims can be found, verification will fail.
* When none of the `nbf` and `exp` claims can be found, verification will fail.

* `nbf` and `exp` claims are expected to be expressed in the jwt as numerical values. Wouldn't that be the case, verification will fail.
* `nbf` and `exp` claims are expected to be expressed in the jwt as numerical values. Wouldn't that be the case, verification will fail.

* Specifying this option is equivalent to calling:
```
validators.set_system_leeway(leeway)
```

and specifying as a `claim_spec`:
```
{
__jwt = validators.require_one_of({ "nbf", "exp" }),
nbf = validators.opt_is_not_before(),
exp = validators.opt_is_not_expired()
}
```

* `require_nbf_claim`: Express if the `nbf` claim is optional or not. Value should be a boolean.

* When this validation option is set to `true` and no `lifetime_grace_period` has been specified, a zero (`0`) leeway is implied.
* When this validation option is set to `true` and no `lifetime_grace_period` has been specified, a zero (`0`) leeway is implied.

* Specifying this option is equivalent to specifying as a `claim_spec`:
```
{
nbf = validators.is_not_before(),
}
```

* `require_exp_claim`: Express if the `exp` claim is optional or not. Value should be a boolean.

* When this validation option is set to `true` and no `lifetime_grace_period` has been specified, a zero (`0`) leeway is implied.
* When this validation option is set to `true` and no `lifetime_grace_period` has been specified, a zero (`0`) leeway is implied.

* Specifying this option is equivalent to specifying as a `claim_spec`:
```
{
exp = validators.is_not_expired(),
}
```

* `valid_issuers`: Whitelist the vetted issuers of the jwt. Value should be a array of strings.

* When this validation option is specified, the process will compare the jwt `iss` claim against the list of valid issuers. Comparison is done in a case sensitive manner. Would the jwt issuer not be found in the whitelist, verification will fail.
* When this validation option is specified, the process will compare the jwt `iss` claim against the list of valid issuers. Comparison is done in a case sensitive manner. Would the jwt issuer not be found in the whitelist, verification will fail.

* `iss` claim is expected to be expressed in the jwt as a string. Wouldn't that be the case, verification will fail.
* `iss` claim is expected to be expressed in the jwt as a string. Wouldn't that be the case, verification will fail.

* `claims`: Table of other claims to check in the form of:
```
{
claim: function_or_string
...
}
```
* Specifying this option is equivalent to specifying as a `claim_spec`:
```
{
iss = validators.equals_any_of(valid_issuers),
}
```

* When this validation option is specified, all claims listed as a key (`claim` in the above example) will be required. The value of that claim will be matched (using string.match) if `function_or_string` is a string, or the value will be passed as a single parameter to `function_or_string` if it is a function. When processed as a function, your function can either throw an error *or* `return false` to indicate that the check failed. If no error is thrown, and `false` is not returned, then only the existence of the value is validated.

### sample of validation_options usage ###
```
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" },
claims = { sub = "My Test Subject" }
valid_issuers = { "my-trusted-issuer", "my-other-trusteed-issuer" }
}
)
```



Examples
========
* [JWT Auth With Query and Cookie](examples/README.md#jwt-auth-using-query-and-cookie)
Expand Down
Loading