standard_id 0.41.1 → 0.42.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/CHANGELOG.md +66 -0
- data/README.md +251 -13
- data/app/controllers/concerns/standard_id/inertia_rendering.rb +23 -5
- data/app/controllers/concerns/standard_id/lifecycle_hooks.rb +1 -0
- data/app/controllers/concerns/standard_id/passwordless_flow.rb +11 -2
- data/app/controllers/concerns/standard_id/social_authentication.rb +1 -1
- data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +1 -8
- data/app/controllers/standard_id/web/login_verify_controller.rb +2 -0
- data/app/jobs/standard_id/password_reset_delivery_job.rb +1 -1
- data/app/models/concerns/standard_id/credentiable.rb +8 -1
- data/app/models/standard_id/application_record.rb +26 -0
- data/app/models/standard_id/authorization_code.rb +3 -1
- data/app/models/standard_id/identifier.rb +1 -0
- data/app/models/standard_id/session.rb +2 -1
- data/app/views/standard_id/web/login/_social_buttons.html.erb +2 -2
- data/app/views/standard_id/web/login/show.html.erb +5 -5
- data/app/views/standard_id/web/signup/show.html.erb +3 -3
- data/db/migrate/20250830000000_create_standard_id_client_applications.rb +2 -0
- data/db/migrate/20250830171553_create_standard_id_password_credentials.rb +2 -0
- data/db/migrate/20250830232800_create_standard_id_identifiers.rb +2 -0
- data/db/migrate/20250831075703_create_standard_id_credentials.rb +2 -0
- data/db/migrate/20250831154635_create_standard_id_sessions.rb +2 -0
- data/db/migrate/20250901134520_create_standard_id_client_secret_credentials.rb +2 -0
- data/db/migrate/20250903063000_create_standard_id_authorization_codes.rb +2 -0
- data/db/migrate/20250907090000_create_standard_id_code_challenges.rb +2 -0
- data/db/migrate/20260311100000_create_standard_id_refresh_tokens.rb +2 -0
- data/db/migrate/20260414200000_add_target_created_at_index_to_code_challenges.rb +1 -0
- data/db/migrate/20260416180511_add_partial_indexes_for_active_session_and_challenge_lookups.rb +25 -8
- data/db/migrate/20260611000000_create_standard_id_client_grants.rb +2 -0
- data/lib/generators/standard_id/install/install_generator.rb +64 -3
- data/lib/generators/standard_id/install/templates/standard_id.rb +42 -13
- data/lib/standard_id/checks/migrations.rb +61 -0
- data/lib/standard_id/config/schema.rb +36 -9
- data/lib/standard_id/config_schema.rb +28 -5
- data/lib/standard_id/deprecator.rb +17 -0
- data/lib/standard_id/engine.rb +21 -0
- data/lib/standard_id/instrumentation.rb +49 -0
- data/lib/standard_id/migration_check.rb +183 -0
- data/lib/standard_id/migration_helpers.rb +65 -0
- data/lib/standard_id/oauth/audience_profile_resolver.rb +8 -2
- data/lib/standard_id/oauth/refresh_token_flow.rb +1 -1
- data/lib/standard_id/oauth/token_grant_flow.rb +25 -3
- data/lib/standard_id/provider_registry.rb +113 -2
- data/lib/standard_id/providers/base.rb +222 -8
- data/lib/standard_id/providers/plugin_railtie.rb +59 -0
- data/lib/standard_id/scope_config.rb +24 -7
- data/lib/standard_id/testing/provider_examples.rb +117 -0
- data/lib/standard_id/testing.rb +1 -0
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id.rb +26 -0
- metadata +23 -17
- data/config/initializers/migration_helpers.rb +0 -32
|
@@ -181,7 +181,7 @@ module StandardId
|
|
|
181
181
|
# loop and leave later sessions without their SESSION_REVOKED event, which
|
|
182
182
|
# would permanently desync audit-trail consumers from the DB.
|
|
183
183
|
def self.publish_session_revocations(sessions, account:, reason:, now:)
|
|
184
|
-
shared_account = account || sessions.first.account
|
|
184
|
+
shared_account = account || preload_associations(sessions.first, :account).first&.account
|
|
185
185
|
|
|
186
186
|
sessions.each do |session|
|
|
187
187
|
session.revoked_at = now
|
|
@@ -277,6 +277,7 @@ module StandardId
|
|
|
277
277
|
end
|
|
278
278
|
|
|
279
279
|
def emit_session_revoked_event
|
|
280
|
+
preload_associations(:account)
|
|
280
281
|
StandardId::Events.publish(
|
|
281
282
|
StandardId::Events::SESSION_REVOKED,
|
|
282
283
|
session: self,
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
%>
|
|
7
7
|
<section aria-label="Other sign-in options">
|
|
8
8
|
<p>Or continue with</p>
|
|
9
|
-
<% if StandardId.
|
|
9
|
+
<% if StandardId.social_provider_enabled?(:google) %>
|
|
10
10
|
<%= form_with url: login_path, method: :post, local: true, data: { turbo: false } do |form| %>
|
|
11
11
|
<%= form.hidden_field :connection, value: "google" %>
|
|
12
12
|
<%= form.hidden_field :redirect_uri, value: @redirect_uri %>
|
|
13
13
|
<button type="submit">Google</button>
|
|
14
14
|
<% end %>
|
|
15
15
|
<% end %>
|
|
16
|
-
<% if StandardId.
|
|
16
|
+
<% if StandardId.social_provider_enabled?(:apple) %>
|
|
17
17
|
<%= form_with url: login_path, method: :post, local: true, data: { turbo: false } do |form| %>
|
|
18
18
|
<%= form.hidden_field :connection, value: "apple" %>
|
|
19
19
|
<%= form.hidden_field :redirect_uri, value: @redirect_uri %>
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
})();
|
|
54
54
|
</script>
|
|
55
55
|
|
|
56
|
-
<% if StandardId.
|
|
56
|
+
<% if StandardId.social_provider_enabled?(:google) || StandardId.social_provider_enabled?(:apple) %>
|
|
57
57
|
<%= render "standard_id/web/login/social_buttons" %>
|
|
58
58
|
<% end %>
|
|
59
59
|
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
</div>
|
|
129
129
|
<% end %>
|
|
130
130
|
|
|
131
|
-
<% if StandardId.
|
|
131
|
+
<% if StandardId.social_provider_enabled?(:google) || StandardId.social_provider_enabled?(:apple) %>
|
|
132
132
|
<div>
|
|
133
133
|
<div class="mt-10 flex items-center gap-x-6">
|
|
134
134
|
<div class="w-full flex-1 border-t border-gray-200 dark:border-white/10"></div>
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
</div>
|
|
138
138
|
|
|
139
139
|
<div class="mt-6 grid grid-cols-2 gap-4">
|
|
140
|
-
<% if StandardId.
|
|
140
|
+
<% if StandardId.social_provider_enabled?(:google) %>
|
|
141
141
|
<%= form_with url: login_path, method: :post, local: true, data: { turbo: false } do |form| %>
|
|
142
142
|
<%= form.hidden_field :connection, value: "google" %>
|
|
143
143
|
<%= form.hidden_field :redirect_uri, value: @redirect_uri %>
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
<% end %>
|
|
154
154
|
<% end %>
|
|
155
155
|
|
|
156
|
-
<% if StandardId.
|
|
156
|
+
<% if StandardId.social_provider_enabled?(:apple) %>
|
|
157
157
|
<%= form_with url: login_path, method: :post, local: true, data: { turbo: false } do |form| %>
|
|
158
158
|
<%= form.hidden_field :connection, value: "apple" %>
|
|
159
159
|
<%= form.hidden_field :redirect_uri, value: @redirect_uri %>
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
<main>
|
|
186
186
|
<h1>Sign in</h1>
|
|
187
187
|
<p>No login method is enabled.</p>
|
|
188
|
-
<% if StandardId.
|
|
188
|
+
<% if StandardId.social_provider_enabled?(:google) || StandardId.social_provider_enabled?(:apple) %>
|
|
189
189
|
<%= render "standard_id/web/login/social_buttons" %>
|
|
190
190
|
<% end %>
|
|
191
191
|
</main>
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
</div>
|
|
47
47
|
<% end %>
|
|
48
48
|
|
|
49
|
-
<% if StandardId.
|
|
49
|
+
<% if StandardId.social_provider_enabled?(:google) || StandardId.social_provider_enabled?(:apple) %>
|
|
50
50
|
<div>
|
|
51
51
|
<div class="mt-10 flex items-center gap-x-6">
|
|
52
52
|
<div class="w-full flex-1 border-t border-gray-200 dark:border-white/10"></div>
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
</div>
|
|
56
56
|
|
|
57
57
|
<div class="mt-6 grid grid-cols-2 gap-4">
|
|
58
|
-
<% if StandardId.
|
|
58
|
+
<% if StandardId.social_provider_enabled?(:google) %>
|
|
59
59
|
<%= form_with url: signup_path, method: :post, local: true do |form| %>
|
|
60
60
|
<%= form.hidden_field :connection, value: "google" %>
|
|
61
61
|
<%= form.hidden_field :redirect_uri, value: @redirect_uri %>
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<% end %>
|
|
72
72
|
<% end %>
|
|
73
73
|
|
|
74
|
-
<% if StandardId.
|
|
74
|
+
<% if StandardId.social_provider_enabled?(:apple) %>
|
|
75
75
|
<%= form_with url: signup_path, method: :post, local: true do |form| %>
|
|
76
76
|
<%= form.hidden_field :connection, value: "apple" %>
|
|
77
77
|
<%= form.hidden_field :redirect_uri, value: @redirect_uri %>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
class CreateStandardIdIdentifiers < ActiveRecord::Migration[8.0]
|
|
2
|
+
include StandardId::MigrationHelpers
|
|
3
|
+
|
|
2
4
|
def change
|
|
3
5
|
create_table :standard_id_identifiers, id: primary_key_type do |t|
|
|
4
6
|
t.references :account, type: primary_key_type, null: false, foreign_key: { to_table: StandardId.account_class.table_name }, index: true
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
class CreateStandardIdCredentials < ActiveRecord::Migration[8.0]
|
|
2
|
+
include StandardId::MigrationHelpers
|
|
3
|
+
|
|
2
4
|
def change
|
|
3
5
|
create_table :standard_id_credentials, id: primary_key_type do |t|
|
|
4
6
|
t.references :identifier, type: primary_key_type, null: false, foreign_key: { to_table: :standard_id_identifiers }, index: true
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
class CreateStandardIdSessions < ActiveRecord::Migration[8.0]
|
|
2
|
+
include StandardId::MigrationHelpers
|
|
3
|
+
|
|
2
4
|
def change
|
|
3
5
|
create_table :standard_id_sessions, id: primary_key_type do |t|
|
|
4
6
|
t.references :account, type: primary_key_type, null: false, foreign_key: true, index: true
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
class CreateStandardIdRefreshTokens < ActiveRecord::Migration[8.0]
|
|
2
|
+
include StandardId::MigrationHelpers
|
|
3
|
+
|
|
2
4
|
def change
|
|
3
5
|
create_table :standard_id_refresh_tokens, id: primary_key_type do |t|
|
|
4
6
|
t.references :account, type: primary_key_type, null: false, foreign_key: true, index: true
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# Superseded by 20260416180511's partial index_code_challenges_on_active_target_created_at; hosts that installed that may skip this one (StandardId::MigrationCheck::SUPERSEDED_BY).
|
|
1
2
|
class AddTargetCreatedAtIndexToCodeChallenges < ActiveRecord::Migration[8.0]
|
|
2
3
|
def change
|
|
3
4
|
add_index :standard_id_code_challenges,
|
data/db/migrate/20260416180511_add_partial_indexes_for_active_session_and_challenge_lookups.rb
CHANGED
|
@@ -7,8 +7,10 @@ class AddPartialIndexesForActiveSessionAndChallengeLookups < ActiveRecord::Migra
|
|
|
7
7
|
# except for the GIN drop in step 3. We guard that drop with an `if_exists`
|
|
8
8
|
# check so StrongMigrations/host apps that never ran the creating migration
|
|
9
9
|
# (e.g. SQLite dummies) don't error. StrongMigrations considers partial-
|
|
10
|
-
# index-add and remove_index safe when concurrently + if_exists are used
|
|
11
|
-
#
|
|
10
|
+
# index-add and remove_index safe when concurrently + if_exists are used —
|
|
11
|
+
# except its "non-unique index with more than three columns" heuristic, which
|
|
12
|
+
# rejects the 4-column code_challenges index below; that one add_index is
|
|
13
|
+
# asserted safe (see #assert_safe and the comment at the call site).
|
|
12
14
|
#
|
|
13
15
|
# Split into def up / def down because `remove_index :table, name: "..."`
|
|
14
16
|
# (name-only, no column list) is not auto-reversible via def change — Rails
|
|
@@ -55,12 +57,20 @@ class AddPartialIndexesForActiveSessionAndChallengeLookups < ActiveRecord::Migra
|
|
|
55
57
|
# The existing [:realm, :channel, :target, :created_at] index works but
|
|
56
58
|
# covers every row, including long-since-consumed ones. A partial variant
|
|
57
59
|
# stays tiny (only live challenges) and matches the exact query shape.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
#
|
|
61
|
+
# StrongMigrations flags any non-unique index over more than three columns.
|
|
62
|
+
# The shape here is deliberate: three equality predicates plus the ORDER BY
|
|
63
|
+
# column, partial on live challenges, built CONCURRENTLY — so assert it safe
|
|
64
|
+
# rather than make every host wrap it by hand (fundbright, luminality and
|
|
65
|
+
# nutripod all had to).
|
|
66
|
+
assert_safe do
|
|
67
|
+
add_index :standard_id_code_challenges,
|
|
68
|
+
[:realm, :channel, :target, :created_at],
|
|
69
|
+
where: "used_at IS NULL",
|
|
70
|
+
name: "index_code_challenges_on_active_target_created_at",
|
|
71
|
+
if_not_exists: true,
|
|
72
|
+
**concurrent
|
|
73
|
+
end
|
|
64
74
|
|
|
65
75
|
# Drop the GIN metadata index on Postgres: metadata is only written to
|
|
66
76
|
# (record_failed_attempt bumps `attempts`), never queried with containment
|
|
@@ -114,4 +124,11 @@ class AddPartialIndexesForActiveSessionAndChallengeLookups < ActiveRecord::Migra
|
|
|
114
124
|
if_exists: true,
|
|
115
125
|
**concurrent
|
|
116
126
|
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
# StrongMigrations is optional for hosts (the gem does not depend on it).
|
|
131
|
+
def assert_safe(&block)
|
|
132
|
+
respond_to?(:safety_assured) ? safety_assured(&block) : yield
|
|
133
|
+
end
|
|
117
134
|
end
|
|
@@ -18,6 +18,8 @@ module StandardId
|
|
|
18
18
|
* writes config/initializers/standard_id.rb
|
|
19
19
|
* mounts StandardId::WebEngine and StandardId::ApiEngine in config/routes.rb
|
|
20
20
|
* copies the engine's migrations into db/migrate/
|
|
21
|
+
* schedules the four cleanup jobs in config/recurring.yml (Solid Queue),
|
|
22
|
+
when that file exists
|
|
21
23
|
|
|
22
24
|
Use --skip-* flags to opt out of individual steps when re-running on an
|
|
23
25
|
existing install. The generator is idempotent — already-installed
|
|
@@ -30,6 +32,20 @@ module StandardId
|
|
|
30
32
|
desc: "Do not append engine mount lines to config/routes.rb"
|
|
31
33
|
class_option :skip_migrations, type: :boolean, default: false,
|
|
32
34
|
desc: "Do not copy StandardId migrations into db/migrate"
|
|
35
|
+
class_option :skip_recurring, type: :boolean, default: false,
|
|
36
|
+
desc: "Do not add the cleanup jobs to config/recurring.yml"
|
|
37
|
+
|
|
38
|
+
# Every cleanup job the engine ships, with the recommended Solid Queue
|
|
39
|
+
# schedule. Hourly, staggered off minute 0: each job is one DELETE, and
|
|
40
|
+
# running it hourly keeps that batch small on busy tables. The jobs'
|
|
41
|
+
# own grace windows (7 days expired / 1 day consumed) are what bound
|
|
42
|
+
# retention, not the cadence — daily is fine for small apps.
|
|
43
|
+
CLEANUP_JOBS = {
|
|
44
|
+
"standard_id_cleanup_expired_sessions" => ["StandardId::CleanupExpiredSessionsJob", "every hour at minute 6"],
|
|
45
|
+
"standard_id_cleanup_expired_refresh_tokens" => ["StandardId::CleanupExpiredRefreshTokensJob", "every hour at minute 3"],
|
|
46
|
+
"standard_id_cleanup_expired_authorization_codes" => ["StandardId::CleanupExpiredAuthorizationCodesJob", "every hour at minute 9"],
|
|
47
|
+
"standard_id_cleanup_expired_code_challenges" => ["StandardId::CleanupExpiredCodeChallengesJob", "every hour at minute 13"]
|
|
48
|
+
}.freeze
|
|
33
49
|
|
|
34
50
|
def create_initializer_file
|
|
35
51
|
return say_status("skip", "config/initializers/standard_id.rb (--skip-initializer)", :yellow) if options[:skip_initializer]
|
|
@@ -79,6 +95,35 @@ module StandardId
|
|
|
79
95
|
run_migration_copy_task
|
|
80
96
|
end
|
|
81
97
|
|
|
98
|
+
# Solid Queue reads a single recurring schedule (config/recurring.yml);
|
|
99
|
+
# an engine cannot contribute entries to it, so the generator writes
|
|
100
|
+
# them. Inserted under the `production:` key only — the one environment
|
|
101
|
+
# every recurring.yml has and where cleanup matters.
|
|
102
|
+
def schedule_cleanup_jobs
|
|
103
|
+
return say_status("skip", "config/recurring.yml (--skip-recurring)", :yellow) if options[:skip_recurring]
|
|
104
|
+
|
|
105
|
+
path = "config/recurring.yml"
|
|
106
|
+
full_path = File.join(destination_root, path)
|
|
107
|
+
|
|
108
|
+
unless File.exist?(full_path)
|
|
109
|
+
say_status("skip", "#{path} not found — schedule the cleanup jobs with your scheduler (see below)", :yellow)
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
content = File.read(full_path)
|
|
114
|
+
if content.include?("StandardId::CleanupExpired")
|
|
115
|
+
say_status("identical", "#{path} (StandardId cleanup jobs already scheduled)", :blue)
|
|
116
|
+
return
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
unless content.match?(/^production:[ \t]*\r?\n/)
|
|
120
|
+
say_status("warn", "#{path} has no top-level `production:` key — add the cleanup jobs manually:\n#{recurring_snippet}", :red)
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
inject_into_file path, indent(recurring_snippet, 2), after: /^production:[ \t]*\r?\n/
|
|
125
|
+
end
|
|
126
|
+
|
|
82
127
|
def print_post_install_message
|
|
83
128
|
say ""
|
|
84
129
|
say "=" * 79
|
|
@@ -116,10 +161,13 @@ module StandardId
|
|
|
116
161
|
say ""
|
|
117
162
|
say " bin/rails db:migrate"
|
|
118
163
|
say ""
|
|
119
|
-
say "5. Scheduled maintenance —
|
|
164
|
+
say "5. Scheduled maintenance — the cleanup jobs must run on a schedule"
|
|
165
|
+
say " (added to config/recurring.yml if you use Solid Queue; otherwise"
|
|
166
|
+
say " schedule them yourself, hourly or at least daily):"
|
|
167
|
+
say ""
|
|
168
|
+
CLEANUP_JOBS.each_value { |(job, _)| say " #{job}" }
|
|
120
169
|
say ""
|
|
121
|
-
say "
|
|
122
|
-
say " StandardId::CleanupExpiredRefreshTokensJob"
|
|
170
|
+
say " See the README's Scheduled Maintenance section."
|
|
123
171
|
say ""
|
|
124
172
|
say "6. Social providers — install provider plugins and register them:"
|
|
125
173
|
say ""
|
|
@@ -160,6 +208,19 @@ module StandardId
|
|
|
160
208
|
text.each_line.map { |line| line.strip.empty? ? line : prefix + line }.join
|
|
161
209
|
end
|
|
162
210
|
|
|
211
|
+
def recurring_snippet
|
|
212
|
+
entries = CLEANUP_JOBS.map do |key, (job, schedule)|
|
|
213
|
+
<<~YAML
|
|
214
|
+
#{key}:
|
|
215
|
+
class: #{job}
|
|
216
|
+
schedule: #{schedule}
|
|
217
|
+
YAML
|
|
218
|
+
end
|
|
219
|
+
"# StandardId cleanup jobs (added by standard_id:install). Retention is\n" \
|
|
220
|
+
"# bounded by each job's grace window; the cadence only sizes the DELETE.\n" +
|
|
221
|
+
entries.join
|
|
222
|
+
end
|
|
223
|
+
|
|
163
224
|
def engine_mount_snippet
|
|
164
225
|
<<~RUBY
|
|
165
226
|
# Mount the StandardId engines. The web engine serves cookie-based
|
|
@@ -253,9 +253,18 @@ StandardId.configure do |c|
|
|
|
253
253
|
# Account.create!(email: identifier.value)
|
|
254
254
|
# }
|
|
255
255
|
|
|
256
|
-
#
|
|
257
|
-
#
|
|
258
|
-
#
|
|
256
|
+
# c.passwordless_email_sender / c.passwordless_sms_sender are DEPRECATED
|
|
257
|
+
# (assigning them emits a StandardId deprecation warning). Deliver codes from
|
|
258
|
+
# an event subscriber instead — it runs synchronously in the request, so
|
|
259
|
+
# I18n.locale etc. are still available:
|
|
260
|
+
#
|
|
261
|
+
# StandardId::Events.subscribe(StandardId::Events::PASSWORDLESS_CODE_GENERATED) do |event|
|
|
262
|
+
# next if event[:skip_sender] # Otp.issue(delivery: :manual)
|
|
263
|
+
# case event[:channel]
|
|
264
|
+
# when "email" then PasswordlessMailer.with(code: event[:code_challenge].code, to: event[:identifier]).deliver_later
|
|
265
|
+
# when "sms" then SmsProvider.send_code(phone: event[:identifier], code: event[:code_challenge].code)
|
|
266
|
+
# end
|
|
267
|
+
# end
|
|
259
268
|
|
|
260
269
|
# ---------------------------------------------------------------------------
|
|
261
270
|
# API engine — OAuth / JWT
|
|
@@ -430,15 +439,37 @@ StandardId.configure do |c|
|
|
|
430
439
|
# declared later from the plugin's Railtie, and apps worked around it by
|
|
431
440
|
# wrapping the writes in `Rails.application.config.after_initialize`. That
|
|
432
441
|
# wrapper is no longer needed, and it still works if you have one.
|
|
433
|
-
|
|
434
|
-
#
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
#
|
|
438
|
-
#
|
|
442
|
+
#
|
|
443
|
+
# ENV defaults (standard_id >= 0.42): every provider field you do not assign
|
|
444
|
+
# falls back to the ENV variable named after it, upper-cased — so with the
|
|
445
|
+
# canonical variables below set, you need none of these lines. Assign a
|
|
446
|
+
# field only to read it from somewhere else (a differently named variable,
|
|
447
|
+
# Rails credentials); an explicit assignment, even of nil, always wins.
|
|
448
|
+
#
|
|
449
|
+
# GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
|
|
450
|
+
# APPLE_CLIENT_ID, APPLE_MOBILE_CLIENT_ID,
|
|
451
|
+
# APPLE_PRIVATE_KEY, APPLE_KEY_ID, APPLE_TEAM_ID
|
|
452
|
+
#
|
|
453
|
+
# A provider is enabled when its client ID is present
|
|
454
|
+
# (StandardId.social_provider_enabled?(:google)). An enabled provider missing
|
|
455
|
+
# fields its plugin marks required is reported at boot — see
|
|
456
|
+
# c.social.provider_misconfiguration below.
|
|
457
|
+
|
|
458
|
+
# c.social.google_client_id = Rails.application.credentials.dig(:google, :client_id)
|
|
459
|
+
# c.social.google_client_secret = Rails.application.credentials.dig(:google, :client_secret)
|
|
460
|
+
# c.social.apple_client_id = ENV["APPLE_CLIENT_ID"] # web (Services ID) flow
|
|
461
|
+
# c.social.apple_mobile_client_id = ENV["APPLE_MOBILE_CLIENT_ID"] # native id_token flow
|
|
462
|
+
# c.social.apple_private_key = ENV["APPLE_PRIVATE_KEY"] # .p8 PEM, newlines intact
|
|
439
463
|
# c.social.apple_key_id = ENV["APPLE_KEY_ID"]
|
|
440
464
|
# c.social.apple_team_id = ENV["APPLE_TEAM_ID"]
|
|
441
465
|
|
|
466
|
+
# What to do at boot when an enabled provider is missing required fields
|
|
467
|
+
# (e.g. APPLE_CLIENT_ID set without APPLE_PRIVATE_KEY — the sign-in flow
|
|
468
|
+
# would start, then fail at the callback). :warn logs a warning; :raise
|
|
469
|
+
# raises StandardId::ConfigurationError in production and warns elsewhere.
|
|
470
|
+
# Default: :warn
|
|
471
|
+
# c.social.provider_misconfiguration = :raise
|
|
472
|
+
|
|
442
473
|
# Mobile redirect URI allow-list — custom schemes used by native apps.
|
|
443
474
|
# Default: []
|
|
444
475
|
# c.social.allowed_redirect_url_prefixes = ["example-app://"]
|
|
@@ -479,12 +510,10 @@ StandardId.configure do |c|
|
|
|
479
510
|
|
|
480
511
|
# Login limits. The login action branches password OR passwordless, so on a
|
|
481
512
|
# passwordless app these govern the OTP-SEND limit. Prefer the
|
|
482
|
-
# mechanism-agnostic names; the
|
|
483
|
-
# (the new name wins when both are set).
|
|
513
|
+
# mechanism-agnostic names; the old password_login_per_ip/_per_email names
|
|
514
|
+
# still work but emit a deprecation warning (the new name wins when both are set).
|
|
484
515
|
# c.rate_limits.login_per_ip = 20 # per 15 minutes
|
|
485
516
|
# c.rate_limits.login_per_email = 5 # per 15 minutes
|
|
486
|
-
# c.rate_limits.password_login_per_ip = 20 # deprecated alias of login_per_ip
|
|
487
|
-
# c.rate_limits.password_login_per_email = 5 # deprecated alias of login_per_email
|
|
488
517
|
# c.rate_limits.otp_verify_per_ip = 20 # per 15 minutes
|
|
489
518
|
# c.rate_limits.verification_start_per_target = 3 # per 15 minutes
|
|
490
519
|
# c.rate_limits.verification_start_per_ip = 10 # per hour
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Checks
|
|
3
|
+
# A StandardHealth-compatible readiness check reporting StandardId
|
|
4
|
+
# migrations the host never installed or never ran (see
|
|
5
|
+
# StandardId::MigrationCheck).
|
|
6
|
+
#
|
|
7
|
+
# Duck-typed like StandardAudit::Checks::Retention — no dependency on
|
|
8
|
+
# standard_health; it exposes the `#initialize(name:, critical:)` + `#run`
|
|
9
|
+
# contract the aggregator calls. Register it NON-critical, so a missing
|
|
10
|
+
# index degrades /health/ready (HTTP 200) instead of failing the probe:
|
|
11
|
+
#
|
|
12
|
+
# c.register_check :standard_id_migrations,
|
|
13
|
+
# StandardId::Checks::Migrations,
|
|
14
|
+
# critical: false
|
|
15
|
+
#
|
|
16
|
+
# Cheap: one directory scan plus one `schema_migrations` read, and once
|
|
17
|
+
# everything is present the :ok result is memoized for the life of the
|
|
18
|
+
# process (migration files cannot change without a deploy).
|
|
19
|
+
class Migrations
|
|
20
|
+
attr_reader :name
|
|
21
|
+
|
|
22
|
+
def initialize(name: :standard_id_migrations, critical: false)
|
|
23
|
+
@name = name
|
|
24
|
+
@critical = critical
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def critical? = !!@critical
|
|
28
|
+
|
|
29
|
+
def run
|
|
30
|
+
return { status: :ok } if self.class.all_present?
|
|
31
|
+
|
|
32
|
+
all_missing = StandardId::MigrationCheck.pending(check_database: true)
|
|
33
|
+
if all_missing.empty?
|
|
34
|
+
self.class.all_present = true
|
|
35
|
+
return { status: :ok }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Deferred upgrade steps (MigrationCheck::DEFERRED_UPGRADE_STEPS) are
|
|
39
|
+
# reported but never degrade readiness.
|
|
40
|
+
deferred, missing = all_missing.partition(&:info?)
|
|
41
|
+
if missing.empty?
|
|
42
|
+
return { status: :ok, pending_upgrade_steps: deferred.map(&:to_s) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
status: :warn,
|
|
47
|
+
message: "#{missing.size} StandardId migration(s) missing: #{missing.map(&:to_s).join(', ')}",
|
|
48
|
+
missing: missing.map { |m| { name: m.name, version: m.version, state: m.state } }
|
|
49
|
+
}
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
{ status: :fail, error: e.message, error_class: e.class.name }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class << self
|
|
55
|
+
attr_writer :all_present
|
|
56
|
+
|
|
57
|
+
def all_present? = !!@all_present
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -9,8 +9,11 @@ StandardId::ConfigSchema.define do
|
|
|
9
9
|
field :cache_store, type: :any, default: nil
|
|
10
10
|
field :logger, type: :any, default: nil
|
|
11
11
|
field :web_layout, type: :string, default: nil
|
|
12
|
-
|
|
13
|
-
field :
|
|
12
|
+
# Deprecated since 0.1.7 (see docs/MIGRATION_GUIDE.md), still honoured.
|
|
13
|
+
field :passwordless_email_sender, type: :any, default: nil,
|
|
14
|
+
deprecated: "deliver the code from a StandardId::Events::PASSWORDLESS_CODE_GENERATED subscriber instead (it runs synchronously in the request, so I18n.locale is still available; skip it when event[:skip_sender] is true) and call Otp.issue with its default delivery: :built_in. Removal in v2.0; see docs/MIGRATION_GUIDE.md."
|
|
15
|
+
field :passwordless_sms_sender, type: :any, default: nil,
|
|
16
|
+
deprecated: "deliver the code from a StandardId::Events::PASSWORDLESS_CODE_GENERATED subscriber instead (it runs synchronously in the request, so I18n.locale is still available; skip it when event[:skip_sender] is true) and call Otp.issue with its default delivery: :built_in. Removal in v2.0; see docs/MIGRATION_GUIDE.md."
|
|
14
17
|
field :issuer, type: :string, default: nil
|
|
15
18
|
|
|
16
19
|
# Whether `JwtService.decode` REQUIRES a matching `iss` claim.
|
|
@@ -55,6 +58,17 @@ StandardId::ConfigSchema.define do
|
|
|
55
58
|
# loading for every app that never asked for it.
|
|
56
59
|
field :association_strict_loading, type: :any, default: nil
|
|
57
60
|
|
|
61
|
+
# Boot-time check for StandardId migrations the host never copied in
|
|
62
|
+
# (StandardId::MigrationCheck). :warn, :raise or :ignore. nil (default)
|
|
63
|
+
# means :warn in development/test and :ignore elsewhere — it never raises
|
|
64
|
+
# in production unless you ask it to.
|
|
65
|
+
field :missing_migrations, type: :symbol, default: nil
|
|
66
|
+
|
|
67
|
+
# Gem migration names (e.g. "add_target_created_at_index_to_code_challenges")
|
|
68
|
+
# or original versions the check should skip — for a migration the host
|
|
69
|
+
# deliberately superseded or deferred.
|
|
70
|
+
field :ignored_migrations, type: :array, default: -> { [] }
|
|
71
|
+
|
|
58
72
|
# Scope-aware authentication: maps scope names to profile-based access config.
|
|
59
73
|
# Each scope is a hash with keys: :profile_types (Array<String>), :after_sign_in_path,
|
|
60
74
|
# :no_profile_message, :label, :allow_registration, :authorizer.
|
|
@@ -139,9 +153,11 @@ StandardId::ConfigSchema.define do
|
|
|
139
153
|
end
|
|
140
154
|
|
|
141
155
|
scope :passwordless do
|
|
142
|
-
# Deprecated: use web.passwordless_login to control WebEngine
|
|
143
|
-
#
|
|
144
|
-
|
|
156
|
+
# Deprecated since 0.8: use web.passwordless_login to control WebEngine
|
|
157
|
+
# passwordless login. Never read; retained so host initializers that set it
|
|
158
|
+
# still boot.
|
|
159
|
+
field :enabled, type: :boolean, default: false,
|
|
160
|
+
deprecated: "it has had no effect since 0.8. Use web.passwordless_login (WebEngine) instead, and remove this line. Removal in v2.0."
|
|
145
161
|
field :connection, type: :string, default: "email"
|
|
146
162
|
field :code_ttl, type: :integer, default: 600 # 10 minutes in seconds
|
|
147
163
|
|
|
@@ -296,8 +312,12 @@ StandardId::ConfigSchema.define do
|
|
|
296
312
|
# RefreshTokenFlow::MAX_REUSE_LEEWAY_SECONDS.
|
|
297
313
|
field :refresh_token_reuse_leeway, type: :integer, default: 0
|
|
298
314
|
field :token_lifetimes, type: :hash, default: -> { {} }
|
|
299
|
-
|
|
300
|
-
|
|
315
|
+
# Never read by the gem. OAuth clients are StandardId::ClientApplication
|
|
316
|
+
# rows with ClientSecretCredential secrets.
|
|
317
|
+
field :client_id, type: :string, default: nil,
|
|
318
|
+
deprecated: "it is not read anywhere — OAuth clients are StandardId::ClientApplication records. Remove this line. Removal in v2.0."
|
|
319
|
+
field :client_secret, type: :string, default: nil,
|
|
320
|
+
deprecated: "it is not read anywhere — client secrets are StandardId::ClientSecretCredential records. Remove this line. Removal in v2.0."
|
|
301
321
|
field :scope_claims, type: :hash, default: -> { {} }
|
|
302
322
|
field :claim_resolvers, type: :hash, default: -> { {} }
|
|
303
323
|
# List of audience values that tokens issued and accepted by this app may
|
|
@@ -551,6 +571,11 @@ StandardId::ConfigSchema.define do
|
|
|
551
571
|
field :allowed_redirect_url_prefixes, type: :array, default: []
|
|
552
572
|
field :available_scopes, type: :array, default: -> { [] }
|
|
553
573
|
field :link_strategy, type: :symbol, default: :strict
|
|
574
|
+
# What to do at boot when an enabled social provider is missing required
|
|
575
|
+
# config (e.g. apple_client_id set without apple_private_key). :warn logs
|
|
576
|
+
# in every environment; :raise raises StandardId::ConfigurationError in
|
|
577
|
+
# production and logs elsewhere. See ProviderRegistry.validate_configuration!
|
|
578
|
+
field :provider_misconfiguration, type: :symbol, default: :warn
|
|
554
579
|
end
|
|
555
580
|
|
|
556
581
|
scope :web do
|
|
@@ -583,8 +608,10 @@ StandardId::ConfigSchema.define do
|
|
|
583
608
|
# these values (see StandardId::RateLimitHandling.login_per_ip). New name
|
|
584
609
|
# wins when explicitly set. Mirrors the max_attempts ->
|
|
585
610
|
# max_attempts_per_challenge deprecation-alias precedent.
|
|
586
|
-
field :password_login_per_ip, type: :integer, default: 20
|
|
587
|
-
|
|
611
|
+
field :password_login_per_ip, type: :integer, default: 20, # per 15 minutes; deprecated alias of login_per_ip
|
|
612
|
+
deprecated: "use rate_limits.login_per_ip (same meaning; it also governs passwordless OTP sends). Removal in v2.0."
|
|
613
|
+
field :password_login_per_email, type: :integer, default: 5, # per 15 minutes; deprecated alias of login_per_email
|
|
614
|
+
deprecated: "use rate_limits.login_per_email (same meaning; it also governs passwordless OTP sends). Removal in v2.0."
|
|
588
615
|
|
|
589
616
|
# RAR-60: OTP verification
|
|
590
617
|
field :otp_verify_per_ip, type: :integer, default: 20 # per 15 minutes
|