idsimple-rack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +274 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/idsimple-rack.gemspec +25 -0
- data/lib/idsimple/rack/access_token_helper.rb +16 -0
- data/lib/idsimple/rack/access_token_validation_result.rb +27 -0
- data/lib/idsimple/rack/access_token_validator.rb +55 -0
- data/lib/idsimple/rack/api.rb +73 -0
- data/lib/idsimple/rack/authenticator_app.rb +50 -0
- data/lib/idsimple/rack/configuration.rb +73 -0
- data/lib/idsimple/rack/helper.rb +64 -0
- data/lib/idsimple/rack/railtie.rb +16 -0
- data/lib/idsimple/rack/validator_middleware.rb +87 -0
- data/lib/idsimple/rack/version.rb +5 -0
- data/lib/idsimple/rack.rb +21 -0
- data/lib/idsimple-rack.rb +1 -0
- metadata +98 -0
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
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,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,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: []
|