keyless 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a742a4e768b944a62453aa8cc6d94e1afd138a5cd3d4059ed6f970ade717618d
4
+ data.tar.gz: 0c8e5392cc8443b303ddbfe67672ca54a56462041d84e95e4adcbddbb66bbc78
5
+ SHA512:
6
+ metadata.gz: 23f6a2b71ce821364c0c66aed4f585c80e74a4412bf204a08f95b265ab3790eacea59dcb1d5e5c8496bf1d06f2b0cf0d32c162f7a824f34a79a9bb0124d3b41a
7
+ data.tar.gz: fd1d10b84e31ca6eb624832a3c2426b2df9989fe9ceb14c1c5638d87c3673be66c0d6972c3f50bc0cbd6fb73dde677a01e82be8cecddf472c94a2ae644a79085
@@ -0,0 +1,30 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = space
6
+ indent_size = 2
7
+ end_of_line = lf
8
+ charset = utf-8
9
+ trim_trailing_whitespace = true
10
+ insert_final_newline = true
11
+
12
+ [*.md]
13
+ trim_trailing_whitespace = true
14
+
15
+ [*.json]
16
+ indent_style = space
17
+ indent_size = 2
18
+
19
+ [*.yml]
20
+ indent_style = space
21
+ indent_size = 2
22
+
23
+ [Makefile]
24
+ trim_trailing_whitespace = true
25
+ indent_style = tab
26
+ indent_size = 4
27
+
28
+ [*.sh]
29
+ indent_style = space
30
+ indent_size = 2
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/api/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /Gemfile.lock
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,41 @@
1
+ require: rubocop-rspec
2
+
3
+ Rails:
4
+ Enabled: true
5
+
6
+ Documentation:
7
+ Enabled: true
8
+
9
+ AllCops:
10
+ DisplayCopNames: true
11
+ TargetRubyVersion: 2.3
12
+ Exclude:
13
+ - db/schema.rb
14
+ - bin/**/*
15
+ - db/migrate/**/*
16
+ - vendor/cache/**/*
17
+ - vendor/bundle/**/*
18
+ - build/**/*
19
+
20
+ Metrics/BlockLength:
21
+ Exclude:
22
+ - Rakefile
23
+ - keyless.gemspec
24
+ - spec/**/*.rb
25
+ - '**/*.rake'
26
+
27
+ # Document all the things.
28
+ Style/DocumentationMethod:
29
+ Enabled: true
30
+ RequireForNonPublicMethods: true
31
+
32
+ # It's a deliberate idiom in RSpec.
33
+ # See: https://github.com/bbatsov/rubocop/issues/4222
34
+ Lint/AmbiguousBlockAssociation:
35
+ Exclude:
36
+ - "spec/**/*"
37
+
38
+ # Because +expect_any_instance_of().to have_received()+ is not
39
+ # supported with the +with(hash_including)+ matchers
40
+ RSpec/MessageSpies:
41
+ EnforcedStyle: receive
@@ -0,0 +1,3 @@
1
+ SimpleCov.start 'test_frameworks' do
2
+ add_filter '/vendor/bundle/'
3
+ end
@@ -0,0 +1,20 @@
1
+ env:
2
+ global:
3
+ - CC_TEST_REPORTER_ID=ecb753423174dbd8e4aaf04fb62bf4ef9c2a54904ac49a33fdf2b908b3c5e5f3
4
+
5
+ sudo: false
6
+ language: ruby
7
+ rvm:
8
+ - 2.6
9
+ - 2.5
10
+ - 2.4
11
+
12
+ before_install: gem install bundler
13
+
14
+ before_script:
15
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
16
+ - chmod +x ./cc-test-reporter
17
+ - ./cc-test-reporter before-build
18
+ script: bundle exec rake
19
+ after_script:
20
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
@@ -0,0 +1,8 @@
1
+ ### 1.0.1
2
+
3
+ * Renamed the Gem to `keyless`.
4
+
5
+ ### 1.0.0
6
+
7
+ * Initial release, extracted from former [grape-jwt-authentication](https://github.com/hausgold/grape-jwt-authentication)
8
+ code at v1.3.0.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in keyless.gemspec
8
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 HAUSGOLD | talocasa GmbH
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.
@@ -0,0 +1,279 @@
1
+ ![keyless](doc/assets/project.svg)
2
+
3
+ [![Build Status](https://travis-ci.com/hausgold/keyless.svg?branch=master)](https://travis-ci.com/hausgold/keyless)
4
+ [![Gem Version](https://badge.fury.io/rb/keyless.svg)](https://badge.fury.io/rb/keyless)
5
+ [![API docs](https://img.shields.io/badge/docs-API-blue.svg)](https://www.rubydoc.info/gems/keyless)
6
+
7
+ This gem is dedicated to easily integrate a JWT authentication to your
8
+ ruby application. The real authentication
9
+ functionality must be provided by the user and this makes this gem highy
10
+ flexible on the JWT verification level.
11
+
12
+ - [Installation](#installation)
13
+ - [Configuration](#configuration)
14
+ - [Authenticator](#authenticator)
15
+ - [RSA public key helper](#rsa-public-key-helper)
16
+ - [RSA public key location (URL)](#rsa-public-key-location-url)
17
+ - [RSA public key caching](#rsa-public-key-caching)
18
+ - [RSA public key cache expiration](#rsa-public-key-cache-expiration)
19
+ - [JWT instance helper](#jwt-instance-helper)
20
+ - [Issuer verification](#issuer-verification)
21
+ - [Beholder (audience) verification](#beholder-audience-verification)
22
+ - [Custom JWT verification options](#custom-jwt-verification-options)
23
+ - [Custom JWT verification key](#custom-jwt-verification-key)
24
+ - [Full RSA256 example](#full-rsa256-example)
25
+ - [Development](#development)
26
+ - [Contributing](#contributing)
27
+
28
+ ## Installation
29
+
30
+ Add this line to your application's Gemfile:
31
+
32
+ ```ruby
33
+ gem 'keyless'
34
+ ```
35
+
36
+ And then execute:
37
+
38
+ ```bash
39
+ $ bundle
40
+ ```
41
+
42
+ Or install it yourself as:
43
+
44
+ ```bash
45
+ $ gem install keyless
46
+ ```
47
+
48
+ ## Configuration
49
+
50
+ This gem is quite customizable and flexible to fulfill your needs. You can make
51
+ use of some parts and leave other if you do not care about them. We are not
52
+ going to force the way how to verify JWT or work with them. Here comes a
53
+ overview of the configurations you can do.
54
+
55
+ ### Authenticator
56
+
57
+ The authenticator function which must be defined by the user to verify the
58
+ given JSON Web Token. Here comes all your logic to lookup the related user on
59
+ your database, the token claim verification and/or the token cryptographic
60
+ signing. The function must return true or false to indicate the validity of the
61
+ token.
62
+
63
+ ```ruby
64
+ Keyless.configure do |conf|
65
+ conf.authenticator = proc do |token|
66
+ # Verify the token the way you like. (true, false)
67
+ end
68
+ end
69
+ ```
70
+
71
+ ### RSA public key helper
72
+
73
+ We provide a straightforward solution to deal with the provision of RSA public
74
+ keys. Somethimes you want to distribute them by file to each machine and have
75
+ a local access, and somethimes you provide an endpoint on your identity
76
+ provider to fetch the RSA public key via HTTP/HTTPS. The `RsaPublicKey` class
77
+ helps you to fulfill this task easily.
78
+
79
+ **Heads up!** You can skip this if you do not care about RSA verification or
80
+ have your own mechanism.
81
+
82
+ ```ruby
83
+ # Get your public key, by using the global configuration
84
+ public_key = Keyless::RsaPublicKey.fetch
85
+ # => OpenSSL::PKey::RSA
86
+
87
+ # Using a local configuration
88
+ fetcher = Keyless::RsaPublicKey.instance
89
+ fetcher.url = 'https://your.identity.provider/rsa_public_key'
90
+ public_key = fetcher.fetch
91
+ # => OpenSSL::PKey::RSA
92
+ ```
93
+
94
+ The following examples show you how to configure the
95
+ `Keyless::RsaPublicKey` class the global way. This is useful
96
+ for a shared initializer place.
97
+
98
+ #### RSA public key location (URL)
99
+
100
+ Whenever you want to use the `RsaPublicKey` class you configure the default URL
101
+ on the singleton instance, or use the gem configure method and set it up
102
+ accordingly. We allow the fetch of the public key from a remote server
103
+ (HTTP/HTTPS) or from a local file which is accessible by the ruby process.
104
+ Specify the URL or the local path here. Not specified by default.
105
+
106
+ ```ruby
107
+ Keyless.configure do |conf|
108
+ # Local file
109
+ conf.rsa_public_key_url = '/tmp/jwt_rsa.pub'
110
+ # Remote URL
111
+ conf.rsa_public_key_url = 'https://your.identity.provider/rsa_public_key'
112
+ end
113
+ ```
114
+
115
+ #### RSA public key caching
116
+
117
+ You can configure the `RsaPublickey` class to enable/disable caching. For a
118
+ remote public key location it is handy to cache the result for some time to
119
+ keep the traffic low to the resource server. For a local file you can skip
120
+ this. Disabled by default.
121
+
122
+ ```ruby
123
+ Keyless.configure do |conf|
124
+ conf.rsa_public_key_caching = true
125
+ end
126
+ ```
127
+
128
+ #### RSA public key cache expiration
129
+
130
+ When you make use of the cache of the `RsaPublicKey` class you can fine tune
131
+ the expiration time. The RSA public key from your identity
132
+ provider should not change this frequent, so a cache for at least one hour is
133
+ fine. You should not set it lower than one minute. Keep this setting in mind
134
+ when you change keys. Your infrastructure could be inoperable for this
135
+ configured time. One hour by default.
136
+
137
+ ```ruby
138
+ Keyless.configure do |conf|
139
+ conf.rsa_public_key_expiration = 1.hour
140
+ end
141
+ ```
142
+
143
+ ### JWT instance helper
144
+
145
+ We ship a little wrapper class to ease the validation of JSON Web Tokens with
146
+ the help of the great [ruby-jwt](https://github.com/jwt/ruby-jwt) library. This
147
+ wrapper class provides some helpers like `#access_token?`, `#refresh_token?` or
148
+ `#expires_at` which returns a ActiveSupport time-zoned representation of the
149
+ token expiration timestamp. It is initially opinionated to RSA verification,
150
+ but can be tuned to verify HMAC or ECDSA signed tokens. It integrated well with
151
+ the `RsaPublicKey` fetcher class. (by default)
152
+
153
+ **Heads up!** You can skip this if you have your own JWT verification mechanism.
154
+
155
+ ```ruby
156
+ # A raw JWT (no signing, payload: {test: true})
157
+ raw_token = 'eyJ0eXAiOiJKV1QifQ.eyJ0ZXN0Ijp0cnVlfQ.'
158
+
159
+ # Parse the raw token and create a instance of it
160
+ token = Keyless::Jwt.new(raw_token)
161
+
162
+ # Access the payload easily (recursive-open-struct)
163
+ token.payload.test
164
+ # => true
165
+
166
+ # Validate the token (we assume you configured the verification key, an/or
167
+ # you own custom JWT verification options here)
168
+ token.valid?
169
+ # => true
170
+ ```
171
+
172
+ The following examples show you how to configure the
173
+ `Keyless::Jwt` class the global way. This is useful for a
174
+ shared initializer place.
175
+
176
+ #### Issuer verification
177
+
178
+ The JSON Web Token issuer which should be used for verification. When `nil` we
179
+ also turn off the verification by default. (See the default JWT options)
180
+
181
+ ```ruby
182
+ Keyless.configure do |conf|
183
+ conf.jwt_issuer = 'your-identity-provider'
184
+ end
185
+ ```
186
+
187
+ #### Beholder (audience) verification
188
+
189
+ The resource server (namely the one which configures this right now)
190
+ which MUST be present on the JSON Web Token audience claim. When `nil` we
191
+ also turn off the verification by default. (See the default JWT options)
192
+
193
+ ```ruby
194
+ Keyless.configure do |conf|
195
+ conf.jwt_beholder = 'your-resource-server'
196
+ end
197
+ ```
198
+
199
+ #### Custom JWT verification options
200
+
201
+ You can configure a different JSON Web Token verification option hash if your
202
+ algorithm differs or you want some extra/different options. Just watch out
203
+ that you have to pass a proc to this configuration property. On the
204
+ `Keyless::Jwt` class it has to be a simple hash. The default
205
+ is here the `RS256` algorithm with enabled expiration check, and issuer+audience
206
+ check when the `jwt_issuer` / `jwt_beholder` are configured accordingly.
207
+
208
+ ```ruby
209
+ Keyless.configure do |conf|
210
+ conf.jwt_options = proc do
211
+ # See: https://github.com/jwt/ruby-jwt
212
+ { algorithm: 'HS256' }
213
+ end
214
+ end
215
+ ```
216
+
217
+ #### Custom JWT verification key
218
+
219
+ You can configure your own verification key on the `Jwt` wrapper class. This
220
+ way you can pass your HMAC secret or your ECDSA public key to the JSON Web
221
+ Token validation method. Here you need to pass a proc, on the
222
+ `Keyless::Jwt` class it has to be a scalar value. By default
223
+ we use the `RsaPublicKey` class to retrieve the RSA public key.
224
+
225
+ ```ruby
226
+ Keyless.configure do |conf|
227
+ conf.jwt_verification_key = proc do
228
+ # Retrieve your verification key (RSA, ECDSA, HMAC secret)
229
+ # the way you like, and pass it back here.
230
+ end
231
+ end
232
+ ```
233
+
234
+ ### Full RSA256 example
235
+
236
+ Here comes a full example of the opinionated `RSA256` algorithm usage with a
237
+ remote RSA public key location, enabled caching and a full token payload
238
+ verification.
239
+
240
+ ```ruby
241
+ # On an initializer ..
242
+ Keyless.configure do |conf|
243
+ # The remote RSA public key location and enabled caching to limit the
244
+ # traffic on the remote server.
245
+ conf.rsa_public_key_url = 'https://your.identity.provider/rsa_public_key'
246
+ conf.rsa_public_key_caching = true
247
+ conf.rsa_public_key_expiration = 10.minutes
248
+
249
+ # Configure the JWT wrapper.
250
+ conf.jwt_issuer = 'The Identity Provider'
251
+ conf.jwt_beholder = 'example-api'
252
+
253
+ # Custom verification logic.
254
+ conf.authenticator = proc do |token|
255
+ # Parse and instantiate a JWT verification instance
256
+ jwt = Keyless::Jwt.new(token)
257
+
258
+ # We just allow valid access tokens
259
+ jwt.access_token? && jwt.valid?
260
+ end
261
+ end
262
+ ```
263
+
264
+ ## Development
265
+
266
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
267
+ `bundle exec rake spec` to run the tests. You can also run `bin/console` for an
268
+ interactive prompt that will allow you to experiment.
269
+
270
+ To install this gem onto your local machine, run `bundle exec rake install`. To
271
+ release a new version, update the version number in `version.rb`, and then run
272
+ `bundle exec rake release`, which will create a git tag for the version, push
273
+ git commits and tags, and push the `.gem` file to
274
+ [rubygems.org](https://rubygems.org).
275
+
276
+ ## Contributing
277
+
278
+ Bug reports and pull requests are welcome on GitHub at
279
+ https://github.com/hausgold/keyless.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "keyless"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,68 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
4
+ xmlns:cc="http://creativecommons.org/ns#"
5
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
+ xmlns:svg="http://www.w3.org/2000/svg"
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ version="1.1"
9
+ id="Ebene_1"
10
+ x="0px"
11
+ y="0px"
12
+ viewBox="0 0 800 200"
13
+ xml:space="preserve"
14
+ width="800"
15
+ height="200"><metadata
16
+ id="metadata33"><rdf:RDF><cc:Work
17
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
18
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
19
+ id="defs31" />
20
+ <style
21
+ type="text/css"
22
+ id="style2">
23
+ .st0{fill-rule:evenodd;clip-rule:evenodd;fill:#E73E11;}
24
+ .st1{fill-rule:evenodd;clip-rule:evenodd;fill:#0371B9;}
25
+ .st2{fill:#132E48;}
26
+ .st3{font-family:'OpenSans-Bold';}
27
+ .st4{font-size:29.5168px;}
28
+ .st5{fill-rule:evenodd;clip-rule:evenodd;fill:none;}
29
+ .st6{opacity:0.5;fill:#132E48;}
30
+ .st7{font-family:'OpenSans';}
31
+ .st8{font-size:12px;}
32
+ </style>
33
+ <g
34
+ transform="translate(0,1.53584)"
35
+ id="g828"><g
36
+ transform="translate(35.93985,35.66416)"
37
+ id="g8">
38
+ <path
39
+ style="clip-rule:evenodd;fill:#e73e11;fill-rule:evenodd"
40
+ id="path4"
41
+ d="m -0.1,124.4 c 0,0 33.7,-123.2 66.7,-123.2 12.8,0 26.9,21.9 38.8,47.2 -23.6,27.9 -66.6,59.7 -94,76 -7.1,0 -11.5,0 -11.5,0 z"
42
+ class="st0" />
43
+ <path
44
+ style="clip-rule:evenodd;fill:#0371b9;fill-rule:evenodd"
45
+ id="path6"
46
+ d="m 88.1,101.8 c 13.5,-10.4 18.4,-16.2 27.1,-25.4 10,25.7 16.7,48 16.7,48 0,0 -41.4,0 -78,0 14.6,-7.9 18.7,-10.7 34.2,-22.6 z"
47
+ class="st1" />
48
+ </g><text
49
+ y="106.40316"
50
+ x="192.43155"
51
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:29.51733398px;font-family:'Open Sans', sans-serif;-inkscape-font-specification:'OpenSans-Bold, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#132e48"
52
+ id="text10"
53
+ class="st2 st3 st4">keyless</text>
54
+ <rect
55
+ style="clip-rule:evenodd;fill:none;fill-rule:evenodd"
56
+ id="rect12"
57
+ height="24"
58
+ width="314.5"
59
+ class="st5"
60
+ y="118.06416"
61
+ x="194.23985" /><text
62
+ y="127.22146"
63
+ x="194.21715"
64
+ style="font-size:12px;font-family:'Open Sans', sans-serif;opacity:0.5;fill:#132e48;-inkscape-font-specification:'Open Sans, sans-serif, Normal';font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;"
65
+ id="text14"
66
+ class="st6 st7 st8">A reusable JWT authentication concern</text>
67
+ </g>
68
+ </svg>
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'keyless/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'keyless'
9
+ spec.version = Keyless::VERSION
10
+ spec.authors = ['Hermann Mayer', 'Christopher Mühl', 'Marcus Geißler']
11
+ spec.email = ['hermann.mayer92@gmail.com', 'christopher@padarom.xyz', 'mg@hausgold.de']
12
+
13
+ spec.summary = 'A reusable JWT authentication helper'
14
+ spec.description = 'A reusable JWT authentication helper'
15
+ spec.homepage = 'https://github.com/hausgold/keyless'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'bundler', '>= 1.16', '< 3'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rspec', '~> 3.0'
27
+ spec.add_development_dependency 'simplecov', '~> 0.15'
28
+ spec.add_development_dependency 'timecop', '~> 0.9.1'
29
+ spec.add_development_dependency 'vcr', '~> 3.0'
30
+ spec.add_development_dependency 'webmock', '~> 3.1'
31
+
32
+ spec.add_runtime_dependency 'activesupport', '>= 3.2.0'
33
+ spec.add_runtime_dependency 'httparty'
34
+ spec.add_runtime_dependency 'jwt', '~> 2.1'
35
+ spec.add_runtime_dependency 'recursive-open-struct', '~> 1.0'
36
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/concern'
5
+ require 'active_support/configurable'
6
+ require 'active_support/cache'
7
+ require 'active_support/core_ext/hash'
8
+ require 'active_support/time'
9
+ require 'active_support/time_with_zone'
10
+
11
+ require 'jwt'
12
+
13
+ require 'keyless/version'
14
+ require 'keyless/configuration'
15
+ require 'keyless/jwt'
16
+ require 'keyless/rsa_public_key'
17
+
18
+ # The JWT authentication concern.
19
+ module Keyless
20
+ extend ActiveSupport::Concern
21
+
22
+ class << self
23
+ attr_writer :configuration
24
+ end
25
+
26
+ # Retrieve the current configuration object.
27
+ #
28
+ # @return [Configuration]
29
+ def self.configuration
30
+ @configuration ||= Configuration.new
31
+ end
32
+
33
+ # Configure the concern by providing a block which takes
34
+ # care of this task. Example:
35
+ #
36
+ # Keyless.configure do |conf|
37
+ # # conf.xyz = [..]
38
+ # end
39
+ def self.configure
40
+ yield(configuration)
41
+ end
42
+
43
+ # Reset the current configuration with the default one.
44
+ def self.reset_configuration!
45
+ self.configuration = Configuration.new
46
+ end
47
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keyless
4
+ # The configuration for the JWT authentication concern.
5
+ class Configuration
6
+ include ActiveSupport::Configurable
7
+
8
+ # The authenticator function which must be defined by the user to
9
+ # verify the given JSON Web Token. Here comes all your logic to lookup
10
+ # the related user on your database, the token claim verification
11
+ # and/or the token cryptographic signing. The function must return true
12
+ # or false to indicate the validity of the token.
13
+ #
14
+ # Keyless.configure do |conf|
15
+ # conf.authenticator = proc do |token|
16
+ # # Verify the token the way you like.
17
+ # end
18
+ # end
19
+ config_accessor(:authenticator) { proc { false } }
20
+
21
+ # Whenever you want to use the {RsaPublicKey} class you configure the
22
+ # default URL on the singleton instance, or use the gem configure
23
+ # method and set it up accordingly. We allow the fetch of the public
24
+ # key from a remote server (HTTP/HTTPS) or from a local file which is
25
+ # accessible by the ruby process. Specify the URL or the local path
26
+ # here.
27
+ config_accessor(:rsa_public_key_url) { nil }
28
+
29
+ # You can preconfigure the {RsaPublickey} class to enable/disable
30
+ # caching. For a remote public key location it is handy to cache the
31
+ # result for some time to keep the traffic low to this resource server.
32
+ # For a local file you can skip this.
33
+ config_accessor(:rsa_public_key_caching) { false }
34
+
35
+ # When you make use of the caching of the {RsaPublicKey} class you can
36
+ # fine tune the expiration time of this cache. The RSA public key from
37
+ # your identity provider should not change this frequent, so a cache
38
+ # for at least one hour is fine. You should not set it lower than one
39
+ # minute. Keep this setting in mind when you change keys. Your
40
+ # infrastructure could be inoperable for this configured time.
41
+ config_accessor(:rsa_public_key_expiration) { 1.hour }
42
+
43
+ # The JSON Web Token isser which should be used for verification.
44
+ config_accessor(:jwt_issuer) { nil }
45
+
46
+ # The resource server (namely the one which configures this right now)
47
+ # which MUST be present on the JSON Web Token audience claim.
48
+ config_accessor(:jwt_beholder) { nil }
49
+
50
+ # You can configure a different JSON Web Token verification option hash
51
+ # if your algorithm differs or you want some extra/different options.
52
+ # Just watch out that you have to pass a proc to this configuration
53
+ # property. On the {Keyless::Jwt} class it has to be
54
+ # a simple hash. The default is here the RS256 algorithm with enabled
55
+ # expiration check, and issuer+audience check when the
56
+ # {jwt_issuer}/{jwt_beholder} are configured accordingly.
57
+ config_accessor(:jwt_options) do
58
+ proc do
59
+ conf = ::Keyless.configuration
60
+ { algorithm: 'RS256',
61
+ exp_leeway: 30.seconds.to_i,
62
+ iss: conf.jwt_issuer,
63
+ verify_iss: !conf.jwt_issuer.nil?,
64
+ aud: conf.jwt_beholder,
65
+ verify_aud: !conf.jwt_beholder.nil?,
66
+ # @TODO: https://github.com/jwt/ruby-jwt/issues/247
67
+ verify_iat: false }
68
+ end
69
+ end
70
+
71
+ # You can configure your own verification key on the Jwt wrapper class.
72
+ # This way you can pass your HMAC secret or your ECDSA public key to
73
+ # the JSON Web Token validation method. Here you need to pass a proc,
74
+ # on the {Keyless::Jwt} class it has to be a scalar
75
+ # value. By default we use the
76
+ # {Keyless::RsaPublicKey} class to retrieve the RSA
77
+ # public key.
78
+ config_accessor(:jwt_verification_key) do
79
+ proc { RsaPublicKey.instance.fetch }
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'recursive-open-struct'
4
+
5
+ module Keyless
6
+ # A easy to use model for verification of JSON Web Tokens. This is just a
7
+ # wrapper class for the excellent ruby-jwt gem. It's completely up to you
8
+ # to use it. But be aware, its a bit optinionated by default.
9
+ class Jwt
10
+ # All the following JWT verification issues lead to a failed validation.
11
+ RESCUE_JWT_EXCEPTIONS = [
12
+ ::JWT::DecodeError,
13
+ ::JWT::VerificationError,
14
+ ::JWT::ExpiredSignature,
15
+ ::JWT::IncorrectAlgorithm,
16
+ ::JWT::ImmatureSignature,
17
+ ::JWT::InvalidIssuerError,
18
+ ::JWT::InvalidIatError,
19
+ ::JWT::InvalidAudError,
20
+ ::JWT::InvalidSubError,
21
+ ::JWT::InvalidJtiError,
22
+ ::JWT::InvalidPayload
23
+ ].freeze
24
+
25
+ # :reek:Attribute because its fine to be extern-modifiable at these
26
+ # instances
27
+ attr_reader :payload, :token
28
+ attr_writer :verification_key, :jwt_options
29
+ attr_accessor :issuer, :beholder
30
+
31
+ # Setup a new JWT instance. You have to pass the raw JSON Web Token to
32
+ # the initializer. Example:
33
+ #
34
+ # Jwt.new('j.w.t')
35
+ # # => <Jwt>
36
+ #
37
+ # @return [Jwt]
38
+ def initialize(token)
39
+ parsed_payload = JWT.decode(token, nil, false).first.symbolize_keys
40
+ @token = token
41
+ @payload = RecursiveOpenStruct.new(parsed_payload)
42
+ end
43
+
44
+ # Checks if the payload says this is a refresh token.
45
+ #
46
+ # @return [Boolean] Whenever this is a access token
47
+ def access_token?
48
+ payload.typ == 'access'
49
+ end
50
+
51
+ # Checks if the payload says this is a refresh token.
52
+ #
53
+ # @return [Boolean] Whenever this is a refresh token
54
+ def refresh_token?
55
+ payload.typ == 'refresh'
56
+ end
57
+
58
+ # Retrives the expiration date from the payload when set.
59
+ #
60
+ # @return [nil|ActiveSupport::TimeWithZone] The expiration date
61
+ def expires_at
62
+ exp = payload.exp
63
+ return nil unless exp
64
+
65
+ Time.zone.at(exp)
66
+ end
67
+
68
+ # Deliver the public key for verification by default. This uses the
69
+ # {RsaPublicKey} class, but you can configure the verification key the
70
+ # way you like. (Especially for different algorithms, like HMAC or
71
+ # ECDSA) Just make use of the same named setter.
72
+ #
73
+ # @return [OpenSSL::PKey::RSA|Mixed] The verification key
74
+ def verification_key
75
+ unless @verification_key
76
+ conf = ::Keyless.configuration
77
+ return conf.jwt_verification_key.call
78
+ end
79
+ @verification_key
80
+ end
81
+
82
+ # This getter passes back the default JWT verification option hash
83
+ # which is optinionated. You can change this the way you like by
84
+ # configuring your options with the help of the same named setter.
85
+ #
86
+ # @return [Hash] The JWT verification options hash
87
+ def jwt_options
88
+ unless @jwt_options
89
+ conf = ::Keyless.configuration
90
+ return conf.jwt_options.call
91
+ end
92
+ @jwt_options
93
+ end
94
+
95
+ # Verify the current token by our hard and strict rules. Whenever the
96
+ # token was not parsed from a string, we encode the current state to a
97
+ # JWT string representation and check this.
98
+ #
99
+ # @return [Boolean] Whenever the token is valid or not
100
+ #
101
+ # :reek:NilCheck because we have to check the token
102
+ # origin and react on it
103
+ def valid?
104
+ JWT.decode(token, verification_key, true, jwt_options) && true
105
+ rescue *RESCUE_JWT_EXCEPTIONS
106
+ false
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'openssl'
5
+ require 'httparty'
6
+
7
+ module Keyless
8
+ # A common purpose RSA public key fetching/caching helper. With the help
9
+ # of this class you are able to retrieve the RSA public key from a remote
10
+ # server or a local file. This is naturally only useful if you care about
11
+ # JSON Web Token which are signed by the RSA algorithm.
12
+ class RsaPublicKey
13
+ # A internal exception handling for failed fetch attempts.
14
+ class FetchError < StandardError; end
15
+
16
+ include Singleton
17
+
18
+ # Setup all the getters and setters.
19
+ attr_accessor :cache
20
+ attr_writer :url, :expiration, :caching
21
+
22
+ # Setup the instance.
23
+ def initialize
24
+ @expiration = 1.hour
25
+ @cache = ActiveSupport::Cache::MemoryStore.new
26
+ end
27
+
28
+ # Just a simple shortcut class method to access the fetch method
29
+ # without specifying the singleton instance.
30
+ #
31
+ # @return [OpenSSL::PKey::RSA]
32
+ def self.fetch
33
+ instance.fetch
34
+ end
35
+
36
+ # Configure the single instance. This is just a wrapper (like tap)
37
+ # to the instance itself.
38
+ def configure
39
+ yield(self)
40
+ end
41
+
42
+ # Fetch the public key with the help of the configuration. You can
43
+ # configure the public key location (local file, remote (HTTP/HTTPS)
44
+ # file), whenever we should cache and how long to cache.
45
+ #
46
+ # @return [OpenSSL::PKey::RSA]
47
+ def fetch
48
+ encoded_key = if cache?
49
+ cache.fetch('encoded_key', expires_in: expiration) do
50
+ fetch_encoded_key
51
+ end
52
+ else
53
+ fetch_encoded_key
54
+ end
55
+
56
+ OpenSSL::PKey::RSA.new(encoded_key)
57
+ end
58
+
59
+ # Fetch the encoded (DER, or PEM) public key from a remote or local
60
+ # location.
61
+ #
62
+ # @return [String] The encoded public key
63
+ def fetch_encoded_key
64
+ raise ArgumentError, 'No URL for RsaPublicKey configured' unless url
65
+
66
+ if remote?
67
+ res = HTTParty.get(url)
68
+ raise FetchError, res.inspect unless (200..299).include? res.code
69
+ res.body
70
+ else
71
+ File.read(url)
72
+ end
73
+ end
74
+
75
+ # A helper for the caching configuration.
76
+ #
77
+ # @return [Boolean]
78
+ def cache?
79
+ caching && true
80
+ end
81
+
82
+ # A helper to determine if the configured URL is on a remote server or
83
+ # it is local on the filesystem. Whenever the configured URL specifies
84
+ # the HTTP/HTTPS protocol, we assume it is remote.
85
+ #
86
+ # @return [Boolean]
87
+ def remote?
88
+ !(url =~ /^https?/).nil?
89
+ end
90
+
91
+ # This getter passes back the default RSA public key. You can change
92
+ # this the way you like by configuring your URL with the help of the
93
+ # same named setter.
94
+ #
95
+ # @return [String] The configured public key location
96
+ def url
97
+ unless @url
98
+ conf = ::Keyless.configuration
99
+ return conf.rsa_public_key_url
100
+ end
101
+ @url
102
+ end
103
+
104
+ # This getter passes back the default public key cache expiration time.
105
+ # You can change this time with the help of the same named setter.
106
+ #
107
+ # @return [Integer] The configured cache expiration time
108
+ def expiration
109
+ unless @expiration
110
+ conf = ::Keyless.configuration
111
+ return conf.rsa_public_key_expiration
112
+ end
113
+ @expiration
114
+ end
115
+
116
+ # This getter passes back the caching flag. You can change this flag
117
+ # with the help of the same named setter.
118
+ #
119
+ # @return [Boolean] Whenever we should cache or not
120
+ def caching
121
+ unless @caching
122
+ conf = ::Keyless.configuration
123
+ return conf.rsa_public_key_caching
124
+ end
125
+ @caching
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keyless
4
+ VERSION = '1.0.1'.freeze
5
+ end
metadata ADDED
@@ -0,0 +1,226 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: keyless
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Hermann Mayer
8
+ - Christopher Mühl
9
+ - Marcus Geißler
10
+ autorequire:
11
+ bindir: exe
12
+ cert_chain: []
13
+ date: 2020-09-01 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '1.16'
22
+ - - "<"
23
+ - !ruby/object:Gem::Version
24
+ version: '3'
25
+ type: :development
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: '1.16'
32
+ - - "<"
33
+ - !ruby/object:Gem::Version
34
+ version: '3'
35
+ - !ruby/object:Gem::Dependency
36
+ name: rake
37
+ requirement: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '10.0'
42
+ type: :development
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '10.0'
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ requirement: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.0'
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '3.0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: simplecov
65
+ requirement: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '0.15'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.15'
77
+ - !ruby/object:Gem::Dependency
78
+ name: timecop
79
+ requirement: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: 0.9.1
84
+ type: :development
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: 0.9.1
91
+ - !ruby/object:Gem::Dependency
92
+ name: vcr
93
+ requirement: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '3.0'
98
+ type: :development
99
+ prerelease: false
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '3.0'
105
+ - !ruby/object:Gem::Dependency
106
+ name: webmock
107
+ requirement: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '3.1'
112
+ type: :development
113
+ prerelease: false
114
+ version_requirements: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '3.1'
119
+ - !ruby/object:Gem::Dependency
120
+ name: activesupport
121
+ requirement: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 3.2.0
126
+ type: :runtime
127
+ prerelease: false
128
+ version_requirements: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: 3.2.0
133
+ - !ruby/object:Gem::Dependency
134
+ name: httparty
135
+ requirement: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ type: :runtime
141
+ prerelease: false
142
+ version_requirements: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ - !ruby/object:Gem::Dependency
148
+ name: jwt
149
+ requirement: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: '2.1'
154
+ type: :runtime
155
+ prerelease: false
156
+ version_requirements: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '2.1'
161
+ - !ruby/object:Gem::Dependency
162
+ name: recursive-open-struct
163
+ requirement: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '1.0'
168
+ type: :runtime
169
+ prerelease: false
170
+ version_requirements: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - "~>"
173
+ - !ruby/object:Gem::Version
174
+ version: '1.0'
175
+ description: A reusable JWT authentication helper
176
+ email:
177
+ - hermann.mayer92@gmail.com
178
+ - christopher@padarom.xyz
179
+ - mg@hausgold.de
180
+ executables: []
181
+ extensions: []
182
+ extra_rdoc_files: []
183
+ files:
184
+ - ".editorconfig"
185
+ - ".gitignore"
186
+ - ".rspec"
187
+ - ".rubocop.yml"
188
+ - ".simplecov"
189
+ - ".travis.yml"
190
+ - CHANGELOG.md
191
+ - Gemfile
192
+ - LICENSE
193
+ - README.md
194
+ - Rakefile
195
+ - bin/console
196
+ - bin/setup
197
+ - doc/assets/project.svg
198
+ - keyless.gemspec
199
+ - lib/keyless.rb
200
+ - lib/keyless/configuration.rb
201
+ - lib/keyless/jwt.rb
202
+ - lib/keyless/rsa_public_key.rb
203
+ - lib/keyless/version.rb
204
+ homepage: https://github.com/hausgold/keyless
205
+ licenses: []
206
+ metadata: {}
207
+ post_install_message:
208
+ rdoc_options: []
209
+ require_paths:
210
+ - lib
211
+ required_ruby_version: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ required_rubygems_version: !ruby/object:Gem::Requirement
217
+ requirements:
218
+ - - ">="
219
+ - !ruby/object:Gem::Version
220
+ version: '0'
221
+ requirements: []
222
+ rubygems_version: 3.0.8
223
+ signing_key:
224
+ specification_version: 4
225
+ summary: A reusable JWT authentication helper
226
+ test_files: []