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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/CONTRIBUTING.md +145 -0
  4. data/LICENSE +20 -0
  5. data/README.md +192 -0
  6. data/Rakefile +14 -0
  7. data/app/controllers/concerns/rails_iam/auth_concern.rb +60 -0
  8. data/app/controllers/concerns/rails_iam/authentication/authenticatable.rb +44 -0
  9. data/app/controllers/concerns/rails_iam/authorization/authorizable.rb +170 -0
  10. data/app/controllers/rails_iam/application_controller.rb +7 -0
  11. data/app/controllers/rails_iam/auth_controller.rb +113 -0
  12. data/app/helpers/rails_iam/config_loader.rb +71 -0
  13. data/app/models/concerns/rails_iam/auditable.rb +57 -0
  14. data/app/models/concerns/rails_iam/user_concern.rb +59 -0
  15. data/app/models/rails_iam/application_record.rb +8 -0
  16. data/app/models/rails_iam/permission.rb +14 -0
  17. data/app/models/rails_iam/refresh_token.rb +7 -0
  18. data/app/models/rails_iam/role.rb +14 -0
  19. data/app/models/rails_iam/role_permission.rb +20 -0
  20. data/app/models/rails_iam/user.rb +16 -0
  21. data/app/models/rails_iam/user_denied_permission.rb +12 -0
  22. data/app/models/rails_iam/user_permission.rb +12 -0
  23. data/app/models/rails_iam/user_role.rb +12 -0
  24. data/app/services/application_service.rb +4 -0
  25. data/app/services/rails_iam/authentication/authenticated_user.rb +13 -0
  26. data/app/services/rails_iam/authentication/authenticator.rb +39 -0
  27. data/app/services/rails_iam/authentication/current.rb +9 -0
  28. data/app/services/rails_iam/authentication/exceptions/authentication_error.rb +10 -0
  29. data/app/services/rails_iam/authentication/jwt_decoder.rb +38 -0
  30. data/app/services/rails_iam/authentication/jwt_encoder.rb +45 -0
  31. data/app/services/rails_iam/authentication/login.rb +67 -0
  32. data/app/services/rails_iam/authentication/logout.rb +35 -0
  33. data/app/services/rails_iam/authentication/refresh_token_encoder.rb +11 -0
  34. data/app/services/rails_iam/authentication/skip_authentication_rule.rb +20 -0
  35. data/app/services/rails_iam/authentication/token_extractor.rb +64 -0
  36. data/app/services/rails_iam/authorization/authorization_context.rb +41 -0
  37. data/app/services/rails_iam/authorization/authorization_rule.rb +38 -0
  38. data/app/services/rails_iam/authorization/authorizer.rb +35 -0
  39. data/app/services/rails_iam/authorization/cache.rb +39 -0
  40. data/app/services/rails_iam/authorization/denied_permission_checker.rb +33 -0
  41. data/app/services/rails_iam/authorization/exceptions/authorization_error.rb +10 -0
  42. data/app/services/rails_iam/authorization/exceptions/missing_permission_error.rb +35 -0
  43. data/app/services/rails_iam/authorization/exceptions/missing_role_error.rb +35 -0
  44. data/app/services/rails_iam/authorization/permission_checker.rb +42 -0
  45. data/app/services/rails_iam/authorization/permission_resolver.rb +70 -0
  46. data/app/services/rails_iam/authorization/permission_sources/role_permission_source.rb +37 -0
  47. data/app/services/rails_iam/authorization/permission_sources/user_denied_permission_source.rb +29 -0
  48. data/app/services/rails_iam/authorization/permission_sources/user_permission_source.rb +28 -0
  49. data/app/services/rails_iam/authorization/resolved_permissions.rb +63 -0
  50. data/app/services/rails_iam/authorization/role_checker.rb +35 -0
  51. data/app/services/rails_iam/authorization/role_resolver.rb +30 -0
  52. data/app/services/rails_iam/authorization/skip_authorization_rule.rb +19 -0
  53. data/app/services/rails_iam/rails_iam_service.rb +11 -0
  54. data/config/routes.rb +25 -0
  55. data/lib/generators/rails_iam/install_generator.rb +49 -0
  56. data/lib/generators/rails_iam/templates/create_rails_iam_tables.rb.erb +119 -0
  57. data/lib/generators/rails_iam/templates/initializer.rb +162 -0
  58. data/lib/rails_iam/config/authentication.rb +65 -0
  59. data/lib/rails_iam/config/authorization.rb +36 -0
  60. data/lib/rails_iam/config/jwt.rb +27 -0
  61. data/lib/rails_iam/configuration.rb +30 -0
  62. data/lib/rails_iam/controller_dsl.rb +41 -0
  63. data/lib/rails_iam/engine.rb +34 -0
  64. data/lib/rails_iam/model_dsl.rb +13 -0
  65. data/lib/rails_iam/version.rb +5 -0
  66. data/lib/rails_iam.rb +27 -0
  67. data/lib/tasks/rails_iam_tasks.rake +6 -0
  68. data/rails_iam.gemspec +46 -0
  69. 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsIam
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ include RailsIam::Auditable
7
+ end
8
+ 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsIam
4
+ class RefreshToken < ApplicationRecord
5
+ belongs_to :user, class_name: RailsIam.configuration.user_class
6
+ end
7
+ 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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationService
4
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsIam
4
+ module Authentication
5
+ AuthenticatedUser = Data.define(
6
+ :user,
7
+ :access_token,
8
+ :access_token_expires_in,
9
+ :refresh_token,
10
+ :refresh_token_expires_in,
11
+ )
12
+ end
13
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsIam
4
+ module Authentication
5
+ class Current < ActiveSupport::CurrentAttributes
6
+ attribute :user
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsIam
4
+ module Authentication
5
+ module Exceptions
6
+ class AuthenticationError < StandardError
7
+ end
8
+ end
9
+ end
10
+ 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