Skip to content

Commit b22e4ec

Browse files
authored
Merge pull request #3 from peter-evans/0.3.0
0.3.0
2 parents b61bc2f + a385e8f commit b22e4ec

6 files changed

Lines changed: 625 additions & 95 deletions

File tree

.dev/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ RUN apt-get -y update \
1313
&& luarocks install basexx \
1414
&& luarocks install lua-struct \
1515
&& luarocks install lua-cjson 2.1.0 \
16+
&& luarocks install date \
1617
&& luarocks install busted \
1718
&& luarocks install luacheck \
1819
&& luarocks install luacov

README.md

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ This implementation doesn't support the v1 protocol. Please note that v1 should
2121
- [x] v2 local authentication (Blake2b and XChaCha20-Poly1305)
2222
- [x] v2 public authentication (Ed25519 signatures)
2323
- [x] JSON payload and footer processing
24-
- [ ] Claims validation
25-
- [ ] Integrated key ID (kid) support
24+
- [x] Registered claims validation
25+
- [x] Custom claims validation
2626
- [ ] High-level token builder API
27+
- [ ] Integrated key ID (kid) support
2728
- [ ] API documentation
2829

2930
## Installation
@@ -66,48 +67,98 @@ __v2.local__ :
6667
```
6768
local paseto = require "paseto.v2"
6869
69-
local key, payload_claims, token, footer_claims
70-
local extracted_footer_claims, extracted_footer, decrypted_claims
71-
payload_claims = {}
72-
payload_claims["clientid"] = 100099
73-
payload_claims["message"] = "secret"
74-
footer_claims = { kid = "123456789" }
70+
local key, payload_claims, token, footer_claims, claim_rules
71+
local extracted_footer_claims, extracted_footer, decrypted_claims, enforced_claims
72+
payload_claims = {
73+
iss = "paragonie.com",
74+
jti = "87IFSGFgPNtQNNuw0AtuLttP",
75+
aud = "some-audience.com",
76+
sub = "test",
77+
iat = "2018-01-01T00:00:00+00:00",
78+
nbf = "2018-01-01T00:00:00+00:00",
79+
exp = "2099-01-01T00:00:00+00:00",
80+
data = "this is a secret message",
81+
myclaim = "required value"
82+
}
83+
footer_claims = { kid = "MDlCMUIwNzU4RTA2QzZFMDQ4" }
84+
claim_rules = {
85+
IssuedBy = "paragonie.com",
86+
IdentifiedBy = "87IFSGFgPNtQNNuw0AtuLttP",
87+
ForAudience = "some-audience.com",
88+
Subject = "test",
89+
NotExpired = true,
90+
ValidAt = true,
91+
ContainsClaim = "data",
92+
myclaim = "required value"
93+
}
7594
7695
-- generate symmetric key
7796
key = paseto.generate_symmetric_key()
7897
79-
-- encrypt/decrypt without footer
98+
-- encrypt/decrypt without footer and without enforcing claim rules
8099
token = paseto.encrypt(key, payload_claims)
81100
decrypted_claims = paseto.decrypt(key, token)
82101
83-
-- encrypt/decrypt with footer
102+
-- encrypt with footer
84103
token = paseto.encrypt(key, payload_claims, footer_claims)
104+
105+
-- extract footer claims (e.g. to determine public key from kid claim)
85106
extracted_footer_claims, extracted_footer = paseto.extract_footer_claims(token)
86-
decrypted_claims = paseto.decrypt(key, token, extracted_footer)
107+
108+
-- decrypt without enforcing claim rules
109+
decrypted_claims = paseto.decrypt(key, token, nil, extracted_footer)
110+
111+
-- decrypt and enforce claim rules
112+
enforced_claims = paseto.decrypt(key, token, claim_rules, extracted_footer)
87113
```
88114

