jwt_sessions 1.0.0.pre.alpha.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +295 -1
- data/lib/jwt_sessions/authorization.rb +4 -4
- data/lib/jwt_sessions/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bfd4418b2bba47f5127afce7c69075372ed995f1
|
4
|
+
data.tar.gz: 5d51066a3533865f9c329d203f87819b704d47d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39ee0e2618f6e10577e88ed8c574799888c84e73a4d757d800487b30cb901fb6042e7b53a05e57b6b30f7c2f5f46c78c2fd35ec44653b0f6fa99ef5d7fb0a14c
|
7
|
+
data.tar.gz: 1a84c6f9d28a1b5571d114b9d6fd2fba37dacc3d0f4eec340e5699728425712ccc271374cd94019a3d54bdc258db579da793e00ca61f7fc66274b24557ebde39
|
data/README.md
CHANGED
@@ -3,4 +3,298 @@
|
|
3
3
|
|
4
4
|
XSS/CSRF safe JWT auth designed for SPA
|
5
5
|
|
6
|
-
|
6
|
+
## Synopsis
|
7
|
+
|
8
|
+
Main goal of this gem is to provide configurable, manageable, and safe stateful sessions based on JSON Web Tokens.
|
9
|
+
|
10
|
+
It's designed to be framework agnostic yet is easily integrable so Rails integration is also available out of the box.
|
11
|
+
|
12
|
+
Core concept behind jwt_sessions is that each session is represented by a pair of tokens: access and refresh,
|
13
|
+
and a session store used to handle CSRF checks and refresh token hijacking. Default token store is based on redis
|
14
|
+
but you can freely implement your own store with whichever backend you prefer.
|
15
|
+
|
16
|
+
All tokens are encoded and decoded by [ruby-jwt](https://github.com/jwt/ruby-jwt) gem, and its reserved claim names are supported
|
17
|
+
as well as it's allowed to configure claim checks and cryptographic signing algorithms supported by it.
|
18
|
+
jwt_sessions itself uses `ext` claim and `HS256` signing by default.
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
Put this line in your Gemfile
|
23
|
+
|
24
|
+
```
|
25
|
+
gem 'jwt_sessions'
|
26
|
+
```
|
27
|
+
|
28
|
+
Then run
|
29
|
+
|
30
|
+
```
|
31
|
+
bundle install
|
32
|
+
```
|
33
|
+
|
34
|
+
## Getting Started
|
35
|
+
|
36
|
+
`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.
|
37
|
+
|
38
|
+
### Rails integration
|
39
|
+
|
40
|
+
Include `JWTSessions::RailsAuthorization` in your controllers, add `JWTSessions::Errors::Unauthorized` exceptions handling if needed.
|
41
|
+
|
42
|
+
```
|
43
|
+
class ApplicationController < ActionController::API
|
44
|
+
include JWTSessions::RailsAuthorization
|
45
|
+
rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def not_authorized
|
50
|
+
render json: { error: 'Not authorized' }, status: :unauthorized
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
Specify an encryption key for JSON Web Tokens in `config/initializers/jwt_session.rb` \
|
56
|
+
It's adviced to store the key itself within the app secrets.
|
57
|
+
|
58
|
+
```
|
59
|
+
JWTSessions.encryption_key = Rails.application.secrets.secret_jwt_encryption_key
|
60
|
+
```
|
61
|
+
|
62
|
+
Generate access/refresh/csrf tokens with a custom payload. \
|
63
|
+
The payload will be available in the controllers once the access (or refresh) token is authorized.
|
64
|
+
|
65
|
+
```
|
66
|
+
> payload = { user_id: user.id }
|
67
|
+
=> {:user_id=>1}
|
68
|
+
|
69
|
+
> session = JWTSessions::Session.new(payload: payload)
|
70
|
+
=> #<JWTSessions::Session:0x00007fbe2cce9ea0...>
|
71
|
+
|
72
|
+
> session.login
|
73
|
+
=> {:csrf=>"BmhxDRW5NAEIx...",
|
74
|
+
:access=>"eyJhbGciOiJIUzI1NiJ9...",
|
75
|
+
:refresh=>"eyJhbGciOiJIUzI1NiJ9..."}
|
76
|
+
```
|
77
|
+
|
78
|
+
You can build login controller to receive access, refresh and csrf tokens in exchange for user's login/password. \
|
79
|
+
Refresh controller - to be able to get a new access token using refresh token after access is expired. \
|
80
|
+
Here is example of a simple login controller, which returns set of tokens as a plain JSON response. \
|
81
|
+
It's also possible to set tokens as cookies in the response instead.
|
82
|
+
|
83
|
+
```
|
84
|
+
class LoginController < ApplicationController
|
85
|
+
def create
|
86
|
+
user = User.find_by!(email: params[:email])
|
87
|
+
if user.authenticate(params[:password])
|
88
|
+
payload = { user_id: user.id }
|
89
|
+
session = JWTSessions::Session.new(payload: payload)
|
90
|
+
render json: session.login
|
91
|
+
else
|
92
|
+
render json: 'Invalid user', status: :unauthorized
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
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.
|
99
|
+
|
100
|
+
```
|
101
|
+
session = JWTSessions::Session.new(payload: payload, refresh_payload: refresh_payload)
|
102
|
+
```
|
103
|
+
|
104
|
+
Now you can build a refresh endpoint. To protect the endpoint use before_action `authorize_refresh_request!`. \
|
105
|
+
In the example `found_token` - is a token fetched from request headers or cookies.
|
106
|
+
|
107
|
+
```
|
108
|
+
class RefreshController < ApplicationController
|
109
|
+
before_action :authorize_refresh_request!
|
110
|
+
|
111
|
+
def create
|
112
|
+
session = JWTSessions::Session.new(payload: access_payload)
|
113
|
+
render json: session.refresh(found_token)
|
114
|
+
end
|
115
|
+
|
116
|
+
def access_payload
|
117
|
+
# payload here stands for refresh token payload
|
118
|
+
build_access_payload_based_on_refresh(payload)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
The refresh request with headers must include `X-Refresh-Token` (header name is configurable) with refresh token.
|
124
|
+
|
125
|
+
```
|
126
|
+
X-Refresh-Token: eyJhbGciOiJIUzI1NiJ9...
|
127
|
+
POST /refresh
|
128
|
+
```
|
129
|
+
|
130
|
+
Now when there're login and refresh endpoints, you can protect the rest of your secure controllers with `before_action :authorize_access_request!`.
|
131
|
+
|
132
|
+
```
|
133
|
+
class UsersController < ApplicationController
|
134
|
+
before_action :authorize_access_request!
|
135
|
+
|
136
|
+
def index
|
137
|
+
...
|
138
|
+
end
|
139
|
+
|
140
|
+
def show
|
141
|
+
...
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
Headers must include `Authorization: Bearer` with access token.
|
146
|
+
|
147
|
+
```
|
148
|
+
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
|
149
|
+
GET /users
|
150
|
+
```
|
151
|
+
|
152
|
+
The `payload` method is available to fetch encoded data from the token.
|
153
|
+
|
154
|
+
```
|
155
|
+
def current_user
|
156
|
+
@current_user ||= User.find(payload['user_id'])
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
### Non-Rails usage
|
161
|
+
|
162
|
+
You must include `JWTSessions::Authorization` module to your auth class and implement within it next methods:
|
163
|
+
|
164
|
+
1. request_headers
|
165
|
+
|
166
|
+
```
|
167
|
+
def request_headers
|
168
|
+
# must return hash-like object with request headers
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
2. request_cookies
|
173
|
+
|
174
|
+
```
|
175
|
+
def request_cookies
|
176
|
+
# must return hash-like object with request cookies
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
3. request_method
|
181
|
+
|
182
|
+
```
|
183
|
+
def request_method
|
184
|
+
# must return current request verb as a string in upcase, f.e. 'GET', 'HEAD', 'POST', 'PATCH', etc
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
Example Sinatra app
|
189
|
+
|
190
|
+
```
|
191
|
+
require 'sinatra/base'
|
192
|
+
|
193
|
+
class SimpleApp < Sinatra::Base
|
194
|
+
include JWTSessions::Authorization
|
195
|
+
|
196
|
+
def request_headers
|
197
|
+
request.headers
|
198
|
+
end
|
199
|
+
|
200
|
+
def request_cookies
|
201
|
+
request.cookies
|
202
|
+
end
|
203
|
+
|
204
|
+
def request_method
|
205
|
+
request.request_method
|
206
|
+
end
|
207
|
+
|
208
|
+
post '/refresh' do
|
209
|
+
content_type :json
|
210
|
+
authorize_refresh_request!
|
211
|
+
session = JWTSessions::Session.new(payload: payload)
|
212
|
+
session.refresh(found_token).to_json
|
213
|
+
end
|
214
|
+
|
215
|
+
....
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
## Configuration
|
220
|
+
|
221
|
+
List of configurable settings with their default values.
|
222
|
+
|
223
|
+
##### Redis
|
224
|
+
|
225
|
+
Default token store configurations
|
226
|
+
|
227
|
+
```
|
228
|
+
JWTSessions.redis_host = '127.0.0.1'
|
229
|
+
JWTSessions.redis_port = '6379'
|
230
|
+
JWTSessions.redis_db_name = 'jwtokens'
|
231
|
+
JWTSessions.token_prefix = 'jwt_' # used for redis db keys
|
232
|
+
```
|
233
|
+
|
234
|
+
##### JWT encryption
|
235
|
+
|
236
|
+
```
|
237
|
+
JWTSessions.algorithm = 'HS256'
|
238
|
+
```
|
239
|
+
|
240
|
+
You need to specify a secret to use for HMAC, this setting doesn't have a default value.
|
241
|
+
|
242
|
+
```
|
243
|
+
JWTSessions.secret = 'secret'
|
244
|
+
```
|
245
|
+
|
246
|
+
Or you need to specify public and private keys for RSA/EDCSA/EDDSA, there are no default values for keys. You can use instructions from [ruby-jwt](https://github.com/jwt/ruby-jwt) to generate keys corresponding keys.
|
247
|
+
|
248
|
+
```
|
249
|
+
JWTSessions.private_key = 'private_key'
|
250
|
+
JWTSessions.public_key = 'public_key_for_private'
|
251
|
+
```
|
252
|
+
|
253
|
+
##### Request headers and cookies names
|
254
|
+
|
255
|
+
Default request headers/cookies names can be re-configured
|
256
|
+
|
257
|
+
```
|
258
|
+
JWTSessions.access_header = 'Authorization'
|
259
|
+
JWTSessions.access_cookie = 'jwt_access'
|
260
|
+
JWTSessions.refresh_header = 'X-Refresh-Token'
|
261
|
+
JWTSessions.refresh_cookie = 'jwt_refresh'
|
262
|
+
JWTSessions.csrf_header = 'X-CSRF-Token'
|
263
|
+
```
|
264
|
+
|
265
|
+
##### Expiration time
|
266
|
+
|
267
|
+
Acces token must have a short life span, while refresh tokens can be stored for a longer time period
|
268
|
+
|
269
|
+
```
|
270
|
+
JWTSessions.access_exp_time = 3600 # 1 hour in seconds
|
271
|
+
JWTSessions.refresh_exp_time = 604800 # 1 week in seconds
|
272
|
+
```
|
273
|
+
|
274
|
+
#### CSRF and cookies
|
275
|
+
|
276
|
+
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. \
|
277
|
+
Although you don't need to mitigate BREACH attacks it's still possible to generate a new masked token with the access token
|
278
|
+
|
279
|
+
```
|
280
|
+
session = JWTSessions::Session.new
|
281
|
+
session.masked_csrf(access_token)
|
282
|
+
```
|
283
|
+
|
284
|
+
#### Refresh token hijack protection
|
285
|
+
|
286
|
+
There is a security recommendation regarding the usage of refresh tokens: only perform refresh when an access token gets expired. \
|
287
|
+
Since sessions are always defined by a pair of tokens and there can't be multiple access tokens for a single refresh token simultaneous usage of the refresh token by multiple users can be easily noticed as refresh will be perfomed before the expiration of the access token by one of the users. Because of that `refresh` method of the `Session` class supports optional block as one of its arguments which will be executed only in case of refresh being performed before the expiration of the access token.
|
288
|
+
|
289
|
+
```
|
290
|
+
session = JwtSessions::Session.new(payload: payload)
|
291
|
+
session.refresh(refresh_token) { |refresh_token_uid, access_token_expiration| ... }
|
292
|
+
```
|
293
|
+
|
294
|
+
## Contributing
|
295
|
+
|
296
|
+
Fork & Pull Request
|
297
|
+
|
298
|
+
## License
|
299
|
+
|
300
|
+
MIT
|
@@ -8,25 +8,25 @@ module JWTSessions
|
|
8
8
|
protected
|
9
9
|
|
10
10
|
TOKEN_TYPES.each do |token_type|
|
11
|
-
define_method("
|
11
|
+
define_method("authorize_#{token_type}_request!") do
|
12
12
|
begin
|
13
13
|
cookieless_auth(token_type)
|
14
14
|
rescue Errors::Unauthorized
|
15
15
|
cookie_based_auth(token_type)
|
16
16
|
end
|
17
|
-
|
17
|
+
invalid_authorization unless Token.valid_payload?(payload)
|
18
18
|
check_csrf(token_type)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
private
|
23
23
|
|
24
|
-
def
|
24
|
+
def invalid_authorization
|
25
25
|
raise Errors::Unauthorized
|
26
26
|
end
|
27
27
|
|
28
28
|
def check_csrf(token_type)
|
29
|
-
|
29
|
+
invalid_authorization if should_check_csrf? && @_csrf_check && !valid_csrf_token?(retrieve_csrf, token_type)
|
30
30
|
end
|
31
31
|
|
32
32
|
def should_check_csrf?
|
data/lib/jwt_sessions/version.rb
CHANGED
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: 1.0.0
|
4
|
+
version: 1.0.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: 2018-
|
11
|
+
date: 2018-04-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jwt
|
@@ -91,9 +91,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
91
|
version: '0'
|
92
92
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
96
|
+
version: '0'
|
97
97
|
requirements: []
|
98
98
|
rubyforge_project:
|
99
99
|
rubygems_version: 2.6.13
|