rails_iam 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/CONTRIBUTING.md +145 -0
- data/LICENSE +20 -0
- data/README.md +192 -0
- data/Rakefile +14 -0
- data/app/controllers/concerns/rails_iam/auth_concern.rb +60 -0
- data/app/controllers/concerns/rails_iam/authentication/authenticatable.rb +44 -0
- data/app/controllers/concerns/rails_iam/authorization/authorizable.rb +170 -0
- data/app/controllers/rails_iam/application_controller.rb +7 -0
- data/app/controllers/rails_iam/auth_controller.rb +113 -0
- data/app/helpers/rails_iam/config_loader.rb +71 -0
- data/app/models/concerns/rails_iam/auditable.rb +57 -0
- data/app/models/concerns/rails_iam/user_concern.rb +59 -0
- data/app/models/rails_iam/application_record.rb +8 -0
- data/app/models/rails_iam/permission.rb +14 -0
- data/app/models/rails_iam/refresh_token.rb +7 -0
- data/app/models/rails_iam/role.rb +14 -0
- data/app/models/rails_iam/role_permission.rb +20 -0
- data/app/models/rails_iam/user.rb +16 -0
- data/app/models/rails_iam/user_denied_permission.rb +12 -0
- data/app/models/rails_iam/user_permission.rb +12 -0
- data/app/models/rails_iam/user_role.rb +12 -0
- data/app/services/application_service.rb +4 -0
- data/app/services/rails_iam/authentication/authenticated_user.rb +13 -0
- data/app/services/rails_iam/authentication/authenticator.rb +39 -0
- data/app/services/rails_iam/authentication/current.rb +9 -0
- data/app/services/rails_iam/authentication/exceptions/authentication_error.rb +10 -0
- data/app/services/rails_iam/authentication/jwt_decoder.rb +38 -0
- data/app/services/rails_iam/authentication/jwt_encoder.rb +45 -0
- data/app/services/rails_iam/authentication/login.rb +67 -0
- data/app/services/rails_iam/authentication/logout.rb +35 -0
- data/app/services/rails_iam/authentication/refresh_token_encoder.rb +11 -0
- data/app/services/rails_iam/authentication/skip_authentication_rule.rb +20 -0
- data/app/services/rails_iam/authentication/token_extractor.rb +64 -0
- data/app/services/rails_iam/authorization/authorization_context.rb +41 -0
- data/app/services/rails_iam/authorization/authorization_rule.rb +38 -0
- data/app/services/rails_iam/authorization/authorizer.rb +35 -0
- data/app/services/rails_iam/authorization/cache.rb +39 -0
- data/app/services/rails_iam/authorization/denied_permission_checker.rb +33 -0
- data/app/services/rails_iam/authorization/exceptions/authorization_error.rb +10 -0
- data/app/services/rails_iam/authorization/exceptions/missing_permission_error.rb +35 -0
- data/app/services/rails_iam/authorization/exceptions/missing_role_error.rb +35 -0
- data/app/services/rails_iam/authorization/permission_checker.rb +42 -0
- data/app/services/rails_iam/authorization/permission_resolver.rb +70 -0
- data/app/services/rails_iam/authorization/permission_sources/role_permission_source.rb +37 -0
- data/app/services/rails_iam/authorization/permission_sources/user_denied_permission_source.rb +29 -0
- data/app/services/rails_iam/authorization/permission_sources/user_permission_source.rb +28 -0
- data/app/services/rails_iam/authorization/resolved_permissions.rb +63 -0
- data/app/services/rails_iam/authorization/role_checker.rb +35 -0
- data/app/services/rails_iam/authorization/role_resolver.rb +30 -0
- data/app/services/rails_iam/authorization/skip_authorization_rule.rb +19 -0
- data/app/services/rails_iam/rails_iam_service.rb +11 -0
- data/config/routes.rb +25 -0
- data/lib/generators/rails_iam/install_generator.rb +49 -0
- data/lib/generators/rails_iam/templates/create_rails_iam_tables.rb.erb +119 -0
- data/lib/generators/rails_iam/templates/initializer.rb +162 -0
- data/lib/rails_iam/config/authentication.rb +65 -0
- data/lib/rails_iam/config/authorization.rb +36 -0
- data/lib/rails_iam/config/jwt.rb +27 -0
- data/lib/rails_iam/configuration.rb +30 -0
- data/lib/rails_iam/controller_dsl.rb +41 -0
- data/lib/rails_iam/engine.rb +34 -0
- data/lib/rails_iam/model_dsl.rb +13 -0
- data/lib/rails_iam/version.rb +5 -0
- data/lib/rails_iam.rb +27 -0
- data/lib/tasks/rails_iam_tasks.rake +6 -0
- data/rails_iam.gemspec +46 -0
- metadata +161 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
class AuthController < ApplicationController
|
|
5
|
+
include ActionController::Cookies
|
|
6
|
+
include RailsIam::AuthConcern
|
|
7
|
+
include RailsIam::ConfigLoader
|
|
8
|
+
|
|
9
|
+
rescue_from RailsIam::Authentication::Exceptions::AuthenticationError, with: :render_rails_iam_authentication_error
|
|
10
|
+
|
|
11
|
+
def sign_in
|
|
12
|
+
result = RailsIam::Authentication::Login.call(
|
|
13
|
+
email: login_params[:email],
|
|
14
|
+
password: login_params[:password],
|
|
15
|
+
request: request,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
ensure_valid_strategy!
|
|
19
|
+
|
|
20
|
+
create_authenticated_user_cookies!(result) if strategy_includes?(:cookie)
|
|
21
|
+
|
|
22
|
+
if strategy_includes?(:bearer)
|
|
23
|
+
render json: token_response_payload(result)
|
|
24
|
+
else
|
|
25
|
+
render json: { user: result.user }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sign_out
|
|
30
|
+
refresh_token = current_refresh_token
|
|
31
|
+
|
|
32
|
+
RailsIam::Authentication::Logout.call(
|
|
33
|
+
refresh_token: refresh_token,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
delete_authenticated_user_cookies! if strategy_includes?(:cookie)
|
|
37
|
+
|
|
38
|
+
head :no_content
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def refresh
|
|
42
|
+
refresh_token = current_refresh_token
|
|
43
|
+
|
|
44
|
+
validate_refresh_token(refresh_token)
|
|
45
|
+
|
|
46
|
+
user = refresh_token.user
|
|
47
|
+
jti = SecureRandom.uuid
|
|
48
|
+
|
|
49
|
+
new_access_token = RailsIam::Authentication::JwtEncoder.call(user, jti: jti)
|
|
50
|
+
|
|
51
|
+
cookies.encrypted[access_token_key] = {
|
|
52
|
+
value: new_access_token,
|
|
53
|
+
expires: access_token_expires_in,
|
|
54
|
+
**cookie_options,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
refresh_token.update!(last_used_at: Time.current, jti: jti)
|
|
58
|
+
|
|
59
|
+
render json: { success: true }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def create_authenticated_user_cookies!(result)
|
|
65
|
+
cookies.encrypted[access_token_key] = {
|
|
66
|
+
value: result.access_token,
|
|
67
|
+
expires: result.access_token_expires_in,
|
|
68
|
+
**cookie_options,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
cookies.encrypted[refresh_token_key] = {
|
|
72
|
+
value: result.refresh_token,
|
|
73
|
+
expires: result.refresh_token_expires_in,
|
|
74
|
+
**cookie_options,
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def delete_authenticated_user_cookies!
|
|
79
|
+
cookies.delete(access_token_key)
|
|
80
|
+
cookies.delete(refresh_token_key)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_refresh_token(refresh_token)
|
|
84
|
+
return unless refresh_token.nil? || refresh_token.revoked_at.present? || refresh_token.expires_at < Time.current
|
|
85
|
+
|
|
86
|
+
raise RailsIam::Authentication::Exceptions::AuthenticationError, token_error
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ensure_valid_strategy!
|
|
90
|
+
return unless (authentication_strategies & %i[cookie bearer]).empty?
|
|
91
|
+
|
|
92
|
+
raise RailsIam::Authentication::Exceptions::AuthenticationError, authentication_strategy_error
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def strategy_includes?(strategy)
|
|
96
|
+
authentication_strategies.include?(strategy)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def token_response_payload(result)
|
|
100
|
+
{
|
|
101
|
+
user: result.user,
|
|
102
|
+
access_token: result.access_token,
|
|
103
|
+
refresh_token: result.refresh_token,
|
|
104
|
+
access_token_expires_in: result.access_token_expires_in,
|
|
105
|
+
refresh_token_expires_in: result.refresh_token_expires_in,
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def login_params
|
|
110
|
+
params.expect(auth: %i[email password])
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module ConfigLoader
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
def user_class
|
|
8
|
+
@user_class ||= RailsIam.configuration.user_class.constantize
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Authentication configuration
|
|
12
|
+
def_delegators :"RailsIam.configuration.authentication",
|
|
13
|
+
:access_token_key,
|
|
14
|
+
:refresh_token_key,
|
|
15
|
+
:token_error,
|
|
16
|
+
:token_expired_error,
|
|
17
|
+
:authentication_error,
|
|
18
|
+
:authentication_strategy_error,
|
|
19
|
+
:session_strategy_error,
|
|
20
|
+
:refresh_token_transport_error,
|
|
21
|
+
:refresh_token_expires_in,
|
|
22
|
+
:access_token_expires_in
|
|
23
|
+
def session_strategy
|
|
24
|
+
@session_strategy ||= RailsIam.configuration.authentication.session_strategy&.to_sym
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def authentication_strategies
|
|
28
|
+
@authentication_strategies ||= Array(RailsIam.configuration.authentication.authentication_strategies).map(&:to_sym)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def refresh_token_transport
|
|
32
|
+
@refresh_token_transport ||= RailsIam.configuration.authentication.refresh_token_transport.to_sym
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cookie_options
|
|
36
|
+
@cookie_options ||= RailsIam.configuration.authentication.cookie_options.except(:value, :expires)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Authorization configuration
|
|
40
|
+
def_delegators :"RailsIam.configuration.authorization",
|
|
41
|
+
:missing_permission_error,
|
|
42
|
+
:missing_role_error,
|
|
43
|
+
:excluded_controller_patterns
|
|
44
|
+
|
|
45
|
+
def authorization_rule_error
|
|
46
|
+
error = RailsIam.configuration.authorization.authorization_rule_error
|
|
47
|
+
"#{error.to_s.strip.ljust(1)} #{controller_name}##{action_name}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Jwt configuration
|
|
51
|
+
def jwt_algorithm
|
|
52
|
+
RailsIam.configuration.jwt.algorithm
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def jwt_expiry
|
|
56
|
+
RailsIam.configuration.jwt.jwt_expires_in
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def jwt_issuer
|
|
60
|
+
RailsIam.configuration.jwt.issuer
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def jwt_audience
|
|
64
|
+
RailsIam.configuration.jwt.audience
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def jwt_secret_key
|
|
68
|
+
RailsIam.configuration.jwt.secret
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
# frozen_string_literal: true
|
|
5
|
+
|
|
6
|
+
module Auditable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
before_create :set_created_by, if: :created_by_column_present?
|
|
11
|
+
before_destroy :set_deleted_by, if: :deleted_by_column_present?
|
|
12
|
+
before_update :set_updated_by, if: :updated_by_column_present?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def set_created_by
|
|
18
|
+
self.created_by = performer if respond_to?(:created_by=)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_updated_by
|
|
22
|
+
self.updated_by = performer if respond_to?(:updated_by=)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def set_deleted_by
|
|
26
|
+
self.deleted_by = performer if respond_to?(:deleted_by=)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def performer
|
|
30
|
+
user = RailsIam::Authentication::Current.user
|
|
31
|
+
return if user.nil?
|
|
32
|
+
|
|
33
|
+
method = RailsIam.configuration.audit_performer_method
|
|
34
|
+
|
|
35
|
+
case method
|
|
36
|
+
when Symbol
|
|
37
|
+
user.public_send(method)
|
|
38
|
+
when Proc
|
|
39
|
+
method.call(user)
|
|
40
|
+
else
|
|
41
|
+
user.id
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def created_by_column_present?
|
|
46
|
+
self.class.column_names.include?("created_by")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def deleted_by_column_present?
|
|
50
|
+
self.class.column_names.include?("deleted_by")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def updated_by_column_present?
|
|
54
|
+
self.class.column_names.include?("updated_by")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module UserConcern
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
has_many :user_roles,
|
|
9
|
+
class_name: "RailsIam::UserRole",
|
|
10
|
+
foreign_key: :user_id,
|
|
11
|
+
dependent: :destroy
|
|
12
|
+
|
|
13
|
+
has_many :roles,
|
|
14
|
+
through: :user_roles,
|
|
15
|
+
source: :role
|
|
16
|
+
|
|
17
|
+
has_many :user_permissions,
|
|
18
|
+
class_name: "RailsIam::UserPermission",
|
|
19
|
+
foreign_key: :user_id,
|
|
20
|
+
dependent: :destroy
|
|
21
|
+
|
|
22
|
+
has_many :granted_permissions,
|
|
23
|
+
through: :user_permissions,
|
|
24
|
+
source: :permission
|
|
25
|
+
|
|
26
|
+
has_many :user_denied_permissions,
|
|
27
|
+
class_name: "RailsIam::UserDeniedPermission",
|
|
28
|
+
foreign_key: :user_id,
|
|
29
|
+
dependent: :destroy
|
|
30
|
+
|
|
31
|
+
has_many :denied_permissions,
|
|
32
|
+
through: :user_denied_permissions,
|
|
33
|
+
source: :permission
|
|
34
|
+
|
|
35
|
+
has_many :refresh_tokens,
|
|
36
|
+
class_name: "RailsIam::RefreshToken",
|
|
37
|
+
foreign_key: :user_id,
|
|
38
|
+
dependent: :destroy
|
|
39
|
+
|
|
40
|
+
scope :with_allowed_permissions, -> { includes(user_permissions: :permission) }
|
|
41
|
+
scope :with_denied_permissions, -> { includes(user_denied_permissions: :permission) }
|
|
42
|
+
scope :with_authorization, lambda {
|
|
43
|
+
includes(
|
|
44
|
+
roles: :permissions,
|
|
45
|
+
user_permissions: :permission,
|
|
46
|
+
user_denied_permissions: :permission,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def permission_codes
|
|
52
|
+
RailsIam::Authorization::PermissionResolver.call(self).effective_codes.to_a
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def role_names
|
|
56
|
+
RailsIam::Authorization::RoleResolver.call(self).to_a
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
class Permission < ApplicationRecord
|
|
5
|
+
has_many :role_permissions, dependent: :destroy, class_name: "RailsIam::RolePermission"
|
|
6
|
+
has_many :roles, through: :role_permissions, class_name: "RailsIam::Role"
|
|
7
|
+
has_many :user_permissions, dependent: :destroy, class_name: "RailsIam::UserPermission"
|
|
8
|
+
has_many :users, through: :user_permissions, class_name: RailsIam.configuration.user_class
|
|
9
|
+
|
|
10
|
+
validates :code, presence: true, uniqueness: true
|
|
11
|
+
|
|
12
|
+
normalizes :code, with: ->(code) { code.to_s.downcase.strip }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
class Role < ApplicationRecord
|
|
5
|
+
has_many :user_roles, dependent: :destroy, class_name: "RailsIam::UserRole"
|
|
6
|
+
has_many :users, through: :user_roles, class_name: RailsIam.configuration.user_class
|
|
7
|
+
has_many :role_permissions, dependent: :destroy, class_name: "RailsIam::RolePermission"
|
|
8
|
+
has_many :permissions, through: :role_permissions, class_name: "RailsIam::Permission"
|
|
9
|
+
|
|
10
|
+
validates :name, presence: true, uniqueness: true
|
|
11
|
+
|
|
12
|
+
normalizes :name, with: ->(code) { code.to_s.downcase.strip }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
class RolePermission < ApplicationRecord
|
|
5
|
+
belongs_to :permission, class_name: "RailsIam::Permission"
|
|
6
|
+
belongs_to :role, class_name: "RailsIam::Role"
|
|
7
|
+
|
|
8
|
+
validates :permission_id, uniqueness: { scope: :role_id }
|
|
9
|
+
|
|
10
|
+
after_commit :clear_user_permission_caches
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def clear_user_permission_caches
|
|
15
|
+
role.users.find_each do |user|
|
|
16
|
+
RailsIam::Authorization::Cache.delete_all(user.id)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
class User < ApplicationRecord
|
|
5
|
+
has_secure_password
|
|
6
|
+
include RailsIam::UserConcern
|
|
7
|
+
|
|
8
|
+
validates :email, presence: true, uniqueness: true
|
|
9
|
+
validates :password, presence: true, length: { minimum: 6, maximum: 20 }, if: :password
|
|
10
|
+
normalizes :email, with: ->(email) { email.to_s.downcase.strip }
|
|
11
|
+
|
|
12
|
+
def as_json(options = {})
|
|
13
|
+
super.except("password_digest")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
class UserDeniedPermission < ApplicationRecord
|
|
5
|
+
belongs_to :permission, class_name: "RailsIam::Permission"
|
|
6
|
+
belongs_to :user, class_name: RailsIam.configuration.user_class
|
|
7
|
+
|
|
8
|
+
validates :permission_id, uniqueness: { scope: :user_id }
|
|
9
|
+
|
|
10
|
+
after_commit -> { RailsIam::Authorization::Cache.delete_all(user_id) }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
class UserPermission < ApplicationRecord
|
|
5
|
+
belongs_to :permission, class_name: "RailsIam::Permission"
|
|
6
|
+
belongs_to :user, class_name: RailsIam.configuration.user_class
|
|
7
|
+
|
|
8
|
+
validates :permission_id, uniqueness: { scope: :user_id }
|
|
9
|
+
|
|
10
|
+
after_commit -> { RailsIam::Authorization::Cache.delete_all(user_id) }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
class UserRole < ApplicationRecord
|
|
5
|
+
belongs_to :user, class_name: RailsIam.configuration.user_class
|
|
6
|
+
belongs_to :role, class_name: "RailsIam::Role"
|
|
7
|
+
|
|
8
|
+
validates :role_id, uniqueness: { scope: :user_id }
|
|
9
|
+
|
|
10
|
+
after_commit -> { RailsIam::Authorization::Cache.delete_all(user_id) }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authentication
|
|
5
|
+
class Authenticator < RailsIam::RailsIamService
|
|
6
|
+
include RailsIam::ConfigLoader
|
|
7
|
+
|
|
8
|
+
def initialize(request, raise_on_failure: true)
|
|
9
|
+
super()
|
|
10
|
+
@request = request
|
|
11
|
+
@raise_on_failure = raise_on_failure
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
token = TokenExtractor.call(request)
|
|
16
|
+
|
|
17
|
+
if token.blank?
|
|
18
|
+
return nil unless @raise_on_failure
|
|
19
|
+
|
|
20
|
+
raise RailsIam::Authentication::Exceptions::AuthenticationError, token_error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
payload = JwtDecoder.call(token)
|
|
24
|
+
|
|
25
|
+
user = User.find_by(id: payload[:sub])
|
|
26
|
+
|
|
27
|
+
raise Authentication::Exceptions::AuthenticationError, token_error unless user
|
|
28
|
+
|
|
29
|
+
user
|
|
30
|
+
rescue JwtDecoder::InvalidToken => e
|
|
31
|
+
raise Authentication::Exceptions::AuthenticationError, e.message # => token_error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :request
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jwt"
|
|
4
|
+
|
|
5
|
+
module RailsIam
|
|
6
|
+
module Authentication
|
|
7
|
+
class JwtDecoder < RailsIam::RailsIamService
|
|
8
|
+
include RailsIam::ConfigLoader
|
|
9
|
+
|
|
10
|
+
class InvalidToken < StandardError; end
|
|
11
|
+
|
|
12
|
+
def initialize(token, headers = {})
|
|
13
|
+
super()
|
|
14
|
+
@token = token
|
|
15
|
+
@headers = headers
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
decoded = JWT.decode(
|
|
20
|
+
token,
|
|
21
|
+
jwt_secret_key,
|
|
22
|
+
true,
|
|
23
|
+
{ algorithm: jwt_algorithm, verify_jti: true }.merge(headers),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
decoded.first.deep_symbolize_keys
|
|
27
|
+
rescue JWT::ExpiredSignature
|
|
28
|
+
raise InvalidToken, token_expired_error
|
|
29
|
+
rescue JWT::DecodeError
|
|
30
|
+
raise InvalidToken, token_error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :token, :headers
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authentication
|
|
5
|
+
class JwtEncoder < RailsIam::RailsIamService
|
|
6
|
+
def initialize(user, extra_claims = {}, headers = {})
|
|
7
|
+
super()
|
|
8
|
+
@user = user
|
|
9
|
+
@extra_claims = extra_claims
|
|
10
|
+
@headers = headers
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
if jwt_secret_key.blank?
|
|
15
|
+
raise RailsIam::Authentication::Exceptions::AuthenticationError,
|
|
16
|
+
"Token generation failed due to a missing cryptographic configuration/jwt secret."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
JWT.encode(payload, jwt_secret_key, jwt_algorithm, headers)
|
|
20
|
+
rescue JWT::EncodeError => e
|
|
21
|
+
raise RailsIam::Authentication::Exceptions::AuthenticationError e.message
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :user, :extra_claims, :headers
|
|
27
|
+
|
|
28
|
+
def payload
|
|
29
|
+
# Base claims recommended by RFC 7519 standards
|
|
30
|
+
claims = {
|
|
31
|
+
sub: user&.id.to_s, # Standards recommend 'sub' string values
|
|
32
|
+
email: user&.email,
|
|
33
|
+
iat: Time.current.to_i,
|
|
34
|
+
# nbf: Time.current.to_i, # Not Before: valid immediately
|
|
35
|
+
exp: jwt_expiry.from_now.to_i,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
claims[:iss] = jwt_issuer if jwt_issuer.present?
|
|
39
|
+
claims[:aud] = jwt_audience if jwt_audience.present?
|
|
40
|
+
|
|
41
|
+
claims.merge(extra_claims)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsIam
|
|
4
|
+
module Authentication
|
|
5
|
+
class Login < RailsIam::RailsIamService
|
|
6
|
+
include RailsIam::ConfigLoader
|
|
7
|
+
|
|
8
|
+
def initialize(email:, password:, request: nil)
|
|
9
|
+
super()
|
|
10
|
+
|
|
11
|
+
@email = email.to_s.downcase.strip
|
|
12
|
+
@password = password
|
|
13
|
+
@request = request
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
user = user_class.find_by(email: email)
|
|
18
|
+
|
|
19
|
+
raise RailsIam::Authentication::Exceptions::AuthenticationError, authentication_error unless user&.authenticate(password)
|
|
20
|
+
|
|
21
|
+
jti = SecureRandom.uuid
|
|
22
|
+
|
|
23
|
+
access_token = JwtEncoder.call(user, jti: jti)
|
|
24
|
+
refresh_token = RefreshTokenEncoder.call
|
|
25
|
+
|
|
26
|
+
if session_strategy == :single
|
|
27
|
+
ActiveRecord::Base.transaction do
|
|
28
|
+
remove_existing_sessions(user)
|
|
29
|
+
create_refresh_token(user, refresh_token, jti)
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
create_refresh_token(user, refresh_token, jti)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
AuthenticatedUser.new(
|
|
36
|
+
user: user,
|
|
37
|
+
access_token: access_token,
|
|
38
|
+
refresh_token: refresh_token,
|
|
39
|
+
access_token_expires_in: access_token_expires_in.from_now,
|
|
40
|
+
refresh_token_expires_in: refresh_token_expires_in.from_now,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
attr_reader :email, :password, :request
|
|
47
|
+
|
|
48
|
+
def remove_existing_sessions(user)
|
|
49
|
+
user.refresh_tokens.delete_all
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_refresh_token(user, refresh_token, jti)
|
|
53
|
+
user.refresh_tokens.create!(refresh_token_attributes(refresh_token, jti))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def refresh_token_attributes(refresh_token, jti)
|
|
57
|
+
{
|
|
58
|
+
token_digest: Digest::SHA256.hexdigest(refresh_token),
|
|
59
|
+
expires_at: refresh_token_expires_in.from_now,
|
|
60
|
+
jti: jti,
|
|
61
|
+
ip_address: request&.remote_ip,
|
|
62
|
+
user_agent: request&.user_agent,
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|