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