katalyst-koi 5.4.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +6 -12
- data/app/controllers/admin/admin_users_controller.rb +1 -0
- data/app/controllers/admin/credentials_controller.rb +1 -0
- data/app/controllers/admin/device_authorizations_controller.rb +58 -0
- data/app/controllers/admin/device_tokens_controller.rb +18 -0
- data/app/controllers/admin/otps_controller.rb +2 -0
- data/app/controllers/admin/profiles_controller.rb +2 -0
- data/app/controllers/admin/sessions_controller.rb +3 -3
- data/app/controllers/concerns/koi/controller/has_admin_users.rb +8 -11
- data/app/controllers/concerns/koi/controller/has_webauthn.rb +3 -2
- data/app/controllers/concerns/koi/controller.rb +7 -0
- data/app/jobs/admin/device_authorizations_cleanup_job.rb +9 -0
- data/app/models/admin/device_authorization.rb +113 -0
- data/app/models/admin/user.rb +1 -0
- data/app/models/koi/current.rb +8 -0
- data/app/views/admin/device_authorizations/show.html.erb +38 -0
- data/app/views/layouts/koi/_application_navigation.html.erb +2 -2
- data/config/routes.rb +3 -0
- data/db/migrate/20260413014834_create_admin_device_authorizations.rb +20 -0
- data/lib/koi/middleware/admin_authentication.rb +54 -10
- data/spec/factories/admin_device_authorizations.rb +29 -0
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a64f8ea56e122d169ec52b8c304c79a1b81289bba216d89d2b97c9f0aaf9e24
|
|
4
|
+
data.tar.gz: e07ac3a269c0cf2958f33fb9e3da25dec86a84d27f25f3d0314ecc4d4fed5f4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c337a6f84e5ce47739dc9242207b9eafd2a0fbf6c9bf3f5b89338910175b3598dc575037849fb1a844e1a1608b54cb9e08e891286047b1548bbe127484ae446a
|
|
7
|
+
data.tar.gz: 84b32b36bd70147f24f0b975df0b722ba37f860185c1df483c1947c55ec17c74d1332e16b29d30b95f1f0f59021dced986db12ae005f7962349f3cd30df74246
|
data/README.md
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
# Koi
|
|
1
|
+
# Katalyst Koi
|
|
2
2
|
|
|
3
3
|
Koi is a framework for building Rails admin functionality.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
In most cases, Koi is installed using `https://github.com/katalyst/koi-template`.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
gem "katalyst-koi"
|
|
11
|
-
```
|
|
9
|
+
## API Access
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```sh
|
|
16
|
-
bundle install
|
|
17
|
-
```
|
|
11
|
+
See [docs/api-access.md](docs/api-access.md).
|
|
18
12
|
|
|
19
13
|
## Contributing
|
|
20
14
|
|
|
21
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/katalyst/
|
|
15
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/katalyst/koi.
|
|
22
16
|
|
|
23
17
|
## License
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
Koi is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -4,6 +4,7 @@ module Admin
|
|
|
4
4
|
class CredentialsController < ApplicationController
|
|
5
5
|
include Koi::Controller::HasWebauthn
|
|
6
6
|
|
|
7
|
+
before_action :requires_session_authentication!
|
|
7
8
|
before_action :set_admin_user, only: %i[new create]
|
|
8
9
|
before_action :set_credential, only: %i[show update destroy]
|
|
9
10
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
class DeviceAuthorizationsController < ApplicationController
|
|
5
|
+
EXPIRES_IN = 10.minutes
|
|
6
|
+
INTERVAL = 5
|
|
7
|
+
|
|
8
|
+
rate_limit to: 3, within: 1.minute, only: :create
|
|
9
|
+
skip_before_action :verify_authenticity_token, only: :create
|
|
10
|
+
|
|
11
|
+
before_action :set_device_authorization, only: %i[show update]
|
|
12
|
+
|
|
13
|
+
attr_reader :device_authorization
|
|
14
|
+
|
|
15
|
+
delegate :admin_user, to: ::Koi::Current
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
render locals: { device_authorization: }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create
|
|
22
|
+
device_authorization, device_code = Admin::DeviceAuthorization.issue!(
|
|
23
|
+
requested_ip: request.remote_ip,
|
|
24
|
+
user_agent: request.user_agent,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
render json: {
|
|
28
|
+
device_code:,
|
|
29
|
+
user_code: device_authorization.user_code,
|
|
30
|
+
verification_uri: admin_device_authorization_url(device_authorization.user_code),
|
|
31
|
+
verification_uri_complete: admin_device_authorization_url(device_authorization.user_code),
|
|
32
|
+
expires_in: EXPIRES_IN.to_i,
|
|
33
|
+
interval: INTERVAL,
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def update
|
|
38
|
+
if device_authorization.actionable?
|
|
39
|
+
case params[:decision]
|
|
40
|
+
when "approve"
|
|
41
|
+
device_authorization.approve!(admin_user:)
|
|
42
|
+
else
|
|
43
|
+
device_authorization.deny!(admin_user:)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
redirect_to(admin_device_authorization_path(device_authorization.user_code), status: :see_other)
|
|
47
|
+
else
|
|
48
|
+
render(:show, status: :unprocessable_content, locals: { device_authorization: })
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def set_device_authorization
|
|
55
|
+
@device_authorization = Admin::DeviceAuthorization.find_by!(user_code: params[:user_code])
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
class DeviceTokensController < ApplicationController
|
|
5
|
+
GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
|
|
6
|
+
|
|
7
|
+
rate_limit to: 20, within: 1.minute, only: :create
|
|
8
|
+
skip_before_action :verify_authenticity_token, only: :create
|
|
9
|
+
|
|
10
|
+
def create
|
|
11
|
+
return render(json: { error: "invalid_request" }, status: :bad_request) unless params[:grant_type] == GRANT_TYPE
|
|
12
|
+
|
|
13
|
+
render json: Admin::DeviceAuthorization.issue_access_token!(device_code: params[:device_code])
|
|
14
|
+
rescue Admin::DeviceAuthorization::TokenError => e
|
|
15
|
+
render json: { error: e.code }, status: :bad_request
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -41,7 +41,7 @@ module Admin
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def destroy
|
|
44
|
-
record_sign_out!(
|
|
44
|
+
record_sign_out!(Koi::Current.admin_user)
|
|
45
45
|
|
|
46
46
|
session[:admin_user_id] = nil
|
|
47
47
|
|
|
@@ -103,11 +103,11 @@ module Admin
|
|
|
103
103
|
def authenticate_local_admin
|
|
104
104
|
return if admin_signed_in? || !Rails.env.development?
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
Koi::Current.admin_user = Admin::User.find_by(email: "#{ENV.fetch('USER', nil)}@katalyst.com.au")
|
|
107
107
|
|
|
108
108
|
return unless admin_signed_in?
|
|
109
109
|
|
|
110
|
-
session[:admin_user_id] =
|
|
110
|
+
session[:admin_user_id] = Koi::Current.admin_user.id
|
|
111
111
|
|
|
112
112
|
flash.delete(:redirect) if (redirect = flash[:redirect])
|
|
113
113
|
|
|
@@ -14,23 +14,20 @@ module Koi
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def admin_signed_in?
|
|
17
|
-
|
|
18
|
-
rescue ActiveRecord::RecordNotFound
|
|
19
|
-
false
|
|
17
|
+
Koi::Current.admin_user.present?
|
|
20
18
|
end
|
|
21
19
|
|
|
22
20
|
def current_admin_user
|
|
23
|
-
|
|
24
|
-
return @current_admin_user = nil unless session.has_key?(:admin_user_id)
|
|
25
|
-
|
|
26
|
-
@current_admin_user = Admin::User.find(session[:admin_user_id])
|
|
27
|
-
ensure
|
|
28
|
-
session.delete(:admin_user_id) unless @current_admin_user
|
|
21
|
+
Koi::Current.admin_user
|
|
29
22
|
end
|
|
30
23
|
|
|
31
24
|
# @deprecated Use current_admin_user instead
|
|
32
25
|
alias_method :current_admin, :current_admin_user
|
|
33
26
|
|
|
27
|
+
def requires_session_authentication!
|
|
28
|
+
head(:forbidden) if session[:admin_user_id].blank?
|
|
29
|
+
end
|
|
30
|
+
|
|
34
31
|
module Test
|
|
35
32
|
# Include in view specs to stub out the current admin user
|
|
36
33
|
module ViewHelper
|
|
@@ -40,11 +37,11 @@ module Koi
|
|
|
40
37
|
before do
|
|
41
38
|
view.singleton_class.module_eval do
|
|
42
39
|
def admin_signed_in?
|
|
43
|
-
|
|
40
|
+
Koi::Current.admin_user.present?
|
|
44
41
|
end
|
|
45
42
|
|
|
46
43
|
def current_admin_user
|
|
47
|
-
respond_to?(:admin_user) ? admin_user : nil
|
|
44
|
+
Koi::Current.admin_user = (respond_to?(:admin_user) ? admin_user : nil)
|
|
48
45
|
end
|
|
49
46
|
|
|
50
47
|
alias_method :current_admin, :current_admin_user
|
|
@@ -27,7 +27,7 @@ module Koi
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def webauthn_registration_options_value
|
|
30
|
-
user =
|
|
30
|
+
user = Koi::Current.admin_user.tap do |u|
|
|
31
31
|
u.update!(webauthn_id: WebAuthn.generate_user_id) unless u.webauthn_id
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -74,7 +74,8 @@ module Koi
|
|
|
74
74
|
session.delete(:registration_challenge),
|
|
75
75
|
)
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
Koi::Current
|
|
78
|
+
.admin_user
|
|
78
79
|
.credentials
|
|
79
80
|
.create_with(nickname: webauthn_nickname,
|
|
80
81
|
public_key: webauthn_credential.public_key,
|
|
@@ -38,6 +38,13 @@ module Koi
|
|
|
38
38
|
layout -> { turbo_frame_request? ? "koi/frame" : "koi/application" }
|
|
39
39
|
|
|
40
40
|
protect_from_forgery with: :exception
|
|
41
|
+
skip_forgery_protection if: :bearer_token_request?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def bearer_token_request?
|
|
47
|
+
request.authorization.to_s.match?(/\ABearer .+\z/)
|
|
41
48
|
end
|
|
42
49
|
end
|
|
43
50
|
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
class DeviceAuthorization < ApplicationRecord
|
|
5
|
+
EXPIRES_IN = 10.minutes
|
|
6
|
+
|
|
7
|
+
class TokenError < StandardError
|
|
8
|
+
attr_reader :code
|
|
9
|
+
|
|
10
|
+
def initialize(code)
|
|
11
|
+
@code = code
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
self.table_name = :admin_device_authorizations
|
|
17
|
+
|
|
18
|
+
belongs_to :admin_user, class_name: "Admin::User", optional: true
|
|
19
|
+
|
|
20
|
+
enum :status, %w[pending approved denied consumed].index_with(&:to_s)
|
|
21
|
+
|
|
22
|
+
validates :device_code_digest, presence: true, uniqueness: true
|
|
23
|
+
validates :request_expires_at, presence: true
|
|
24
|
+
validates :status, presence: true, inclusion: { in: statuses.values }
|
|
25
|
+
validates :user_code, presence: true, uniqueness: true
|
|
26
|
+
|
|
27
|
+
def self.issue!(requested_ip:, user_agent:)
|
|
28
|
+
device_code = SecureRandom.urlsafe_base64(32)
|
|
29
|
+
|
|
30
|
+
device_authorization = create!(
|
|
31
|
+
device_code_digest: digest(device_code),
|
|
32
|
+
user_code: generate_user_code,
|
|
33
|
+
request_expires_at: EXPIRES_IN.from_now,
|
|
34
|
+
requested_ip:,
|
|
35
|
+
user_agent:,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
[device_authorization, device_code]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.digest(device_code)
|
|
42
|
+
Digest::SHA256.hexdigest(device_code)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.generate_user_code
|
|
46
|
+
"#{SecureRandom.alphanumeric(4).upcase}-#{SecureRandom.alphanumeric(4).upcase}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.issue_access_token!(device_code:, token_expires_in: 12.hours)
|
|
50
|
+
device_authorization = find_by(device_code_digest: digest(device_code.to_s))
|
|
51
|
+
raise TokenError.new("invalid_grant") unless device_authorization
|
|
52
|
+
|
|
53
|
+
device_authorization.with_lock do
|
|
54
|
+
device_authorization.reload
|
|
55
|
+
|
|
56
|
+
if (error = device_authorization.token_error)
|
|
57
|
+
raise TokenError.new(error)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
access_token = device_authorization.admin_user.generate_token_for(:api_access)
|
|
61
|
+
device_authorization.consume!(token_expires_in:)
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
access_token:,
|
|
65
|
+
token_type: "Bearer",
|
|
66
|
+
expires_in: token_expires_in.to_i,
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def expired?
|
|
72
|
+
request_expires_at <= Time.current
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def issuable?
|
|
76
|
+
approved? && !expired?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def token_error
|
|
80
|
+
return "authorization_pending" if pending?
|
|
81
|
+
return "access_denied" if denied?
|
|
82
|
+
|
|
83
|
+
"invalid_grant" if consumed? || expired?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def consume!(token_expires_in:)
|
|
87
|
+
update!(
|
|
88
|
+
status: "consumed",
|
|
89
|
+
consumed_at: Time.current,
|
|
90
|
+
token_expires_at: token_expires_in.from_now,
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def approve!(admin_user:)
|
|
95
|
+
update!(
|
|
96
|
+
status: "approved",
|
|
97
|
+
approved_at: Time.current,
|
|
98
|
+
admin_user:,
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def deny!(admin_user:)
|
|
103
|
+
update!(
|
|
104
|
+
status: "denied",
|
|
105
|
+
admin_user:,
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def actionable?
|
|
110
|
+
pending? && !expired?
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/app/models/admin/user.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Admin
|
|
|
14
14
|
# disable validations for password_digest
|
|
15
15
|
has_secure_password validations: false
|
|
16
16
|
|
|
17
|
+
generates_token_for(:api_access, expires_in: 12.hours) { current_sign_in_at }
|
|
17
18
|
generates_token_for(:password_reset, expires_in: 30.minutes) { current_sign_in_at }
|
|
18
19
|
|
|
19
20
|
has_many :credentials, inverse_of: :admin, class_name: "Admin::Credential", dependent: :destroy
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<%# locals: (device_authorization:) %>
|
|
2
|
+
|
|
3
|
+
<%= content_for(:roadblock) do %>
|
|
4
|
+
<header>
|
|
5
|
+
<icon aria-hidden="true" class="icon" data-icon="koi"> </icon>
|
|
6
|
+
<h1>API access request</h1>
|
|
7
|
+
</header>
|
|
8
|
+
|
|
9
|
+
<% if device_authorization.pending? && !device_authorization.expired? %>
|
|
10
|
+
<p>
|
|
11
|
+
A device is requesting a short-lived API token.
|
|
12
|
+
</p>
|
|
13
|
+
<dl>
|
|
14
|
+
<dt>Code</dt><dd><%= device_authorization.user_code %></dd>
|
|
15
|
+
</dl>
|
|
16
|
+
|
|
17
|
+
<div class="actions">
|
|
18
|
+
<%= button_to("Approve",
|
|
19
|
+
admin_device_authorization_path(device_authorization.user_code),
|
|
20
|
+
method: :patch,
|
|
21
|
+
params: { decision: "approve" },
|
|
22
|
+
class: "button") %>
|
|
23
|
+
<%= button_to("Deny",
|
|
24
|
+
admin_device_authorization_path(device_authorization.user_code),
|
|
25
|
+
method: :patch,
|
|
26
|
+
params: { decision: "deny" },
|
|
27
|
+
class: "button button--secondary") %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<%= summary_table_with(model: device_authorization) do |row| %>
|
|
32
|
+
<% row.enum(:status) %>
|
|
33
|
+
<% row.text(:requested_ip) %>
|
|
34
|
+
<% row.text(:user_agent) %>
|
|
35
|
+
<% row.datetime(:request_expires_at) %>
|
|
36
|
+
<% row.datetime(:token_expires_at) %>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% end %>
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
keydown.enter->navigation#go keydown.esc->navigation#clear">
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
-
<% if
|
|
17
|
+
<% if Koi::Current.admin_user %>
|
|
18
18
|
<ul class="navigation-group | flow" role="list">
|
|
19
19
|
<li>
|
|
20
|
-
<span><%=
|
|
20
|
+
<span><%= Koi::Current.admin_user.name %></span>
|
|
21
21
|
<ul class="navigation-list | flow" role="list">
|
|
22
22
|
<li><%= link_to("Profile", main_app.admin_profile_path) %></li>
|
|
23
23
|
<li><%= link_to("Log out", main_app.admin_session_path, data: { turbo_method: :delete }) %></li>
|
data/config/routes.rb
CHANGED
|
@@ -13,6 +13,9 @@ Rails.application.routes.draw do
|
|
|
13
13
|
resource :cache, only: %i[destroy]
|
|
14
14
|
resource :dashboard, only: %i[show]
|
|
15
15
|
|
|
16
|
+
resources :device_authorizations, param: :user_code, only: %i[create show update]
|
|
17
|
+
resources :device_tokens, only: %i[create]
|
|
18
|
+
|
|
16
19
|
resource :profile, only: %i[show edit update], shallow: true do
|
|
17
20
|
resources :credentials, only: %i[show new create update destroy]
|
|
18
21
|
resource :otp, only: %i[new create destroy]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateAdminDeviceAuthorizations < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :admin_device_authorizations do |t|
|
|
6
|
+
t.string :device_code_digest, null: false, index: { unique: true }
|
|
7
|
+
t.string :user_code, null: false, index: { unique: true }
|
|
8
|
+
t.string :status, null: false, default: "pending"
|
|
9
|
+
t.datetime :request_expires_at, null: false
|
|
10
|
+
t.datetime :approved_at
|
|
11
|
+
t.datetime :consumed_at
|
|
12
|
+
t.datetime :token_expires_at
|
|
13
|
+
t.references :admin_user, null: true, foreign_key: { to_table: :admins }
|
|
14
|
+
t.string :requested_ip
|
|
15
|
+
t.string :user_agent
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -19,27 +19,71 @@ module Koi
|
|
|
19
19
|
request = ActionDispatch::Request.new(env)
|
|
20
20
|
session = ActionDispatch::Request::Session.find(request)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
# Always retrieve user to ensure we are not vulnerable to timing attacks
|
|
23
|
+
Koi::Current.admin_user = if bearer_token(request).present?
|
|
24
|
+
bearer_admin_user(request)
|
|
25
|
+
else
|
|
26
|
+
session_admin_user(session)
|
|
27
|
+
end
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# Remove from session if not found
|
|
30
|
+
session.delete(:admin_user_id) if session.has_key?(:admin_user_id) && !authenticated?
|
|
31
|
+
|
|
32
|
+
if requires_authentication?(request) && !authenticated?
|
|
33
|
+
unauthorized_response(request)
|
|
30
34
|
else
|
|
31
35
|
@app.call(env)
|
|
32
36
|
end
|
|
37
|
+
ensure
|
|
38
|
+
Koi::Current.admin_user = nil
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
private
|
|
36
42
|
|
|
37
43
|
def requires_authentication?(request)
|
|
38
|
-
!request.path.starts_with?("/admin/session")
|
|
44
|
+
!request.path.starts_with?("/admin/session") && !device_flow_request?(request)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def authenticated?
|
|
48
|
+
Koi::Current.admin_user.present?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def bearer_admin_user(request)
|
|
52
|
+
token = bearer_token(request)
|
|
53
|
+
return if token.blank?
|
|
54
|
+
|
|
55
|
+
request.session_options[:skip] = true
|
|
56
|
+
|
|
57
|
+
Admin::User.find_by_token_for(:api_access, token)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def session_admin_user(session)
|
|
61
|
+
Admin::User.find_by(id: session[:admin_user_id])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bearer_token(request)
|
|
65
|
+
return nil if request.authorization.blank?
|
|
66
|
+
|
|
67
|
+
request.authorization.match(/^Bearer (?<token>.+)$/)&.named_captures&.fetch("token", nil)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def unauthorized_response(request)
|
|
71
|
+
if bearer_token(request).present?
|
|
72
|
+
# If the user provided a token, it was not valid, and the request requires authentication
|
|
73
|
+
[401, {}, []]
|
|
74
|
+
else
|
|
75
|
+
if request.get?
|
|
76
|
+
# Set the redirection path for returning the user to their requested path after login
|
|
77
|
+
request.flash[:redirect] = request.fullpath
|
|
78
|
+
request.commit_flash
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
[303, { "Location" => "/admin/session/new" }, []]
|
|
82
|
+
end
|
|
39
83
|
end
|
|
40
84
|
|
|
41
|
-
def
|
|
42
|
-
|
|
85
|
+
def device_flow_request?(request)
|
|
86
|
+
request.post? && %w[/admin/device_authorizations /admin/device_tokens].include?(request.path)
|
|
43
87
|
end
|
|
44
88
|
end
|
|
45
89
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FactoryBot.define do
|
|
4
|
+
factory :admin_device_authorization, class: "Admin::DeviceAuthorization" do
|
|
5
|
+
sequence(:device_code_digest) { |n| "digest-#{n}" }
|
|
6
|
+
sequence(:user_code) { |n| "CODE-#{n.to_s.rjust(4, '0')}" }
|
|
7
|
+
status { :pending }
|
|
8
|
+
request_expires_at { 10.minutes.from_now }
|
|
9
|
+
requested_ip { "127.0.0.1" }
|
|
10
|
+
user_agent { "RSpec" }
|
|
11
|
+
|
|
12
|
+
trait :approved do
|
|
13
|
+
status { :approved }
|
|
14
|
+
approved_at { Time.current }
|
|
15
|
+
admin_user
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
trait :denied do
|
|
19
|
+
status { :denied }
|
|
20
|
+
admin_user
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
trait :consumed do
|
|
24
|
+
status { :consumed }
|
|
25
|
+
consumed_at { Time.current }
|
|
26
|
+
admin_user
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: katalyst-koi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Katalyst Interactive
|
|
@@ -356,6 +356,8 @@ files:
|
|
|
356
356
|
- app/controllers/admin/caches_controller.rb
|
|
357
357
|
- app/controllers/admin/credentials_controller.rb
|
|
358
358
|
- app/controllers/admin/dashboards_controller.rb
|
|
359
|
+
- app/controllers/admin/device_authorizations_controller.rb
|
|
360
|
+
- app/controllers/admin/device_tokens_controller.rb
|
|
359
361
|
- app/controllers/admin/otps_controller.rb
|
|
360
362
|
- app/controllers/admin/profiles_controller.rb
|
|
361
363
|
- app/controllers/admin/sessions_controller.rb
|
|
@@ -389,13 +391,16 @@ files:
|
|
|
389
391
|
- app/javascript/koi/elements/index.js
|
|
390
392
|
- app/javascript/koi/elements/toolbar.js
|
|
391
393
|
- app/javascript/koi/utils/transition.js
|
|
394
|
+
- app/jobs/admin/device_authorizations_cleanup_job.rb
|
|
392
395
|
- app/jobs/koi/application_job.rb
|
|
393
396
|
- app/models/admin/collection.rb
|
|
394
397
|
- app/models/admin/credential.rb
|
|
398
|
+
- app/models/admin/device_authorization.rb
|
|
395
399
|
- app/models/admin/user.rb
|
|
396
400
|
- app/models/application_record.rb
|
|
397
401
|
- app/models/concerns/koi/model/archivable.rb
|
|
398
402
|
- app/models/concerns/koi/model/otp.rb
|
|
403
|
+
- app/models/koi/current.rb
|
|
399
404
|
- app/models/url_rewrite.rb
|
|
400
405
|
- app/models/well_known.rb
|
|
401
406
|
- app/views/admin/admin_users/_form.html.erb
|
|
@@ -408,6 +413,7 @@ files:
|
|
|
408
413
|
- app/views/admin/credentials/new.html.erb
|
|
409
414
|
- app/views/admin/credentials/show.html.erb
|
|
410
415
|
- app/views/admin/dashboards/show.html.erb
|
|
416
|
+
- app/views/admin/device_authorizations/show.html.erb
|
|
411
417
|
- app/views/admin/otps/_form.html.erb
|
|
412
418
|
- app/views/admin/otps/create.turbo_stream.erb
|
|
413
419
|
- app/views/admin/otps/new.html.erb
|
|
@@ -463,6 +469,7 @@ files:
|
|
|
463
469
|
- db/migrate/20231211005214_add_status_code_to_url_rewrites.rb
|
|
464
470
|
- db/migrate/20241214060913_add_otp_secret_to_admin_users.rb
|
|
465
471
|
- db/migrate/20250204060748_create_well_knowns.rb
|
|
472
|
+
- db/migrate/20260413014834_create_admin_device_authorizations.rb
|
|
466
473
|
- db/seeds.rb
|
|
467
474
|
- lib/generators/koi/admin/USAGE
|
|
468
475
|
- lib/generators/koi/admin/admin_generator.rb
|
|
@@ -499,6 +506,7 @@ files:
|
|
|
499
506
|
- lib/koi/middleware/admin_authentication.rb
|
|
500
507
|
- lib/koi/middleware/url_redirect.rb
|
|
501
508
|
- lib/koi/release.rb
|
|
509
|
+
- spec/factories/admin_device_authorizations.rb
|
|
502
510
|
- spec/factories/admins.rb
|
|
503
511
|
- spec/factories/url_rewrites.rb
|
|
504
512
|
- spec/factories/well_knowns.rb
|
|
@@ -521,7 +529,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
521
529
|
- !ruby/object:Gem::Version
|
|
522
530
|
version: '0'
|
|
523
531
|
requirements: []
|
|
524
|
-
rubygems_version: 4.0.
|
|
532
|
+
rubygems_version: 4.0.6
|
|
525
533
|
specification_version: 4
|
|
526
534
|
summary: Koi CMS admin framework
|
|
527
535
|
test_files: []
|