zaikio-oauth_client 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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.
@@ -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).
@@ -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,7 @@
1
+ module Zaikio
2
+ module OAuthClient
3
+ class SessionsController < ApplicationController
4
+ include Zaikio::OAuthClient::Authenticatable
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module Zaikio
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Zaikio
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,7 @@
1
+ module Zaikio
2
+ class CleanupAccessTokensJob < ApplicationJob
3
+ def perform
4
+ Zaikio::AccessToken.with_invalid_refresh_token.delete_all
5
+ end
6
+ end
7
+ 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
@@ -0,0 +1,6 @@
1
+ en:
2
+ zaikio:
3
+ forms:
4
+ optional: Optional
5
+ learn_more: Learn more
6
+
@@ -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,5 @@
1
+ class EnablePostgresExtensionsForUuids < ActiveRecord::Migration[6.0]
2
+ def change
3
+ enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
4
+ end
5
+ 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,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :zaikio do
3
+ # # Task goes here
4
+ # 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,9 @@
1
+ module Zaikio
2
+ module OAuthClient
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace Zaikio::OAuthClient
5
+ engine_name "zaikio_oauth_client"
6
+ config.generators.api_only = true
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,5 @@
1
+ module Zaikio
2
+ module OAuthClient
3
+ VERSION = "0.0.0".freeze
4
+ end
5
+ 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: []