philiprehberger-jwt_kit 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +42 -0
- data/lib/philiprehberger/jwt_kit/configuration.rb +86 -0
- data/lib/philiprehberger/jwt_kit/decoder.rb +1 -0
- data/lib/philiprehberger/jwt_kit/encoder.rb +3 -1
- data/lib/philiprehberger/jwt_kit/revocation.rb +15 -7
- data/lib/philiprehberger/jwt_kit/token_pair.rb +3 -1
- data/lib/philiprehberger/jwt_kit/version.rb +1 -1
- data/lib/philiprehberger/jwt_kit.rb +18 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4cd6d9f8d91d59046e8427bb209a93e9220a3ff0398ef1c84758a9396fae2eb7
|
|
4
|
+
data.tar.gz: 9fc834f733af1020130928f51ee99ff24d94f468c64ab86a638d7859dbba658e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2751b52e541a834214dee62b4eaa136798d7f244900c206e063738dae3fd2ae90a2c350916ca82dcf714558028eeecb96ea4ac269a02ede7d6bb7c7c5116b7f2
|
|
7
|
+
data.tar.gz: 44de4708a12b6bf7b0317a98db9a05d3375a5083791cd126458b9f63f716942a054a4de5327a6a13c918fe1421936a72f5e19451c402f9dbb75070d234169b47
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-04-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Lifecycle callbacks on `Configuration`: `on_encode`, `on_decode`, `on_refresh`, `on_revoke` for audit logging and metrics without monkey-patching
|
|
14
|
+
|
|
15
|
+
## [0.4.0] - 2026-04-15
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `JwtKit.expired?(token)` — check a token's `exp` claim without verifying the signature
|
|
19
|
+
|
|
10
20
|
## [0.3.2] - 2026-04-09
|
|
11
21
|
|
|
12
22
|
### Fixed
|
data/README.md
CHANGED
|
@@ -108,6 +108,15 @@ result[:header] # => {"alg"=>"HS256", "typ"=>"JWT"}
|
|
|
108
108
|
result[:payload] # => {"user_id"=>42, "exp"=>..., "iat"=>..., "jti"=>...}
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
### Expiration Check
|
|
112
|
+
|
|
113
|
+
Check whether a token's `exp` claim is in the past without verifying the signature:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
Philiprehberger::JwtKit.expired?(token) # => false
|
|
117
|
+
# Use to decide whether to refresh before the authoritative decode
|
|
118
|
+
```
|
|
119
|
+
|
|
111
120
|
### Audience Validation
|
|
112
121
|
|
|
113
122
|
```ruby
|
|
@@ -171,6 +180,34 @@ Replace the default in-memory store with any object that responds to `#revoke`,
|
|
|
171
180
|
Philiprehberger::JwtKit.revocation_store = MyRedisRevocationStore.new
|
|
172
181
|
```
|
|
173
182
|
|
|
183
|
+
### Lifecycle Callbacks
|
|
184
|
+
|
|
185
|
+
Hook into encode, decode, refresh, and revoke without monkey-patching. Useful for audit logging, metrics, and tracing:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
Philiprehberger::JwtKit.configure do |c|
|
|
189
|
+
c.secret = 'your-secret-key'
|
|
190
|
+
|
|
191
|
+
c.on_encode do |token, payload|
|
|
192
|
+
Metrics.increment('jwt.encoded', tags: { iss: payload['iss'] })
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
c.on_decode do |payload|
|
|
196
|
+
Audit.log('jwt.decoded', user_id: payload['user_id'], jti: payload['jti'])
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
c.on_refresh do |new_token|
|
|
200
|
+
Metrics.increment('jwt.refreshed')
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
c.on_revoke do |jti|
|
|
204
|
+
Audit.log('jwt.revoked', jti: jti)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Callbacks fire only after a successful operation. Exceptions raised inside a callback are swallowed so they cannot break the calling JWT operation.
|
|
210
|
+
|
|
174
211
|
## API
|
|
175
212
|
|
|
176
213
|
| Method | Description |
|
|
@@ -186,8 +223,13 @@ Philiprehberger::JwtKit.revocation_store = MyRedisRevocationStore.new
|
|
|
186
223
|
| `JwtKit.revoke(token)` | Revokes a token by its JTI |
|
|
187
224
|
| `JwtKit.revoked?(token)` | Checks if a token has been revoked |
|
|
188
225
|
| `JwtKit.peek(token)` | Decode header and payload without signature verification |
|
|
226
|
+
| `JwtKit.expired?(token)` | Check `exp` claim without verifying the signature |
|
|
189
227
|
| `JwtKit.revocation_store=` | Set a custom revocation store |
|
|
190
228
|
| `MemoryStore#cleanup!(max_age:)` | Remove revocation entries older than max_age seconds |
|
|
229
|
+
| `Configuration#on_encode { \|token, payload\| ... }` | Register a callback fired after a successful encode |
|
|
230
|
+
| `Configuration#on_decode { \|payload\| ... }` | Register a callback fired after a successful decode |
|
|
231
|
+
| `Configuration#on_refresh { \|new_token\| ... }` | Register a callback fired after a successful refresh |
|
|
232
|
+
| `Configuration#on_revoke { \|jti\| ... }` | Register a callback fired after a successful revoke |
|
|
191
233
|
|
|
192
234
|
## Development
|
|
193
235
|
|
|
@@ -41,6 +41,92 @@ module Philiprehberger
|
|
|
41
41
|
@expiration = 3600
|
|
42
42
|
@refresh_expiration = 86_400 * 7
|
|
43
43
|
@secrets = nil
|
|
44
|
+
@on_encode = nil
|
|
45
|
+
@on_decode = nil
|
|
46
|
+
@on_refresh = nil
|
|
47
|
+
@on_revoke = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Registers a callback fired after a successful encode.
|
|
51
|
+
#
|
|
52
|
+
# @yieldparam token [String] the encoded JWT token
|
|
53
|
+
# @yieldparam payload [Hash] the merged payload that was encoded
|
|
54
|
+
# @return [Proc] the stored callback
|
|
55
|
+
def on_encode(&block)
|
|
56
|
+
@on_encode = block
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Registers a callback fired after a successful decode.
|
|
60
|
+
#
|
|
61
|
+
# @yieldparam payload [Hash] the decoded payload
|
|
62
|
+
# @return [Proc] the stored callback
|
|
63
|
+
def on_decode(&block)
|
|
64
|
+
@on_decode = block
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Registers a callback fired after a successful refresh.
|
|
68
|
+
#
|
|
69
|
+
# @yieldparam new_token [String] the newly issued access token
|
|
70
|
+
# @return [Proc] the stored callback
|
|
71
|
+
def on_refresh(&block)
|
|
72
|
+
@on_refresh = block
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Registers a callback fired after a successful revoke.
|
|
76
|
+
#
|
|
77
|
+
# @yieldparam jti [String, nil] the revoked token's JTI
|
|
78
|
+
# @return [Proc] the stored callback
|
|
79
|
+
def on_revoke(&block)
|
|
80
|
+
@on_revoke = block
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Invokes the on_encode callback if registered. Errors raised by the callback are swallowed.
|
|
84
|
+
#
|
|
85
|
+
# @param token [String]
|
|
86
|
+
# @param payload [Hash]
|
|
87
|
+
# @return [void]
|
|
88
|
+
def fire_on_encode(token, payload)
|
|
89
|
+
return unless @on_encode
|
|
90
|
+
|
|
91
|
+
@on_encode.call(token, payload)
|
|
92
|
+
rescue StandardError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Invokes the on_decode callback if registered. Errors raised by the callback are swallowed.
|
|
97
|
+
#
|
|
98
|
+
# @param payload [Hash]
|
|
99
|
+
# @return [void]
|
|
100
|
+
def fire_on_decode(payload)
|
|
101
|
+
return unless @on_decode
|
|
102
|
+
|
|
103
|
+
@on_decode.call(payload)
|
|
104
|
+
rescue StandardError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Invokes the on_refresh callback if registered. Errors raised by the callback are swallowed.
|
|
109
|
+
#
|
|
110
|
+
# @param new_token [String]
|
|
111
|
+
# @return [void]
|
|
112
|
+
def fire_on_refresh(new_token)
|
|
113
|
+
return unless @on_refresh
|
|
114
|
+
|
|
115
|
+
@on_refresh.call(new_token)
|
|
116
|
+
rescue StandardError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Invokes the on_revoke callback if registered. Errors raised by the callback are swallowed.
|
|
121
|
+
#
|
|
122
|
+
# @param jti [String, nil]
|
|
123
|
+
# @return [void]
|
|
124
|
+
def fire_on_revoke(jti)
|
|
125
|
+
return unless @on_revoke
|
|
126
|
+
|
|
127
|
+
@on_revoke.call(jti)
|
|
128
|
+
rescue StandardError
|
|
129
|
+
nil
|
|
44
130
|
end
|
|
45
131
|
|
|
46
132
|
# Returns the OpenSSL digest algorithm name.
|
|
@@ -43,7 +43,9 @@ module Philiprehberger
|
|
|
43
43
|
signing_input = "#{header_segment}.#{payload_segment}"
|
|
44
44
|
signature = sign(signing_input, config, secret: signing_secret)
|
|
45
45
|
|
|
46
|
-
"#{signing_input}.#{signature}"
|
|
46
|
+
token = "#{signing_input}.#{signature}"
|
|
47
|
+
config.fire_on_encode(token, merged)
|
|
48
|
+
token
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
# Base64url-encodes a string without padding.
|
|
@@ -4,6 +4,20 @@ module Philiprehberger
|
|
|
4
4
|
module JwtKit
|
|
5
5
|
# Token revocation support with an in-memory store.
|
|
6
6
|
module Revocation
|
|
7
|
+
# Extracts the JTI claim from a JWT token without verifying its signature.
|
|
8
|
+
#
|
|
9
|
+
# @param token [String] JWT token
|
|
10
|
+
# @return [String, nil] the JTI, or nil if the token is malformed
|
|
11
|
+
def self.extract_jti(token)
|
|
12
|
+
parts = token.split('.')
|
|
13
|
+
return nil unless parts.length == 3
|
|
14
|
+
|
|
15
|
+
payload = JSON.parse(Base64.urlsafe_decode64(parts[1]))
|
|
16
|
+
payload['jti']
|
|
17
|
+
rescue JSON::ParserError, ArgumentError
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
7
21
|
# Thread-safe in-memory revocation store backed by a Hash.
|
|
8
22
|
class MemoryStore
|
|
9
23
|
def initialize
|
|
@@ -60,13 +74,7 @@ module Philiprehberger
|
|
|
60
74
|
private
|
|
61
75
|
|
|
62
76
|
def extract_jti(token)
|
|
63
|
-
|
|
64
|
-
return nil unless parts.length == 3
|
|
65
|
-
|
|
66
|
-
payload = JSON.parse(Base64.urlsafe_decode64(parts[1]))
|
|
67
|
-
payload['jti']
|
|
68
|
-
rescue JSON::ParserError, ArgumentError
|
|
69
|
-
nil
|
|
77
|
+
Revocation.extract_jti(token)
|
|
70
78
|
end
|
|
71
79
|
end
|
|
72
80
|
end
|
|
@@ -34,7 +34,9 @@ module Philiprehberger
|
|
|
34
34
|
raise InvalidToken, 'Token is not a refresh token' unless payload['type'] == 'refresh'
|
|
35
35
|
|
|
36
36
|
new_payload = payload.except('exp', 'nbf', 'iat', 'jti', 'iss', 'aud', 'type')
|
|
37
|
-
Encoder.encode(new_payload, config)
|
|
37
|
+
new_token = Encoder.encode(new_payload, config)
|
|
38
|
+
config.fire_on_refresh(new_token)
|
|
39
|
+
new_token
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
end
|
|
@@ -57,6 +57,22 @@ module Philiprehberger
|
|
|
57
57
|
{ valid: false, payload: nil, error: e.message }
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
# Checks whether a token's `exp` claim is in the past without verifying the signature.
|
|
61
|
+
# Useful for proactive refresh decisions. Returns `true` for malformed tokens or when
|
|
62
|
+
# `exp` is missing.
|
|
63
|
+
#
|
|
64
|
+
# @param token [String] JWT token
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def expired?(token)
|
|
67
|
+
payload = peek(token)[:payload]
|
|
68
|
+
exp = payload['exp']
|
|
69
|
+
return true unless exp.is_a?(Numeric)
|
|
70
|
+
|
|
71
|
+
Time.now.to_i >= exp
|
|
72
|
+
rescue DecodeError
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
|
|
60
76
|
# Encodes a payload into a signed JWT token.
|
|
61
77
|
#
|
|
62
78
|
# @param payload [Hash] custom claims
|
|
@@ -109,6 +125,8 @@ module Philiprehberger
|
|
|
109
125
|
# @return [void]
|
|
110
126
|
def revoke(token)
|
|
111
127
|
revocation_store.revoke(token)
|
|
128
|
+
jti = Revocation.extract_jti(token)
|
|
129
|
+
configuration.fire_on_revoke(jti)
|
|
112
130
|
end
|
|
113
131
|
|
|
114
132
|
# Checks whether a token has been revoked.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-jwt_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A complete JWT toolkit for Ruby. Encode and decode tokens with automatic
|
|
14
14
|
claim management (exp, iat, iss, jti), generate access/refresh token pairs, validate
|