zaikio-oauth_client 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +248 -0
- data/Rakefile +43 -0
- data/app/controllers/zaikio/oauth_client/connections_controller.rb +17 -0
- data/app/controllers/zaikio/oauth_client/sessions_controller.rb +7 -0
- data/app/helpers/zaikio/application_helper.rb +4 -0
- data/app/jobs/zaikio/application_job.rb +4 -0
- data/app/jobs/zaikio/cleanup_access_tokens_job.rb +7 -0
- data/app/models/zaikio/access_token.rb +87 -0
- data/config/initializers/inflections.rb +17 -0
- data/config/locales/en.yml +6 -0
- data/config/routes.rb +15 -0
- data/db/migrate/20190426155505_enable_postgres_extensions_for_uuids.rb +5 -0
- data/db/migrate/20191017132048_create_zaikio_access_tokens.rb +16 -0
- data/lib/tasks/zaikio_tasks.rake +4 -0
- data/lib/zaikio/oauth_client.rb +101 -0
- data/lib/zaikio/oauth_client/authenticatable.rb +90 -0
- data/lib/zaikio/oauth_client/client_configuration.rb +68 -0
- data/lib/zaikio/oauth_client/configuration.rb +70 -0
- data/lib/zaikio/oauth_client/engine.rb +9 -0
- data/lib/zaikio/oauth_client/test_helper.rb +16 -0
- data/lib/zaikio/oauth_client/version.rb +5 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fecfed2440981eaf2c59ba4384fdea548e92fa367d6450a7807d75abce1ae75c
|
4
|
+
data.tar.gz: c2b1bdeaa040528a057407a5eed4d63823aa40c1f46f2f669d0ea547f1e9b388
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 58f935b305aad9ac07f0363258978aae4d72a50de4f10a70d5435581dc270e146f4affb663ebfc500c40313c1f63bf5d1139bd8118d0eacfa7b508b0636254bf
|
7
|
+
data.tar.gz: cfa0466451e2d7e026281b808932520b21fb5ef253075982619472f15adf8e8c1961b9b172b5b5185a860f964ea402c9118b08b1e446e6d4c706b017fdc08fc2
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Christian Weyer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
# Zaikio::OAuthClient
|
2
|
+
|
3
|
+
This Gem enables you to easily connect to the Zaikio Directory and use the OAuth2 flow and easily lookup matching Access Tokens.
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Simply add the following in your Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem "zaikio-oauth_client"
|
12
|
+
```
|
13
|
+
Then run `bundle install`.
|
14
|
+
|
15
|
+
## Setup & Configuration
|
16
|
+
|
17
|
+
### 1. Copy & run Migrations
|
18
|
+
|
19
|
+
```bash
|
20
|
+
rails zaikio_oauth_client:install:migrations
|
21
|
+
rails db:migrate
|
22
|
+
```
|
23
|
+
|
24
|
+
This will create the tables:
|
25
|
+
+ `zaikio_access_tokens`
|
26
|
+
|
27
|
+
### 2. Mount routes
|
28
|
+
|
29
|
+
Add this to `config/routes.rb`:
|
30
|
+
|
31
|
+
```rb
|
32
|
+
mount Zaikio::OAuthClient::Engine => "/zaikio"
|
33
|
+
```
|
34
|
+
|
35
|
+
### 3. Configure Gem
|
36
|
+
|
37
|
+
```rb
|
38
|
+
# config/initializers/zaikio_oauth_client.rb
|
39
|
+
Zaikio::OAuthClient.configure do |config|
|
40
|
+
config.environment = :sandbox
|
41
|
+
|
42
|
+
config.register_client :warehouse do |warehouse|
|
43
|
+
warehouse.client_id = "52022d7a-7ba2-41ed-8890-97d88e6472f6"
|
44
|
+
warehouse.client_secret = "ShiKTnHqEf3M8nyHQPyZgbz7"
|
45
|
+
warehouse.default_scopes = %w[directory.person.r]
|
46
|
+
|
47
|
+
warehouse.register_organization_connection do |org|
|
48
|
+
org.default_scopes = %w[directory.organization.r]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
config.register_client :warehouse_goods_call_of do |warehouse_goods_call_of|
|
53
|
+
warehouse_goods_call_of.client_id = "12345-7ba2-41ed-8890-97d88e6472f6"
|
54
|
+
warehouse_goods_call_of.client_secret = "secret"
|
55
|
+
warehouse_goods_call_of.default_scopes = %w[directory.person.r]
|
56
|
+
|
57
|
+
warehouse_goods_call_of.register_organization_connection do |org|
|
58
|
+
org.default_scopes = %w[directory.organization.r]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
config.around_auth do |access_token, block|
|
63
|
+
Zaikio::Directory.with_token(access_token.token) do
|
64
|
+
block.call(access_token)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
|
71
|
+
### 4. Clean up outdated access tokens (recommended)
|
72
|
+
|
73
|
+
To avoid keeping all expired oath and refresh tokens in your database, we recommend to implement their scheduled deletion. We recommend therefore to use a schedule gems such as [sidekiq](https://github.com/mperham/sidekiq) and [sidekiq-scheduler](https://github.com/moove-it/sidekiq-scheduler).
|
74
|
+
|
75
|
+
Simply add the following to your Gemfile:
|
76
|
+
|
77
|
+
```rb
|
78
|
+
gem "sidekiq"
|
79
|
+
gem "sidekiq-scheduler"
|
80
|
+
```
|
81
|
+
Then run `bundle install`.
|
82
|
+
|
83
|
+
Configure sidekiq scheduler in `config/sidekiq.yml`:
|
84
|
+
```yaml
|
85
|
+
:schedule:
|
86
|
+
cleanup_acces_tokens_job:
|
87
|
+
cron: '0 3 * * *' # This will delete all expired tokens every day at 3am.
|
88
|
+
class: 'Zaikio::CleanupAccessTokensJob'
|
89
|
+
```
|
90
|
+
|
91
|
+
|
92
|
+
## Usage
|
93
|
+
|
94
|
+
### OAuth Flow
|
95
|
+
|
96
|
+
From any point in your application you can start using the Zaikio Directory OAuth2 flow with
|
97
|
+
|
98
|
+
```rb
|
99
|
+
redirect_to zaikio_oauth_client.new_session_path
|
100
|
+
# or
|
101
|
+
redirect_to zaikio_oauth_client.new_session_path(client_name: 'my_other_client')
|
102
|
+
# or install as organization
|
103
|
+
redirect_to zaikio_oauth_client.new_connection_path(client_name: 'my_other_client')
|
104
|
+
```
|
105
|
+
|
106
|
+
This will redirect the user to the OAuth Authorize endpoint of the Zaikio Directory `.../oauth/authorize` and include all necessary parameters like your client_id.
|
107
|
+
|
108
|
+
#### Session handling
|
109
|
+
|
110
|
+
The Zaikio gem engine will set a cookie for the user after a successful OAuth flow: `cookies.encrypted[:zaikio_person_id]`.
|
111
|
+
|
112
|
+
If you are using for example `Zaikio::Directory::Models`, you can use this snippet to set the current user:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
Current.user ||= Zaikio::Directory::Models::Person.find_by(id: cookies.encrypted[:zaikio_person_id])
|
116
|
+
````
|
117
|
+
|
118
|
+
You can then use `Current.user` anywhere.
|
119
|
+
|
120
|
+
For **logout** use: `zaikio_oauth_client.session_path, method: :delete` or build your own controller for deleting the cookie.
|
121
|
+
|
122
|
+
#### Multiple clients
|
123
|
+
|
124
|
+
When performing requests against directory APIs, it is important to always provide the correct client in order to use the client credentials flow correctly. Otherwise always the first client will be used. It is recommended to specify an `around_action`:
|
125
|
+
|
126
|
+
```rb
|
127
|
+
class ApplicationController < ActionController::Base
|
128
|
+
around_action :with_client
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def with_client
|
133
|
+
Zaikio::OAuthClient.with_client Current.client_name do
|
134
|
+
yield
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
#### Redirecting
|
141
|
+
|
142
|
+
The `zaikio_oauth_client.new_session_path` which was used for the first initiation of the OAuth flow, accepts an optional parameter `origin` which will then be used to redirect the user at the end of a completed & successful OAuth flow.
|
143
|
+
|
144
|
+
Additionally you can also specify your own redirect handlers in your `ApplicationController`:
|
145
|
+
|
146
|
+
```rb
|
147
|
+
class ApplicationController < ActionController::Base
|
148
|
+
def after_approve_path_for(access_token, origin)
|
149
|
+
cookies.encrypted[:zaikio_person_id] = access_token.bearer_id unless access_token.organization?
|
150
|
+
|
151
|
+
# Sync data on login
|
152
|
+
Zaikio::Directory.with_token(access_token.token) do
|
153
|
+
access_token.bearer_klass.find_and_reload!(access_token.bearer_id, includes: :all)
|
154
|
+
end
|
155
|
+
|
156
|
+
origin || main_app.root_path
|
157
|
+
end
|
158
|
+
|
159
|
+
def after_destroy_path_for(access_token_id)
|
160
|
+
cookies.delete :zaikio_person_id
|
161
|
+
|
162
|
+
main_app.root_path
|
163
|
+
end
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
#### Custom behavior
|
168
|
+
|
169
|
+
Since the built in `SessionsController` and `ConnectionsController` are inheriting from the main app's `ApplicationController` all behaviour will be added there, too. In some cases you might want to explicitly skip a `before_action` or add custom `before_action` callbacks.
|
170
|
+
|
171
|
+
You can achieve this by adding a custom controller name to your configuration:
|
172
|
+
|
173
|
+
```rb
|
174
|
+
# app/controllers/sessions_controller.rb
|
175
|
+
class SessionsController < Zaikio::OAuthClient::SessionsController
|
176
|
+
skip_before_action :redirect_unless_authenticated
|
177
|
+
end
|
178
|
+
|
179
|
+
# config/initializers/zaikio_oauth_client.rb
|
180
|
+
Zaikio::OAuthClient.configure do |config|
|
181
|
+
# ...
|
182
|
+
config.sessions_controller_name = "sessions"
|
183
|
+
# config.connections_controller_name = "connections"
|
184
|
+
# ...
|
185
|
+
end
|
186
|
+
```
|
187
|
+
|
188
|
+
#### Testing
|
189
|
+
|
190
|
+
You can use our test helper to login different users:
|
191
|
+
|
192
|
+
```rb
|
193
|
+
# test_helper.rb
|
194
|
+
class ActiveSupport::TestCase
|
195
|
+
# ...
|
196
|
+
include Zaikio::OAuthClient::TestHelper
|
197
|
+
# ...
|
198
|
+
end
|
199
|
+
|
200
|
+
# my_controller_test.rb
|
201
|
+
class MyControllerTest < ActionDispatch::IntegrationTest
|
202
|
+
test "does request" do
|
203
|
+
person = people(:my_person)
|
204
|
+
logged_in_as(person)
|
205
|
+
|
206
|
+
# ... make the request
|
207
|
+
end
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
#### Authenticated requests
|
212
|
+
|
213
|
+
Now further requests to the Directory API or to other Zaikio APIs should be made. For this purpose the OAuthClient provides a helper method `with_auth` that automatically fetches an access token from the database, requests a refresh token or creates a new access token via client credentials flow.
|
214
|
+
|
215
|
+
```rb
|
216
|
+
Zaikio::OAuthClient.with_auth(bearer_type: "Organization", bearer_id: "fd61f5f5-038b-44cf-b554-dfe9555f1e29", scopes: %w[directory.organization.r directory.organization_members.r]) do |access_token|
|
217
|
+
# call config.around_auth with given access token
|
218
|
+
end
|
219
|
+
```
|
220
|
+
|
221
|
+
## Use of dummy app
|
222
|
+
|
223
|
+
You can use the included dummy app as a showcase for the workflow and to adjust your own application. To set up the dummy application properly, go into `test/dummy` and use [puma-dev](https://github.com/puma/puma-dev) like this:
|
224
|
+
|
225
|
+
```shell
|
226
|
+
puma-dev link -n 'zaikio-oauth-client'
|
227
|
+
```
|
228
|
+
This will make the dummy app available at: [http://zaikio-oauth-client.test](http://zaikio-oauth-client.test/)
|
229
|
+
|
230
|
+
If you use the provided OAuth credentials from above and test this against the Sandbox, everything should work as the redirect URLs for [http://zaikio-oauth-client.test](http://zaikio-oauth-client.test/) are approved within the Sandbox.
|
231
|
+
|
232
|
+
|
233
|
+
## Contributing
|
234
|
+
|
235
|
+
**Make sure you have the dummy app running locally to validate your changes.**
|
236
|
+
|
237
|
+
Make your changes and adjust `version.rb`. Please make sure to update `CHANGELOG.md`.
|
238
|
+
|
239
|
+
**To push a new release:**
|
240
|
+
|
241
|
+
- `gem build zaikio-oauth_client.gemspec`
|
242
|
+
- `gem push zaikio-oauth_client-0.1.0.gem`
|
243
|
+
*Adjust the version accordingly.*
|
244
|
+
|
245
|
+
|
246
|
+
## License
|
247
|
+
|
248
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Zaikio'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
33
|
+
|
34
|
+
require 'rubocop/rake_task'
|
35
|
+
|
36
|
+
namespace :test do
|
37
|
+
desc 'Runs RuboCop on specified directories'
|
38
|
+
RuboCop::RakeTask.new(:rubocop) do |task|
|
39
|
+
task.fail_on_error = false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Rake::Task[:test].enhance ['test:rubocop']
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Zaikio
|
2
|
+
module OAuthClient
|
3
|
+
class ConnectionsController < ApplicationController
|
4
|
+
include Zaikio::OAuthClient::Authenticatable
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def approve_url(client_name = nil)
|
9
|
+
zaikio_oauth_client.approve_connection_url(client_name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def use_org_config?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require "jwt"
|
2
|
+
require "zaikio/jwt_auth"
|
3
|
+
|
4
|
+
module Zaikio
|
5
|
+
class AccessToken < ApplicationRecord
|
6
|
+
self.table_name = "zaikio_access_tokens"
|
7
|
+
|
8
|
+
def self.build_from_access_token(access_token) # rubocop:disable Metrics/AbcSize
|
9
|
+
payload = JWT.decode(access_token.token, nil, false).first rescue {} # rubocop:disable Style/RescueModifier
|
10
|
+
new(
|
11
|
+
id: payload["jti"],
|
12
|
+
bearer_type: access_token.params["bearer"]["type"],
|
13
|
+
bearer_id: access_token.params["bearer"]["id"],
|
14
|
+
audience: access_token.params["audiences"].first,
|
15
|
+
token: access_token.token,
|
16
|
+
refresh_token: access_token.refresh_token,
|
17
|
+
expires_at: Time.strptime(access_token.expires_at.to_s, "%s"),
|
18
|
+
scopes: access_token.params["scope"].split(",")
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.refresh_token_valid_for
|
23
|
+
7.days
|
24
|
+
end
|
25
|
+
|
26
|
+
# Scopes
|
27
|
+
scope :valid, lambda {
|
28
|
+
where("expires_at > :now", now: Time.current)
|
29
|
+
.where.not(id: Zaikio::JWTAuth.revoked_token_ids)
|
30
|
+
}
|
31
|
+
scope :with_invalid_refresh_token, lambda {
|
32
|
+
where("created_at <= ?", Time.current - Zaikio::AccessToken.refresh_token_valid_for)
|
33
|
+
}
|
34
|
+
scope :valid_refresh, lambda {
|
35
|
+
where("expires_at <= :now AND created_at > :created_at_max",
|
36
|
+
now: Time.current,
|
37
|
+
created_at_max: Time.current - refresh_token_valid_for)
|
38
|
+
.where("refresh_token IS NOT NULL")
|
39
|
+
.where.not(id: Zaikio::JWTAuth.revoked_token_ids)
|
40
|
+
}
|
41
|
+
scope :by_bearer, lambda { |bearer_type: "Person", bearer_id:, scopes: []|
|
42
|
+
where(bearer_type: bearer_type, bearer_id: bearer_id)
|
43
|
+
.where("scopes @> ARRAY[?]::varchar[]", scopes)
|
44
|
+
}
|
45
|
+
scope :usable, lambda { |options|
|
46
|
+
by_bearer(**options).valid.or(by_bearer(**options).valid_refresh)
|
47
|
+
.order(expires_at: :desc)
|
48
|
+
}
|
49
|
+
|
50
|
+
def expired?
|
51
|
+
expires_at < Time.current
|
52
|
+
end
|
53
|
+
|
54
|
+
def organization?
|
55
|
+
bearer_type == "Organization"
|
56
|
+
end
|
57
|
+
|
58
|
+
def expires_in
|
59
|
+
(expires_at - Time.current).to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
def bearer_klass
|
63
|
+
return unless Zaikio.const_defined?("Directory::Models")
|
64
|
+
|
65
|
+
if Zaikio::Directory::Models.configuration.respond_to?(:"#{bearer_type.underscore}_class_name")
|
66
|
+
Zaikio::Directory::Models.configuration.public_send(:"#{bearer_type.underscore}_class_name").constantize
|
67
|
+
else
|
68
|
+
"Zaikio::#{bearer_type}".constantize
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def refresh!
|
73
|
+
Zaikio::OAuthClient.with_oauth_scheme(:basic_auth) do
|
74
|
+
refreshed_token = OAuth2::AccessToken.from_hash(
|
75
|
+
Zaikio::OAuthClient.for(audience),
|
76
|
+
attributes.slice("token", "refresh_token")
|
77
|
+
).refresh!
|
78
|
+
|
79
|
+
access_token = self.class.build_from_access_token(refreshed_token)
|
80
|
+
|
81
|
+
transaction { destroy if access_token.save! }
|
82
|
+
|
83
|
+
access_token
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# Add new inflection rules using the following format. Inflections
|
4
|
+
# are locale specific, and you may define rules for as many different
|
5
|
+
# locales as you wish. All of these examples are active by default:
|
6
|
+
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
7
|
+
# inflect.plural /^(ox)$/i, '\1en'
|
8
|
+
# inflect.singular /^(ox)en/i, '\1'
|
9
|
+
# inflect.irregular 'person', 'people'
|
10
|
+
# inflect.uncountable %w( fish sheep )
|
11
|
+
# end
|
12
|
+
|
13
|
+
# These inflection rules are supported but not enabled by default:
|
14
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
15
|
+
inflect.acronym "JSON"
|
16
|
+
inflect.acronym "OAuth"
|
17
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Zaikio::OAuthClient::Engine.routes.draw do
|
2
|
+
sessions_controller = Zaikio::OAuthClient.configuration.sessions_controller_name
|
3
|
+
connections_controller = Zaikio::OAuthClient.configuration.connections_controller_name
|
4
|
+
|
5
|
+
# People
|
6
|
+
get "(/:client_name)/sessions/new", action: :new, controller: sessions_controller, as: :new_session
|
7
|
+
get "(/:client_name)/sessions/approve", action: :approve, controller: sessions_controller, as: :approve_session
|
8
|
+
delete "(/:client_name)/session", action: :destroy, controller: sessions_controller, as: :session
|
9
|
+
|
10
|
+
# Organizations
|
11
|
+
get "(/:client_name)/connections/new", action: :new,
|
12
|
+
controller: connections_controller, as: :new_connection
|
13
|
+
get "(/:client_name)/connections/approve", action: :approve,
|
14
|
+
controller: connections_controller, as: :approve_connection
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreateZaikioAccessTokens < ActiveRecord::Migration[6.0]
|
2
|
+
def change
|
3
|
+
create_table :zaikio_access_tokens, id: :uuid do |t|
|
4
|
+
t.string :bearer_type, null: false, default: "Organization"
|
5
|
+
t.string :bearer_id, null: false
|
6
|
+
t.string :audience, null: false
|
7
|
+
t.string :token, null: false, index: { unique: true }
|
8
|
+
t.string :refresh_token, index: { unique: true }
|
9
|
+
t.datetime :expires_at, index: true
|
10
|
+
t.string :scopes, array: true, default: [], null: false
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :zaikio_access_tokens, %i[bearer_type bearer_id]
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "oauth2"
|
2
|
+
|
3
|
+
require "zaikio/oauth_client/engine"
|
4
|
+
require "zaikio/oauth_client/configuration"
|
5
|
+
require "zaikio/oauth_client/authenticatable"
|
6
|
+
|
7
|
+
module Zaikio
|
8
|
+
module OAuthClient
|
9
|
+
class << self
|
10
|
+
attr_reader :client_name
|
11
|
+
|
12
|
+
def configure
|
13
|
+
@configuration ||= Configuration.new
|
14
|
+
yield(configuration)
|
15
|
+
end
|
16
|
+
|
17
|
+
def configuration
|
18
|
+
@configuration ||= Configuration.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def for(client_name = nil)
|
22
|
+
client_config_for(client_name).oauth_client
|
23
|
+
end
|
24
|
+
|
25
|
+
def oauth_scheme
|
26
|
+
@oauth_scheme ||= :request_body
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_oauth_scheme(scheme = :request_body)
|
30
|
+
@oauth_scheme = scheme
|
31
|
+
yield
|
32
|
+
ensure
|
33
|
+
@oauth_scheme = :request_body
|
34
|
+
end
|
35
|
+
|
36
|
+
def with_client(client_name)
|
37
|
+
original_client_name = @client_name || nil
|
38
|
+
@client_name = client_name
|
39
|
+
yield
|
40
|
+
ensure
|
41
|
+
@client_name = original_client_name
|
42
|
+
end
|
43
|
+
|
44
|
+
def with_auth(options_or_access_token, &block)
|
45
|
+
access_token = if options_or_access_token.is_a?(Zaikio::AccessToken)
|
46
|
+
options_or_access_token
|
47
|
+
else
|
48
|
+
get_access_token(options_or_access_token)
|
49
|
+
end
|
50
|
+
|
51
|
+
return unless block_given?
|
52
|
+
|
53
|
+
if configuration.around_auth_block
|
54
|
+
configuration.around_auth_block.call(access_token, block)
|
55
|
+
else
|
56
|
+
yield(access_token)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_access_token(client_name: nil, bearer_type: "Person", bearer_id: nil, scopes: nil) # rubocop:disable Metrics/MethodLength
|
61
|
+
client_name ||= self.client_name
|
62
|
+
client_config = client_config_for(client_name)
|
63
|
+
scopes ||= client_config.default_scopes_for(bearer_type)
|
64
|
+
|
65
|
+
access_token = Zaikio::AccessToken.where(audience: client_config.client_name)
|
66
|
+
.usable(bearer_type: bearer_type, bearer_id: bearer_id, scopes: scopes)
|
67
|
+
.first
|
68
|
+
|
69
|
+
if access_token.blank?
|
70
|
+
access_token = Zaikio::AccessToken.build_from_access_token(
|
71
|
+
client_config.token_by_client_credentials(
|
72
|
+
bearer_type: bearer_type,
|
73
|
+
bearer_id: bearer_id,
|
74
|
+
scopes: scopes
|
75
|
+
)
|
76
|
+
)
|
77
|
+
access_token.save!
|
78
|
+
elsif access_token&.expired?
|
79
|
+
access_token = access_token.refresh!
|
80
|
+
end
|
81
|
+
|
82
|
+
access_token
|
83
|
+
end
|
84
|
+
|
85
|
+
def get_plain_scopes(scopes)
|
86
|
+
regex = /^((Org|Per)\.)?(.*)$/
|
87
|
+
scopes.map do |scope|
|
88
|
+
(regex.match(scope) || [])[3]
|
89
|
+
end.compact
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def client_config_for(client_name = nil)
|
95
|
+
raise StandardError.new, "Zaikio::OAuthClient was not configured" unless configuration
|
96
|
+
|
97
|
+
configuration.find!(client_name || configuration.all_client_names.first)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Zaikio
|
2
|
+
module OAuthClient
|
3
|
+
module Authenticatable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def new
|
7
|
+
cookies.encrypted[:origin] = params[:origin]
|
8
|
+
|
9
|
+
redirect_to oauth_client.auth_code.authorize_url(
|
10
|
+
redirect_uri: approve_url(params[:client_name]),
|
11
|
+
scope: oauth_scope
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def approve
|
16
|
+
access_token = create_access_token
|
17
|
+
|
18
|
+
origin = cookies.encrypted[:origin]
|
19
|
+
cookies.delete :origin
|
20
|
+
|
21
|
+
cookies.encrypted[:zaikio_access_token_id] = access_token.id unless access_token.organization?
|
22
|
+
|
23
|
+
redirect_to send(
|
24
|
+
respond_to?(:after_approve_path_for) ? :after_approve_path_for : :default_after_approve_path_for,
|
25
|
+
access_token, origin
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def destroy
|
30
|
+
access_token_id = cookies.encrypted[:zaikio_access_token_id]
|
31
|
+
cookies.delete :zaikio_access_token_id
|
32
|
+
|
33
|
+
redirect_to send(
|
34
|
+
respond_to?(:after_destroy_path_for) ? :after_destroy_path_for : :default_after_destroy_path_for,
|
35
|
+
access_token_id
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def approve_url(client_name = nil)
|
42
|
+
zaikio_oauth_client.approve_session_url(client_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def use_org_config?
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_access_token
|
50
|
+
access_token_response = oauth_client.auth_code.get_token(params[:code])
|
51
|
+
|
52
|
+
access_token = Zaikio::AccessToken.build_from_access_token(access_token_response)
|
53
|
+
access_token.save!
|
54
|
+
|
55
|
+
access_token
|
56
|
+
end
|
57
|
+
|
58
|
+
def client_name
|
59
|
+
params[:client_name] || Zaikio::OAuthClient.configuration.all_client_names.first
|
60
|
+
end
|
61
|
+
|
62
|
+
def client_config
|
63
|
+
client_config = Zaikio::OAuthClient.configuration.find!(client_name)
|
64
|
+
client_config = use_org_config? ? client_config.org_config : client_config
|
65
|
+
|
66
|
+
client_config or raise ActiveRecord::RecordNotFound
|
67
|
+
end
|
68
|
+
|
69
|
+
def oauth_client
|
70
|
+
Zaikio::OAuthClient.for(client_name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def oauth_scope
|
74
|
+
client_config.scopes_for_auth(params[:organization_id]).join(",")
|
75
|
+
end
|
76
|
+
|
77
|
+
def default_after_approve_path_for(access_token, origin)
|
78
|
+
cookies.encrypted[:zaikio_person_id] = access_token.bearer_id unless access_token.organization?
|
79
|
+
|
80
|
+
origin || main_app.root_path
|
81
|
+
end
|
82
|
+
|
83
|
+
def default_after_destroy_path_for(_access_token_id)
|
84
|
+
cookies.delete :zaikio_person_id
|
85
|
+
|
86
|
+
main_app.root_path
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Zaikio
|
2
|
+
module OAuthClient
|
3
|
+
class ClientConfiguration
|
4
|
+
attr_reader :org_config, :client_name
|
5
|
+
attr_accessor :client_id, :client_secret, :default_scopes
|
6
|
+
|
7
|
+
def initialize(client_name)
|
8
|
+
@default_scopes = []
|
9
|
+
@client_name = client_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def register_organization_connection
|
13
|
+
@org_config ||= OrganizationConnection.new
|
14
|
+
yield(@org_config)
|
15
|
+
end
|
16
|
+
|
17
|
+
def oauth_client
|
18
|
+
@oauth_client ||= OAuth2::Client.new(
|
19
|
+
client_id,
|
20
|
+
client_secret,
|
21
|
+
authorize_url: "oauth/authorize",
|
22
|
+
token_url: "oauth/access_token",
|
23
|
+
connection_opts: { headers: { "Accept": "application/json" } },
|
24
|
+
site: Zaikio::OAuthClient.configuration.host
|
25
|
+
)
|
26
|
+
|
27
|
+
@oauth_client.options[:auth_scheme] = Zaikio::OAuthClient.oauth_scheme
|
28
|
+
|
29
|
+
@oauth_client
|
30
|
+
end
|
31
|
+
|
32
|
+
def scopes_for_auth(_id = nil)
|
33
|
+
default_scopes
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_scopes_for(type = "Person")
|
37
|
+
type == "Organization" ? org_config.default_scopes : default_scopes
|
38
|
+
end
|
39
|
+
|
40
|
+
def token_by_client_credentials(bearer_id: nil, bearer_type: "Person", scopes: [])
|
41
|
+
plain_scopes = Zaikio::OAuthClient.get_plain_scopes(scopes)
|
42
|
+
scopes_with_prefix = plain_scopes.map do |scope|
|
43
|
+
"#{bearer_type[0..2]}/#{bearer_id}.#{scope}"
|
44
|
+
end
|
45
|
+
|
46
|
+
Zaikio::OAuthClient.with_oauth_scheme(:basic_auth) do
|
47
|
+
oauth_client.client_credentials.get_token(scope: scopes_with_prefix.join(","))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class OrganizationConnection
|
52
|
+
attr_accessor :default_scopes
|
53
|
+
|
54
|
+
def initialize
|
55
|
+
@default_scopes = []
|
56
|
+
end
|
57
|
+
|
58
|
+
def scopes_for_auth(id = nil)
|
59
|
+
plain_scopes = Zaikio::OAuthClient.get_plain_scopes(default_scopes)
|
60
|
+
|
61
|
+
plain_scopes.map do |scope|
|
62
|
+
id ? "Org/#{id}.#{scope}" : "Org.#{scope}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "zaikio/oauth_client/client_configuration"
|
3
|
+
|
4
|
+
module Zaikio
|
5
|
+
module OAuthClient
|
6
|
+
class Configuration
|
7
|
+
HOSTS = {
|
8
|
+
development: "http://hub.zaikio.test",
|
9
|
+
test: "http://hub.zaikio.test",
|
10
|
+
staging: "https://hub.staging.zaikio.com",
|
11
|
+
sandbox: "https://hub.sandbox.zaikio.com",
|
12
|
+
production: "https://hub.zaikio.com"
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
attr_accessor :host
|
16
|
+
attr_writer :logger
|
17
|
+
attr_reader :client_configurations, :environment, :around_auth_block,
|
18
|
+
:sessions_controller_name, :connections_controller_name
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@client_configurations = {}
|
22
|
+
@around_auth_block = nil
|
23
|
+
@sessions_controller_name = "sessions"
|
24
|
+
@connections_controller_name = "connections"
|
25
|
+
end
|
26
|
+
|
27
|
+
def logger
|
28
|
+
@logger ||= Logger.new(STDOUT)
|
29
|
+
end
|
30
|
+
|
31
|
+
def register_client(name)
|
32
|
+
@client_configurations[name.to_s] ||= ClientConfiguration.new(name.to_s)
|
33
|
+
yield(@client_configurations[name.to_s])
|
34
|
+
end
|
35
|
+
|
36
|
+
def find!(name)
|
37
|
+
@client_configurations[name.to_s] or raise ActiveRecord::RecordNotFound
|
38
|
+
end
|
39
|
+
|
40
|
+
def all_client_names
|
41
|
+
client_configurations.keys
|
42
|
+
end
|
43
|
+
|
44
|
+
def environment=(env)
|
45
|
+
@environment = env.to_sym
|
46
|
+
@host = host_for(environment)
|
47
|
+
end
|
48
|
+
|
49
|
+
def around_auth(&block)
|
50
|
+
@around_auth_block = block
|
51
|
+
end
|
52
|
+
|
53
|
+
def sessions_controller_name=(name)
|
54
|
+
@sessions_controller_name = "/#{name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def connections_controller_name=(name)
|
58
|
+
@connections_controller_name = "/#{name}"
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def host_for(environment)
|
64
|
+
HOSTS.fetch(environment) do
|
65
|
+
raise StandardError.new, "Invalid Zaikio::OAuthClient environment '#{environment}'"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Zaikio
|
2
|
+
module OAuthClient
|
3
|
+
module TestHelper
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def logged_in_as(person)
|
7
|
+
# We need to manually encrypt the value since the tests cookie jar does not
|
8
|
+
# support encrypted or signed cookies
|
9
|
+
encrypted_cookies = ActionDispatch::Request.new(Rails.application.env_config.deep_dup).cookie_jar
|
10
|
+
encrypted_cookies.encrypted[:zaikio_person_id] = person.id
|
11
|
+
|
12
|
+
cookies["zaikio_person_id"] = encrypted_cookies["zaikio_person_id"]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zaikio-oauth_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Zaikio GmbH
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-01-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: oauth2
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: zaikio-jwt_auth
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.2.1
|
48
|
+
- - "<"
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 0.5.0
|
51
|
+
type: :runtime
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 0.2.1
|
58
|
+
- - "<"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.5.0
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: pg
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: byebug
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
description: This gem provides a mountable Rails engine that provides single sign
|
90
|
+
on, directory access and further Zaikio platform connectivity.
|
91
|
+
email:
|
92
|
+
- sb@zaikio.com
|
93
|
+
- cw@zaikio.com
|
94
|
+
- mp@zaikio.com
|
95
|
+
- js@zaikio.com
|
96
|
+
executables: []
|
97
|
+
extensions: []
|
98
|
+
extra_rdoc_files: []
|
99
|
+
files:
|
100
|
+
- MIT-LICENSE
|
101
|
+
- README.md
|
102
|
+
- Rakefile
|
103
|
+
- app/controllers/zaikio/oauth_client/connections_controller.rb
|
104
|
+
- app/controllers/zaikio/oauth_client/sessions_controller.rb
|
105
|
+
- app/helpers/zaikio/application_helper.rb
|
106
|
+
- app/jobs/zaikio/application_job.rb
|
107
|
+
- app/jobs/zaikio/cleanup_access_tokens_job.rb
|
108
|
+
- app/models/zaikio/access_token.rb
|
109
|
+
- config/initializers/inflections.rb
|
110
|
+
- config/locales/en.yml
|
111
|
+
- config/routes.rb
|
112
|
+
- db/migrate/20190426155505_enable_postgres_extensions_for_uuids.rb
|
113
|
+
- db/migrate/20191017132048_create_zaikio_access_tokens.rb
|
114
|
+
- lib/tasks/zaikio_tasks.rake
|
115
|
+
- lib/zaikio/oauth_client.rb
|
116
|
+
- lib/zaikio/oauth_client/authenticatable.rb
|
117
|
+
- lib/zaikio/oauth_client/client_configuration.rb
|
118
|
+
- lib/zaikio/oauth_client/configuration.rb
|
119
|
+
- lib/zaikio/oauth_client/engine.rb
|
120
|
+
- lib/zaikio/oauth_client/test_helper.rb
|
121
|
+
- lib/zaikio/oauth_client/version.rb
|
122
|
+
homepage: https://www.zaikio.com
|
123
|
+
licenses:
|
124
|
+
- MIT
|
125
|
+
metadata:
|
126
|
+
changelog_uri: https://github.com/zaikio/zaikio-oauth_client/blob/master/CHANGELOG.md
|
127
|
+
post_install_message:
|
128
|
+
rdoc_options: []
|
129
|
+
require_paths:
|
130
|
+
- lib
|
131
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubygems_version: 3.2.3
|
143
|
+
signing_key:
|
144
|
+
specification_version: 4
|
145
|
+
summary: Zaikio Platform Connectivity
|
146
|
+
test_files: []
|