devise-api 0.1.3 → 0.3.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/.claude/commands/commit.md +38 -0
- data/.claude/commands/create-branch.md +57 -0
- data/.claude/commands/create-pr.md +41 -0
- data/CHANGELOG.md +54 -1
- data/CLAUDE.md +49 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +11 -1
- data/README.md +320 -159
- data/Rakefile +6 -0
- data/app/controllers/devise/api/tokens_controller.rb +50 -109
- data/app/services/devise/api/resource_owner_service/authenticate.rb +9 -2
- data/app/services/devise/api/tokens_service/create.rb +16 -8
- data/app/services/devise/api/tokens_service/refresh.rb +17 -2
- data/app/services/devise/api/tokens_service/revoke.rb +1 -1
- data/config/locales/en.yml +1 -0
- data/docs/README.md +32 -0
- data/docs/analysis/known-issues.md +56 -0
- data/docs/analysis/security-review.md +53 -0
- data/docs/api-reference.md +95 -0
- data/docs/architecture.md +145 -0
- data/docs/configuration.md +84 -0
- data/docs/data-model.md +80 -0
- data/docs/development.md +52 -0
- data/docs/extending.md +75 -0
- data/docs/services.md +72 -0
- data/docs/testing.md +65 -0
- data/lib/devise/api/configuration.rb +8 -0
- data/lib/devise/api/controllers/helpers.rb +5 -3
- data/lib/devise/api/generators/templates/migration.rb.erb +2 -2
- data/lib/devise/api/rails/engine.rb +6 -0
- data/lib/devise/api/responses/error_response.rb +40 -29
- data/lib/devise/api/responses/token_response.rb +15 -7
- data/lib/devise/api/token.rb +34 -2
- data/lib/devise/api/version.rb +1 -1
- metadata +18 -4
- data/sig/devise/api.rbs +0 -6
|
@@ -5,6 +5,12 @@ module Devise
|
|
|
5
5
|
module Rails
|
|
6
6
|
class Engine < ::Rails::Engine
|
|
7
7
|
isolate_namespace Devise::Api
|
|
8
|
+
|
|
9
|
+
# Keep raw token secrets out of the host app's request logs (tokens can arrive as
|
|
10
|
+
# query/body params when authorization.location is :params or :both)
|
|
11
|
+
initializer 'devise.api.filter_parameters' do |app|
|
|
12
|
+
app.config.filter_parameters |= %i[access_token refresh_token previous_refresh_token]
|
|
13
|
+
end
|
|
8
14
|
end
|
|
9
15
|
end
|
|
10
16
|
end
|
|
@@ -15,6 +15,7 @@ module Devise
|
|
|
15
15
|
sign_up_disabled
|
|
16
16
|
invalid_refresh_token
|
|
17
17
|
invalid_email
|
|
18
|
+
invalid_login
|
|
18
19
|
invalid_resource_owner
|
|
19
20
|
resource_owner_create_error
|
|
20
21
|
devise_api_token_create_error
|
|
@@ -22,6 +23,15 @@ module Devise
|
|
|
22
23
|
invalid_authentication
|
|
23
24
|
].freeze
|
|
24
25
|
|
|
26
|
+
UNAUTHORIZED_ERRORS = %i[
|
|
27
|
+
invalid_token expired_token expired_refresh_token revoked_token invalid_authentication
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
BAD_REQUEST_ERRORS = %i[
|
|
31
|
+
invalid_email invalid_login invalid_refresh_token refresh_token_disabled sign_up_disabled
|
|
32
|
+
invalid_resource_owner
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
25
35
|
ERROR_TYPES.each do |error_type|
|
|
26
36
|
method_name = error_type.end_with?('_error') ? error_type : "#{error_type}_error"
|
|
27
37
|
|
|
@@ -55,33 +65,34 @@ module Devise
|
|
|
55
65
|
|
|
56
66
|
private
|
|
57
67
|
|
|
58
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
59
68
|
def error_description
|
|
60
69
|
return [I18n.t("devise.api.error_response.#{error}")] if record.blank?
|
|
61
|
-
if invalid_authentication_error?
|
|
62
|
-
return [I18n.t('devise.api.error_response.lockable.locked')]
|
|
63
|
-
end
|
|
64
|
-
if invalid_authentication_error? && devise_confirmable_info.present? && !record.confirmed?
|
|
65
|
-
return [I18n.t('devise.api.error_response.confirmable.unconfirmed')]
|
|
66
|
-
end
|
|
67
|
-
return [I18n.t('devise.api.error_response.invalid_authentication')] if invalid_authentication_error?
|
|
70
|
+
return invalid_authentication_description if invalid_authentication_error?
|
|
68
71
|
|
|
69
72
|
record.errors.full_messages
|
|
70
73
|
end
|
|
71
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
72
74
|
|
|
73
|
-
def
|
|
74
|
-
unless
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
def invalid_authentication_description
|
|
76
|
+
unless Devise.api.config.paranoid
|
|
77
|
+
return [I18n.t('devise.api.error_response.lockable.locked')] if lockable_record? && record.access_locked?
|
|
78
|
+
if confirmable_record? && !record.confirmed?
|
|
79
|
+
return [I18n.t('devise.api.error_response.confirmable.unconfirmed')]
|
|
80
|
+
end
|
|
78
81
|
end
|
|
79
82
|
|
|
83
|
+
[I18n.t('devise.api.error_response.invalid_authentication')]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def devise_lockable_info
|
|
87
|
+
return nil unless verbose_account_state? && lockable_record? && invalid_authentication_error?
|
|
88
|
+
|
|
80
89
|
unlock_at = record.access_locked? ? record.locked_at + ::Devise.unlock_in : nil
|
|
81
90
|
|
|
82
91
|
{
|
|
83
92
|
locked: record.access_locked?,
|
|
84
93
|
max_attempts: ::Devise.maximum_attempts,
|
|
94
|
+
failed_attempts: record.failed_attempts,
|
|
95
|
+
# Deprecated misspelling kept for backward compatibility; will be removed in the next major release
|
|
85
96
|
failed_attemps: record.failed_attempts,
|
|
86
97
|
locked_at: record.locked_at,
|
|
87
98
|
unlock_at: unlock_at
|
|
@@ -89,11 +100,7 @@ module Devise
|
|
|
89
100
|
end
|
|
90
101
|
|
|
91
102
|
def devise_confirmable_info
|
|
92
|
-
unless
|
|
93
|
-
resource_class.supported_devise_modules.confirmable? &&
|
|
94
|
-
invalid_authentication_error?
|
|
95
|
-
return nil
|
|
96
|
-
end
|
|
103
|
+
return nil unless verbose_account_state? && confirmable_record? && invalid_authentication_error?
|
|
97
104
|
|
|
98
105
|
{
|
|
99
106
|
confirmed: record.confirmed?,
|
|
@@ -101,20 +108,24 @@ module Devise
|
|
|
101
108
|
}.compact
|
|
102
109
|
end
|
|
103
110
|
|
|
111
|
+
def lockable_record?
|
|
112
|
+
record.present? && resource_class.present? && resource_class.supported_devise_modules.lockable?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def confirmable_record?
|
|
116
|
+
record.present? && resource_class.present? && resource_class.supported_devise_modules.confirmable?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def verbose_account_state?
|
|
120
|
+
!Devise.api.config.paranoid && Devise.api.config.error_response.verbose_account_state
|
|
121
|
+
end
|
|
122
|
+
|
|
104
123
|
def unauthorized_status?
|
|
105
|
-
|
|
106
|
-
expired_token_error? ||
|
|
107
|
-
expired_refresh_token_error? ||
|
|
108
|
-
revoked_token_error? ||
|
|
109
|
-
invalid_authentication_error?
|
|
124
|
+
UNAUTHORIZED_ERRORS.include?(error)
|
|
110
125
|
end
|
|
111
126
|
|
|
112
127
|
def bad_request_status?
|
|
113
|
-
|
|
114
|
-
invalid_refresh_token_error? ||
|
|
115
|
-
refresh_token_disabled_error? ||
|
|
116
|
-
sign_up_disabled_error? ||
|
|
117
|
-
invalid_resource_owner_error?
|
|
128
|
+
BAD_REQUEST_ERRORS.include?(error)
|
|
118
129
|
end
|
|
119
130
|
end
|
|
120
131
|
end
|
|
@@ -41,15 +41,19 @@ module Devise
|
|
|
41
41
|
refresh_token: Devise.api.config.refresh_token.enabled ? token.refresh_token : nil,
|
|
42
42
|
expires_in: token.expires_in,
|
|
43
43
|
token_type: ::Devise.api.config.authorization.scheme,
|
|
44
|
-
resource_owner:
|
|
45
|
-
id: resource_owner.id,
|
|
46
|
-
email: resource_owner.email,
|
|
47
|
-
created_at: resource_owner.created_at,
|
|
48
|
-
updated_at: resource_owner.updated_at
|
|
49
|
-
}
|
|
44
|
+
resource_owner: default_resource_owner
|
|
50
45
|
}.compact
|
|
51
46
|
end
|
|
52
47
|
|
|
48
|
+
def default_resource_owner
|
|
49
|
+
keys_to_extract = %i[id email created_at updated_at]
|
|
50
|
+
if Devise.api.config.sign_up.extra_fields.present?
|
|
51
|
+
keys_to_extract |= Devise.api.config.sign_up.extra_fields.map(&:to_sym)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
resource_owner.slice(*keys_to_extract)
|
|
55
|
+
end
|
|
56
|
+
|
|
53
57
|
def status
|
|
54
58
|
return :created if sign_up_action?
|
|
55
59
|
return :no_content if revoke_action?
|
|
@@ -62,7 +66,11 @@ module Devise
|
|
|
62
66
|
def signed_up_body
|
|
63
67
|
return default_body unless resource_owner.class.supported_devise_modules.confirmable?
|
|
64
68
|
|
|
65
|
-
message = resource_owner.confirmed?
|
|
69
|
+
message = if resource_owner.confirmed?
|
|
70
|
+
nil
|
|
71
|
+
else
|
|
72
|
+
I18n.t('devise.api.error_response.registerable.signed_up_but_unconfirmed')
|
|
73
|
+
end
|
|
66
74
|
|
|
67
75
|
default_body.merge(confirmable: { confirmed: resource_owner.confirmed?, message: message }.compact)
|
|
68
76
|
end
|
data/lib/devise/api/token.rb
CHANGED
|
@@ -7,6 +7,10 @@ module Devise
|
|
|
7
7
|
class Token < ::ActiveRecord::Base
|
|
8
8
|
self.table_name = 'devise_api_tokens'
|
|
9
9
|
|
|
10
|
+
# Redact raw token secrets from #inspect / pretty_print output (see also the
|
|
11
|
+
# devise.api.filter_parameters engine initializer for request-log filtering)
|
|
12
|
+
self.filter_attributes += %i[access_token refresh_token previous_refresh_token]
|
|
13
|
+
|
|
10
14
|
# associations
|
|
11
15
|
belongs_to :resource_owner,
|
|
12
16
|
polymorphic: true,
|
|
@@ -36,6 +40,21 @@ module Devise
|
|
|
36
40
|
revoked_at.present?
|
|
37
41
|
end
|
|
38
42
|
|
|
43
|
+
def revoke!
|
|
44
|
+
return self if revoked?
|
|
45
|
+
|
|
46
|
+
update!(revoked_at: Time.current)
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Revokes every token in this token's refresh chain (ancestors and descendants). Used for
|
|
51
|
+
# refresh-token reuse detection when refresh_token.rotation_enabled is on.
|
|
52
|
+
def revoke_family!
|
|
53
|
+
transaction do
|
|
54
|
+
family_tokens.each(&:revoke!)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
39
58
|
def active?
|
|
40
59
|
!inactive?
|
|
41
60
|
end
|
|
@@ -47,13 +66,13 @@ module Devise
|
|
|
47
66
|
def expired?
|
|
48
67
|
return false if Devise.api.config.access_token.expires_in_infinite.call(resource_owner)
|
|
49
68
|
|
|
50
|
-
!!(expires_in && Time.
|
|
69
|
+
!!(expires_in && Time.current > expires_at)
|
|
51
70
|
end
|
|
52
71
|
|
|
53
72
|
def refresh_token_expired?
|
|
54
73
|
return false if Devise.api.config.refresh_token.expires_in_infinite.call(resource_owner)
|
|
55
74
|
|
|
56
|
-
Time.
|
|
75
|
+
Time.current > refresh_token_expires_at
|
|
57
76
|
end
|
|
58
77
|
|
|
59
78
|
def self.generate_uniq_access_token(resource_owner)
|
|
@@ -83,6 +102,19 @@ module Devise
|
|
|
83
102
|
def refresh_token_expires_at
|
|
84
103
|
created_at + Devise.api.config.refresh_token.expires_in.seconds
|
|
85
104
|
end
|
|
105
|
+
|
|
106
|
+
def family_tokens
|
|
107
|
+
root = self
|
|
108
|
+
root = root.previous_refresh while root.previous_refresh.present?
|
|
109
|
+
|
|
110
|
+
tokens = []
|
|
111
|
+
queue = [root]
|
|
112
|
+
while (token = queue.shift)
|
|
113
|
+
tokens << token
|
|
114
|
+
queue.concat(token.refreshes.to_a)
|
|
115
|
+
end
|
|
116
|
+
tokens
|
|
117
|
+
end
|
|
86
118
|
end
|
|
87
119
|
end
|
|
88
120
|
end
|
data/lib/devise/api/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: devise-api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- nejdetkadir
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-08-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: devise
|
|
@@ -113,9 +113,13 @@ executables: []
|
|
|
113
113
|
extensions: []
|
|
114
114
|
extra_rdoc_files: []
|
|
115
115
|
files:
|
|
116
|
+
- ".claude/commands/commit.md"
|
|
117
|
+
- ".claude/commands/create-branch.md"
|
|
118
|
+
- ".claude/commands/create-pr.md"
|
|
116
119
|
- ".rspec"
|
|
117
120
|
- ".rubocop.yml"
|
|
118
121
|
- CHANGELOG.md
|
|
122
|
+
- CLAUDE.md
|
|
119
123
|
- CODE_OF_CONDUCT.md
|
|
120
124
|
- Gemfile
|
|
121
125
|
- Gemfile.lock
|
|
@@ -132,6 +136,17 @@ files:
|
|
|
132
136
|
- app/services/devise/api/tokens_service/revoke.rb
|
|
133
137
|
- config/locales/en.yml
|
|
134
138
|
- devise-api.gemspec
|
|
139
|
+
- docs/README.md
|
|
140
|
+
- docs/analysis/known-issues.md
|
|
141
|
+
- docs/analysis/security-review.md
|
|
142
|
+
- docs/api-reference.md
|
|
143
|
+
- docs/architecture.md
|
|
144
|
+
- docs/configuration.md
|
|
145
|
+
- docs/data-model.md
|
|
146
|
+
- docs/development.md
|
|
147
|
+
- docs/extending.md
|
|
148
|
+
- docs/services.md
|
|
149
|
+
- docs/testing.md
|
|
135
150
|
- lib/devise/api.rb
|
|
136
151
|
- lib/devise/api/configuration.rb
|
|
137
152
|
- lib/devise/api/controllers/helpers.rb
|
|
@@ -143,7 +158,6 @@ files:
|
|
|
143
158
|
- lib/devise/api/responses/token_response.rb
|
|
144
159
|
- lib/devise/api/token.rb
|
|
145
160
|
- lib/devise/api/version.rb
|
|
146
|
-
- sig/devise/api.rbs
|
|
147
161
|
homepage: https://github.com/nejdetkadir/devise-api
|
|
148
162
|
licenses:
|
|
149
163
|
- MIT
|
|
@@ -166,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
166
180
|
- !ruby/object:Gem::Version
|
|
167
181
|
version: '0'
|
|
168
182
|
requirements: []
|
|
169
|
-
rubygems_version: 3.4.
|
|
183
|
+
rubygems_version: 3.4.19
|
|
170
184
|
signing_key:
|
|
171
185
|
specification_version: 4
|
|
172
186
|
summary: The devise-api gem is a convenient way to add authentication to your Ruby
|