jwt_auth_engine 1.0.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 (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +37 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +422 -0
  5. data/app/controllers/concerns/jwt_auth_engine/authenticatable.rb +52 -0
  6. data/app/controllers/concerns/jwt_auth_engine/rescuable.rb +23 -0
  7. data/app/controllers/concerns/jwt_auth_engine/response_renderable.rb +29 -0
  8. data/app/controllers/concerns/jwt_auth_engine/serializable.rb +21 -0
  9. data/app/controllers/concerns/jwt_auth_engine/tokenizable.rb +22 -0
  10. data/app/controllers/jwt_auth_engine/application_controller.rb +21 -0
  11. data/app/controllers/jwt_auth_engine/passwords_controller.rb +26 -0
  12. data/app/controllers/jwt_auth_engine/ping_controller.rb +21 -0
  13. data/app/controllers/jwt_auth_engine/profiles_controller.rb +15 -0
  14. data/app/controllers/jwt_auth_engine/registrations_controller.rb +33 -0
  15. data/app/controllers/jwt_auth_engine/sessions_controller.rb +41 -0
  16. data/app/controllers/jwt_auth_engine/tokens_controller.rb +21 -0
  17. data/app/services/jwt_auth_engine/base_service.rb +26 -0
  18. data/app/services/jwt_auth_engine/change_password_service.rb +48 -0
  19. data/app/services/jwt_auth_engine/login_service.rb +46 -0
  20. data/app/services/jwt_auth_engine/refresh_token_service.rb +35 -0
  21. data/app/services/jwt_auth_engine/signup_service.rb +23 -0
  22. data/app/services/jwt_auth_engine/token_service.rb +72 -0
  23. data/config/routes.rb +47 -0
  24. data/lib/generators/jwt_auth_engine/install/install_generator.rb +95 -0
  25. data/lib/generators/jwt_auth_engine/install/templates/add_jwt_auth_engine_columns_migration.rb.tt +11 -0
  26. data/lib/generators/jwt_auth_engine/install/templates/auth_model_concern.rb.tt +32 -0
  27. data/lib/generators/jwt_auth_engine/install/templates/jwt_auth_engine_initializer.rb.tt +22 -0
  28. data/lib/jwt_auth_engine/configuration.rb +50 -0
  29. data/lib/jwt_auth_engine/constants.rb +16 -0
  30. data/lib/jwt_auth_engine/engine.rb +10 -0
  31. data/lib/jwt_auth_engine/errors.rb +14 -0
  32. data/lib/jwt_auth_engine/version.rb +5 -0
  33. data/lib/jwt_auth_engine.rb +79 -0
  34. data/sig/jwt_auth_engine.rbs +4 -0
  35. metadata +137 -0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Health check endpoints for public and authenticated connectivity.
5
+ class PingController < ApplicationController
6
+ skip_before_action :authenticate_auth_model!, only: %i[ping]
7
+
8
+ # GET /ping
9
+ # Public — no auth required
10
+ def ping
11
+ render_success(message: 'pong!')
12
+ end
13
+
14
+ # GET /authenticated_ping
15
+ # Protected — valid Bearer access token required
16
+ def authenticated_ping
17
+ recipient = current_auth_model_instance.public_send(JwtAuthEngine.identifier_field)
18
+ render_success(message: "pong! Hello #{recipient}, you are authenticated.")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Profile retrieval endpoint for the authenticated auth model.
5
+ class ProfilesController < ApplicationController
6
+ include Serializable
7
+
8
+ # ── GET /me ───────────────────────────────────────────────────────────────
9
+ def me
10
+ render_success(
11
+ JwtAuthEngine.auth_model_name => serialize_auth_model_instance(current_auth_model_instance)
12
+ )
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Signup endpoint for creating new auth model records.
5
+ class RegistrationsController < ApplicationController
6
+ include Serializable
7
+ include Tokenizable
8
+
9
+ skip_before_action :authenticate_auth_model!, only: %i[signup]
10
+
11
+ # ── POST /signup ─────────────────────────────────────────────────────────
12
+ def signup
13
+ result = SignupService.new(signup_params: signup_params).call
14
+
15
+ return render_validation_error(errors: result[:errors]) unless result[:success]
16
+
17
+ auth_model_instance = result[JwtAuthEngine.auth_model_name]
18
+
19
+ render_success(
20
+ message: 'Signup successful.',
21
+ JwtAuthEngine.auth_model_name => serialize_auth_model_instance(auth_model_instance),
22
+ **issue_tokens(auth_model_instance),
23
+ status: :created
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def signup_params
30
+ params.permit(JwtAuthEngine.identifier_field, JwtAuthEngine.password_field)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Login and logout endpoints for session token lifecycle.
5
+ class SessionsController < ApplicationController
6
+ include Serializable
7
+ include Tokenizable
8
+
9
+ skip_before_action :authenticate_auth_model!, only: %i[login]
10
+
11
+ # ── POST /login ──────────────────────────────────────────────────────────
12
+ def login
13
+ result = LoginService.new(login_params: login_params).call
14
+
15
+ return render_unauthorized(result[:error]) unless result[:success]
16
+
17
+ auth_model_instance = result[JwtAuthEngine.auth_model_name]
18
+
19
+ render_success(
20
+ message: 'Login successful.',
21
+ JwtAuthEngine.auth_model_name => serialize_auth_model_instance(auth_model_instance),
22
+ **issue_tokens(auth_model_instance)
23
+ )
24
+ end
25
+
26
+ # ── DELETE /logout ─────────────────────────────────────────────────────────
27
+ # Stateless logout: the client should discard tokens.
28
+ def logout
29
+ render_success(status: :no_content)
30
+ end
31
+
32
+ private
33
+
34
+ def login_params
35
+ params.permit(
36
+ JwtAuthEngine.identifier_field,
37
+ JwtAuthEngine.password_field
38
+ )
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Endpoint for exchanging refresh tokens for a new token pair.
5
+ class TokensController < ApplicationController
6
+ include Tokenizable
7
+
8
+ skip_before_action :authenticate_auth_model!, only: %i[refresh_token]
9
+
10
+ # ── POST /refresh_token ───────────────────────────────────────────────────
11
+ # Verifies refresh JWT and re-issues a new token pair.
12
+ def refresh_token
13
+ result = RefreshTokenService.new(refresh_token: bearer_token).call
14
+ return render_unauthorized(result[:error]) unless result[:success]
15
+
16
+ auth_model_instance = result[JwtAuthEngine.auth_model_name]
17
+
18
+ render_success(**issue_tokens(auth_model_instance))
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Base service with common success/failure response helpers.
5
+ class BaseService
6
+ def success(**data)
7
+ { success: true, **data }
8
+ end
9
+
10
+ def failure(**data)
11
+ { success: false, **data }
12
+ end
13
+
14
+ private
15
+
16
+ def authenticate(auth_model_instance, password)
17
+ return false if auth_model_instance.blank? || password.blank?
18
+
19
+ auth_model_instance.public_send(authentication_method, password)
20
+ end
21
+
22
+ def authentication_method
23
+ JwtAuthEngine.password_field == :password ? :authenticate : :"authenticate_#{JwtAuthEngine.password_field}"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Changes password for an authenticated auth model record.
5
+ class ChangePasswordService < BaseService
6
+ attr_reader :auth_model_instance, :current_password, :new_password
7
+
8
+ def initialize(auth_model_instance:, change_password_params:)
9
+ @auth_model_instance = auth_model_instance
10
+ @current_password = change_password_params[JwtAuthEngine.current_password_field]
11
+ @new_password = change_password_params[JwtAuthEngine.new_password_field]
12
+ super()
13
+ end
14
+
15
+ def call
16
+ return failure(invalid: required_fields_message) if missing_password_fields?
17
+ return failure(invalid: incorrect_password_message) unless current_password_valid?
18
+
19
+ update_password
20
+ end
21
+
22
+ private
23
+
24
+ def missing_password_fields?
25
+ current_password.blank? || new_password.blank?
26
+ end
27
+
28
+ def current_password_valid?
29
+ authenticate(auth_model_instance, current_password)
30
+ end
31
+
32
+ def update_password
33
+ if auth_model_instance.update(JwtAuthEngine.password_field => new_password.to_s)
34
+ success(message: 'Password changed successfully.')
35
+ else
36
+ failure(errors: auth_model_instance.errors.full_messages)
37
+ end
38
+ end
39
+
40
+ def required_fields_message
41
+ "#{JwtAuthEngine.current_password_field} and #{JwtAuthEngine.new_password_field} are required."
42
+ end
43
+
44
+ def incorrect_password_message
45
+ "Current #{JwtAuthEngine.password_field} is incorrect."
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Authenticates identifier/password and returns the matched auth model.
5
+ class LoginService < BaseService
6
+ def initialize(login_params:)
7
+ @identifier = login_params[JwtAuthEngine.identifier_field]
8
+ @password = login_params[JwtAuthEngine.password_field]
9
+ super()
10
+ end
11
+
12
+ def call
13
+ if identifier.blank? || password.blank?
14
+ return failure(error: "#{JwtAuthEngine.identifier_field} and #{JwtAuthEngine.password_field} are required.")
15
+ end
16
+
17
+ auth_model_instance = find_auth_model_instance
18
+
19
+ if authenticate(auth_model_instance, password)
20
+ success(JwtAuthEngine.auth_model_name => auth_model_instance)
21
+ else
22
+ failure(error: 'Invalid credentials.')
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :identifier, :password
29
+
30
+ def find_auth_model_instance
31
+ model = JwtAuthEngine.auth_model_class
32
+
33
+ unless case_insensitive_identifier_field?(model)
34
+ return model.find_by(JwtAuthEngine.identifier_field => identifier)
35
+ end
36
+
37
+ identifier_column = model.connection.quote_column_name(JwtAuthEngine.identifier_field)
38
+ model.where("LOWER(#{identifier_column}) = ?", identifier.to_s.downcase).first
39
+ end
40
+
41
+ def case_insensitive_identifier_field?(model)
42
+ identifier_type = model.type_for_attribute(JwtAuthEngine.identifier_field.to_s).type
43
+ %i[string text].include?(identifier_type)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Validates refresh tokens and resolves the associated auth model.
5
+ class RefreshTokenService < BaseService
6
+ attr_reader :refresh_token
7
+
8
+ def initialize(refresh_token:)
9
+ @refresh_token = refresh_token
10
+ super()
11
+ end
12
+
13
+ def call
14
+ return failure(error: 'refresh_token is required.') if refresh_token.blank?
15
+
16
+ success(JwtAuthEngine.auth_model_name => auth_model_instance_from_token)
17
+ rescue JwtAuthEngine::TokenExpired
18
+ failure(error: 'Refresh token has expired.')
19
+ rescue JwtAuthEngine::InvalidToken
20
+ failure(error: 'Invalid refresh token.')
21
+ rescue JwtAuthEngine::InvalidTokenType
22
+ failure(error: 'Refresh token expected')
23
+ rescue ActiveRecord::RecordNotFound
24
+ failure(error: "#{JwtAuthEngine.auth_model_name.to_s.humanize} not found.")
25
+ end
26
+
27
+ private
28
+
29
+ def auth_model_instance_from_token
30
+ payload = TokenService.decode_refresh(refresh_token)
31
+ auth_model_id = payload[JwtAuthEngine.auth_model_token_payload_key]
32
+ JwtAuthEngine.auth_model_class.find(auth_model_id)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Creates a new auth model record from signup parameters.
5
+ class SignupService < BaseService
6
+ attr_reader :signup_params
7
+
8
+ def initialize(signup_params:)
9
+ @signup_params = signup_params
10
+ super()
11
+ end
12
+
13
+ def call
14
+ auth_model_instance = JwtAuthEngine.auth_model_class.new(signup_params)
15
+
16
+ if auth_model_instance.save
17
+ success(JwtAuthEngine.auth_model_name => auth_model_instance)
18
+ else
19
+ failure(errors: auth_model_instance.errors.full_messages)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module JwtAuthEngine
6
+ # Encodes and decodes JWT access/refresh tokens for the engine.
7
+ class TokenService
8
+ ACCESS_TYPE = 'access'
9
+ REFRESH_TYPE = 'refresh'
10
+ ALGORITHM = 'HS256'
11
+
12
+ class << self
13
+ # ── Encoding ─────────────────────────────────────────────────────────────
14
+
15
+ # Encodes an access token with the given payload. Automatically adds exp and token_type.
16
+ def encode_access(payload)
17
+ exp = JwtAuthEngine.configuration.access_token_expiry.from_now.to_i
18
+ encode(payload.merge(exp: exp, token_type: ACCESS_TYPE))
19
+ end
20
+
21
+ # Encodes a refresh token with the given payload. Automatically adds exp and token_type.
22
+ def encode_refresh(payload)
23
+ exp = JwtAuthEngine.configuration.refresh_token_expiry.from_now.to_i
24
+ encode(payload.merge(exp: exp, token_type: REFRESH_TYPE))
25
+ end
26
+
27
+ # ── Decoding ─────────────────────────────────────────────────────────────
28
+
29
+ # Decodes an access token. Raises on invalid/expired token.
30
+ def decode_access(token)
31
+ decode(token, expected_type: ACCESS_TYPE)
32
+ end
33
+
34
+ # Decodes a refresh token. Raises on invalid/expired token.
35
+ def decode_refresh(token)
36
+ decode(token, expected_type: REFRESH_TYPE)
37
+ end
38
+
39
+ # ── Private ──────────────────────────────────────────────────────────────
40
+
41
+ private
42
+
43
+ def encode(payload)
44
+ JWT.encode(payload, secret, ALGORITHM)
45
+ end
46
+
47
+ def decode(token, expected_type:)
48
+ decoded = JWT.decode(token, secret, true, { algorithm: ALGORITHM })
49
+ payload = HashWithIndifferentAccess.new(decoded[0])
50
+
51
+ validate_token_type!(payload, expected_type)
52
+
53
+ payload
54
+ rescue JWT::ExpiredSignature
55
+ raise JwtAuthEngine::TokenExpired, 'Token has expired'
56
+ rescue JWT::DecodeError => e
57
+ raise JwtAuthEngine::InvalidToken, "Invalid token: #{e.message}"
58
+ end
59
+
60
+ def validate_token_type!(payload, expected_type)
61
+ return if payload[:token_type] == expected_type
62
+
63
+ raise JwtAuthEngine::InvalidTokenType,
64
+ "Expected a #{expected_type} token but received #{payload[:token_type]}"
65
+ end
66
+
67
+ def secret
68
+ JwtAuthEngine.configuration.jwt_secret_key
69
+ end
70
+ end
71
+ end
72
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ JwtAuthEngine::Engine.routes.draw do
4
+ defaults format: :json do
5
+ # ── Ping endpoints ──────────────────────────────────────────────────────
6
+ controller :ping do
7
+ # GET /ping - public health check endpoint
8
+ get :ping
9
+
10
+ # GET /authenticated_ping - protected endpoint requiring valid access token
11
+ get :authenticated_ping
12
+ end
13
+
14
+ # ── Registration ─────────────────────────────────────────────────────────
15
+ controller :registrations do
16
+ # POST /signup - register a new account
17
+ post :signup
18
+ end
19
+
20
+ # ── Sessions ─────────────────────────────────────────────────────────────
21
+ controller :sessions do
22
+ # POST /login - authenticate and receive tokens
23
+ post :login
24
+
25
+ # DELETE /logout - stateless logout (client should discard tokens)
26
+ delete :logout
27
+ end
28
+
29
+ # ── Tokens ───────────────────────────────────────────────────────────────
30
+ controller :tokens do
31
+ # POST /refresh_token - exchange a valid refresh token for new tokens
32
+ post :refresh_token
33
+ end
34
+
35
+ # ── Passwords ────────────────────────────────────────────────────────────
36
+ controller :passwords do
37
+ # POST /change_password - change password for authenticated auth_model
38
+ post :change_password
39
+ end
40
+
41
+ # ── Profiles ─────────────────────────────────────────────────────────────
42
+ controller :profiles do
43
+ # GET /me - retrieve profile of authenticated auth_model
44
+ get :me
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/migration'
5
+
6
+ module JwtAuthEngine
7
+ module Generators
8
+ # Generator that installs initializer, migration, and model concern wiring.
9
+ class InstallGenerator < Rails::Generators::Base
10
+ include Rails::Generators::Migration
11
+
12
+ source_root File.expand_path('templates', __dir__)
13
+
14
+ AUTH_MODEL_CONCERN_INCLUDE = 'include JwtAuthEngine::AuthModelConcern'
15
+
16
+ class_option :auth_model,
17
+ type: :string, default: 'User', banner: 'User',
18
+ desc: 'Auth model class name (e.g., User, Account, Admin::User)'
19
+
20
+ class_option :identifier_field,
21
+ type: :string, default: 'email', banner: 'email',
22
+ desc: 'Identifier field for login (e.g., email, username)'
23
+
24
+ class_option :password_field,
25
+ type: :string, default: 'password', banner: 'password',
26
+ desc: 'Password attribute name for has_secure_password'
27
+
28
+ def create_initializer
29
+ @auth_model = options[:auth_model]
30
+ @identifier_field = options[:identifier_field].to_sym
31
+ @password_field = options[:password_field].to_sym
32
+ template 'jwt_auth_engine_initializer.rb.tt', 'config/initializers/jwt_auth_engine.rb'
33
+ end
34
+
35
+ def copy_migration
36
+ @auth_model_table = options[:auth_model].tableize
37
+ @identifier_field = options[:identifier_field].to_sym
38
+ @password_digest_field = :"#{options[:password_field]}_digest"
39
+ @migration_class_name = "AddJwtAuthEngineColumnsTo#{@auth_model_table.classify.tr('::', '')}"
40
+ @migration_version = host_active_record_migration_version
41
+
42
+ migration_template(
43
+ 'add_jwt_auth_engine_columns_migration.rb.tt',
44
+ "db/migrate/add_jwt_auth_engine_columns_to_#{@auth_model_table}.rb"
45
+ )
46
+ end
47
+
48
+ def create_auth_model_concern
49
+ template 'auth_model_concern.rb.tt', 'app/models/concerns/jwt_auth_engine/auth_model_concern.rb'
50
+ end
51
+
52
+ def inject_auth_model_concern
53
+ return warn_missing_auth_model_file unless File.exist?(auth_model_path)
54
+ return note_existing_auth_model_concern if auth_model_concern_included?
55
+
56
+ inject_into_class auth_model_path, options[:auth_model], " #{AUTH_MODEL_CONCERN_INCLUDE}\n\n"
57
+ end
58
+
59
+ def self.next_migration_number(dirname)
60
+ if defined?(ActiveRecord::Generators::Base)
61
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
62
+ else
63
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def auth_model_path
70
+ "app/models/#{options[:auth_model].underscore}.rb"
71
+ end
72
+
73
+ def warn_missing_auth_model_file
74
+ warning_message = "#{auth_model_path} not found. " \
75
+ "Add `#{AUTH_MODEL_CONCERN_INCLUDE}` to #{options[:auth_model]} manually."
76
+ say_status :warning, warning_message, :yellow
77
+ end
78
+
79
+ def auth_model_concern_included?
80
+ File.read(auth_model_path).include?(AUTH_MODEL_CONCERN_INCLUDE)
81
+ end
82
+
83
+ def note_existing_auth_model_concern
84
+ message = "#{auth_model_path} already includes JwtAuthEngine::AuthModelConcern"
85
+ say_status :identical, message, :blue
86
+ end
87
+
88
+ def host_active_record_migration_version
89
+ return ActiveRecord::Migration.current_version if ActiveRecord::Migration.respond_to?(:current_version)
90
+
91
+ "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @migration_class_name %> < ActiveRecord::Migration[<%= @migration_version %>]
4
+ def change
5
+ create_table :<%= @auth_model_table %>, if_not_exists: true, &:timestamps
6
+
7
+ add_column :<%= @auth_model_table %>, :<%= @identifier_field %>, :string, if_not_exists: true
8
+ add_column :<%= @auth_model_table %>, :<%= @password_digest_field %>, :string, if_not_exists: true
9
+ add_index :<%= @auth_model_table %>, :<%= @identifier_field %>, unique: true, if_not_exists: true
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ module AuthModelConcern
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_secure_password JwtAuthEngine.password_field
9
+
10
+ validates JwtAuthEngine.identifier_field, presence: true, uniqueness: { case_sensitive: false }
11
+ validates JwtAuthEngine.password_field, length: { minimum: 8 }, allow_nil: true
12
+
13
+ # Generated default normalization for the configured identifier field.
14
+ # You can customize or remove this callback to match your identifier semantics.
15
+ before_validation :normalize_identifier
16
+ end
17
+
18
+ private
19
+
20
+ def normalize_identifier
21
+ field = JwtAuthEngine.identifier_field
22
+ reader = field.to_sym
23
+ writer = :"#{field}="
24
+ return unless respond_to?(reader) && respond_to?(writer)
25
+
26
+ value = public_send(reader)
27
+ return unless value.is_a?(String)
28
+
29
+ public_send(writer, value.strip.downcase)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ JwtAuthEngine.configure do |config|
4
+ # Use your host application's auth model (e.g. User, Account, Admin::User)
5
+ config.auth_model = '<%= @auth_model %>'
6
+
7
+ # Field used by login/signup lookup. Most apps should keep :email.
8
+ config.identifier_field = :<%= @identifier_field %>
9
+
10
+ # Base password attribute used by has_secure_password.
11
+ # Digest column is derived as "#{password_field}_digest".
12
+ config.password_field = :<%= @password_field %>
13
+
14
+ # Required: set a secure secret key from your host app configuration.
15
+ # Example:
16
+ # config.jwt_secret_key = Rails.application.credentials.dig(:jwt_auth_engine, :secret_key)
17
+ config.jwt_secret_key = nil
18
+
19
+ # Token expiry windows.
20
+ config.access_token_expiry = 8.hours
21
+ config.refresh_token_expiry = 7.days
22
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Runtime configuration container for auth model and token settings.
5
+ class Configuration
6
+ def initialize
7
+ @jwt_secret_key = nil
8
+ @auth_model = nil
9
+ @identifier_field = nil
10
+ @password_field = nil
11
+ @access_token_expiry = nil
12
+ @refresh_token_expiry = nil
13
+ end
14
+
15
+ attr_writer :jwt_secret_key,
16
+ :auth_model,
17
+ :identifier_field,
18
+ :password_field,
19
+ :access_token_expiry,
20
+ :refresh_token_expiry
21
+
22
+ def jwt_secret_key
23
+ @jwt_secret_key.presence || (
24
+ raise MissingSecretKey,
25
+ 'Missing JWT secret key. ' \
26
+ "Set `config.#{JwtAuthEngine::Constants::JWT_SECRET_CONFIG_KEY}` in your initializer."
27
+ )
28
+ end
29
+
30
+ def auth_model
31
+ @auth_model.presence || Constants::DEFAULT_AUTH_MODEL
32
+ end
33
+
34
+ def identifier_field
35
+ @identifier_field.presence || Constants::DEFAULT_IDENTIFIER_FIELD
36
+ end
37
+
38
+ def password_field
39
+ @password_field.presence || Constants::DEFAULT_PASSWORD_FIELD
40
+ end
41
+
42
+ def access_token_expiry
43
+ @access_token_expiry.presence || Constants::DEFAULT_ACCESS_TOKEN_EXPIRY
44
+ end
45
+
46
+ def refresh_token_expiry
47
+ @refresh_token_expiry.presence || Constants::DEFAULT_REFRESH_TOKEN_EXPIRY
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/integer/time'
4
+
5
+ module JwtAuthEngine
6
+ module Constants
7
+ JWT_SECRET_CONFIG_KEY = 'jwt_secret_key'
8
+
9
+ DEFAULT_AUTH_MODEL = 'User'
10
+ DEFAULT_IDENTIFIER_FIELD = :email
11
+ DEFAULT_PASSWORD_FIELD = :password
12
+
13
+ DEFAULT_ACCESS_TOKEN_EXPIRY = 8.hours
14
+ DEFAULT_REFRESH_TOKEN_EXPIRY = 7.days
15
+ end
16
+ end