add_auth 0.2.1 → 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/CHANGELOG.md +36 -0
- data/README.md +57 -18
- data/ROADMAP.md +260 -242
- data/SECURITY.md +6 -0
- data/app/controllers/add_auth/accounts_controller.rb +138 -0
- data/app/controllers/add_auth/assets_controller.rb +2 -1
- data/app/controllers/add_auth/external_identities_controller.rb +56 -0
- data/app/controllers/add_auth/mobile_sessions_controller.rb +120 -0
- data/app/controllers/add_auth/provider_sign_ins_controller.rb +248 -0
- data/app/helpers/add_auth/sign_ins_helper.rb +2 -1
- data/app/jobs/add_auth/account_delivery_job.rb +14 -0
- data/app/jobs/add_auth/account_request_job.rb +17 -0
- data/app/mailers/add_auth/account_mailer.rb +12 -0
- data/app/views/add_auth/account_mailer/link.text.erb +7 -0
- data/app/views/add_auth/accounts/_check_email.html.erb +5 -0
- data/app/views/add_auth/accounts/_delete.html.erb +6 -0
- data/app/views/add_auth/accounts/_email.html.erb +12 -0
- data/app/views/add_auth/accounts/_invalid_proof.html.erb +5 -0
- data/app/views/add_auth/accounts/_password.html.erb +12 -0
- data/app/views/add_auth/accounts/_proof.html.erb +16 -0
- data/app/views/add_auth/accounts/_register.html.erb +17 -0
- data/app/views/add_auth/accounts/_request.html.erb +14 -0
- data/app/views/add_auth/accounts/_unavailable.html.erb +3 -0
- data/app/views/add_auth/external_identities/_index.html.erb +22 -0
- data/app/views/add_auth/external_identities/_unavailable.html.erb +3 -0
- data/app/views/add_auth/passkeys/_controls.html.erb +3 -1
- data/app/views/add_auth/provider_sign_ins/_enrollment.html.erb +12 -0
- data/app/views/add_auth/provider_sign_ins/_failure.html.erb +3 -0
- data/app/views/add_auth/provider_sign_ins/_prepare.html.erb +7 -0
- data/app/views/add_auth/reauthentications/_form.html.erb +9 -0
- data/app/views/add_auth/sessions/_list.html.erb +1 -1
- data/app/views/add_auth/sessions/index.html.erb +7 -0
- data/app/views/add_auth/sign_ins/_confirmation.html.erb +1 -0
- data/app/views/add_auth/sign_ins/_form.html.erb +17 -0
- data/app/views/add_auth/sign_ins/_remember.html.erb +6 -0
- data/app/views/layouts/add_auth/authentication.html.erb +4 -3
- data/exe/add_auth-preflight +26 -0
- data/lib/add_auth/configuration.rb +78 -4
- data/lib/add_auth/core/access_policy.rb +11 -1
- data/lib/add_auth/core/account_lifecycle.rb +251 -0
- data/lib/add_auth/core/account_policy.rb +50 -0
- data/lib/add_auth/core/external_identities/evidence.rb +98 -0
- data/lib/add_auth/core/external_identities.rb +278 -0
- data/lib/add_auth/core/intake.rb +6 -1
- data/lib/add_auth/core/maintenance.rb +5 -3
- data/lib/add_auth/core/migration/account_adoption.rb +79 -0
- data/lib/add_auth/core/migration/readiness.rb +41 -0
- data/lib/add_auth/core/mobile_authentication.rb +35 -0
- data/lib/add_auth/core/mobile_handoffs.rb +83 -0
- data/lib/add_auth/core/mobile_profile.rb +46 -0
- data/lib/add_auth/core/mobile_response.rb +12 -0
- data/lib/add_auth/core/native_authentication.rb +77 -0
- data/lib/add_auth/core/passwords/bcrypt_support.rb +28 -0
- data/lib/add_auth/core/passwords/credential.rb +49 -0
- data/lib/add_auth/core/passwords/legacy_bcrypt.rb +43 -0
- data/lib/add_auth/core/passwords/lifecycle.rb +60 -0
- data/lib/add_auth/core/remaining_factors.rb +24 -0
- data/lib/add_auth/core/sessions.rb +102 -17
- data/lib/add_auth/core/step_up.rb +13 -1
- data/lib/add_auth/migration/static_inventory.rb +116 -0
- data/lib/add_auth/rails/account_password_entry.rb +24 -0
- data/lib/add_auth/rails/authentication.rb +1 -1
- data/lib/add_auth/rails/authentication_pages.rb +5 -0
- data/lib/add_auth/rails/doctor.rb +73 -3
- data/lib/add_auth/rails/ejection.rb +1 -1
- data/lib/add_auth/rails/engine.rb +6 -1
- data/lib/add_auth/rails/migration/account_store.rb +41 -0
- data/lib/add_auth/rails/migration/effective_inventory.rb +162 -0
- data/lib/add_auth/rails/migration/key_type.rb +23 -0
- data/lib/add_auth/rails/migration/source_bridge.rb +53 -0
- data/lib/add_auth/rails/password_adoption.rb +37 -0
- data/lib/add_auth/rails/provider_enrollment.rb +61 -0
- data/lib/add_auth/rails/provider_libraries/apple_form_post_correlation.rb +127 -0
- data/lib/add_auth/rails/provider_libraries/apple_jwks.rb +68 -0
- data/lib/add_auth/rails/provider_libraries/apple_native.rb +55 -0
- data/lib/add_auth/rails/provider_libraries/callback_route.rb +16 -0
- data/lib/add_auth/rails/provider_libraries/omniauth.rb +136 -0
- data/lib/add_auth/rails/provider_libraries/omniauth_correlation.rb +107 -0
- data/lib/add_auth/rails/provider_libraries/openid_connect.rb +62 -0
- data/lib/add_auth/rails/provider_libraries/request_protection.rb +43 -0
- data/lib/add_auth/rails/rate_limit_cache.rb +17 -0
- data/lib/add_auth/rails/runtime.rb +173 -9
- data/lib/add_auth/rails/stores/account_credentials.rb +44 -0
- data/lib/add_auth/rails/stores/account_lock.rb +16 -7
- data/lib/add_auth/rails/stores/account_tokens.rb +96 -0
- data/lib/add_auth/rails/stores/authority.rb +28 -0
- data/lib/add_auth/rails/stores/commit_dispatch.rb +32 -0
- data/lib/add_auth/rails/stores/external_identities.rb +91 -0
- data/lib/add_auth/rails/stores/maintenance.rb +1 -1
- data/lib/add_auth/rails/stores/mobile_handoffs.rb +53 -0
- data/lib/add_auth/rails/stores/security_events.rb +3 -6
- data/lib/add_auth/rails/user_lifecycle.rb +8 -10
- data/lib/add_auth/result.rb +1 -0
- data/lib/add_auth/version.rb +1 -1
- data/lib/add_auth.rb +4 -0
- data/lib/generators/add_auth/accounts/accounts_generator.rb +52 -0
- data/lib/generators/add_auth/accounts/templates/add_add_auth_accounts.rb.tt +49 -0
- data/lib/generators/add_auth/accounts/templates/add_auth_account_token.rb +6 -0
- data/lib/generators/add_auth/accounts/templates/add_auth_address_claim.rb +6 -0
- data/lib/generators/add_auth/devise_accounts/devise_accounts_generator.rb +23 -0
- data/lib/generators/add_auth/devise_accounts/templates/prepare_add_auth_devise_accounts.rb.tt +17 -0
- data/lib/generators/add_auth/email_link/templates/add_auth_challenge.js +1 -0
- data/lib/generators/add_auth/email_tokens/templates/create_add_auth_sign_in_tokens.rb.tt +3 -1
- data/lib/generators/add_auth/external_identities/external_identities_generator.rb +52 -0
- data/lib/generators/add_auth/external_identities/templates/add_add_auth_external_enrollment.rb.tt +5 -0
- data/lib/generators/add_auth/external_identities/templates/add_add_auth_external_identity_validity.rb.tt +5 -0
- data/lib/generators/add_auth/external_identities/templates/add_auth_external_identity.rb +4 -0
- data/lib/generators/add_auth/external_identities/templates/add_auth_external_transaction.rb +6 -0
- data/lib/generators/add_auth/external_identities/templates/create_add_auth_external_identities.rb.tt +50 -0
- data/lib/generators/add_auth/external_identities/templates/providers.rb +51 -0
- data/lib/generators/add_auth/install/templates/initializer.rb +7 -1
- data/lib/generators/add_auth/javascript/templates/passkey.js +8 -5
- data/lib/generators/add_auth/mobile_sessions/mobile_sessions_generator.rb +48 -0
- data/lib/generators/add_auth/mobile_sessions/templates/add_add_auth_mobile_sessions.rb.tt +9 -0
- data/lib/generators/add_auth/mobile_sessions/templates/add_auth_mobile_handoff.rb +5 -0
- data/lib/generators/add_auth/mobile_sessions/templates/create_add_auth_mobile_handoffs.rb.tt +27 -0
- data/lib/generators/add_auth/mobile_sessions/templates/mobile.rb +8 -0
- data/lib/generators/add_auth/notifications/templates/create_add_auth_security_events.rb.tt +3 -1
- data/lib/generators/add_auth/passkeys/templates/add_add_auth_passkeys.rb.tt +7 -3
- data/lib/generators/add_auth/step_up/templates/add_add_auth_reauthentication.rb.tt +3 -1
- data/lib/tasks/add_auth.rake +18 -1
- metadata +86 -2
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% content_for :title, "Change your email" %>
|
|
2
|
+
<h1 id="add_auth-title">Change your email</h1>
|
|
3
|
+
<p class="<%= add_auth_class(:muted) %>">Your current email stays active until you confirm the new address. Confirmation signs out your existing sessions.</p>
|
|
4
|
+
<% if @error %><p role="alert" class="<%= add_auth_class(:notice) %>"><%= @error %></p><% end %>
|
|
5
|
+
<%= form_with url: "/account/email", data: {turbo_frame: "_top"} do |form| %>
|
|
6
|
+
<div class="<%= add_auth_class(:field) %>">
|
|
7
|
+
<%= form.label :email_address, "New email address" %>
|
|
8
|
+
<%= form.email_field :email_address, required: true, maxlength: 254, autocomplete: "email", value: (params[:email_address] if params[:email_address].is_a?(String)), class: add_auth_class(:input) %>
|
|
9
|
+
</div>
|
|
10
|
+
<%= form.submit "Send confirmation link", class: add_auth_class(:button) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<p><%= link_to "Back to your sessions", "/sessions" %></p>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<% content_for :title, "Request a new link" %>
|
|
2
|
+
<h1 id="add_auth-title">Request a new link</h1>
|
|
3
|
+
<p>This link is unavailable, expired, or already used. Request a new link to finish your account update.</p>
|
|
4
|
+
<p><%= link_to "Request another link", "/account/requests/#{@purpose}" %></p>
|
|
5
|
+
<p><%= link_to "Back to sign in", "/sign-in" %></p>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% content_for :title, "Change your password" %>
|
|
2
|
+
<h1 id="add_auth-title">Change your password</h1>
|
|
3
|
+
<p class="<%= add_auth_class(:muted) %>">Verify it’s you before changing your password. This signs out your existing sessions.</p>
|
|
4
|
+
<% if @error %><p role="alert" class="<%= add_auth_class(:notice) %>"><%= @error %></p><% end %>
|
|
5
|
+
<%= form_with url: "/account/password", data: {turbo_frame: "_top"} do |form| %>
|
|
6
|
+
<div class="<%= add_auth_class(:field) %>">
|
|
7
|
+
<%= form.label :password, "New password" %>
|
|
8
|
+
<%= form.password_field :password, required: true, autocomplete: "new-password", class: add_auth_class(:input) %>
|
|
9
|
+
</div>
|
|
10
|
+
<%= form.submit "Change password", class: add_auth_class(:button) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<p><%= link_to "Back to your sessions", "/sessions" %></p>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<% title = {"confirm" => "Confirm this email address", "reset_password" => "Choose a new password", "unlock" => "Unlock this account"}.fetch(@purpose) %>
|
|
2
|
+
<% content_for :title, title %>
|
|
3
|
+
<h1 id="add_auth-title"><%= title %></h1>
|
|
4
|
+
<p class="<%= add_auth_class(:muted) %>">Use the button below to complete your request. You’ll sign in afterward.</p>
|
|
5
|
+
<% if @error %><p role="alert" class="<%= add_auth_class(:notice) %>"><%= @error %></p><% end %>
|
|
6
|
+
<%= form_with url: "/account/proofs/#{@purpose}", data: {turbo_frame: "_top"} do |form| %>
|
|
7
|
+
<%= form.hidden_field :token, value: (params[:token] if params[:token].is_a?(String)) %>
|
|
8
|
+
<% if @purpose == "reset_password" %>
|
|
9
|
+
<div class="<%= add_auth_class(:field) %>">
|
|
10
|
+
<%= form.label :password, "New password" %>
|
|
11
|
+
<%= form.password_field :password, required: true, autocomplete: "new-password", class: add_auth_class(:input) %>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= form.submit title, class: add_auth_class(:button) %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<p><%= link_to "Back to sign in", "/sign-in" %></p>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<% content_for :title, "Create an account" %>
|
|
2
|
+
<h1 id="add_auth-title">Create an account</h1>
|
|
3
|
+
<p class="<%= add_auth_class(:muted) %>">We’ll email you a link to confirm your address before you sign in.</p>
|
|
4
|
+
<% if @error %><p role="alert" class="<%= add_auth_class(:notice) %>"><%= @error %></p><% end %>
|
|
5
|
+
<%= form_with url: "/account/sign-up", data: add_auth_challenge_form_data(:register) do |form| %>
|
|
6
|
+
<div class="<%= add_auth_class(:field) %>">
|
|
7
|
+
<%= form.label :email_address, "Email address" %>
|
|
8
|
+
<%= form.email_field :email_address, required: true, maxlength: 254, autocomplete: "email", value: (params[:email_address] if params[:email_address].is_a?(String)), class: add_auth_class(:input) %>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="<%= add_auth_class(:field) %>">
|
|
11
|
+
<%= form.label :password, "New password" %>
|
|
12
|
+
<%= form.password_field :password, required: true, autocomplete: "new-password", class: add_auth_class(:input) %>
|
|
13
|
+
</div>
|
|
14
|
+
<%= add_auth_challenge_tag :register %>
|
|
15
|
+
<%= form.submit "Create account", class: add_auth_class(:button), data: {add_auth_challenge_target: "submit"} %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<p><%= link_to "Already have an account? Sign in", "/sign-in" %></p>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<% title = {"confirm" => "Confirm your email", "reset_password" => "Reset your password", "unlock" => "Unlock your account"}.fetch(@purpose) %>
|
|
2
|
+
<% content_for :title, title %>
|
|
3
|
+
<h1 id="add_auth-title"><%= title %></h1>
|
|
4
|
+
<p class="<%= add_auth_class(:muted) %>">Enter your account email address to request a secure link.</p>
|
|
5
|
+
<% if @error %><p role="alert" class="<%= add_auth_class(:notice) %>"><%= @error %></p><% end %>
|
|
6
|
+
<%= form_with url: "/account/requests/#{@purpose}", data: add_auth_challenge_form_data(:account_request) do |form| %>
|
|
7
|
+
<div class="<%= add_auth_class(:field) %>">
|
|
8
|
+
<%= form.label :email_address, "Email address" %>
|
|
9
|
+
<%= form.email_field :email_address, required: true, maxlength: 254, autocomplete: "email", value: (params[:email_address] if params[:email_address].is_a?(String)), class: add_auth_class(:input) %>
|
|
10
|
+
</div>
|
|
11
|
+
<%= add_auth_challenge_tag :account_request %>
|
|
12
|
+
<%= form.submit "Send account link", class: add_auth_class(:button), data: {add_auth_challenge_target: "submit"} %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<p><%= link_to "Back to sign in", "/sign-in" %></p>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<h1 id="add_auth-title">External sign-in methods</h1>
|
|
2
|
+
<% if @error %><p class="<%= add_auth_class(:notice) %>" role="alert"><%= @error %></p><% end %>
|
|
3
|
+
<p>Add another way to sign in. You’ll verify your account before linking or removing a provider.</p>
|
|
4
|
+
<% @providers.each do |provider| %>
|
|
5
|
+
<%= form_with url: "/account/external-identities/providers/#{provider.middleware_name}", method: :post, data: add_auth_challenge_form_data(:provider).merge(turbo: false) do |form| %>
|
|
6
|
+
<%= add_auth_challenge_tag :provider, id: "link_#{provider.middleware_name}" %>
|
|
7
|
+
<%= form.submit "Link #{provider.label}", class: add_auth_class(:button), data: {add_auth_challenge_target: "submit"} %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% @identities.each do |identity| %>
|
|
11
|
+
<% provider = @providers.find { |item| item.configuration.id == identity.provider_id } %>
|
|
12
|
+
<section id="external_identity_<%= identity.id %>">
|
|
13
|
+
<h2><%= provider&.label || "External provider" %></h2>
|
|
14
|
+
<p>Linked <%= identity.linked_at.strftime("%Y-%m-%d") %>.</p>
|
|
15
|
+
<details>
|
|
16
|
+
<summary>Remove this sign-in method</summary>
|
|
17
|
+
<p>Keep another usable sign-in method before removing this one.</p>
|
|
18
|
+
<%= button_to "Confirm removal", "/account/external-identities/#{identity.id}", method: :delete, form: {data: {turbo: false}} %>
|
|
19
|
+
</details>
|
|
20
|
+
</section>
|
|
21
|
+
<% end %>
|
|
22
|
+
<p><%= link_to "Back to sign in", "/sign-in" %></p>
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
<div data-controller="add-auth-passkey"
|
|
2
|
+
data-add-auth-passkey-turbo-value="<%= AddAuth.configuration.turbo_enabled %>"
|
|
2
3
|
data-add-auth-passkey-options-url-value="<%= options_url %>"
|
|
3
4
|
data-add-auth-passkey-finish-url-value="<%= finish_url %>"
|
|
4
5
|
data-add-auth-passkey-mode-value="<%= mode %>"
|
|
5
6
|
data-add-auth-passkey-purpose-value="<%= local_assigns[:purpose] %>"
|
|
6
7
|
data-add-auth-passkey-conditional-value="<%= local_assigns.fetch(:conditional, false) && !AddAuth.configuration.challenge_on.include?(action) %>"
|
|
7
8
|
data-add-auth-passkey-csrf-value="<%= form_authenticity_token %>"
|
|
8
|
-
data-action="turbo:before-cache@document->add-auth-passkey#beforeCache submit@document->add-auth-passkey#otherSubmission">
|
|
9
|
+
data-action="turbo:before-cache@document->add-auth-passkey#beforeCache pagehide@window->add-auth-passkey#beforeCache pageshow@window->add-auth-passkey#restore submit@document->add-auth-passkey#otherSubmission">
|
|
9
10
|
<p data-add-auth-passkey-target="unavailable">Passkeys need JavaScript and a compatible browser or device. Use another method allowed by your account policy if available.</p>
|
|
10
11
|
<%= form_with url: options_url, data: add_auth_challenge_form_data(action).merge(add_auth_passkey_target: "form", action: [add_auth_challenge_form_data(action)[:action], "submit->add-auth-passkey#submit"].compact.join(" ")) do |form| %>
|
|
11
12
|
<%= add_auth_challenge_tag action, id: "passkey_#{action}" %>
|
|
12
13
|
<div hidden data-add-auth-passkey-target="controls">
|
|
14
|
+
<%= render "add_auth/sign_ins/remember", form: form, id: "passkey" if action == :sign_in %>
|
|
13
15
|
<%= form.submit label, class: add_auth_class(:button), data: {add_auth_challenge_target: "submit", add_auth_passkey_target: "button"} %>
|
|
14
16
|
</div>
|
|
15
17
|
<% end %>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% content_for :title, "Create an account" %>
|
|
2
|
+
<h1 id="add_auth-title">Create an account with <%= @provider.label %></h1>
|
|
3
|
+
<p>Enter your email, then continue to your provider. We’ll email you a separate confirmation link before you can sign in.</p>
|
|
4
|
+
<%= form_with url: "/account/sign-up/providers/#{@provider.middleware_name}", method: :post, data: add_auth_challenge_form_data(:register).merge(turbo: false) do |form| %>
|
|
5
|
+
<div class="<%= add_auth_class(:field) %>">
|
|
6
|
+
<%= form.label :email_address, "Email address" %>
|
|
7
|
+
<%= form.email_field :email_address, required: true, maxlength: 254, autocomplete: "email", class: add_auth_class(:input) %>
|
|
8
|
+
</div>
|
|
9
|
+
<%= add_auth_challenge_tag :register %>
|
|
10
|
+
<%= form.submit "Continue with #{@provider.label}", class: add_auth_class(:button), data: {add_auth_challenge_target: "submit"} %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<p><%= link_to "Already have an account? Sign in", "/sign-in" %></p>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<h1 id="add_auth-title"><%= @heading %></h1>
|
|
2
|
+
<p class="<%= add_auth_class(:muted) %>">You will continue to <%= @provider.label %> to verify this request.</p>
|
|
3
|
+
<%= form_with url: @provider_path, method: :post, authenticity_token: false, data: {turbo: false} do |form| %>
|
|
4
|
+
<%= hidden_field_tag @omniauth_csrf_parameter, @omniauth_csrf_token %>
|
|
5
|
+
<%= form.submit "Continue with #{@provider.label}", class: add_auth_class(:button) %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<p><%= link_to "Cancel", "/sign-in" %></p>
|
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
<%= form.submit "Email me a verification link", class: add_auth_class(:button), data: {add_auth_challenge_target: "submit"} %>
|
|
20
20
|
<% end %>
|
|
21
21
|
<% end %>
|
|
22
|
+
<% if Array(@methods).include?(:external_identity) && AddAuth.configuration.external_identities.enabled %>
|
|
23
|
+
<% AddAuth.configuration.external_identities.providers.select(&:reauthentication?).each do |provider| %>
|
|
24
|
+
<%= form_with url: "/reauthenticate/providers/#{provider.middleware_name}", method: :post, data: add_auth_challenge_form_data(:reauthenticate).merge(turbo: false) do |form| %>
|
|
25
|
+
<%= form.hidden_field :purpose, value: @purpose %>
|
|
26
|
+
<%= add_auth_challenge_tag :reauthenticate, id: "verify_#{provider.middleware_name}" %>
|
|
27
|
+
<%= form.submit "Verify with #{provider.label}", class: add_auth_class(:button), data: {add_auth_challenge_target: "submit"} %>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% end %>
|
|
22
31
|
<% if Array(@methods).empty? %><p role="status">No permitted verification method is available here. Contact your account administrator for an allowed way to continue.</p><% end %>
|
|
23
32
|
<p><%= link_to "Cancel", @rule&.return_to || "/", class: add_auth_class(:link) %></p>
|
|
24
33
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<% @sessions.each do |session| %>
|
|
3
3
|
<li class="add_auth-session" id="session_<%= session.id %>">
|
|
4
4
|
<div>
|
|
5
|
-
<strong><%= session.current ? "This browser" : "Signed-in browser" %></strong>
|
|
5
|
+
<strong><%= session.transport == "mobile" ? "Signed-in mobile device (#{session.client_id})" : (session.current ? "This browser" : "Signed-in browser") %></strong>
|
|
6
6
|
<p class="<%= add_auth_class(:muted) %>">
|
|
7
7
|
<%= session.authenticated_with ? "Signed in with #{session.authenticated_with.tr("_", " ")}" : "Imported session" %>
|
|
8
8
|
· <%= session.last_seen_at ? "Last active #{session.last_seen_at.strftime("%Y-%m-%d %H:%M UTC")}" : "Activity unknown" %>
|
|
@@ -8,5 +8,12 @@
|
|
|
8
8
|
<h1 id="add_auth-title">Your sessions</h1>
|
|
9
9
|
<p class="<%= add_auth_class(:muted) %>">Review browsers signed in to your account and revoke any you no longer use.</p>
|
|
10
10
|
<p><%= link_to "Sign out everywhere", "/sessions/revoke-all", class: add_auth_class(:link) %></p>
|
|
11
|
+
<% if AddAuth.configuration.lifecycle.enabled %>
|
|
12
|
+
<p><%= link_to "Change email", "/account/email" %> · <%= link_to "Change password", "/account/password" %> · <%= link_to "Delete account", "/account/delete" %></p>
|
|
13
|
+
<% end %>
|
|
11
14
|
<div id="add_auth-session-content"><%= render "add_auth/sessions/list" %></div>
|
|
12
15
|
</main>
|
|
16
|
+
|
|
17
|
+
<% if AddAuth.configuration.external_identities.enabled %>
|
|
18
|
+
<p><%= link_to "Manage sign-in providers", "/account/external-identities" %></p>
|
|
19
|
+
<% end %>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<%= form_with url: "/sign-in/link", data: {turbo_frame: "_top"} do |form| %>
|
|
5
5
|
<%= form.hidden_field :token, value: params[:token] %>
|
|
6
6
|
<%= form.hidden_field :switch_account, value: "1" %>
|
|
7
|
+
<%= render "add_auth/sign_ins/remember", form: form, id: "email" %>
|
|
7
8
|
<%= form.submit "Sign in to this account", class: add_auth_class(:button) %>
|
|
8
9
|
<% end %>
|
|
9
10
|
<p><%= link_to "Cancel", "/sign-in", class: add_auth_class(:link) %></p>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<h1 id="add_auth-title">Sign in</h1>
|
|
2
2
|
<p class="<%= add_auth_class(:muted) %>">Choose an available sign-in method.</p>
|
|
3
|
+
<% if AddAuth.configuration.lifecycle.enabled %>
|
|
4
|
+
<p><%= link_to "Create an account", "/account/sign-up" %> · <%= link_to "Forgot your password?", "/account/requests/reset_password" %></p>
|
|
5
|
+
<% end %>
|
|
3
6
|
<% if @error %><p class="<%= add_auth_class(:notice) %>" role="alert"><%= @error %></p><% end %>
|
|
4
7
|
<% if AddAuth.configuration.passwords_enabled %>
|
|
5
8
|
<%= form_with url: "/sign-in/password", id: "add_auth-password-form", data: add_auth_challenge_form_data(:sign_in) do |form| %>
|
|
@@ -12,6 +15,7 @@
|
|
|
12
15
|
<%= form.password_field :password, required: true, autocomplete: "current-password", class: add_auth_class(:input) %>
|
|
13
16
|
</div>
|
|
14
17
|
<%= add_auth_challenge_tag :sign_in %>
|
|
18
|
+
<%= render "add_auth/sign_ins/remember", form: form, id: "password" %>
|
|
15
19
|
<%= form.submit "Sign in with password", class: add_auth_class(:button), data: {add_auth_challenge_target: "submit"} %>
|
|
16
20
|
<% end %>
|
|
17
21
|
<% end %>
|
|
@@ -32,3 +36,16 @@
|
|
|
32
36
|
<%= render "add_auth/passkeys/controls", mode: "get", options_url: "/passkeys/sign-in/options", finish_url: "/passkeys/sign-in", label: "Sign in with a passkey", action: :sign_in, conditional: true %>
|
|
33
37
|
<p><%= link_to "I can’t use my passkeys", "/recover" %></p>
|
|
34
38
|
<% end %>
|
|
39
|
+
<% if AddAuth.configuration.external_identities.enabled && AddAuth.configuration.external_identities.providers.any? %>
|
|
40
|
+
<hr class="<%= add_auth_class(:divider) %>">
|
|
41
|
+
<% AddAuth.configuration.external_identities.providers.each do |provider| %>
|
|
42
|
+
<%= form_with url: "/sign-in/providers/#{provider.middleware_name}", method: :post, data: add_auth_challenge_form_data(:provider).merge(turbo: false) do |form| %>
|
|
43
|
+
<%= render "add_auth/sign_ins/remember", form: form, id: "provider_#{provider.middleware_name}" %>
|
|
44
|
+
<%= add_auth_challenge_tag :provider, id: "provider_#{provider.middleware_name}" %>
|
|
45
|
+
<%= form.submit "Continue with #{provider.label}", class: add_auth_class(:button), data: {add_auth_challenge_target: "submit"} %>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% if AddAuth.configuration.lifecycle.enabled %>
|
|
48
|
+
<p><%= link_to "Create an account with #{provider.label}", "/account/sign-up/providers/#{provider.middleware_name}" %></p>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|
|
51
|
+
<% end %>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en"<% unless AddAuth.configuration.turbo_enabled %> data-turbo="false"<% end %>>
|
|
3
3
|
<head>
|
|
4
|
-
<title
|
|
4
|
+
<title><%= content_for?(:title) ? yield(:title) : "Sign in" %></title>
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<meta name="referrer" content="no-referrer">
|
|
6
|
+
<meta name="referrer" content="<%= response.headers["Referrer-Policy"] || "no-referrer" %>">
|
|
7
7
|
<% turbo_exempts_page_from_cache %>
|
|
8
8
|
<% turbo_page_requires_reload if request.headers["Turbo-Frame"].present? %>
|
|
9
9
|
<%= yield :head %>
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
</head>
|
|
14
14
|
<body class="<%= add_auth_class(:body) %>">
|
|
15
15
|
<main class="<%= add_auth_class(:panel) %>" aria-labelledby="add_auth-title">
|
|
16
|
+
<% if flash[:notice] %><p role="status" data-turbo-temporary><%= flash[:notice] %></p><% end %>
|
|
16
17
|
<div id="add_auth-content"><%= yield %></div>
|
|
17
18
|
</main>
|
|
18
19
|
</body>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "add_auth/migration/static_inventory"
|
|
7
|
+
|
|
8
|
+
options = {root: Dir.pwd}
|
|
9
|
+
parser = OptionParser.new do |opts|
|
|
10
|
+
opts.banner = "Usage: add_auth-preflight [--root PATH] (static inspection; does not boot Rails)"
|
|
11
|
+
opts.on("--root PATH", "Host source directory") { |value| options[:root] = value }
|
|
12
|
+
opts.on("-h", "--help") {
|
|
13
|
+
puts opts
|
|
14
|
+
exit
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
begin
|
|
18
|
+
parser.parse!
|
|
19
|
+
raise OptionParser::InvalidArgument unless ARGV.empty?
|
|
20
|
+
report = AddAuth::Migration::StaticInventory.new(**options).call
|
|
21
|
+
puts JSON.pretty_generate(report)
|
|
22
|
+
exit(report.dig(:facts, :complete) ? 0 : 2)
|
|
23
|
+
rescue OptionParser::ParseError, SystemCallError, ArgumentError
|
|
24
|
+
warn "Unable to inspect source. Supply a readable host directory with --root; see --help."
|
|
25
|
+
exit 2
|
|
26
|
+
end
|
|
@@ -1,24 +1,98 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "add_auth/core/passwords/bcrypt_support"
|
|
4
|
+
|
|
3
5
|
module AddAuth
|
|
4
6
|
class Configuration
|
|
5
7
|
attr_accessor :challenge, :challenge_on,
|
|
6
|
-
:digest_secret, :eligible, :stylesheet, :css_classes, :mail_from, :base_url, :rate_limit_store, :trusted_recovery_address, :support_url
|
|
8
|
+
:digest_secret, :eligible, :stylesheet, :css_classes, :mail_from, :base_url, :rate_limit_store, :trusted_recovery_address, :support_url, :legacy_password_verifier, :current_password_support
|
|
7
9
|
attr_writer :session_token_digest, :sign_in_token_digest
|
|
8
10
|
|
|
9
11
|
SessionOptions = Struct.new(:enabled, :lifetime, :idle_timeout, :legacy_bridge_until)
|
|
12
|
+
MobileOptions = Struct.new(:enabled, :lifetime, :idle_timeout, :clients, :callbacks, :apple_providers)
|
|
10
13
|
EmailOptions = Struct.new(:enabled, :token_lifetime, :same_browser)
|
|
11
14
|
StepUpOptions = Struct.new(:enabled, :purposes, :fresh_for, :strong_for)
|
|
12
15
|
PasskeyOptions = Struct.new(:enabled, :rp_id, :origins, :name, :anonymous_limit)
|
|
13
16
|
NotificationOptions = Struct.new(:enabled)
|
|
14
|
-
MaintenanceOptions = Struct.new(:batch_size, :session_retention, :email_retention, :notification_retention)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
MaintenanceOptions = Struct.new(:batch_size, :session_retention, :email_retention, :notification_retention, :account_retention)
|
|
18
|
+
LifecycleOptions = Struct.new(:enabled, :password_policy, :eligible, :provision, :profile_attributes, :maximum_attempts, :unlock_in, :proof_lifetime, :remember_lifetime, :remember_idle_timeout, :deletion_allowed, :delete_account)
|
|
19
|
+
class ExternalProvider
|
|
20
|
+
attr_reader :id, :label, :middleware_name, :configuration, :apple_form_post, :reauthentication
|
|
21
|
+
|
|
22
|
+
def initialize(id:, label:, middleware_name:, configuration:, apple_form_post: false, reauthentication: false)
|
|
23
|
+
@id, @label, @middleware_name = [id, label, middleware_name].map { |value| text(value) }
|
|
24
|
+
raise ArgumentError, "external provider middleware name is invalid" unless middleware_name.match?(/\A[a-z][a-z0-9_]{0,63}\z/)
|
|
25
|
+
raise ArgumentError, "external provider configuration does not match its id" unless configuration.respond_to?(:id) && configuration.id == id
|
|
26
|
+
raise ArgumentError, "Apple form_post must be explicit" unless apple_form_post == true || apple_form_post == false
|
|
27
|
+
|
|
28
|
+
raise ArgumentError, "provider reauthentication must be explicit" unless [true, false].include?(reauthentication)
|
|
29
|
+
@reauthentication = reauthentication
|
|
30
|
+
@configuration, @apple_form_post = configuration, apple_form_post
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reauthentication? = reauthentication
|
|
35
|
+
def apple_form_post? = apple_form_post
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def text(value)
|
|
40
|
+
raise ArgumentError, "external provider value is invalid" unless value.is_a?(String) && value.valid_encoding? &&
|
|
41
|
+
value.bytesize.between?(1, 2048) && !value.match?(/[\x00-\x1f\x7f]/)
|
|
42
|
+
|
|
43
|
+
value.dup.freeze
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class ExternalIdentityOptions
|
|
48
|
+
attr_accessor :enabled
|
|
49
|
+
|
|
50
|
+
def initialize
|
|
51
|
+
@enabled = false
|
|
52
|
+
@providers = []
|
|
53
|
+
@native_providers = {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def register(**attributes)
|
|
57
|
+
provider = ExternalProvider.new(**attributes)
|
|
58
|
+
raise ArgumentError, "external provider id is already registered" if configurations.any? { |item| item.id == provider.id }
|
|
59
|
+
raise ArgumentError, "external provider middleware is already registered" if @providers.any? { |item| item.middleware_name == provider.middleware_name }
|
|
60
|
+
|
|
61
|
+
@providers << provider
|
|
62
|
+
provider
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def provider(id)
|
|
66
|
+
@providers.find { |item| item.id == id.to_s }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def providers = @providers.dup.freeze
|
|
70
|
+
|
|
71
|
+
def register_native(configuration:)
|
|
72
|
+
raise ArgumentError, "a native provider configuration is required" unless configuration.is_a?(Core::ExternalIdentities::Configuration)
|
|
73
|
+
raise ArgumentError, "external provider id is already registered" if configurations.any? { |item| item.id == configuration.id }
|
|
74
|
+
@native_providers[configuration.id] = configuration
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def native_provider(id) = @native_providers[id]
|
|
78
|
+
def configurations = (providers.map(&:configuration) + @native_providers.values).freeze
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
attr_accessor :passwords_enabled, :turbo_enabled
|
|
82
|
+
attr_reader :maintenance, :notifications, :passkeys, :session, :email_link, :step_up, :challenge_when_unavailable, :lifecycle, :external_identities, :mobile
|
|
17
83
|
|
|
18
84
|
def initialize
|
|
19
85
|
@passwords_enabled = true
|
|
86
|
+
@current_password_support = Core::Passwords::BcryptSupport.new
|
|
87
|
+
@turbo_enabled = true
|
|
88
|
+
@lifecycle = LifecycleOptions.new(enabled: false, password_policy: ->(password) { password.length >= 12 && password.bytesize <= 72 },
|
|
89
|
+
eligible: ->(_user) { true }, provision: ->(_user) {}, profile_attributes: ->(_profile) { {} }, maximum_attempts: 20, unlock_in: 3600, proof_lifetime: 3600,
|
|
90
|
+
remember_lifetime: 14 * 86_400, remember_idle_timeout: 7 * 86_400,
|
|
91
|
+
deletion_allowed: ->(_user) { true }, delete_account: ->(user) { user.destroy! })
|
|
20
92
|
@session = SessionOptions.new(enabled: false, lifetime: 43_200, idle_timeout: 1800)
|
|
93
|
+
@mobile = MobileOptions.new(enabled: false, clients: [], callbacks: {}, apple_providers: {})
|
|
21
94
|
@email_link = EmailOptions.new(enabled: false, token_lifetime: 1200, same_browser: false)
|
|
95
|
+
@external_identities = ExternalIdentityOptions.new
|
|
22
96
|
@notifications = NotificationOptions.new(enabled: false)
|
|
23
97
|
@maintenance = MaintenanceOptions.new(batch_size: 100)
|
|
24
98
|
@passkeys = PasskeyOptions.new(enabled: false, origins: [], name: "Your account", anonymous_limit: 1000)
|
|
@@ -5,10 +5,11 @@ module AddAuth
|
|
|
5
5
|
# Account policy survives feature toggles; disabling passkeys cannot restore
|
|
6
6
|
# weaker access to an account that explicitly adopted strict policy.
|
|
7
7
|
class AccessPolicy
|
|
8
|
-
def initialize(credentials:, passkeys_enabled:, email_enabled:, trusted_recovery_address:, password_enabled: true)
|
|
8
|
+
def initialize(credentials:, passkeys_enabled:, email_enabled:, trusted_recovery_address:, password_enabled: true, external_enabled: false, external_current: nil)
|
|
9
9
|
@credentials, @passkeys_enabled, @email_enabled = credentials, passkeys_enabled, email_enabled
|
|
10
10
|
@trusted_recovery_address = trusted_recovery_address
|
|
11
11
|
@password_enabled = password_enabled
|
|
12
|
+
@external_enabled, @external_current = external_enabled == true, external_current
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def strict?(user) = user.respond_to?(:add_auth_strict) && user.add_auth_strict == true
|
|
@@ -20,6 +21,7 @@ module AddAuth
|
|
|
20
21
|
when :password then @password_enabled && user.respond_to?(:password_digest) && user.password_digest.is_a?(String) && !user.password_digest.empty?
|
|
21
22
|
when :email_link then @email_enabled
|
|
22
23
|
when :passkey then @passkeys_enabled
|
|
24
|
+
when :external_identity then @external_enabled
|
|
23
25
|
else false
|
|
24
26
|
end
|
|
25
27
|
end
|
|
@@ -41,8 +43,16 @@ module AddAuth
|
|
|
41
43
|
credential && credential.user_id == user.id && !credential.revoked_at
|
|
42
44
|
end
|
|
43
45
|
|
|
46
|
+
def external_credential_current?(user:, id:, version:)
|
|
47
|
+
@external_enabled && @external_current && @external_current.call(user: user, id: id, version: version) == true
|
|
48
|
+
end
|
|
49
|
+
|
|
44
50
|
def session_allowed?(user, row)
|
|
45
51
|
return false if row.respond_to?(:authentication_policy_version) && row.authentication_policy_version != version(user)
|
|
52
|
+
if row.authenticated_with == "external_identity"
|
|
53
|
+
return false unless row.respond_to?(:authentication_external_id) &&
|
|
54
|
+
external_credential_current?(user: user, id: row.authentication_external_id, version: row.authentication_external_version)
|
|
55
|
+
end
|
|
46
56
|
if row.authenticated_with == "passkey"
|
|
47
57
|
return false unless row.authentication_uv && credential_current?(user: user, id: row.authentication_credential_id)
|
|
48
58
|
end
|