rack-json_web_token_auth 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|