warden-jwt_auth 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +14 -1
- data/lib/warden/jwt_auth.rb +6 -0
- data/lib/warden/jwt_auth/env_helper.rb +8 -0
- data/lib/warden/jwt_auth/errors.rb +5 -0
- data/lib/warden/jwt_auth/hooks.rb +10 -3
- data/lib/warden/jwt_auth/interfaces.rb +8 -0
- data/lib/warden/jwt_auth/payload_user_helper.rb +7 -0
- data/lib/warden/jwt_auth/strategy.rb +2 -1
- data/lib/warden/jwt_auth/user_decoder.rb +9 -6
- data/lib/warden/jwt_auth/user_encoder.rb +17 -8
- data/lib/warden/jwt_auth/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48b37f5ba1a7935103634d140ab0c1fbbaea54f3
|
4
|
+
data.tar.gz: 853f242af0f389a92a4f20c66800ed5447708830
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d15c993a973fade506472bd525a886e2be2a2b5b03ca99223711254c3876739e94704b6f8bdd64a7c01832b5d8f96978768f18638b972be1db4027e2c24bde4d
|
7
|
+
data.tar.gz: ffc3fe32c2dd1cc8d4206d031bb57612615a61219f3daa136c3865dca8445bc437da056760299b0ce5988ce15e74155445936385fefce9e3a215a4ef0426c751
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
6
6
|
|
7
|
+
## [0.3.0] - 2017-12-06
|
8
|
+
### Added
|
9
|
+
- Add and call hook method `on_jwt_dispatch` on user instance
|
10
|
+
- Encode and validate an `aud` claim from the request headers
|
11
|
+
|
7
12
|
## [0.2.1] - 2017-12-04
|
8
13
|
### Added
|
9
14
|
- Allow configuring classes as strings
|
data/README.md
CHANGED
@@ -24,7 +24,7 @@ If what you need is a JWT authentication library for [devise](https://github.com
|
|
24
24
|
## Installation
|
25
25
|
|
26
26
|
```ruby
|
27
|
-
gem 'warden-jwt_auth', '~> 0.
|
27
|
+
gem 'warden-jwt_auth', '~> 0.3.0'
|
28
28
|
```
|
29
29
|
|
30
30
|
And then execute:
|
@@ -100,6 +100,13 @@ def jwt_payload
|
|
100
100
|
end
|
101
101
|
```
|
102
102
|
|
103
|
+
Just when a token is going to be dispatched to a client, a hook method `on_jwt_dispatch` is invoked, only when it exist, on the user record. This method takes the `token` and the `payload` as arguments.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
def on_jwt_dispatch(token, payload)
|
107
|
+
# Do something
|
108
|
+
end
|
109
|
+
|
103
110
|
### Middlewares addition
|
104
111
|
|
105
112
|
You need to add `Warden::JWTAuth::Middleware` to your rack middlewares stack. Actually, it is just a wrapper which adds two middlewares that do the actual job: dispatching tokens and revoking tokens.
|
@@ -164,6 +171,12 @@ module RevocationStrategy
|
|
164
171
|
end
|
165
172
|
```
|
166
173
|
|
174
|
+
### Requesting client validation
|
175
|
+
|
176
|
+
Authentication will be refused if a client requesting to be authenticated through a token is not the same to which it was originally issued. To do so, the content of the header `JWT_AUD` (configurable via `config.aud_header`) is stored as `aud` claim. If you don't want to differentiate between clients, you don't need to provide that header.
|
177
|
+
|
178
|
+
**Important:** Be aware that this workflow is not bullet proof. In some scenarios a user can handcraft the request headers, therefore being able to impersonate any client. In such cases you could need something more robust, like an OAuth workflow with client id and client secret.
|
179
|
+
|
167
180
|
## Development
|
168
181
|
|
169
182
|
There are docker and docker-compose files configured to create a development environment for this gem. So, if you use Docker you only need to run:
|
data/lib/warden/jwt_auth.rb
CHANGED
@@ -25,6 +25,12 @@ module Warden
|
|
25
25
|
# Expiration time for tokens
|
26
26
|
setting :expiration_time, 3600
|
27
27
|
|
28
|
+
# Request header which value will be encoded as `aud` claim in JWT. If
|
29
|
+
# the header is not present `aud` will be `nil`.
|
30
|
+
setting(:aud_header, 'JWT_AUD') do |value|
|
31
|
+
('HTTP_' + value.upcase).tr('-', '_')
|
32
|
+
end
|
33
|
+
|
28
34
|
# A hash of warden scopes as keys and user repositories as values. The
|
29
35
|
# values can be either the constants themselves or the constant names.
|
30
36
|
#
|
@@ -47,6 +47,14 @@ module Warden
|
|
47
47
|
env['HTTP_AUTHORIZATION'] = value
|
48
48
|
env
|
49
49
|
end
|
50
|
+
|
51
|
+
# Returns header configured through `aud_header` option
|
52
|
+
#
|
53
|
+
# @param env [Hash] Rack env
|
54
|
+
# @return [String]
|
55
|
+
def self.aud_header(env)
|
56
|
+
env[JWTAuth.config.aud_header]
|
57
|
+
end
|
50
58
|
end
|
51
59
|
end
|
52
60
|
end
|
@@ -16,6 +16,11 @@ module Warden
|
|
16
16
|
# match the one encoded in the payload
|
17
17
|
class WrongScope < JWT::DecodeError
|
18
18
|
end
|
19
|
+
|
20
|
+
# Error raised when trying to decode a token which `aud` claim does not
|
21
|
+
# match with the expected one
|
22
|
+
class WrongAud < JWT::DecodeError
|
23
|
+
end
|
19
24
|
end
|
20
25
|
end
|
21
26
|
end
|
@@ -4,7 +4,7 @@ module Warden
|
|
4
4
|
module JWTAuth
|
5
5
|
# Warden hooks
|
6
6
|
class Hooks
|
7
|
-
include JWTAuth::Import['mappings', 'dispatch_requests']
|
7
|
+
include JWTAuth::Import['mappings', 'dispatch_requests', 'aud_header']
|
8
8
|
|
9
9
|
# `env` key where JWT is added
|
10
10
|
PREPARED_TOKEN_ENV_KEY = 'warden-jwt_auth.token'
|
@@ -24,8 +24,7 @@ module Warden
|
|
24
24
|
env = auth.env
|
25
25
|
scope = opts[:scope]
|
26
26
|
return unless token_should_be_added?(scope, env)
|
27
|
-
|
28
|
-
env[PREPARED_TOKEN_ENV_KEY] = token
|
27
|
+
add_token_to_env(user, scope, env)
|
29
28
|
end
|
30
29
|
|
31
30
|
def token_should_be_added?(scope, env)
|
@@ -34,6 +33,14 @@ module Warden
|
|
34
33
|
jwt_scope?(scope) && request_matches?(path_info, method)
|
35
34
|
end
|
36
35
|
|
36
|
+
# :reek:ManualDispatch
|
37
|
+
# :reek:FeatureEnvy
|
38
|
+
def add_token_to_env(user, scope, env)
|
39
|
+
token, payload = UserEncoder.new.call(user, scope, env[aud_header])
|
40
|
+
user.on_jwt_dispatch(token, payload) if user.respond_to?(:on_jwt_dispatch)
|
41
|
+
env[PREPARED_TOKEN_ENV_KEY] = token
|
42
|
+
end
|
43
|
+
|
37
44
|
def jwt_scope?(scope)
|
38
45
|
jwt_scopes = mappings.keys
|
39
46
|
jwt_scopes.include?(scope)
|
@@ -31,6 +31,14 @@ module Warden
|
|
31
31
|
def jwt_payload
|
32
32
|
{}
|
33
33
|
end
|
34
|
+
|
35
|
+
# Does something just after a JWT for the user has been dispatched.
|
36
|
+
#
|
37
|
+
# @param _token [String]
|
38
|
+
# @param _payload [Hash]
|
39
|
+
def on_jwt_dispatch(_token, _payload)
|
40
|
+
raise NotImplementedError
|
41
|
+
end
|
34
42
|
end
|
35
43
|
|
36
44
|
# Strategy to manage JWT revocation
|
@@ -22,6 +22,13 @@ module Warden
|
|
22
22
|
payload['scp'] == scope.to_s
|
23
23
|
end
|
24
24
|
|
25
|
+
# Returns whether given aud matches with the one encoded in the payload
|
26
|
+
# @param payload [Hash] JWT payload
|
27
|
+
# @return [Boolean]
|
28
|
+
def self.aud_matches?(payload, aud)
|
29
|
+
payload['aud'] == aud
|
30
|
+
end
|
31
|
+
|
25
32
|
# Returns the payload to encode for a given user in a scope
|
26
33
|
#
|
27
34
|
# @param user [Interfaces::User] an user, whatever it is
|
@@ -18,7 +18,8 @@ module Warden
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def authenticate!
|
21
|
-
|
21
|
+
aud = EnvHelper.aud_header(env)
|
22
|
+
user = UserDecoder.new.call(token, scope, aud)
|
22
23
|
success!(user)
|
23
24
|
rescue JWT::DecodeError => exception
|
24
25
|
fail!(exception.message)
|
@@ -21,15 +21,17 @@ module Warden
|
|
21
21
|
#
|
22
22
|
# @param token [String] a JWT
|
23
23
|
# @param scope [Symbol] Warden scope
|
24
|
+
# @param aud [String] Expected aud claim
|
24
25
|
# @return [Interfaces::User] an user, whatever it is
|
25
26
|
# @raise [Errors::RevokedToken] when token has been revoked for the
|
26
27
|
# encoded user
|
27
28
|
# @raise [Errors::NilUser] when decoded user is nil
|
28
29
|
# @raise [Errors::WrongScope] when encoded scope does not match with scope
|
30
|
+
# @raise [Errors::WrongAud] when encoded aud does not match with aud
|
29
31
|
# argument
|
30
|
-
def call(token, scope)
|
32
|
+
def call(token, scope, aud)
|
31
33
|
payload = TokenDecoder.new.call(token)
|
32
|
-
|
34
|
+
check_valid_claims(payload, scope, aud)
|
33
35
|
user = helper.find_user(payload)
|
34
36
|
check_valid_user(payload, user, scope)
|
35
37
|
user
|
@@ -37,14 +39,15 @@ module Warden
|
|
37
39
|
|
38
40
|
private
|
39
41
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
42
|
+
def check_valid_claims(payload, scope, aud)
|
43
|
+
raise Errors::WrongScope, 'wrong scope' unless helper.scope_matches?(payload, scope)
|
44
|
+
raise Errors::WrongAud, 'wrong aud' unless helper.aud_matches?(payload, aud)
|
43
45
|
end
|
44
46
|
|
45
47
|
def check_valid_user(payload, user, scope)
|
46
48
|
raise Errors::NilUser, 'nil user' unless user
|
47
|
-
|
49
|
+
strategy = revocation_strategies[scope]
|
50
|
+
raise Errors::RevokedToken, 'revoked token' if strategy.jwt_revoked?(payload, user)
|
48
51
|
end
|
49
52
|
end
|
50
53
|
end
|
@@ -12,17 +12,26 @@ module Warden
|
|
12
12
|
@helper = PayloadUserHelper
|
13
13
|
end
|
14
14
|
|
15
|
-
# Encodes a user for given scope into a JWT.
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
15
|
+
# Encodes a user for given scope into a JWT.
|
16
|
+
#
|
17
|
+
# Payload generated includes:
|
18
|
+
#
|
19
|
+
# - a `sub` claim which is build calling `jwt_subject` in `user`
|
20
|
+
# - an `aud` claim taken as it is in the `aud` parameter
|
21
|
+
# - a custom `scp` claim taken as the value of the `scope` parameter
|
22
|
+
# as a string.
|
23
|
+
#
|
24
|
+
# The result of calling `jwt_payload` in user is also merged
|
25
|
+
# into the payload.
|
19
26
|
#
|
20
27
|
# @param user [Interfaces::User] an user, whatever it is
|
21
28
|
# @param scope [Symbol] Warden scope
|
22
|
-
# @
|
23
|
-
|
24
|
-
|
25
|
-
|
29
|
+
# @param aud [String] JWT aud claim
|
30
|
+
# @return [String, String] encoded JWT and decoded payload
|
31
|
+
def call(user, scope, aud)
|
32
|
+
payload = helper.payload_for_user(user, scope).merge('aud' => aud)
|
33
|
+
token = TokenEncoder.new.call(payload)
|
34
|
+
[token, payload]
|
26
35
|
end
|
27
36
|
end
|
28
37
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: warden-jwt_auth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marc Busqué
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-12-
|
11
|
+
date: 2017-12-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-configurable
|