jwt_sessions 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ea880100e93f25c3fc31e32161f6881a21c184cb
4
- data.tar.gz: b549fea16c7d4ea09d8c1f7002a0d5cdd06c36b5
3
+ metadata.gz: 8fe66c91df4011e6e59d05bda8a9d43188b8f07a
4
+ data.tar.gz: 97bb42e7a8f6ecb5037b3bc6b057758dfcf2397d
5
5
  SHA512:
6
- metadata.gz: 78a5845fb2168f8ae8023d6ce6cc291ed2803fecefe2298afbc4423fc7319a29a7a75efd68fa2a17fc28e498a0b73d0c183ca1f1fb1f6005efc6430898acd92b
7
- data.tar.gz: dc9074a35fee61a1749d42ec5bd3de687c683a8aa39c32c683eb58579b5831e735dc9904ff60c61b1add355ed61ddbb2686a74f171ceaa3922fcb737618f3bad
6
+ metadata.gz: a353fc811d0cd645655467088651f33b22e9dde3778a8fe2ef027bb07f5274a98acb0f9243a8791251d203e14445f453e2ee61af6829b078e4150f199a0332cc
7
+ data.tar.gz: baef0f6687c8ec9171e67a74fda5d5f4907a600e7d6ce829a5d01e39cfb62af6777dce235bcfcc989313c6d9b3273abbd914da8d0a074b9f0ceff517398f87e1
data/README.md CHANGED
@@ -11,6 +11,7 @@ XSS/CSRF safe JWT auth designed for SPA
11
11
  - [Synopsis](#synopsis)
12
12
  - [Installation](#installation)
13
13
  - [Getting Started](#getting-started)
14
+ * [Creating a session](#creating-a-session)
14
15
  * [Rails integration](#rails-integration)
15
16
  * [Non-Rails usage](#non-rails-usage)
16
17
  - [Configuration](#configuration)
@@ -32,7 +33,9 @@ XSS/CSRF safe JWT auth designed for SPA
32
33
 
33
34
  Main goal of this gem is to provide configurable, manageable, and safe stateful sessions based on JSON Web Tokens.
34
35
 
35
- It's designed to be framework agnostic yet is easily integrable so Rails integration is also available out of the box.
36
+ The gem stores JWT based sessions on the backend (currently, redis and memory stores are supported), making it possible to manage sessions, reset passwords, logout users in a reliable and secure way.
37
+
38
+ It's designed to be framework agnostic yet is easily integrable, and Rails integration is available out of the box.
36
39
 
37
40
  Core concept behind `jwt_sessions` is that each session is represented by a pair of tokens: access and refresh, and a session store is used to handle CSRF checks and refresh token hijacking. Both tokens have configurable expiration times, but in general refresh token is supposed to have a longer lifespan than an access token. Access token is used to retrieve secured resources and refresh token is used to renew the access token once it's expired. Default token store is based on redis.
38
41
 
@@ -56,7 +59,80 @@ bundle install
56
59
 
57
60
  ## Getting Started
58
61
 
59
- `Authorization` mixin is supposed to be included in your controllers and is used to retrieve access and refresh tokens from incoming requests and verify CSRF token if needed. It assumes that a token is either in a cookie or in a header (cookie and header names are configurable). It tries to retrieve it from headers first, then from cookies (CSRF check included) if the headers check failed.
62
+ You should configure an encryption algorithm and specify the encryption key. By default the gem uses `HS256`.
63
+
64
+ ```ruby
65
+ JWTSessions.encryption_key = "secret"
66
+ ```
67
+
68
+ `Authorization` mixin provides helper methods which are used to retrieve access and refresh tokens from incoming requests and verify CSRF token if needed. It assumes that a token can be found either in a cookie or in a header (cookie and header names are configurable). It tries to retrieve it from headers first, then from cookies (CSRF check included) if the headers check failed.
69
+
70
+ ### Creating a session
71
+
72
+ Each token contains a payload with custom session info. The payload is a regular Ruby hash. \
73
+ Usually, it contains user ID or other data which helps to identify current user but it's not necessary, the payload can be an empty hash as well.
74
+
75
+ ```ruby
76
+ > payload = { user_id: user.id }
77
+ => {:user_id=>1}
78
+ ```
79
+
80
+ Generate the session with a custom payload. By default the same payload is sewn into the session's access and refresh tokens.
81
+
82
+ ```ruby
83
+ > session = JWTSessions::Session.new(payload: payload)
84
+ => #<JWTSessions::Session:0x00007fbe2cce9ea0...>
85
+ ```
86
+
87
+ Sometimes it makes sense to keep different data within the payloads of access and refresh tokens. \
88
+ The access token may contain rich data including user settings, etc., while the appropriate refresh token will include only the bare minimum which will be required to reconstruct a payload for the new access token during refresh.
89
+
90
+ ```ruby
91
+ session = JWTSessions::Session.new(payload: payload, refresh_payload: refresh_payload)
92
+ ```
93
+
94
+ Now we can call `login` method on the session to retrieve a set of tokens.
95
+
96
+ ```ruby
97
+ > session.login
98
+ => {:csrf=>"BmhxDRW5NAEIx...",
99
+ :access=>"eyJhbGciOiJIUzI1NiJ9...",
100
+ :access_expires_at=>"..."
101
+ :refresh=>"eyJhbGciOiJIUzI1NiJ9...",
102
+ :refresh_expires_at=>"..."}
103
+ ```
104
+
105
+ Access/refresh tokens automatically contain expiration time in their payload. Yet expiration times are also added to the output just in case. \
106
+ The token's payload will be available in the controllers once the access (or refresh) token is authorized.
107
+
108
+ To perform the refresh do:
109
+
110
+ ```ruby
111
+ > session.refresh(refresh_token)
112
+ => {:csrf=>"+pk2SQrXHRo1iV1x4O...",
113
+ :access=>"eyJhbGciOiJIUzI1...",
114
+ :access_expires_at=>"..."}
115
+ ```
116
+
117
+ Available `JWTSessions::Session.new` options:
118
+
119
+ - **payload**: a hash object with session data which will be included into an access token payload. Default is an empty hash.
120
+ - **refresh_payload**: a hash object with session data which will be included into a refresh token payload. Default is a value of the access payload.
121
+ - **access_claims**: a hash object with [JWT claims](https://github.com/jwt/ruby-jwt#support-for-reserved-claim-names) which will be validated within the access token payload. F.e. `{ aud: ["admin"], verify_aud: true }` meaning that the token can be used only by "admin" audience. Also, the endpoint can automatically validate claims instead. See `token_claims` method.
122
+ - **refresh_claims**: a hash object with [JWT claims](https://github.com/jwt/ruby-jwt#support-for-reserved-claim-names) which will be validated within the refresh token payload.
123
+ - **namespace**: a string object which helps to group sessions by a custom criteria. For example, sessions can be grouped by user ID, then it'll be possible to logout the user from all devises. More info [Sessions Namespace](#sessions-namespace).
124
+ - **refresh_by_access_allowed**: a boolean value. Default is false. It links access and refresh tokens (adds refresh token ID to access payload), making it possible to perform a session refresh by the last expired access token. See [Refresh with access token](#refresh-with-access-token).
125
+ - **access_exp**: an integer value. Contains an access token expiration time in seconds. The value overrides global settings. See [Expiration time](#expiration-time).
126
+ - **refresh_exp**: an integer value. Contains a refresh token expiration time in seconds. The value overrides global settings. See [Expiration time](#expiration-time).
127
+
128
+ Helper methods within `Authorization` mixin:
129
+
130
+ - **authorize_access_request!**: validates access token within the request.
131
+ - **authorize_refresh_request!**: validates refresh token within the request.
132
+ - **found_token**: a raw token found within the request.
133
+ - **payload**: a decoded token's payload.
134
+ - **claimless_payload**: a decoded token's payload without claims validation (can be used for checking data of an expired token).
135
+ - **token_claims**: the method should be defined by a developer, and is expected to return a hash-like object with claims to be validated within a token's payload.
60
136
 
61
137
  ### Rails integration
62
138
 
@@ -91,25 +167,6 @@ JWTSessions.private_key = OpenSSL::PKey::RSA.generate(2048)
91
167
  JWTSessions.public_key = JWTSessions.private_key.public_key
92
168
  ```
93
169
 
94
- Generate access/refresh/csrf tokens with a custom payload. \
95
- The payload will be available in the controllers once the access (or refresh) token is authorized. \
96
- Access/refresh tokens contain expiration time in their payload. Yet expiration times are also added to the output just in case.
97
-
98
- ```ruby
99
- > payload = { user_id: user.id }
100
- => {:user_id=>1}
101
-
102
- > session = JWTSessions::Session.new(payload: payload)
103
- => #<JWTSessions::Session:0x00007fbe2cce9ea0...>
104
-
105
- > session.login
106
- => {:csrf=>"BmhxDRW5NAEIx...",
107
- :access=>"eyJhbGciOiJIUzI1NiJ9...",
108
- :access_expires_at=>"..."
109
- :refresh=>"eyJhbGciOiJIUzI1NiJ9...",
110
- :refresh_expires_at=>"..."}
111
- ```
112
-
113
170
  You can build login controller to receive access, refresh and csrf tokens in exchange for user's login/password. \
114
171
  Refresh controller - to be able to get a new access token using refresh token after access is expired. \
115
172
  Here is example of a simple login controller, which returns set of tokens as a plain JSON response. \
@@ -130,12 +187,6 @@ class LoginController < ApplicationController
130
187
  end
131
188
  ```
132
189
 
133
- Since it's not required to pass an access token when you want to perform a refresh you may need to have some data in the payload of the refresh token to allow you to construct a payload of the new access token during refresh.
134
-
135
- ```ruby
136
- session = JWTSessions::Session.new(payload: payload, refresh_payload: refresh_payload)
137
- ```
138
-
139
190
  Now you can build a refresh endpoint. To protect the endpoint use before_action `authorize_refresh_request!`. \
140
191
  The endpoint itself should return a renewed access token.
141
192
 
@@ -245,7 +296,7 @@ class SimpleApp < Sinatra::Base
245
296
  include JWTSessions::Authorization
246
297
 
247
298
  def request_headers
248
- env.inject({}){|acc, (k,v)| acc[$1.downcase] = v if k =~ /^http_(.*)/i; acc}
299
+ env.inject({}) { |acc, (k,v)| acc[$1.downcase] = v if k =~ /^http_(.*)/i; acc }
249
300
  end
250
301
 
251
302
  def request_cookies
@@ -355,6 +406,7 @@ class UsersController < ApplicationController
355
406
  def token_claims
356
407
  {
357
408
  aud: ["admin", "staff"],
409
+ verify_aud: true, # can be used locally instead of a global setting
358
410
  exp_leeway: 15 # will be used instead of default leeway only for exp claim
359
411
  }
360
412
  end
@@ -384,6 +436,8 @@ JWTSessions.access_exp_time = 3600 # 1 hour in seconds
384
436
  JWTSessions.refresh_exp_time = 604800 # 1 week in seconds
385
437
  ```
386
438
 
439
+ It's defined globally, but can be overridden on a session level. See `JWTSessions::Session.new` options for more info.
440
+
387
441
  #### CSRF and cookies
388
442
 
389
443
  In case when you use cookies as your tokens transport it gets vulnerable to CSRF. That's why both login and refresh methods of the `Session` class produce CSRF tokens for you. `Authorization` mixin expects that this token is sent with all requests except GET and HEAD in a header specified among this gem's settings (X-CSRF-Token by default). Verification will be done automatically and `Authorization` exception will be raised in case of mismatch between the token from the header and the one stored in session. \
data/lib/jwt_sessions.rb CHANGED
@@ -136,6 +136,14 @@ module JWTSessions
136
136
  Time.now.to_i + refresh_exp_time.to_i
137
137
  end
138
138
 
139
+ def custom_access_expiration(time)
140
+ Time.now.to_i + (time || access_exp_time).to_i
141
+ end
142
+
143
+ def custom_refresh_expiration(time)
144
+ Time.now.to_i + (time || refresh_exp_time).to_i
145
+ end
146
+
139
147
  def header_by(token_type)
140
148
  send("#{token_type}_header")
141
149
  end
@@ -29,8 +29,8 @@ module JWTSessions
29
29
  end
30
30
 
31
31
  class << self
32
- def create(csrf, payload, store)
33
- new(csrf, payload, store).tap do |inst|
32
+ def create(csrf, payload, store, expiration = JWTSessions.access_expiration)
33
+ new(csrf, payload, store, SecureRandom.uuid, expiration).tap do |inst|
34
34
  store.persist_access(inst.uid, inst.csrf, inst.expiration)
35
35
  end
36
36
  end
@@ -13,15 +13,23 @@ module JWTSessions
13
13
  @access_uid = access_uid
14
14
  @access_expiration = access_expiration
15
15
  @store = store
16
- @uid = options.fetch(:uid, SecureRandom.uuid)
17
- @expiration = options.fetch(:expiration, JWTSessions.refresh_expiration)
16
+ @uid = options.fetch(:uid, nil) || SecureRandom.uuid
17
+ @expiration = options.fetch(:expiration, nil) || JWTSessions.refresh_expiration
18
18
  @namespace = options.fetch(:namespace, nil)
19
19
  @token = Token.encode(options.fetch(:payload, {}).merge("uid" => uid, "exp" => expiration.to_i))
20
20
  end
21
21
 
22
22
  class << self
23
- def create(csrf, access_uid, access_expiration, store, payload, namespace)
24
- inst = new(csrf, access_uid, access_expiration, store, payload: payload, namespace: namespace)
23
+ def create(csrf, access_uid, access_expiration, store, payload, namespace, expiration = JWTSessions.refresh_expiration)
24
+ inst = new(
25
+ csrf,
26
+ access_uid,
27
+ access_expiration,
28
+ store,
29
+ payload: payload,
30
+ namespace: namespace,
31
+ expiration: expiration
32
+ )
25
33
  inst.send(:persist_in_store)
26
34
  inst
27
35
  end
@@ -20,6 +20,8 @@ module JWTSessions
20
20
  @refresh_claims = options.fetch(:refresh_claims, {})
21
21
  @namespace = options.fetch(:namespace, nil)
22
22
  @refresh_by_access_allowed = options.fetch(:refresh_by_access_allowed, false)
23
+ @_access_exp = options.fetch(:access_exp, nil)
24
+ @_refresh_exp = options.fetch(:refresh_exp, nil)
23
25
  end
24
26
 
25
27
  def login
@@ -237,18 +239,26 @@ module JWTSessions
237
239
  end
238
240
 
239
241
  def create_refresh_token
240
- @_refresh = RefreshToken.create(@_csrf.encoded,
241
- @_access.uid,
242
- @_access.expiration,
243
- store,
244
- refresh_payload,
245
- namespace)
242
+ @_refresh = RefreshToken.create(
243
+ @_csrf.encoded,
244
+ @_access.uid,
245
+ @_access.expiration,
246
+ store,
247
+ refresh_payload,
248
+ namespace,
249
+ JWTSessions.custom_refresh_expiration(@_refresh_exp)
250
+ )
246
251
  @refresh_token = @_refresh.token
247
252
  link_access_to_refresh
248
253
  end
249
254
 
250
255
  def create_access_token
251
- @_access = AccessToken.create(@_csrf.encoded, payload, store)
256
+ @_access = AccessToken.create(
257
+ @_csrf.encoded,
258
+ payload,
259
+ store,
260
+ JWTSessions.custom_access_expiration(@_access_exp)
261
+ )
252
262
  @access_token = @_access.token
253
263
  end
254
264
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWTSessions
4
- VERSION = "2.3.1"
4
+ VERSION = "2.4.0"
5
5
  end
@@ -27,6 +27,18 @@ class TestSession < Minitest::Test
27
27
  assert_equal payload[:test], decoded_access["test"]
28
28
  end
29
29
 
30
+ def test_login_with_custom_exp
31
+ @new_session = JWTSessions::Session.new(
32
+ payload: payload,
33
+ access_exp: 18000, # 5 hours in seconds
34
+ refresh_exp: 18000
35
+ )
36
+ assert_equal false, tokens[:refresh_expires_at] == tokens[:access_expires_at]
37
+ new_tokens = @new_session.login
38
+ assert_equal LOGIN_KEYS, new_tokens.keys.sort
39
+ assert_equal new_tokens[:refresh_expires_at], new_tokens[:access_expires_at]
40
+ end
41
+
30
42
  def test_refresh
31
43
  refreshed_tokens = session.refresh(tokens[:refresh])
32
44
  decoded_access = JWTSessions::Token.decode(refreshed_tokens[:access]).first
@@ -34,6 +46,18 @@ class TestSession < Minitest::Test
34
46
  assert_equal payload[:test], decoded_access["test"]
35
47
  end
36
48
 
49
+ def test_refresh_with_custom_exp
50
+ @new_session = JWTSessions::Session.new(
51
+ payload: payload,
52
+ access_exp: 18000, # 5 hours in seconds
53
+ refresh_exp: 18000
54
+ )
55
+ new_tokens = @new_session.login
56
+ refreshed_tokens = @new_session.refresh(new_tokens[:refresh])
57
+ assert_equal LOGIN_KEYS, new_tokens.keys.sort
58
+ assert_equal refreshed_tokens[:refresh_expires_at], refreshed_tokens[:access_expires_at]
59
+ end
60
+
37
61
  def test_refresh_expired
38
62
  JWTSessions.refresh_exp_time = 0
39
63
  session = JWTSessions::Session.new(payload: payload)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt_sessions
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yulia Oletskaya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-01 00:00:00.000000000 Z
11
+ date: 2019-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt