rack-json_web_token_auth 0.1.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 +7 -0
- checksums.yaml.gz.sig +2 -0
- data.tar.gz.sig +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +263 -0
- data/lib/custom_contracts.rb +135 -0
- data/lib/rack/json_web_token_auth.rb +117 -0
- data/lib/rack/json_web_token_auth/resource.rb +68 -0
- data/lib/rack/json_web_token_auth/resources.rb +33 -0
- data/lib/rack/json_web_token_auth/version.rb +5 -0
- metadata +228 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 32ba799c3c70c2a2d97847f43783f122061ca956
|
4
|
+
data.tar.gz: 48e78024ba8183d174845324994d98e7a7bb2335
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b1c1bfb51899743df81370e00ea4961eb5af410e90a10c9d1ab544547f8601b55a2e9aede318e590edb9033d4d0d8203904d303fa9fe10533641ea091745b848
|
7
|
+
data.tar.gz: 24257ad9bc718101ce0fe7cbb5c5aaf7df7bdbae56106f7800f6f717291884f31ffe6f65126ac339aa4a62b09d20a493cb17c3d9c77a535bd5bbbc443c542ae1
|
checksums.yaml.gz.sig
ADDED
data.tar.gz.sig
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
�1�
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 Glenn Rempe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,263 @@
|
|
1
|
+
# Rack::JsonWebTokenAuth
|
2
|
+
|
3
|
+
## WARNING
|
4
|
+
|
5
|
+
This is pre-release software. It is pretty well tested but has not yet
|
6
|
+
been used in production. Your feedback is requested.
|
7
|
+
|
8
|
+
## About
|
9
|
+
|
10
|
+
`Rack::JsonWebTokenAuth` is a Rack middleware that makes it easy for your
|
11
|
+
Rack based application (Sinatra, Rails) to authenticate clients that
|
12
|
+
present a valid `Authorization: Bearer token` header with a [JSON Web Token (JWT)](https://jwt.io/).
|
13
|
+
|
14
|
+
This middleware was inspired by the similar [eigenbart/rack-jwt](https://github.com/eigenbart/rack-jwt)
|
15
|
+
middleware but provides a leaner codebase that relies upon the excellent
|
16
|
+
[garyf/jwt_claims](https://github.com/garyf/jwt_claims) and [garyf/json_web_token](https://github.com/garyf/json_web_token) gems to provide
|
17
|
+
all JWT token validation. This gem also makes extensive use of the [contracts](https://egonschiele.github.io/contracts.ruby/) gem to enforce strict
|
18
|
+
type checking on all inputs and outputs. It is designed to fail-fast on errors and
|
19
|
+
reject invalid inputs before even trying to parse them using JWT.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Add this line to your application's `Gemfile`:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
gem 'rack-json_web_token_auth'
|
27
|
+
```
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
```
|
32
|
+
$ bundle install
|
33
|
+
```
|
34
|
+
|
35
|
+
Or install it directly with:
|
36
|
+
|
37
|
+
```
|
38
|
+
$ gem install rack-json_web_token_auth
|
39
|
+
```
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
|
43
|
+
This Rack middleware is designed to allow adding a simple authentication layer,
|
44
|
+
using JSON Web Tokens (JWT), to your Rack based applications. It's easy
|
45
|
+
to configure with a simple Ruby DSL.
|
46
|
+
|
47
|
+
This middleware is not responsible for creating valid JWT tokens for you. It
|
48
|
+
only receives and validates them. If the token provided is valid for a specific
|
49
|
+
path the request will be allowed to continue as normal. If the token is invalid,
|
50
|
+
or the path requested is not a configured path, a `401 Not Authorized`
|
51
|
+
HTTP response will be sent.
|
52
|
+
|
53
|
+
For token creation I recommend the
|
54
|
+
[garyf/json_web_token](https://github.com/garyf/json_web_token) gem.
|
55
|
+
|
56
|
+
### Creating a JWT
|
57
|
+
|
58
|
+
Here is an example of creating a JWT with a pretty full set of claims. You may
|
59
|
+
not need all of these for your application.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
require 'json_web_token'
|
63
|
+
|
64
|
+
key = '4a7b98c31c3b6918f916d809443c096d02bf686d6bead5baa4a162642cea98b3'
|
65
|
+
|
66
|
+
claims = {
|
67
|
+
name: 'John Doe',
|
68
|
+
iat: Time.now.to_i - 1,
|
69
|
+
nbf: Time.now.to_i - 5,
|
70
|
+
exp: Time.now.to_i + 10,
|
71
|
+
aud: %w(api web),
|
72
|
+
sub: 'my-user-id',
|
73
|
+
jti: 'my-unique-token-id',
|
74
|
+
iss: 'https://my.example.com/'
|
75
|
+
}
|
76
|
+
|
77
|
+
# generate a signed token
|
78
|
+
jwt = JsonWebToken.sign(claims, key: key, alg: 'HS256')
|
79
|
+
#=> "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE0NzY0MTUwMjUsIm5iZiI6MTQ3NjQxNTAyMSwiZXhwIjoxNDc2NDE1MDM2LCJhdWQiOlsiYXBpIiwid2ViIl0sInN1YiI6Im15LXVzZXItaWQiLCJqdGkiOiJteS11bmlxdWUtdG9rZW4taWQiLCJpc3MiOiJodHRwczovL215LmV4YW1wbGUuY29tLyJ9.-zu-FGfLmwLX69DC2UIsk-8oEGoRSkCOUqbJwcarSm4"
|
80
|
+
```
|
81
|
+
|
82
|
+
### Submitting a JWT
|
83
|
+
|
84
|
+
Your client of choice needs to submit an [Authorization Bearer](http://self-issued.info/docs/draft-ietf-oauth-v2-bearer.html) request header.
|
85
|
+
|
86
|
+
How you do this is client specific and left as an exercise for the reader.
|
87
|
+
|
88
|
+
```
|
89
|
+
'Authorization' => "Bearer #{jwt}"
|
90
|
+
```
|
91
|
+
|
92
|
+
### Server Config
|
93
|
+
|
94
|
+
This middleware should be inserted as early as possible into your middleware
|
95
|
+
stack.
|
96
|
+
|
97
|
+
Configuring the Rack middleware to accept JWT tokens on your server is just a
|
98
|
+
matter of adding the middleware and configuring which paths are to be considered
|
99
|
+
public and `unsecured` (no JWT needed), and which require a valid token
|
100
|
+
to continue. These are private `secured` paths.
|
101
|
+
|
102
|
+
For each `secured` resource you must also provide the JWT config needed to validate
|
103
|
+
incoming tokens. The available claims are processed by the [garyf/jwt_claims](https://github.com/garyf/jwt_claims) gem and more info about
|
104
|
+
claims can be found in the README for that project. At a minimum a `:key` must
|
105
|
+
be provided except if the `none` algorithm is being used (probably not recommended).
|
106
|
+
|
107
|
+
Configuration directives are processed in the order that you provide and requests
|
108
|
+
match against the first path match. For this reason you should probably put your
|
109
|
+
`unsecured` resources first and order all resources from most specific to least
|
110
|
+
specific.
|
111
|
+
|
112
|
+
The DSL was heavily inspired by the [rack-cors](https://github.com/cyu/rack-cors)
|
113
|
+
gem and the resource path matching code is a direct port from it.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
require 'rack/json_web_token_auth'
|
117
|
+
|
118
|
+
use Rack::JsonWebTokenAuth.new do
|
119
|
+
|
120
|
+
# You can define JWT options for all `secured` resources globally
|
121
|
+
# or you can specify a hash like this inside each block. If you want to
|
122
|
+
# get really granular this config can even be different per `secure` resource.
|
123
|
+
jwt_opts = {
|
124
|
+
key: '4a7b98c31c3b6918f916d809443c096d02bf686d6bead5baa4a162642cea98b3',
|
125
|
+
alg: 'HS256',
|
126
|
+
aud: 'api',
|
127
|
+
sub: 'my-user-id',
|
128
|
+
jti: 'my-unique-token-id',
|
129
|
+
iss: 'https://my.example.com/',
|
130
|
+
leeway_seconds: 30
|
131
|
+
}
|
132
|
+
|
133
|
+
# Resources defined in this block are whitelisted and
|
134
|
+
# require no token for requests to the configured
|
135
|
+
# resource path. You should probably define your unsecured
|
136
|
+
# paths first. Resources in this block will raise an exception
|
137
|
+
# if provided with the :jwt options hash.
|
138
|
+
unsecured do
|
139
|
+
resource '/users/registration'
|
140
|
+
resource '/users/login'
|
141
|
+
end
|
142
|
+
|
143
|
+
# Resources defined in this block require a valid JWT token
|
144
|
+
# for access. Each resource takes a path and a Hash of options.
|
145
|
+
# The only option supported at the moment is `jwt`. The `:jwt` Hash
|
146
|
+
# key should be set to a Hash and only a `:key` must be defined
|
147
|
+
# which is a random key of sufficient strength.
|
148
|
+
#
|
149
|
+
# Additional JWT claims can also be provided in this hash as shown in
|
150
|
+
# this example.
|
151
|
+
#
|
152
|
+
# Resources defined in this block will raise an exception if they
|
153
|
+
# are not provided with the `:jwt` options hash and a valid `:key`
|
154
|
+
# (unless using the 'none' algorithm).
|
155
|
+
secured do
|
156
|
+
# a resource can start with a slash and match an exact path
|
157
|
+
resource '/private', jwt: jwt_opts
|
158
|
+
|
159
|
+
# or it can contain a wildcard '*'. The entire path
|
160
|
+
# can even be specified with '*' if you wanted to
|
161
|
+
# match all paths.
|
162
|
+
resource '/private/*/wildcard', jwt: jwt_opts
|
163
|
+
|
164
|
+
# Every resource can be configured with its own
|
165
|
+
# JWT keys and all other valid JWT claim options.
|
166
|
+
# For example you could require one token config for
|
167
|
+
# login and registration, and on successful login mint
|
168
|
+
# another flavor of token for all other app API access.
|
169
|
+
resource '/another/path', jwt: {key: 'a long random key', alg: 'HS512'}
|
170
|
+
end
|
171
|
+
|
172
|
+
# You can have more than one `unsecured` or `secured` block if you like.
|
173
|
+
unsecured do
|
174
|
+
# WARNING : this resource will never be used since it is masked
|
175
|
+
# by another resource higher in the stack with the same '/private' path.
|
176
|
+
resource '/private'
|
177
|
+
end
|
178
|
+
|
179
|
+
# Requests to any resource path not explictly marked as 'secured' or
|
180
|
+
# `unsecured` above will fail-safe and return a 401 status.
|
181
|
+
# e.g. /path/to/somewhere/else
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
## Development
|
186
|
+
|
187
|
+
After checking out the repo, run `bundle install` to install dependencies. Then,
|
188
|
+
run `bundle exec rake` to run the specs.
|
189
|
+
|
190
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
191
|
+
|
192
|
+
### Installation Security : Signed Ruby Gem
|
193
|
+
|
194
|
+
This gem is cryptographically signed. To be sure the gem you install hasn’t
|
195
|
+
been tampered with you can install it using the following method:
|
196
|
+
|
197
|
+
Add my public key (if you haven’t already) as a trusted certificate
|
198
|
+
|
199
|
+
```
|
200
|
+
# Caveat: Gem certificates are trusted globally, such that adding a
|
201
|
+
# cert.pem for one gem automatically trusts all gems signed by that cert.
|
202
|
+
gem cert --add <(curl -Ls https://raw.github.com/grempe/rack-json_web_token_auth/master/certs/gem-public_cert_grempe_2026.pem)
|
203
|
+
```
|
204
|
+
|
205
|
+
To install, it is possible to specify either `HighSecurity` or `MediumSecurity`
|
206
|
+
mode. Since this gem depends on one or more gems that are not cryptographically
|
207
|
+
signed you will likely need to use `MediumSecurity`. You should receive a warning
|
208
|
+
if any signed gem does not match its signature.
|
209
|
+
|
210
|
+
```
|
211
|
+
# All signed dependent gems must be verified.
|
212
|
+
gem install rack-json_web_token_auth -P MediumSecurity
|
213
|
+
```
|
214
|
+
|
215
|
+
You can [learn more about security and signed Ruby Gems](http://guides.rubygems.org/security/).
|
216
|
+
|
217
|
+
### Installation Security : Signed Git Commits
|
218
|
+
|
219
|
+
Most, if not all, of the commits and tags to this repository are
|
220
|
+
signed with my PGP/GPG code signing key. I have uploaded my code signing public
|
221
|
+
keys to GitHub and you can now verify those signatures with the GitHub UI.
|
222
|
+
See [this list of commits](https://github.com/grempe/rack-json_web_token_auth/commits/master)
|
223
|
+
and look for the `Verified` tag next to each commit. You can click on that tag
|
224
|
+
for additional information.
|
225
|
+
|
226
|
+
You can also clone the repository and verify the signatures locally using your
|
227
|
+
own GnuPG installation. You can find my certificates and read about how to conduct
|
228
|
+
this verification at [https://www.rempe.us/keys/](https://www.rempe.us/keys/).
|
229
|
+
|
230
|
+
### Contributing
|
231
|
+
|
232
|
+
Bug reports and pull requests are welcome on GitHub
|
233
|
+
at [https://github.com/grempe/rack-json_web_token_auth](https://github.com/grempe/rack-json_web_token_auth). This project is intended to be a safe, welcoming space for collaboration, and
|
234
|
+
contributors are expected to adhere to the
|
235
|
+
[Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
236
|
+
|
237
|
+
## Legal
|
238
|
+
|
239
|
+
### Copyright
|
240
|
+
|
241
|
+
(c) 2016 Glenn Rempe <[glenn@rempe.us](mailto:glenn@rempe.us)> ([https://www.rempe.us/](https://www.rempe.us/))
|
242
|
+
|
243
|
+
### License
|
244
|
+
|
245
|
+
The gem is available as open source under the terms of
|
246
|
+
the [MIT License](http://opensource.org/licenses/MIT).
|
247
|
+
|
248
|
+
### Warranty
|
249
|
+
|
250
|
+
Unless required by applicable law or agreed to in writing,
|
251
|
+
software distributed under the License is distributed on an
|
252
|
+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
253
|
+
either express or implied. See the LICENSE.txt file for the
|
254
|
+
specific language governing permissions and limitations under
|
255
|
+
the License.
|
256
|
+
|
257
|
+
## Thank You!
|
258
|
+
|
259
|
+
Thanks to Gary Fleshman ([@garyf](https://github.com/garyf)) for
|
260
|
+
his very well written implementation of JWT and for accepting my patches.
|
261
|
+
|
262
|
+
And of course thanks to Mr. Eigenbart ([@eigenbart](https://github.com/eigenbart))
|
263
|
+
for the inspiration.
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Contracts
|
2
|
+
C = Contracts
|
3
|
+
|
4
|
+
# Custom Contracts
|
5
|
+
# See : https://egonschiele.github.io/contracts.ruby/
|
6
|
+
|
7
|
+
# The last segment gets dropped for 'none' algorithm since there is no
|
8
|
+
# signature so both of these patterns are valid. All character chunks
|
9
|
+
# are base64url format and periods.
|
10
|
+
# Bearer abc123.abc123.abc123
|
11
|
+
# Bearer abc123.abc123.
|
12
|
+
BEARER_TOKEN_REGEX = %r{
|
13
|
+
^Bearer\s{1}( # starts with Bearer and a single space
|
14
|
+
[a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
|
15
|
+
[a-zA-Z0-9\-\_]+\. # 1 or more chars followed by a single period
|
16
|
+
[a-zA-Z0-9\-\_]* # 0 or more chars, no trailing chars
|
17
|
+
)$
|
18
|
+
}x
|
19
|
+
|
20
|
+
class RackRequestHttpAuth
|
21
|
+
def self.valid?(val)
|
22
|
+
Contract.valid?(val, ({ 'HTTP_AUTHORIZATION' => BEARER_TOKEN_REGEX }))
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.to_s
|
26
|
+
'A Rack request with JWT auth header'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class RackResponse
|
31
|
+
def self.valid?(val)
|
32
|
+
Contract.valid?(val, [C::Int, Hash, C::Any])
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.to_s
|
36
|
+
'A Rack response'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Key
|
41
|
+
def self.valid?(val)
|
42
|
+
return false if val.is_a?(String) && val.strip.empty?
|
43
|
+
C::Or[String, OpenSSL::PKey::RSA, OpenSSL::PKey::EC].valid?(val)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.to_s
|
47
|
+
'A JWT secret string or signature key'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Algorithm
|
52
|
+
def self.valid?(val)
|
53
|
+
C::Enum['none', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'].valid?(val)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.to_s
|
57
|
+
'A valid JWT token signature algorithm, or none'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class VerifierOptions
|
62
|
+
def self.valid?(val)
|
63
|
+
C::KeywordArgs[
|
64
|
+
key: C::Optional[C::Key],
|
65
|
+
alg: C::Optional[C::Algorithm],
|
66
|
+
iat: C::Optional[C::Int],
|
67
|
+
nbf: C::Optional[C::Int],
|
68
|
+
exp: C::Optional[C::Int],
|
69
|
+
iss: C::Optional[String],
|
70
|
+
jti: C::Optional[String],
|
71
|
+
aud: C::Optional[C::Or[String, C::ArrayOf[String], Symbol, C::ArrayOf[Symbol]]],
|
72
|
+
sub: C::Optional[String],
|
73
|
+
leeway_seconds: C::Optional[C::Int]
|
74
|
+
].valid?(val)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.to_s
|
78
|
+
'A Hash of token verifier options'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# abc123.abc123.abc123 (w/ signature)
|
83
|
+
# abc123.abc123. ('none')
|
84
|
+
class EncodedToken
|
85
|
+
def self.valid?(val)
|
86
|
+
val =~ /\A([a-zA-Z0-9\-\_]+\.[a-zA-Z0-9\-\_]+\.[a-zA-Z0-9\-\_]*)\z/
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.to_s
|
90
|
+
'A valid encoded token'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class DecodedToken
|
95
|
+
def self.valid?(val)
|
96
|
+
C::ArrayOf[Hash].valid?(val) &&
|
97
|
+
C::DecodedTokenClaims.valid?(val[0]) &&
|
98
|
+
C::DecodedTokenHeader.valid?(val[1])
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.to_s
|
102
|
+
'A valid Array of decoded token claims and header Hashes'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class DecodedTokenClaims
|
107
|
+
def self.valid?(val)
|
108
|
+
C::HashOf[C::Or[String, Symbol] => C::Maybe[C::Or[String, C::Num, C::Bool, C::ArrayOf[C::Any], Hash]]].valid?(val)
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.to_s
|
112
|
+
'A valid decoded token payload attribute'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class DecodedTokenHeader
|
117
|
+
def self.valid?(val)
|
118
|
+
C::HashOf[C::Enum['typ', 'alg'] => C::Or['JWT', C::TokenAlgorithm]].valid?(val)
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.to_s
|
122
|
+
'A valid decoded token header attribute'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class ResourcePath
|
127
|
+
def self.valid?(val)
|
128
|
+
C::Or[String, Regexp]
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.to_s
|
132
|
+
'A valid resource path string or regex'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'contracts'
|
3
|
+
require 'hashie'
|
4
|
+
require 'jwt_claims'
|
5
|
+
|
6
|
+
require 'rack/json_web_token_auth/resources'
|
7
|
+
require 'rack/json_web_token_auth/resource'
|
8
|
+
require 'custom_contracts'
|
9
|
+
|
10
|
+
module Rack
|
11
|
+
# Rack Middleware for JSON Web Token Authentication
|
12
|
+
class JsonWebTokenAuth
|
13
|
+
include Contracts::Core
|
14
|
+
C = Contracts
|
15
|
+
|
16
|
+
ENV_KEY = 'jwt.claims'.freeze
|
17
|
+
PATH_INFO_HEADER_KEY = 'PATH_INFO'.freeze
|
18
|
+
|
19
|
+
Contract C::Any, Proc => C::Any
|
20
|
+
def initialize(app, &block)
|
21
|
+
@app = app
|
22
|
+
# execute the block methods provided in the context of this class
|
23
|
+
instance_eval(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
Contract Proc => C::ArrayOf[Resources]
|
27
|
+
def secured(&block)
|
28
|
+
resources = Resources.new(public_resource: false)
|
29
|
+
# execute the methods in the 'secured' block in the context of
|
30
|
+
# a new Resources object
|
31
|
+
resources.instance_eval(&block)
|
32
|
+
all_resources << resources
|
33
|
+
end
|
34
|
+
|
35
|
+
Contract Proc => C::ArrayOf[Resources]
|
36
|
+
def unsecured(&block)
|
37
|
+
resources = Resources.new(public_resource: true)
|
38
|
+
# execute the methods in the 'unsecured' block in the context of
|
39
|
+
# a new Resources object
|
40
|
+
resources.instance_eval(&block)
|
41
|
+
all_resources << resources
|
42
|
+
end
|
43
|
+
|
44
|
+
Contract Hash => C::RackResponse
|
45
|
+
def call(env)
|
46
|
+
begin
|
47
|
+
resource = resource_for_path(env[PATH_INFO_HEADER_KEY])
|
48
|
+
|
49
|
+
if resource.public_resource?
|
50
|
+
# whitelisted as `unsecured`. skip all token authentication.
|
51
|
+
@app.call(env)
|
52
|
+
elsif resource.nil?
|
53
|
+
# no matching `secured` or `unsecured` resource.
|
54
|
+
# fail-safe with 401 unauthorized
|
55
|
+
raise 'No resource for path defined. Deny by default.'
|
56
|
+
else
|
57
|
+
# a `secured` resource, validate the token to see if authenticated
|
58
|
+
|
59
|
+
# Test that `env` has a well formed Authorization header
|
60
|
+
unless Contract.valid?(env, C::RackRequestHttpAuth)
|
61
|
+
raise 'malformed Authorization header or token'
|
62
|
+
end
|
63
|
+
|
64
|
+
# Extract the token from the 'Authorization: Bearer token' string
|
65
|
+
token = C::BEARER_TOKEN_REGEX.match(env['HTTP_AUTHORIZATION'])[1]
|
66
|
+
|
67
|
+
# Verify the token and its claims are valid
|
68
|
+
jwt_opts = resource.opts[:jwt]
|
69
|
+
jwt = ::JwtClaims.verify(token, jwt_opts)
|
70
|
+
|
71
|
+
# JwtClaims.verify returns a JWT claims set hash, if the
|
72
|
+
# JWT Message Authentication Code (MAC), or signature,
|
73
|
+
# are verified and the registered claims are also verified.
|
74
|
+
if Contract.valid?(jwt, C::HashOf[ok: C::HashOf[Symbol => C::Any]])
|
75
|
+
# Authenticated! Pass all claims into the app env for app use
|
76
|
+
# with the hash keys converted to strings to match Rack env.
|
77
|
+
env[ENV_KEY] = Hashie.stringify_keys(jwt[:ok])
|
78
|
+
elsif Contract.valid?(jwt, C::HashOf[error: C::ArrayOf[Symbol]])
|
79
|
+
# a list of any registered claims that fail validation, if the JWT MAC is verified
|
80
|
+
raise "invalid JWT claims : #{jwt[:error].sort.join(', ')}"
|
81
|
+
elsif Contract.valid?(jwt, C::HashOf[error: 'invalid JWT'])
|
82
|
+
# the JWT MAC is not verified
|
83
|
+
raise 'invalid JWT'
|
84
|
+
elsif Contract.valid?(jwt, C::HashOf[error: 'invalid input'])
|
85
|
+
# otherwise
|
86
|
+
raise 'invalid JWT input'
|
87
|
+
else
|
88
|
+
raise 'unhandled JWT error'
|
89
|
+
end
|
90
|
+
|
91
|
+
@app.call(env)
|
92
|
+
end
|
93
|
+
rescue StandardError => e
|
94
|
+
body = e.message.nil? ? 'Unauthorized' : "Unauthorized : #{e.message}"
|
95
|
+
headers = { 'WWW-Authenticate' => 'Bearer error="invalid_token"',
|
96
|
+
'Content-Type' => 'text/plain',
|
97
|
+
'Content-Length' => body.bytesize.to_s }
|
98
|
+
[401, headers, [body]]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
Contract C::None => C::Or[C::ArrayOf[Resources], []]
|
103
|
+
def all_resources
|
104
|
+
@all_resources ||= []
|
105
|
+
end
|
106
|
+
|
107
|
+
Contract String => C::Maybe[Resource]
|
108
|
+
def resource_for_path(path_info)
|
109
|
+
all_resources.each do |r|
|
110
|
+
if found = r.resource_for_path(path_info)
|
111
|
+
return found
|
112
|
+
end
|
113
|
+
end
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'custom_contracts'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class JsonWebTokenAuth
|
5
|
+
class Resource
|
6
|
+
include Contracts::Core
|
7
|
+
C = Contracts
|
8
|
+
|
9
|
+
attr_accessor :public_resource, :path, :pattern, :opts
|
10
|
+
|
11
|
+
Contract C::Bool, C::ResourcePath, Hash => C::Any
|
12
|
+
def initialize(public_resource, path, opts = {})
|
13
|
+
@public_resource = public_resource
|
14
|
+
@path = path
|
15
|
+
@pattern = compile(path)
|
16
|
+
@opts = opts
|
17
|
+
|
18
|
+
if public_resource
|
19
|
+
# unsecured resources should not have any jwt options defined
|
20
|
+
if @opts.key?(:jwt)
|
21
|
+
raise 'unexpected jwt options provided for unsecured resource'
|
22
|
+
end
|
23
|
+
else
|
24
|
+
# secured resources must have a :jwt hash with a :key
|
25
|
+
unless Contract.valid?(@opts, ({ jwt: { key: nil, alg: 'none' } })) ||
|
26
|
+
Contract.valid?(@opts, ({ jwt: { key: C::Key } }))
|
27
|
+
raise 'invalid or missing jwt options for secured resource'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Contract C::ResourcePath => C::Maybe[Fixnum]
|
33
|
+
def matches_path?(path)
|
34
|
+
pattern =~ path
|
35
|
+
end
|
36
|
+
|
37
|
+
Contract C::None => C::Bool
|
38
|
+
def public_resource?
|
39
|
+
public_resource
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
Contract C::ResourcePath => Regexp
|
45
|
+
def compile(path)
|
46
|
+
if path.respond_to? :to_str
|
47
|
+
special_chars = %w{. + ( )}
|
48
|
+
pattern =
|
49
|
+
path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
|
50
|
+
case match
|
51
|
+
when "*"
|
52
|
+
"(.*?)"
|
53
|
+
when *special_chars
|
54
|
+
Regexp.escape(match)
|
55
|
+
else
|
56
|
+
"([^/?&#]+)"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
/^#{pattern}$/
|
60
|
+
elsif path.respond_to? :match
|
61
|
+
path
|
62
|
+
else
|
63
|
+
raise TypeError, path
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'custom_contracts'
|
2
|
+
require 'rack/json_web_token_auth/resource'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
class JsonWebTokenAuth
|
6
|
+
class Resources
|
7
|
+
include Contracts::Core
|
8
|
+
C = Contracts
|
9
|
+
|
10
|
+
Contract C::KeywordArgs[public_resource: C::Bool] => C::Any
|
11
|
+
def initialize(public_resource: false)
|
12
|
+
@resources = []
|
13
|
+
@public_resource = public_resource
|
14
|
+
end
|
15
|
+
|
16
|
+
Contract C::None => C::Bool
|
17
|
+
def public_resource?
|
18
|
+
@public_resource
|
19
|
+
end
|
20
|
+
|
21
|
+
Contract String, C::Maybe[Hash] => C::ArrayOf[Resource]
|
22
|
+
def resource(path, opts = {})
|
23
|
+
@resources << Resource.new(public_resource?, path, opts)
|
24
|
+
end
|
25
|
+
|
26
|
+
Contract String => C::Maybe[Resource]
|
27
|
+
def resource_for_path(path)
|
28
|
+
# return first match
|
29
|
+
@resources.detect { |r| r.matches_path?(path) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-json_web_token_auth
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Glenn Rempe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIDYDCCAkigAwIBAgIBATANBgkqhkiG9w0BAQUFADA7MQ4wDAYDVQQDDAVnbGVu
|
14
|
+
bjEVMBMGCgmSJomT8ixkARkWBXJlbXBlMRIwEAYKCZImiZPyLGQBGRYCdXMwHhcN
|
15
|
+
MTYxMDEzMDEzMjM5WhcNMjYxMDExMDEzMjM5WjA7MQ4wDAYDVQQDDAVnbGVubjEV
|
16
|
+
MBMGCgmSJomT8ixkARkWBXJlbXBlMRIwEAYKCZImiZPyLGQBGRYCdXMwggEiMA0G
|
17
|
+
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrEuLEy11cjgMC4+ldcgLzBrGcfWWg
|
18
|
+
nUhdCRn3Arzo2EV1d4V4h6VOHmk4o7kumBeajUMMZ0+xKtu8euRCnbDnlxowfJvT
|
19
|
+
S0nzsOt1dm++INeKMpZU84LuH7BbAlyL+B//l1YkI33gsbA8wm06+vV8tUEBuQch
|
20
|
+
vBU2xrCyS2+0LQTCaCS+VvHbV97hzIwSIgUFJuFjrcnnpV8Qt1R0Bi8pzDk+2jyN
|
21
|
+
AgxaWa41UHn70O0gFRRDGXacRpvy3HRSJrvlHPPAC02CjhKjsOLjZowaHxCv9XIJ
|
22
|
+
tCQnVEOUUo9+owG2Gna4k4DMLIjiGChHNFXtO8WyuksukVqcsdc9kvdzAgMBAAGj
|
23
|
+
bzBtMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBR68/Ook0uwfe6t
|
24
|
+
FbLHXIReYQ2VpzAZBgNVHREEEjAQgQ5nbGVubkByZW1wZS51czAZBgNVHRIEEjAQ
|
25
|
+
gQ5nbGVubkByZW1wZS51czANBgkqhkiG9w0BAQUFAAOCAQEAI27KUzTE9BoD2irI
|
26
|
+
CkMVPC0YS6iANrzQy3zIJI4yLKEZmI1jDE+W2APL11Woo5+sttgqY7148W84ZWdK
|
27
|
+
mD9ueqH5hPC8NOd3wYXVMNwmyLhnyh80cOzGeurW1SJ0VV3BqSKEE8q4EFjCzUK9
|
28
|
+
Oq8dW9i9Bxn8qgcOSFTYITJZ/mNyy2shHs5gg0MIz0uOsKaHqrrMseVfG7ZoTgV1
|
29
|
+
kkyRaYAHI1MSDNGFNwgURPQsgnxQrX8YG48q0ypFC1gOl/l6D0e/oF4SKMS156uc
|
30
|
+
vprF5QiDz8HshVP9DjJT2I1wyGyvxEdU3cTRo0upMP/VZLcgyBVFy90N2XYWWk2D
|
31
|
+
GIxGSw==
|
32
|
+
-----END CERTIFICATE-----
|
33
|
+
date: 2016-10-14 00:00:00.000000000 Z
|
34
|
+
dependencies:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: contracts
|
37
|
+
requirement: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0.14'
|
42
|
+
type: :runtime
|
43
|
+
prerelease: false
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0.14'
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: hashie
|
51
|
+
requirement: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "~>"
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '3.4'
|
56
|
+
type: :runtime
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.4'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: json_web_token
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.3.2
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 0.3.2
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
name: jwt_claims
|
79
|
+
requirement: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0.1'
|
84
|
+
type: :runtime
|
85
|
+
prerelease: false
|
86
|
+
version_requirements: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0.1'
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: rake
|
93
|
+
requirement: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '11.3'
|
98
|
+
type: :development
|
99
|
+
prerelease: false
|
100
|
+
version_requirements: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '11.3'
|
105
|
+
- !ruby/object:Gem::Dependency
|
106
|
+
name: bundler
|
107
|
+
requirement: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '1.13'
|
112
|
+
type: :development
|
113
|
+
prerelease: false
|
114
|
+
version_requirements: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '1.13'
|
119
|
+
- !ruby/object:Gem::Dependency
|
120
|
+
name: rspec
|
121
|
+
requirement: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '3.4'
|
126
|
+
type: :development
|
127
|
+
prerelease: false
|
128
|
+
version_requirements: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '3.4'
|
133
|
+
- !ruby/object:Gem::Dependency
|
134
|
+
name: rack-test
|
135
|
+
requirement: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - "~>"
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0.6'
|
140
|
+
type: :development
|
141
|
+
prerelease: false
|
142
|
+
version_requirements: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - "~>"
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0.6'
|
147
|
+
- !ruby/object:Gem::Dependency
|
148
|
+
name: simplecov
|
149
|
+
requirement: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - "~>"
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0.12'
|
154
|
+
type: :development
|
155
|
+
prerelease: false
|
156
|
+
version_requirements: !ruby/object:Gem::Requirement
|
157
|
+
requirements:
|
158
|
+
- - "~>"
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0.12'
|
161
|
+
- !ruby/object:Gem::Dependency
|
162
|
+
name: rubocop
|
163
|
+
requirement: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - "~>"
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0.41'
|
168
|
+
type: :development
|
169
|
+
prerelease: false
|
170
|
+
version_requirements: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - "~>"
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0.41'
|
175
|
+
- !ruby/object:Gem::Dependency
|
176
|
+
name: wwtd
|
177
|
+
requirement: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - "~>"
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '1.3'
|
182
|
+
type: :development
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
requirements:
|
186
|
+
- - "~>"
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: '1.3'
|
189
|
+
description: Rack middleware for authentication using JSON Web Tokens using the jwt_claims
|
190
|
+
and json_web_token gems.
|
191
|
+
email:
|
192
|
+
- glenn@rempe.us
|
193
|
+
executables: []
|
194
|
+
extensions: []
|
195
|
+
extra_rdoc_files: []
|
196
|
+
files:
|
197
|
+
- LICENSE.txt
|
198
|
+
- README.md
|
199
|
+
- lib/custom_contracts.rb
|
200
|
+
- lib/rack/json_web_token_auth.rb
|
201
|
+
- lib/rack/json_web_token_auth/resource.rb
|
202
|
+
- lib/rack/json_web_token_auth/resources.rb
|
203
|
+
- lib/rack/json_web_token_auth/version.rb
|
204
|
+
homepage: https://github.com/grempe/rack-json_web_token_auth
|
205
|
+
licenses:
|
206
|
+
- MIT
|
207
|
+
metadata: {}
|
208
|
+
post_install_message:
|
209
|
+
rdoc_options: []
|
210
|
+
require_paths:
|
211
|
+
- lib
|
212
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
213
|
+
requirements:
|
214
|
+
- - ">="
|
215
|
+
- !ruby/object:Gem::Version
|
216
|
+
version: 2.2.5
|
217
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
218
|
+
requirements:
|
219
|
+
- - ">="
|
220
|
+
- !ruby/object:Gem::Version
|
221
|
+
version: '0'
|
222
|
+
requirements: []
|
223
|
+
rubyforge_project:
|
224
|
+
rubygems_version: 2.5.1
|
225
|
+
signing_key:
|
226
|
+
specification_version: 4
|
227
|
+
summary: Rack middleware for authentication using JSON Web Tokens
|
228
|
+
test_files: []
|
metadata.gz.sig
ADDED
Binary file
|