89115
__v2.public__ :
90116
```
91117
local paseto = require "paseto.v2"
92118
93-
local secret_key, public_key, payload_claims, token, footer_claims
94-
local extracted_footer_claims, extracted_footer, verified_claims
95-
payload_claims = {}
96-
payload_claims["clientid"] = 100099
97-
payload_claims["message"] = "secret"
98-
footer_claims = { kid = "123456789" }
119+
local secret_key, public_key, payload_claims, token, footer_claims, claim_rules
120+
local extracted_footer_claims, extracted_footer, verified_claims, enforced_claims
121+
payload_claims = {
122+
iss = "paragonie.com",
123+
jti = "87IFSGFgPNtQNNuw0AtuLttP",
124+
aud = "some-audience.com",
125+
sub = "test",
126+
iat = "2018-01-01T00:00:00+00:00",
127+
nbf = "2018-01-01T00:00:00+00:00",
128+
exp = "2099-01-01T00:00:00+00:00",
129+
data = "this is a signed message",
130+
myclaim = "required value"
131+
}
132+
footer_claims = { kid = "MDlCMUIwNzU4RTA2QzZFMDQ4" }
133+
claim_rules = {
134+
IssuedBy = "paragonie.com",
135+
IdentifiedBy = "87IFSGFgPNtQNNuw0AtuLttP",
136+
ForAudience = "some-audience.com",
137+
Subject = "test",
138+
NotExpired = true,
139+
ValidAt = true,
140+
ContainsClaim = "data",
141+
myclaim = "required value"
142+
}
99143
100144
-- generate key pair
101145
secret_key, public_key = paseto.generate_asymmetric_secret_key()
102146
103-
-- sign/verify without footer
147+
-- sign/verify without footer and without enforcing claim rules
104148
token = paseto.sign(secret_key, payload_claims)
105149
verified_claims = paseto.verify(public_key, token)
106150
107-
-- sign/verify with footer
151+
-- sign with footer
108152
token = paseto.sign(secret_key, payload_claims, footer_claims)
153+
154+
-- extract footer claims (e.g. to determine public key from kid claim)
109155
extracted_footer_claims, extracted_footer = paseto.extract_footer_claims(token)
110-
verified_claims = paseto.verify(public_key, token, extracted_footer)
156+
157+
-- verify without enforcing claim rules
158+
verified_claims = paseto.verify(public_key, token, nil, extracted_footer)
159+
160+
-- verify and enforce claim rules
161+
enforced_claims = paseto.verify(public_key, token, claim_rules, extracted_footer)
111162
```
112163

113164
#### Core API

