keyless 1.0.1

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.
@@ -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: []