idsimple-rack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8e96f0e33a7a9ae276cceadb129736372de30090009e65627abb8b265c970441
4
+ data.tar.gz: '0892d4f0de694a19808e0d86b3cf160e2cb76568abf21c54870f44269f6eb325'
5
+ SHA512:
6
+ metadata.gz: 179c195156ea7eb3762ae4df5ad1b60dad5894156d78a4b53a65aec1311c67cade941d87c9108df63b3d1e89d7961b34cb23668149afcbe1c0fb0f4cb7c40688
7
+ data.tar.gz: 1fabbc0f43b36e97a60442aaf5bf055ce570666906df603d0bc5d9bdb9775124aabf837dda661b82ef0041853cd30e1168112ff5491982cee472561501832540
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Ari Summer
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,274 @@
1
+ # Idsimple::Rack
2
+
3
+ ## Overview
4
+ Idsimple works with all [Rack](https://github.com/rack/rack)-based applications.
5
+ This includes:
6
+ - [Ruby on Rails](https://rubyonrails.org/)
7
+ - [Sinatra](http://sinatrarb.com/)
8
+ - [Hanami](https://hanamirb.org/)
9
+ - [Camping](http://www.ruby-camping.com/)
10
+ - [Coset](http://leahneukirchen.org/repos/coset/)
11
+ - [Padrino](http://padrinorb.com/)
12
+ - [Ramaze]()
13
+ - [Roda](https://github.com/jeremyevans/roda)
14
+ - [Rum](https://github.com/leahneukirchen/rum)
15
+ - [Utopia](https://github.com/socketry/utopia)
16
+ - [WABuR](https://github.com/ohler55/wabur)
17
+
18
+
19
+ All you need is the [idsimple-rack gem](https://github.com/idsimple/idsimple-rack).
20
+ `idsimple-rack` includes a [Rack app](https://github.com/rack/rack/blob/master/SPEC.rdoc#rack-applications-),
21
+ `Idsimple::Rack::AuthenticatorApp`, for authenticating users and initiating sessions
22
+ and a Rack middleware, `Idsimple::Rack::ValidatorMiddleware`, for validating access tokens and sessions.
23
+
24
+ ## Installation
25
+
26
+ Add this line to your application's Gemfile:
27
+
28
+ ```ruby
29
+ gem "idsimple-rack"
30
+ ```
31
+
32
+ And then execute:
33
+
34
+ ```bash
35
+ bundle install
36
+ ```
37
+
38
+ Or install it yourself as:
39
+
40
+ ```bash
41
+ gem install idsimple-rack
42
+ ```
43
+
44
+ ## Ruby on Rails
45
+ `idsimple-rack` hooks in to Rails automatically using [Rails::Railtie](https://api.rubyonrails.org/classes/Rails/Railtie.html).
46
+ All you need to do is add an initializer with your [configuration options](#configuration):
47
+
48
+ ```ruby
49
+ # config/initializers/idsimple_rack.rb
50
+
51
+ Idsimple::Rack.configure do |config|
52
+ config.app_id = ENV["IDSIMPLE_APP_ID"]
53
+ config.api_key = ENV["IDSIMPLE_API_KEY"]
54
+ config.signing_secret = ENV["IDSIMPLE_SIGNING_SECRET"]
55
+ end
56
+ ```
57
+
58
+ ## Rack
59
+ To add `idsimple-rack` to a Rack application, you need to `run` `Idsimple::Rack::AuthenticatorApp`,
60
+ at `Idsimple::Rack.configuration.authenticate_path`, `use` `Idsimple::Rack::ValidatorMiddleware` in your stack,
61
+ and set your [configuration options](#configuration).
62
+
63
+ ```ruby
64
+ # config.ru
65
+
66
+ class Application
67
+ def call(_)
68
+ status = 200
69
+ headers = { "Content-Type" => "text/html" }
70
+ body = ["<html><body>yay!!!</body></html>"]
71
+
72
+ [status, headers, body]
73
+ end
74
+ end
75
+
76
+ Idsimple::Rack.configure do |config|
77
+ config.app_id = ENV["IDSIMPLE_APP_ID"]
78
+ config.api_key = ENV["IDSIMPLE_API_KEY"]
79
+ config.signing_secret = ENV["IDSIMPLE_SIGNING_SECRET"]
80
+ end
81
+
82
+ App = Rack::Builder.new do
83
+ use Rack::Reloader, 0
84
+
85
+ map Idsimple::Rack.configuration.authenticate_path do
86
+ run Idsimple::Rack::AuthenticatorApp
87
+ end
88
+
89
+ use Idsimple::Rack::ValidatorMiddleware
90
+
91
+ run Application.new
92
+ end.to_app
93
+
94
+ run App
95
+ ```
96
+
97
+ You can see a working example of this in the
98
+ [`idsimple-rack` repo](https://github.com/idsimple/idsimple-rack/blob/main/example_app/config.ru).
99
+
100
+
101
+ ## Configuration
102
+ `idsimple-rack` can be configured by calling `Idsimple::Rack.configure` with a block like so:
103
+
104
+ ```ruby
105
+ Idsimple::Rack.configure do |config|
106
+ config.app_id = ENV["IDSIMPLE_APP_ID"]
107
+ config.api_key = ENV["IDSIMPLE_API_KEY"]
108
+ config.signing_secret = ENV["IDSIMPLE_SIGNING_SECRET"]
109
+ end
110
+ ```
111
+
112
+ ### Configuration Options
113
+ #### `app_id`
114
+ The idsimple App ID. This can be found in the "Keys & Secrets" tab for your app in idsimple.
115
+
116
+ - Type: String
117
+ - Optional: No
118
+
119
+
120
+ #### `api_key`
121
+ The idsimple App Session API Key. This is generated and shown when you create an idsimple app.
122
+ You can view the prefix of the App Session API Key in the "Keys & Secrets" tab for your app in idsimple.
123
+
124
+ - Type: String
125
+ - Optional: No
126
+
127
+ #### `signing_secret`
128
+ The idsimple App signing secret. This is generated and shown when you create an idsimple app.
129
+ You can view the prefix of the signing secret in the "Keys & Secrets" tab for your app in idsimple.
130
+
131
+ - Type: String
132
+ - Optional: No
133
+
134
+ #### `get_access_token`
135
+ Function for retrieving the access token from a store.
136
+ By default, the access token is retrieved from an [HTTP cookie](https://en.wikipedia.org/wiki/HTTP_cookie).
137
+
138
+ - Type: Lambda
139
+ - Optional: Yes
140
+ - Default:
141
+ ```ruby
142
+ -> (req) {
143
+ req.cookies[DEFAULT_COOKIE_NAME]
144
+ }
145
+ ```
146
+
147
+
148
+ #### `set_access_token`
149
+ Function for setting the access token in a store.
150
+ By default, the access token is stored in an [HTTP cookie](https://en.wikipedia.org/wiki/HTTP_cookie).
151
+
152
+ - Type: Lambda
153
+ - Optional: Yes
154
+ - Default:
155
+ ```ruby
156
+ -> (req, res, access_token, decoded_access_token) {
157
+ res.set_cookie(DEFAULT_COOKIE_NAME, {
158
+ value: access_token,
159
+ expires: Time.at(decoded_access_token[0]["exp"]),
160
+ httponly: true,
161
+ path: "/"
162
+ })
163
+ }
164
+ ```
165
+
166
+ #### `remove_access_token`
167
+ Function for removing the access token from a store.
168
+
169
+ - Type: Lambda
170
+ - Optional: Yes
171
+ - Default:
172
+ ```ruby
173
+ -> (req, res) {
174
+ res.delete_cookie(DEFAULT_COOKIE_NAME)
175
+ }
176
+ ```
177
+
178
+ #### `authenticate_path`
179
+ Path to initiate a new session with an access token.
180
+ This is the location to which idsimple will redirect the user once a new access token is generated.
181
+ `Idsimple::Rack::AuthenticatorApp` should be mounted at this path.
182
+
183
+ - Type: String
184
+ - Optional: Yes
185
+ - Default: `/idsimple/session`
186
+
187
+ #### `after_authenticated_path`
188
+ Path to redirect the user after they've been authenticated.
189
+
190
+ - Type: String
191
+ - Optional: Yes
192
+ - Default: `/`
193
+
194
+ #### `skip_on`
195
+ Function used to conditionally skip validation by the middleware.
196
+ By returning `true`, `Idsimple::Rack::ValidatorMiddleware` will skip
197
+ validation for that request.
198
+
199
+ - Type: Lambda
200
+ - Optional: Yes
201
+ - Default: `nil`
202
+
203
+ Example:
204
+
205
+ ```ruby
206
+ -> (req) {
207
+ req.path == "/webhooks"
208
+ }
209
+ ```
210
+
211
+ #### `logger`
212
+ The `logger` option allows you to set your own custom logger.
213
+
214
+ - Type: Logger
215
+ - Optional: yes
216
+ - Default:
217
+ ```ruby
218
+ logger = Logger.new(STDOUT)
219
+ logger.level = Logger::INFO
220
+ default_formatter = Logger::Formatter.new
221
+ logger.formatter = proc do |severity, datetime, progname, msg|
222
+ "Idsimple::Rack #{default_formatter.call(severity, datetime, progname, msg)}"
223
+ end
224
+ ```
225
+
226
+ #### `enabled`
227
+ Boolean indicating whether the idsimple middleware should be enabled.
228
+
229
+ - Type: Boolean
230
+ - Optional: true
231
+ - Default: true
232
+
233
+ #### `unauthorized_response`
234
+ Function for customizing the unauthorized response sent by the middleware.
235
+
236
+ - Type: Lambda
237
+ - Optional: Yes
238
+ - Default:
239
+ ```ruby
240
+ -> (req, res) {
241
+ res.status = 401
242
+ res.content_type = "text/html"
243
+ res.body = ["UNAUTHORIZED"]
244
+ }
245
+ ```
246
+
247
+ #### `redirect_to_authenticate`
248
+ Boolean indicating whether the middleware should redirect users
249
+ to `app.idsimple.io` to authenticate. If set to `false`, unauthorized users
250
+ will receive a `401 UNAUTHORIZED` response when visiting your app
251
+ instead of being redirected to `app.idsimple.io`.
252
+
253
+ - Type: Boolean
254
+ - Optional: Yes
255
+ - Default: `true`
256
+
257
+ ## Development
258
+
259
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
260
+
261
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
262
+
263
+ ## Contributing
264
+
265
+ Bug reports and pull requests are welcome on GitHub at https://github.com/idsimple/idsimple-rack. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/idsimple/idsimple-rack/blob/master/CODE_OF_CONDUCT.md).
266
+
267
+
268
+ ## License
269
+
270
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
271
+
272
+ ## Code of Conduct
273
+
274
+ Everyone interacting in the Idsimple::Rack project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/idsimple/idsimple-rack/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "idsimple-rack"
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__)
data/bin/setup ADDED
@@ -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,25 @@
1
+ require_relative 'lib/idsimple/rack/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "idsimple-rack"
5
+ spec.version = Idsimple::Rack::VERSION
6
+ spec.authors = ["Ari Summer"]
7
+ spec.email = ["support@idsimple.io"]
8
+
9
+ spec.summary = "Rack middleware for idsimple integration."
10
+ spec.homepage = "https://github.com/idsimple/idsimple-rack"
11
+ spec.license = "MIT"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
13
+
14
+ spec.metadata["homepage_uri"] = spec.homepage
15
+ spec.metadata["source_code_uri"] = "https://github.com/idsimple/idsimple-rack"
16
+ spec.metadata["changelog_uri"] = "https://github.com/idsimple/idsimple-rack/CHANGELOG.md"
17
+
18
+ spec.files = Dir.glob("{bin,lib}/**/*") + %w(Rakefile README.md LICENSE.txt idsimple-rack.gemspec)
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_runtime_dependency "rack", ">= 1.0", "< 3"
24
+ spec.add_runtime_dependency "jwt", "~> 2.0"
25
+ end
@@ -0,0 +1,16 @@
1
+ require "jwt"
2
+
3
+ module Idsimple
4
+ module Rack
5
+ class AccessTokenHelper
6
+ def self.decode(access_token, signing_secret, options = {})
7
+ JWT.decode(access_token, signing_secret, true, {
8
+ algorithm: "HS256",
9
+ verify_iss: true,
10
+ verify_aud: true,
11
+ verify_iat: true
12
+ }.merge(options))
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ module Idsimple
2
+ module Rack
3
+ class AccessTokenValidationResult
4
+ attr_reader :errors
5
+
6
+ def initialize
7
+ @errors = []
8
+ end
9
+
10
+ def valid?
11
+ errors.empty?
12
+ end
13
+
14
+ def invalid?
15
+ !valid?
16
+ end
17
+
18
+ def add_error(msg)
19
+ @errors << msg
20
+ end
21
+
22
+ def full_error_message
23
+ "#{errors.join(". ")}." unless errors.empty?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,55 @@
1
+ require "idsimple/rack/access_token_validation_result"
2
+
3
+ module Idsimple
4
+ module Rack
5
+ class AccessTokenValidator
6
+ def self.validate_used_token_custom_claims(decoded_token, req)
7
+ token_payload = decoded_token[0]
8
+ ip = token_payload["idsimple.ip"]
9
+ user_agent = token_payload["idsimple.user_agent"]
10
+ used_at = token_payload["idsimple.used_at"]
11
+
12
+ result = AccessTokenValidationResult.new
13
+
14
+ if ip && req.ip != ip
15
+ result.add_error("IP mismatch")
16
+ end
17
+
18
+ if user_agent && req.user_agent != user_agent
19
+ result.add_error("User agent mismatch")
20
+ end
21
+
22
+ result.add_error("Missing used_at timestamp") if !used_at
23
+ result.add_error("Invalid used_at timestamp") if used_at && used_at > Time.now.to_i
24
+
25
+ result
26
+ end
27
+
28
+ def self.validate_unused_token_custom_claims(decoded_token, req)
29
+ token_payload = decoded_token[0]
30
+ use_by = token_payload["idsimple.use_by"]
31
+ used_at = token_payload["idsimple.used_at"]
32
+ ip = token_payload["idsimple.ip"]
33
+ user_agent = token_payload["idsimple.user_agent"]
34
+
35
+ result = AccessTokenValidationResult.new
36
+
37
+ if ip && req.ip != ip
38
+ result.add_error("IP mismatch")
39
+ end
40
+
41
+ if user_agent && req.user_agent != user_agent
42
+ result.add_error("User agent mismatch")
43
+ end
44
+
45
+ if use_by && Time.now.to_i > use_by
46
+ result.add_error("Token must be used prior to before claim")
47
+ end
48
+
49
+ result.add_error("Token already used") if used_at
50
+
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,73 @@
1
+ require "net/http"
2
+ require "json"
3
+
4
+ module Idsimple
5
+ module Rack
6
+ class Api
7
+ attr_reader :base_url
8
+
9
+ def initialize(base_url, api_key)
10
+ @base_url = base_url
11
+ @api_key = api_key
12
+ end
13
+
14
+ # TODO:
15
+ # - incorporate API secret
16
+ def http_client
17
+ @http_client ||= begin
18
+ uri = URI.parse(base_url)
19
+ client = Net::HTTP.new(uri.host, uri.port)
20
+ client.use_ssl = base_url.start_with?("https")
21
+ client
22
+ end
23
+ end
24
+
25
+ def use_token(token_id)
26
+ response = http_client.patch("/api/v1/access_tokens/#{token_id}/use", "", headers)
27
+ Result.new(response)
28
+ end
29
+
30
+ def refresh_token(token_id)
31
+ response = http_client.patch("/api/v1/access_tokens/#{token_id}/refresh", "", headers)
32
+ Result.new(response)
33
+ end
34
+
35
+ private
36
+
37
+ def headers
38
+ {
39
+ "Authorization" => "Bearer #{@api_key}",
40
+ "Content-Type" => "application/json"
41
+ }
42
+ end
43
+
44
+ class Result
45
+ attr_reader :response
46
+
47
+ def initialize(response)
48
+ @response = response
49
+ end
50
+
51
+ def success?
52
+ response.kind_of?(Net::HTTPSuccess)
53
+ end
54
+
55
+ def fail?
56
+ !success?
57
+ end
58
+
59
+ def status
60
+ response.code
61
+ end
62
+
63
+ def body
64
+ @body ||= JSON.parse(response.body) if response.body
65
+ end
66
+
67
+ def full_error_message
68
+ "#{body["errors"].join(". ")}." if body && body["errors"]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,50 @@
1
+ require "rack"
2
+ require "idsimple/rack/access_token_validator"
3
+ require "idsimple/rack/helper"
4
+
5
+ module Idsimple
6
+ module Rack
7
+ class AuthenticatorApp
8
+ extend Helper
9
+
10
+ def self.call(env)
11
+ return ["404", { "Content-Type" => "text/html" }, ["NOT FOUND"]] unless configuration.enabled?
12
+
13
+ req = ::Rack::Request.new(env)
14
+
15
+ if (access_token = req.params["access_token"])
16
+ logger.debug("Found access token")
17
+
18
+ decoded_access_token = decode_access_token(access_token, signing_secret)
19
+ logger.debug("Decoded access token")
20
+
21
+ validation_result = AccessTokenValidator.validate_unused_token_custom_claims(decoded_access_token, req)
22
+ if validation_result.invalid?
23
+ logger.warn("Attempted to access with invalid token: #{validation_result.full_error_message}")
24
+ return unauthorized_response(req)
25
+ end
26
+
27
+ use_token_response = api.use_token(decoded_access_token[0]["jti"])
28
+ if use_token_response.fail?
29
+ logger.warn("Use token response error. HTTP status #{use_token_response.status}. #{use_token_response.full_error_message}")
30
+ return unauthorized_response(req)
31
+ end
32
+
33
+ new_access_token = use_token_response.body["access_token"]
34
+ new_decoded_access_token = decode_access_token(new_access_token, signing_secret)
35
+
36
+ res = ::Rack::Response.new
37
+ return_to = req.params["return_to"]
38
+ res.redirect(return_to || configuration.after_authenticated_path)
39
+ set_access_token(req, res, new_access_token, new_decoded_access_token)
40
+ res.finish
41
+ else
42
+ unauthorized_response(req)
43
+ end
44
+ rescue JWT::DecodeError => e
45
+ logger.warn("Error while decoding token: #{e.class} - #{e.message}")
46
+ unauthorized_response(req)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,73 @@
1
+ require "rack"
2
+ require "logger"
3
+
4
+ module Idsimple
5
+ module Rack
6
+ class Configuration
7
+ DEFAULT_COOKIE_NAME = "idsimple.access_token"
8
+
9
+ attr_accessor :get_access_token, :set_access_token, :remove_access_token, :signing_secret,
10
+ :authenticate_path, :issuer, :api_base_url, :after_authenticated_path,
11
+ :app_id, :skip_on, :logger, :enabled, :unauthorized_response, :api_key,
12
+ :redirect_to_authenticate
13
+
14
+ def initialize
15
+ set_defaults
16
+ end
17
+
18
+ def enabled?
19
+ enabled
20
+ end
21
+
22
+ private
23
+
24
+ def set_defaults
25
+ @enabled = true
26
+ @authenticate_path = "/idsimple/session"
27
+ @after_authenticated_path = "/"
28
+ @issuer = "https://app.idsimple.com"
29
+ @api_base_url = "https://api.idsimple.com"
30
+ @app_id = nil
31
+ @skip_on = nil
32
+ @signing_secret = nil
33
+ @api_key = nil
34
+ @get_access_token = method(:default_access_token_getter)
35
+ @set_access_token = method(:default_access_token_setter)
36
+ @remove_access_token = method(:default_access_token_remover)
37
+ @unauthorized_response = method(:default_unauthorized_response)
38
+ @redirect_to_authenticate = true
39
+
40
+ logger = Logger.new(STDOUT)
41
+ logger.level = Logger::INFO
42
+ default_formatter = Logger::Formatter.new
43
+ logger.formatter = proc do |severity, datetime, progname, msg|
44
+ "Idsimple::Rack #{default_formatter.call(severity, datetime, progname, msg)}"
45
+ end
46
+ @logger = logger
47
+ end
48
+
49
+ def default_unauthorized_response(req, res)
50
+ res.status = 401
51
+ res.content_type = "text/html"
52
+ res.body = ["UNAUTHORIZED"]
53
+ end
54
+
55
+ def default_access_token_getter(req)
56
+ req.cookies[DEFAULT_COOKIE_NAME]
57
+ end
58
+
59
+ def default_access_token_setter(req, res, access_token, decoded_access_token)
60
+ res.set_cookie(DEFAULT_COOKIE_NAME, {
61
+ value: access_token,
62
+ expires: Time.at(decoded_access_token[0]["exp"]),
63
+ httponly: true,
64
+ path: "/"
65
+ })
66
+ end
67
+
68
+ def default_access_token_remover(req, res)
69
+ res.delete_cookie(DEFAULT_COOKIE_NAME)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,64 @@
1
+ require "idsimple/rack/access_token_helper"
2
+ require "idsimple/rack/api"
3
+
4
+ module Idsimple
5
+ module Rack
6
+ module Helper
7
+ def configuration
8
+ Idsimple::Rack.configuration
9
+ end
10
+
11
+ def logger
12
+ configuration.logger
13
+ end
14
+
15
+ def signing_secret
16
+ configuration.signing_secret
17
+ end
18
+
19
+ def unauthorized_response(req, res = ::Rack::Response.new)
20
+ logger.info("Unauthorized")
21
+ configuration.unauthorized_response.call(req, res)
22
+ res.finish
23
+ end
24
+
25
+ def redirect_to_authenticate_or_unauthorized_response(req, res = ::Rack::Response.new)
26
+ issuer = configuration.issuer
27
+ app_id = configuration.app_id
28
+ access_attempt = req.params["idsimple_access_attempt"]
29
+
30
+ if configuration.redirect_to_authenticate && issuer && app_id && !access_attempt
31
+ logger.info("Redirecting to authenticate")
32
+ access_url = "#{issuer}/apps/#{app_id}/access?return_to=#{req.fullpath}"
33
+ res.redirect(access_url)
34
+ res.finish
35
+ else
36
+ unauthorized_response(req, res)
37
+ end
38
+ end
39
+
40
+ def get_access_token(req)
41
+ configuration.get_access_token.call(req)
42
+ end
43
+
44
+ def set_access_token(req, res, new_access_token, new_decoded_access_token)
45
+ configuration.set_access_token.call(req, res, new_access_token, new_decoded_access_token)
46
+ end
47
+
48
+ def remove_access_token(req, res)
49
+ configuration.remove_access_token.call(req, res)
50
+ end
51
+
52
+ def decode_access_token(access_token, signing_secret)
53
+ AccessTokenHelper.decode(access_token, signing_secret, {
54
+ iss: configuration.issuer,
55
+ aud: configuration.app_id
56
+ })
57
+ end
58
+
59
+ def api
60
+ @api ||= Idsimple::Rack::Api.new(configuration.api_base_url, configuration.api_key)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,16 @@
1
+ require "idsimple/rack/validator_middleware"
2
+ require "idsimple/rack/authenticator_app"
3
+
4
+ module Idsimple
5
+ module Rack
6
+ class Railtie < ::Rails::Engine
7
+ initializer "idsimple-rack.configure" do |app|
8
+ app.routes.append do
9
+ mount Idsimple::Rack::AuthenticatorApp, at: Idsimple::Rack.configuration.authenticate_path
10
+ end
11
+
12
+ app.middleware.use(Idsimple::Rack::ValidatorMiddleware)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,87 @@
1
+ require "rack"
2
+ require "idsimple/rack/access_token_validator"
3
+ require "idsimple/rack/helper"
4
+
5
+ module Idsimple
6
+ module Rack
7
+ class ValidatorMiddleware
8
+ include Helper
9
+
10
+ DECODED_ACCESS_TOKEN_ENV_KEY = "idsimple.decoded_access_token"
11
+
12
+ attr_reader :app
13
+
14
+ def initialize(app)
15
+ @app = app
16
+ end
17
+
18
+ def call(env)
19
+ return app.call(env) unless configuration.enabled?
20
+
21
+ req = ::Rack::Request.new(env)
22
+
23
+ if req.path == configuration.authenticate_path
24
+ logger.debug("Attempting to authenticate. Skipping validation.")
25
+ return app.call(env)
26
+ end
27
+
28
+ if configuration.skip_on && configuration.skip_on.call(req)
29
+ logger.debug("Skipping validator due to skip_on rules")
30
+ return app.call(env)
31
+ end
32
+
33
+ access_token = get_access_token(req)
34
+
35
+ return redirect_to_authenticate_or_unauthorized_response(req) unless access_token
36
+
37
+ logger.debug("Retrieved access token from store")
38
+ decoded_access_token = decode_access_token(access_token, signing_secret)
39
+ logger.debug("Decoded access token")
40
+
41
+ validation_result = AccessTokenValidator.validate_used_token_custom_claims(decoded_access_token, req)
42
+ if validation_result.invalid?
43
+ logger.warn("Attempted to access with invalid used token: #{validation_result.full_error_message}")
44
+ return redirect_to_authenticate_or_unauthorized_response(req)
45
+ end
46
+
47
+ if (refresh_at = decoded_access_token[0]["idsimple.refresh_at"]) && refresh_at < Time.now.to_i
48
+ logger.debug("Refreshing access token")
49
+ jti = decoded_access_token[0]["jti"]
50
+ handle_refresh_access_token(jti, req)
51
+ else
52
+ env[DECODED_ACCESS_TOKEN_ENV_KEY] = decoded_access_token
53
+ app.call(env)
54
+ end
55
+ rescue JWT::DecodeError => e
56
+ logger.warn("Error while decoding token: #{e.class} - #{e.message}")
57
+ redirect_to_authenticate_or_unauthorized_response(req)
58
+ end
59
+
60
+ private
61
+
62
+ def handle_refresh_access_token(jti, req)
63
+ token_refresh_response = api.refresh_token(jti)
64
+
65
+ if token_refresh_response.fail?
66
+ logger.warn("Token refresh failed")
67
+
68
+ res = ::Rack::Response.new
69
+ if token_refresh_response.body["invalid_token"]
70
+ remove_access_token(req, res)
71
+ end
72
+
73
+ redirect_to_authenticate_or_unauthorized_response(req, res)
74
+ else
75
+ logger.debug("Refreshed access token")
76
+ new_access_token = token_refresh_response.body["access_token"]
77
+ new_decoded_access_token = decode_access_token(new_access_token, signing_secret)
78
+ req.env[DECODED_ACCESS_TOKEN_ENV_KEY] = new_decoded_access_token
79
+ status, headers, body = app.call(req.env)
80
+ res = ::Rack::Response.new(body, status, headers)
81
+ set_access_token(req, res, new_access_token, new_decoded_access_token)
82
+ res.finish
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,5 @@
1
+ module Idsimple
2
+ module Rack
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,21 @@
1
+ require "idsimple/rack/version"
2
+ require "idsimple/rack/configuration"
3
+ require "idsimple/rack/validator_middleware"
4
+ require "idsimple/rack/authenticator_app"
5
+ require "idsimple/rack/railtie" if defined?(::Rails)
6
+
7
+ module Idsimple
8
+ module Rack
9
+ def self.configuration
10
+ @configuration ||= Configuration.new
11
+ end
12
+
13
+ def self.reset_configuration
14
+ @configuration = Configuration.new
15
+ end
16
+
17
+ def self.configure
18
+ yield(configuration)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1 @@
1
+ require "idsimple/rack"
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: idsimple-rack
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ari Summer
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-12-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ - !ruby/object:Gem::Dependency
34
+ name: jwt
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ description:
48
+ email:
49
+ - support@idsimple.io
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - bin/console
58
+ - bin/setup
59
+ - idsimple-rack.gemspec
60
+ - lib/idsimple-rack.rb
61
+ - lib/idsimple/rack.rb
62
+ - lib/idsimple/rack/access_token_helper.rb
63
+ - lib/idsimple/rack/access_token_validation_result.rb
64
+ - lib/idsimple/rack/access_token_validator.rb
65
+ - lib/idsimple/rack/api.rb
66
+ - lib/idsimple/rack/authenticator_app.rb
67
+ - lib/idsimple/rack/configuration.rb
68
+ - lib/idsimple/rack/helper.rb
69
+ - lib/idsimple/rack/railtie.rb
70
+ - lib/idsimple/rack/validator_middleware.rb
71
+ - lib/idsimple/rack/version.rb
72
+ homepage: https://github.com/idsimple/idsimple-rack
73
+ licenses:
74
+ - MIT
75
+ metadata:
76
+ homepage_uri: https://github.com/idsimple/idsimple-rack
77
+ source_code_uri: https://github.com/idsimple/idsimple-rack
78
+ changelog_uri: https://github.com/idsimple/idsimple-rack/CHANGELOG.md
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 2.3.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.1.6
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Rack middleware for idsimple integration.
98
+ test_files: []