paseto/v2.lua

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
local v2 = {
2-
_VERSION = 'paseto v0.2.0',
2+
_VERSION = 'paseto v0.3.0',
33
_DESCRIPTION = 'PASETO (Platform-Agnostic Security Tokens) for Lua',
44
_URL = 'https://github.com/peter-evans/paseto-lua',
55
_LICENSE = [[
@@ -29,6 +29,7 @@ local v2 = {
2929

3030
local paseto = require "paseto.v2.core"
3131
local json = require "cjson"
32+
local date = require "date"
3233

3334
local function encode_json(data)
3435
local ok, encoded = pcall(function()
@@ -50,6 +51,120 @@ local function decode_json(data)
5051
return decoded
5152
end
5253

54+
local function required_claim_exists(payload_claims, key)
55+
if payload_claims[key] == nil then
56+
return false, "Missing required claim '" .. key .."'"
57+
end
58+
return true
59+
end
60+
61+
local function claim_matches_expected_value(payload_claims, key, value)
62+
local exists, err = required_claim_exists(payload_claims, key)
63+
if exists == false then
64+
return false, err
65+
end
66+
if payload_claims[key] ~= value then
67+
return false, "Claim '" .. key .."' does not match the expected value"
68+
end
69+
return true
70+
end
71+
72+
local function claim_exp_is_valid(payload_claims)
73+
local key = "exp"
74+
local exists, err = required_claim_exists(payload_claims, key)
75+
if exists == false then
76+
return false, err
77+
end
78+
if date(payload_claims[key]) < date(os.time()) then
79+
return false, "Token has expired"
80+
end
81+
return true
82+
end
83+
84+
local function claim_iat_is_valid(payload_claims)
85+
local key = "iat"
86+
local exists, err = required_claim_exists(payload_claims, key)
87+
if exists == false then
88+
return false, err
89+
end
90+
if date(payload_claims[key]) > date(os.time()) then
91+
return false, "Token was issued in the future"
92+
end
93+
return true
94+
end
95+
96+
local function claim_nbf_is_valid(payload_claims)
97+
local key = "nbf"
98+
local exists, err = required_claim_exists(payload_claims, key)
99+
if exists == false then
100+
return false, err
101+
end
102+
if date(payload_claims[key]) > date(os.time()) then
103+
return false, "Token cannot be used yet"
104+
end
105+
return true
106+
end
107+
108+
local registered_claims = {
109+
ForAudience = function(payload_claims, value)
110+
return claim_matches_expected_value(payload_claims, "aud", value)
111+
end,
112+
IdentifiedBy = function(payload_claims, value)
113+
return claim_matches_expected_value(payload_claims, "jti", value)
114+
end,
115+
IssuedBy = function(payload_claims, value)
116+
return claim_matches_expected_value(payload_claims, "iss", value)
117+
end,
118+
Subject = function(payload_claims, value)
119+
return claim_matches_expected_value(payload_claims, "sub", value)
120+
end,
121+
NotExpired = function(payload_claims)
122+
return claim_exp_is_valid(payload_claims)
123+
end,
124+
ValidAt = function(payload_claims)
125+
local valid, err
126+
valid, err = claim_iat_is_valid(payload_claims)
127+
if valid == false then
128+
return false, err
129+
end
130+
valid, err = claim_nbf_is_valid(payload_claims)
131+
if valid == false then
132+
return false, err
133+
end
134+
valid, err = claim_exp_is_valid(payload_claims)
135+
if valid == false then
136+
return false, err
137+
end
138+
end,
139+
ContainsClaim = function(payload_claims, value)
140+
local exists, err = required_claim_exists(payload_claims, value)
141+
if exists == false then
142+
return false, err
143+
end
144+
return true
145+
end
146+
}
147+
148+
local function validate_claims(payload_claims, claim_rules)
149+
if type(claim_rules) ~= "table" then
150+
return false, "Invalid claim rules format"
151+
end
152+
for key, value in pairs(claim_rules) do
153+
if registered_claims[key] ~= nil then
154+
local valid, err = registered_claims[key](payload_claims, value)
155+
if valid == false then
156+
return false, err
157+
end
158+
else
159+
local matches, err = claim_matches_expected_value(payload_claims, key, value)
160+
if matches == false then
161+
return false, err
162+
end
163+
end
164+
end
165+
return true
166+
end
167+
53168
-- API --
54169

55170
v2.SYMMETRIC_KEY_BYTES = paseto.SYMMETRIC_KEY_BYTES
@@ -101,8 +216,8 @@ function v2.encrypt(key, payload_claims, footer_claims)
101216
return paseto.encrypt(key, payload, footer)
102217
end
103218

104-
function v2.decrypt(key, token, footer)
105-
local err, decrypted, payload_claims
219+
function v2.decrypt(key, token, claim_rules, footer)
220+
local err, decrypted, payload_claims, valid
106221
decrypted, err = paseto.decrypt(key, token, footer)
107222
if decrypted == nil then
108223
return nil, err
@@ -111,6 +226,12 @@ function v2.decrypt(key, token, footer)
111226
if payload_claims == nil then
112227
return nil, err
113228
end
229+
if claim_rules ~= nil then
230+
valid, err = validate_claims(payload_claims, claim_rules)
231+
if valid == false then
232+
return nil, err
233+
end
234+
end
114235
return payload_claims
115236
end
116237

@@ -131,8 +252,8 @@ function v2.sign(secret_key, payload_claims, footer_claims)
131252
return paseto.sign(secret_key, payload, footer)
132253
end
133254

134-
function v2.verify(public_key, token, footer)
135-
local err, verified_claims, payload_claims
255+
function v2.verify(public_key, token, claim_rules, footer)
256+
local err, verified_claims, payload_claims, valid
136257
verified_claims, err = paseto.verify(public_key, token, footer)
137258
if verified_claims == nil then
138259
return nil, err
@@ -141,6 +262,12 @@ function v2.verify(public_key, token, footer)
141262
if payload_claims == nil then
142263
return nil, err
143264
end
265+
if claim_rules ~= nil then
266+
valid, err = validate_claims(payload_claims, claim_rules)
267+
if valid == false then
268+
return nil, err
269+
end
270+
end
144271
return payload_claims
145272
end
146273

0 commit comments

Comments
 (0)