add_auth 0.2.2 → 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 +22 -0
- data/README.md +38 -14
- data/ROADMAP.md +87 -38
- 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 +70 -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/runtime.rb +170 -8
- 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 +4 -0
- 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 +85 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
# This transport never reads/writes browser cookies or invokes the host's
|
|
5
|
+
# browser Authentication concern. Hosts may map the same Core result to a
|
|
6
|
+
# versioned application DTO through Runtime.mobile_authentication.
|
|
7
|
+
class MobileSessionsController < ActionController::API
|
|
8
|
+
before_action :private_response
|
|
9
|
+
before_action :require_mobile_feature
|
|
10
|
+
before_action :require_native_feature, only: %i[apple_challenge apple apple_enroll]
|
|
11
|
+
before_action :require_mobile_session, except: %i[create exchange apple_challenge apple apple_enroll]
|
|
12
|
+
rescue_from AddAuth::Error, ActiveRecord::ActiveRecordError, with: :unavailable
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
result = Rails::Runtime.mobile_authentication.password(identifier: params[:email_address], password: params[:password],
|
|
16
|
+
client_id: params[:client_id], ip: request.remote_ip, user_agent: request.user_agent, challenge_token: params[:challenge_token])
|
|
17
|
+
return failure(result.reason) unless result.success?
|
|
18
|
+
created(result)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show
|
|
22
|
+
render json: {session_id: @mobile_session.id, user_id: @mobile_session.user_id, expires_at: @mobile_session.expires_at}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def exchange
|
|
26
|
+
admitted = Rails::Runtime.intake.anonymous(ip: request.remote_ip, action: :mobile_handoff)
|
|
27
|
+
return failure(admitted) if admitted.is_a?(Symbol)
|
|
28
|
+
result = Rails::Runtime.mobile_handoffs.exchange(code: params[:code], state: params[:state],
|
|
29
|
+
code_verifier: params[:code_verifier], client_id: params[:client_id], ip: request.remote_ip, user_agent: request.user_agent)
|
|
30
|
+
return failure(result.reason) unless result.success?
|
|
31
|
+
created(result)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def apple_challenge
|
|
35
|
+
result = Rails::Runtime.native_apple.start(client_id: params[:client_id], ip: request.remote_ip, challenge_token: params[:challenge_token],
|
|
36
|
+
intent: params.fetch(:intent, "sign_in"))
|
|
37
|
+
return failure(result.reason) unless result.success?
|
|
38
|
+
challenge = result.credential
|
|
39
|
+
render json: {challenge_id: challenge.id, nonce: challenge.nonce, expires_at: challenge.expires_at}, status: :created
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def apple
|
|
43
|
+
result = Rails::Runtime.native_apple.complete(client_id: params[:client_id], challenge_id: params[:challenge_id], nonce: params[:nonce],
|
|
44
|
+
identity_token: params[:identity_token], ip: request.remote_ip, user_agent: request.user_agent)
|
|
45
|
+
return failure(result.reason) unless result.success?
|
|
46
|
+
created(result)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def apple_enroll
|
|
50
|
+
result = Rails::Runtime.native_apple.enroll(client_id: params[:client_id], challenge_id: params[:challenge_id], nonce: params[:nonce],
|
|
51
|
+
identity_token: params[:identity_token], ip: request.remote_ip, identifier: params[:email_address])
|
|
52
|
+
return failure(result.reason) unless result.success?
|
|
53
|
+
render json: {status: "confirmation_required"}, status: :accepted
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def index
|
|
57
|
+
page = Rails::Runtime.sessions.list_page(user: @mobile_user, current_session_id: @mobile_session.id, before: params[:before])
|
|
58
|
+
render json: {sessions: page.entries.map { |entry|
|
|
59
|
+
{id: entry.id, current: entry.current, transport: entry.transport, client_id: entry.client_id,
|
|
60
|
+
authenticated_with: entry.authenticated_with, last_seen_at: entry.last_seen_at, expires_at: entry.expires_at}
|
|
61
|
+
}, next_cursor: page.next_cursor}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def destroy
|
|
65
|
+
Rails::Runtime.sessions.revoke(session: @mobile_session)
|
|
66
|
+
head :no_content
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def revoke
|
|
70
|
+
revoked = Rails::Runtime.sessions.revoke_one(user: @mobile_user, session: @mobile_session, session_id: params[:id])
|
|
71
|
+
head(revoked ? :no_content : :not_found)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def revoke_all
|
|
75
|
+
result = Rails::Runtime.revoke_all(user: @mobile_user, session: @mobile_session,
|
|
76
|
+
password: params[:password], ip: request.remote_ip, challenge_token: params[:challenge_token])
|
|
77
|
+
result.success? ? head(:no_content) : failure(result.reason)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def created(result)
|
|
83
|
+
render json: {token: result.grant.bearer, token_type: "Bearer", expires_at: result.session.expires_at,
|
|
84
|
+
session_id: result.session.id, user_id: result.user.id}, status: :created
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def require_mobile_session
|
|
88
|
+
require "add_auth/core/mobile_authentication"
|
|
89
|
+
bearer = Core::MobileAuthentication.bearer(request.authorization)
|
|
90
|
+
grant = Rails::Runtime.sessions.resume_mobile(bearer: bearer)
|
|
91
|
+
return failure(:invalid_credentials) unless grant
|
|
92
|
+
@mobile_session = grant.session
|
|
93
|
+
@mobile_user = grant.session.user
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def require_mobile_feature
|
|
97
|
+
head :not_found unless Rails::Runtime.config.mobile.enabled
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def require_native_feature
|
|
101
|
+
return unavailable unless Rails::Runtime.config.mobile.apple_providers.is_a?(Hash)
|
|
102
|
+
head :not_found if Rails::Runtime.config.mobile.apple_providers.empty?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def private_response
|
|
106
|
+
response.set_header("Cache-Control", "no-store")
|
|
107
|
+
response.set_header("Referrer-Policy", "no-referrer")
|
|
108
|
+
head :upgrade_required if ::Rails.env.production? && !request.ssl?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def failure(reason)
|
|
112
|
+
status = Core::MobileResponse.status(reason)
|
|
113
|
+
response.set_header("Retry-After", "300") if [429, 503].include?(status)
|
|
114
|
+
response.set_header("WWW-Authenticate", "Bearer") if status == 401
|
|
115
|
+
render json: {error: reason}, status: status
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def unavailable = failure(:challenge_unavailable)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "add_auth/rails/provider_libraries/apple_form_post_correlation"
|
|
4
|
+
require "add_auth/rails/provider_libraries/omniauth"
|
|
5
|
+
require "add_auth/rails/provider_libraries/omniauth_correlation"
|
|
6
|
+
require "add_auth/rails/provider_libraries/request_protection"
|
|
7
|
+
|
|
8
|
+
module AddAuth
|
|
9
|
+
# The protocol library has already verified the callback before this
|
|
10
|
+
# controller sees it. This controller only starts/consumes a Core
|
|
11
|
+
# transaction and maps its typed result to the normal Rails session/UI.
|
|
12
|
+
class ProviderSignInsController < ::ApplicationController
|
|
13
|
+
include Rails::AuthenticationPages
|
|
14
|
+
|
|
15
|
+
skip_before_action :require_authentication, raise: false
|
|
16
|
+
before_action :private_response
|
|
17
|
+
before_action :load_provider
|
|
18
|
+
before_action :require_add_auth_authentication, unless: :anonymous_or_callback?
|
|
19
|
+
protect_from_forgery with: :exception, except: :callback
|
|
20
|
+
helper AddAuth::SignInsHelper
|
|
21
|
+
layout "add_auth/authentication"
|
|
22
|
+
rescue_from AddAuth::Error, with: :provider_unavailable
|
|
23
|
+
|
|
24
|
+
# This deliberately renders an ordinary confirmation form instead of
|
|
25
|
+
# redirecting to /auth/:provider: OmniAuth 2 accepts a CSRF-protected POST,
|
|
26
|
+
# and a 303 would turn that navigation into an unsafe GET. It is equally
|
|
27
|
+
# usable with Turbo, inside a frame, and with JavaScript disabled.
|
|
28
|
+
def enrollment
|
|
29
|
+
return head :not_found unless Rails::Runtime.config.lifecycle.enabled
|
|
30
|
+
page("enrollment")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def prepare
|
|
34
|
+
return head :not_found if mobile? && !Rails::Runtime.config.mobile.enabled
|
|
35
|
+
if enrollment?
|
|
36
|
+
return head :not_found unless Rails::Runtime.config.lifecycle.enabled
|
|
37
|
+
@enrollment_identifier = Rails::Runtime.intake.call(identifier: params[:email_address], ip: request.remote_ip,
|
|
38
|
+
action: :register, challenge_token: challenge_token)
|
|
39
|
+
return provider_intake_failure(@enrollment_identifier) if @enrollment_identifier.is_a?(Symbol)
|
|
40
|
+
else
|
|
41
|
+
admitted = Rails::Runtime.intake.anonymous(ip: request.remote_ip, action: (params[:flow].to_s == "reauthenticate") ? :reauthenticate : :provider, challenge_token: challenge_token)
|
|
42
|
+
return provider_intake_failure(admitted) if admitted.is_a?(Symbol)
|
|
43
|
+
end
|
|
44
|
+
if params[:flow].to_s == "link"
|
|
45
|
+
elevation = Rails::Runtime.sessions.with_elevation(user: Current.user, session: Current.session,
|
|
46
|
+
purpose: Core::ExternalIdentities::LINK, policy: Rails::Runtime.step_up_policy)
|
|
47
|
+
return redirect_to "/reauthenticate?purpose=link_external_identity", status: :see_other unless elevation.success?
|
|
48
|
+
end
|
|
49
|
+
pending = service.begin_transaction(configuration_id: @provider.configuration.id,
|
|
50
|
+
browser_secret: browser_secret, **transaction_subject)
|
|
51
|
+
return provider_failure(pending.reason) unless pending.success?
|
|
52
|
+
|
|
53
|
+
if mobile?
|
|
54
|
+
accepted = Rails::Runtime.mobile_handoffs.begin_transaction(pending: pending.credential,
|
|
55
|
+
client_id: params[:client_id], callback: params[:callback], state: params[:state],
|
|
56
|
+
code_challenge: params[:code_challenge], code_challenge_method: params[:code_challenge_method])
|
|
57
|
+
unless accepted.success?
|
|
58
|
+
service.cancel(transaction: pending.credential.id, browser_secret: browser_secret)
|
|
59
|
+
return provider_failure(accepted.reason)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if enrollment? && !Rails::Runtime.provider_enrollment.write(pending: pending.credential,
|
|
64
|
+
identifier: @enrollment_identifier, profile: registration_profile)
|
|
65
|
+
service.cancel(transaction: pending.credential.id, browser_secret: browser_secret)
|
|
66
|
+
return provider_failure(:invalid_credentials)
|
|
67
|
+
end
|
|
68
|
+
remember_correlation!(pending.credential)
|
|
69
|
+
@provider_path = "/auth/#{@provider.middleware_name}"
|
|
70
|
+
@omniauth_csrf_parameter = Rails::ProviderLibraries::RequestProtection.parameter
|
|
71
|
+
@omniauth_csrf_token = Rails::ProviderLibraries::RequestProtection.token(session: request.env.fetch("rack.session"),
|
|
72
|
+
rails_token: -> { form_authenticity_token })
|
|
73
|
+
@heading = heading_for(pending.credential.purpose)
|
|
74
|
+
# This token-free URL posts to the host's existing OAuth middleware.
|
|
75
|
+
# Preserve its Rails origin check without sending a referrer off-site.
|
|
76
|
+
response.set_header("Referrer-Policy", "same-origin")
|
|
77
|
+
page("prepare")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# OmniAuth performs callback state/nonce/code validation. The correlation
|
|
81
|
+
# middleware has attached the opaque Core transaction only after its
|
|
82
|
+
# state check succeeds; no raw params, profile attributes, or tokens are
|
|
83
|
+
# accepted here. Apple's signed callback cookie supplies that same opaque
|
|
84
|
+
# data when its form_post cannot send the normal SameSite=Lax session.
|
|
85
|
+
def callback
|
|
86
|
+
correlation = request.env[correlation_env_key]
|
|
87
|
+
return provider_failure(:invalid_credentials) unless valid_correlation?(correlation)
|
|
88
|
+
return provider_failure(:invalid_credentials) unless correlation.fetch("configuration_id") == @provider.configuration.id
|
|
89
|
+
|
|
90
|
+
pending = service.pending(transaction: correlation.fetch("transaction"), browser_secret: correlation.fetch("browser_secret"))
|
|
91
|
+
return provider_failure(:invalid_credentials) unless pending && pending.configuration.equal?(@provider.configuration) &&
|
|
92
|
+
pending.purpose.to_s == correlation.fetch("purpose")
|
|
93
|
+
|
|
94
|
+
callback = Rails::ProviderLibraries::OmniAuth::CallbackResult.capture(env: request.env, provider: @provider.middleware_name)
|
|
95
|
+
evidence = pending.configuration.verify(server_result: callback, transaction: pending)
|
|
96
|
+
return provider_failure(:invalid_credentials) unless evidence
|
|
97
|
+
|
|
98
|
+
case pending.purpose
|
|
99
|
+
when Core::ExternalIdentities::MOBILE then finish_mobile(evidence)
|
|
100
|
+
when Core::ExternalIdentities::ENROLL then finish_enrollment(pending, evidence)
|
|
101
|
+
when :sign_in then finish_sign_in(evidence)
|
|
102
|
+
when Core::ExternalIdentities::LINK then finish_link(pending, evidence)
|
|
103
|
+
else finish_reauthentication(pending, evidence)
|
|
104
|
+
end
|
|
105
|
+
ensure
|
|
106
|
+
Rails::Runtime.provider_enrollment.erase(pending: pending) if pending&.purpose == Core::ExternalIdentities::ENROLL
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def service = Rails::Runtime.external_identities
|
|
112
|
+
|
|
113
|
+
def load_provider
|
|
114
|
+
return head :not_found unless Rails::Runtime.config.external_identities.enabled
|
|
115
|
+
|
|
116
|
+
@provider = Rails::Runtime.config.external_identities.providers.find { |item| item.middleware_name == params[:provider].to_s }
|
|
117
|
+
head :not_found unless @provider
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def sign_in? = params[:flow].to_s == "sign_in"
|
|
121
|
+
def mobile? = params[:flow].to_s == "mobile"
|
|
122
|
+
|
|
123
|
+
def enrollment? = params[:flow].to_s == "enroll"
|
|
124
|
+
def anonymous_or_callback? = sign_in? || enrollment? || mobile? || action_name == "callback"
|
|
125
|
+
def registration_profile = {}
|
|
126
|
+
|
|
127
|
+
def transaction_subject
|
|
128
|
+
return {purpose: Core::ExternalIdentities::MOBILE} if mobile?
|
|
129
|
+
return {purpose: Core::ExternalIdentities::ENROLL} if enrollment?
|
|
130
|
+
return {purpose: :sign_in} if sign_in?
|
|
131
|
+
return {user: Current.user, session: Current.session, purpose: Core::ExternalIdentities::LINK} if params[:flow].to_s == "link"
|
|
132
|
+
|
|
133
|
+
{user: Current.user, session: Current.session, purpose: params[:purpose].to_s}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def browser_secret
|
|
137
|
+
session[:add_auth_browser] ||= Rails::Runtime.browser_binding.generate
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def remember_correlation!(pending)
|
|
141
|
+
if @provider.apple_form_post?
|
|
142
|
+
Rails::ProviderLibraries::AppleFormPostCorrelation.remember!(session: session, transaction: pending.id,
|
|
143
|
+
browser_secret: browser_secret, configuration_id: @provider.configuration.id, purpose: pending.purpose, remember: remember?)
|
|
144
|
+
else
|
|
145
|
+
Rails::ProviderLibraries::OmniAuthCorrelation.remember!(session: session, provider: @provider.middleware_name,
|
|
146
|
+
transaction: pending.id, browser_secret: browser_secret, configuration_id: @provider.configuration.id, purpose: pending.purpose, remember: remember?)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def correlation_env_key
|
|
151
|
+
@provider.apple_form_post? ? "add_auth.apple_correlation" : Rails::ProviderLibraries::OmniAuthCorrelation::ENV_KEY
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def valid_correlation?(value)
|
|
155
|
+
value.is_a?(Hash) && %w[transaction browser_secret configuration_id purpose].all? do |key|
|
|
156
|
+
value[key].is_a?(String) && value[key].valid_encoding? && value[key].bytesize.between?(1, 2048)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def finish_enrollment(pending, evidence)
|
|
161
|
+
intake = Rails::Runtime.provider_enrollment.read(pending: pending)
|
|
162
|
+
return provider_failure(:invalid_credentials) unless intake
|
|
163
|
+
result = Rails::Runtime.accounts.register_external(evidence: evidence, **intake)
|
|
164
|
+
return provider_failure(result.reason) unless result.success?
|
|
165
|
+
redirect_to "/account/check-email", status: :see_other
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def provider_intake_failure(reason)
|
|
169
|
+
status = {rate_limited: :too_many_requests, challenge_unavailable: :service_unavailable}.fetch(reason, :unprocessable_entity)
|
|
170
|
+
@error = "This request could not be verified. Please try again shortly."
|
|
171
|
+
response.set_header("Retry-After", "300") if %i[rate_limited challenge_unavailable].include?(reason)
|
|
172
|
+
page("failure", status: status)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def finish_sign_in(evidence)
|
|
176
|
+
result = service.sign_in(evidence: evidence, replacing: add_auth_replacement_session,
|
|
177
|
+
**add_auth_session_hints.merge(remember: correlation_remember?))
|
|
178
|
+
return provider_failure(result.reason) unless result.success?
|
|
179
|
+
|
|
180
|
+
destination = after_authentication_url
|
|
181
|
+
add_auth_accept(result.grant)
|
|
182
|
+
redirect_to destination, status: :see_other
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def finish_mobile(evidence)
|
|
186
|
+
result = service.mobile_handoff(evidence: evidence, handoffs: Rails::Runtime.mobile_handoffs)
|
|
187
|
+
return provider_failure(result.reason) unless result.success?
|
|
188
|
+
handoff = result.credential
|
|
189
|
+
redirect_to "#{handoff.callback}?#{URI.encode_www_form(code: handoff.code, state: handoff.state)}", status: :see_other, allow_other_host: true
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def finish_link(pending, evidence)
|
|
193
|
+
account, current = correlated_session(pending)
|
|
194
|
+
return provider_failure(:elevation_required) unless account && current
|
|
195
|
+
|
|
196
|
+
elevation = Rails::Runtime.sessions.with_elevation(user: account, session: current,
|
|
197
|
+
purpose: Core::ExternalIdentities::LINK, policy: Rails::Runtime.step_up_policy)
|
|
198
|
+
result = service.link(user: account, session: current, evidence: evidence, grant: elevation.credential)
|
|
199
|
+
return provider_failure(result.reason) unless result.success?
|
|
200
|
+
|
|
201
|
+
redirect_to "/account/external-identities", status: :see_other
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def finish_reauthentication(pending, evidence)
|
|
205
|
+
account, current = correlated_session(pending)
|
|
206
|
+
return provider_failure(:elevation_required) unless account && current
|
|
207
|
+
|
|
208
|
+
result = service.reauthenticate(user: account, session: current, evidence: evidence)
|
|
209
|
+
return provider_failure(result.reason) unless result.success?
|
|
210
|
+
|
|
211
|
+
grant = Rails::Runtime.sessions.rotate_for_step_up(user: account, session: current, grant: result.credential)
|
|
212
|
+
return provider_failure(:elevation_required) unless grant
|
|
213
|
+
|
|
214
|
+
add_auth_accept(grant)
|
|
215
|
+
redirect_to Rails::Runtime.step_up_policy.return_to(pending.purpose), status: :see_other
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# A form_post callback can correctly correlate an authenticated transaction
|
|
219
|
+
# while lacking the normal app cookie. Rehydrate only the exact session row
|
|
220
|
+
# recorded by Core; Core rechecks that row's digest and policy version under
|
|
221
|
+
# lock before link/reauthentication can mutate authority.
|
|
222
|
+
def correlated_session(pending)
|
|
223
|
+
account = ::User.find_by(id: pending.user_id)
|
|
224
|
+
current = account && ::Session.find_by(id: pending.session_id, user_id: account.id, token_digest: pending.session_digest)
|
|
225
|
+
[account, current]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def provider_failure(_reason)
|
|
229
|
+
@error = "We could not verify this provider sign-in. Please try again."
|
|
230
|
+
page("failure", status: :unprocessable_entity)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def provider_unavailable
|
|
234
|
+
@error = "Provider sign-in is temporarily unavailable. Please try again shortly."
|
|
235
|
+
page("failure", status: :service_unavailable)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def heading_for(purpose)
|
|
239
|
+
[:sign_in, Core::ExternalIdentities::ENROLL, Core::ExternalIdentities::MOBILE].include?(purpose) ? "Continue to #{@provider.label}" : "Confirm with #{@provider.label}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def correlation_remember? = request.env[correlation_env_key].fetch("remember") == true
|
|
243
|
+
|
|
244
|
+
def remember? = sign_in? && params[:remember].to_s == "1"
|
|
245
|
+
|
|
246
|
+
def authentication_partial(partial) = "add_auth/provider_sign_ins/#{partial}"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -14,6 +14,7 @@ module AddAuth
|
|
|
14
14
|
|
|
15
15
|
def add_auth_challenge_form_data(action)
|
|
16
16
|
data = {turbo_frame: "_top"}
|
|
17
|
+
data[:turbo] = false unless AddAuth.configuration.turbo_enabled
|
|
17
18
|
return data unless AddAuth.configuration.challenge_on.include?(action)
|
|
18
19
|
provider = AddAuth.configuration.challenge
|
|
19
20
|
return data unless provider.site_key
|
|
@@ -23,7 +24,7 @@ module AddAuth
|
|
|
23
24
|
else return data
|
|
24
25
|
end
|
|
25
26
|
data.merge(controller: "add-auth-challenge",
|
|
26
|
-
action: "submit->add-auth-challenge#submit add_auth:proof-used->add-auth-challenge#reset turbo:submit-end->add-auth-challenge#reset turbo:before-cache@document->add-auth-challenge#beforeCache",
|
|
27
|
+
action: "submit->add-auth-challenge#submit add_auth:proof-used->add-auth-challenge#reset turbo:submit-end->add-auth-challenge#reset turbo:before-cache@document->add-auth-challenge#beforeCache pagehide@window->add-auth-challenge#beforeCache pageshow@window->add-auth-challenge#restore",
|
|
27
28
|
add_auth_challenge_provider_value: kind, add_auth_challenge_site_key_value: provider.site_key,
|
|
28
29
|
add_auth_challenge_action_value: (provider.respond_to?(:expected_action) && provider.expected_action) || action,
|
|
29
30
|
add_auth_challenge_script_url_value: provider.script_url)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
class AccountDeliveryJob < DeliveryJob
|
|
5
|
+
def perform(id)
|
|
6
|
+
record = ::AddAuthAccountToken.find_by(id: id)
|
|
7
|
+
return unless record
|
|
8
|
+
deliver(service: Rails::Runtime.accounts, record: record, mailer: AccountMailer) do |delivery|
|
|
9
|
+
AccountMailer.link(recipient: delivery.fetch(:recipient), purpose: delivery.fetch(:purpose),
|
|
10
|
+
url: Rails::Runtime.account_proof_url(delivery.fetch(:token), purpose: delivery.fetch(:purpose)))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
class AccountRequestJob < ActiveJob::Base
|
|
5
|
+
self.log_arguments = false
|
|
6
|
+
retry_on StandardError, wait: :polynomially_longer, attempts: 8
|
|
7
|
+
|
|
8
|
+
def perform(encrypted_identifier)
|
|
9
|
+
payload = Rails::Runtime.decrypt_intake(encrypted_identifier)
|
|
10
|
+
if payload.is_a?(Hash)
|
|
11
|
+
Rails::Runtime.accounts.issue(identifier: payload["identifier"], purpose: payload["purpose"], request_id: job_id)
|
|
12
|
+
end
|
|
13
|
+
record = ::AddAuthAccountToken.find_by(request_id: job_id)
|
|
14
|
+
AccountDeliveryJob.perform_later(record.id) if record
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
class AccountMailer < SignInMailer
|
|
5
|
+
def link(recipient:, url:, purpose:)
|
|
6
|
+
@instruction = {"confirm" => "Confirm your email address", "reset_password" => "Choose a new password", "unlock" => "Unlock your account"}.fetch(purpose)
|
|
7
|
+
@url = url
|
|
8
|
+
mail(to: recipient, from: Rails::Runtime.config.mail_from, subject: @instruction,
|
|
9
|
+
content_type: "text/plain").extend(SignInMailer::DeliveryReceipt)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= @instruction %>
|
|
2
|
+
|
|
3
|
+
Open this link, then use the button on the page to confirm your request:
|
|
4
|
+
<%= @url %>
|
|
5
|
+
|
|
6
|
+
This link expires in <%= AddAuth.configuration.lifecycle.proof_lifetime.to_i / 60 %> minutes and can be used once.
|
|
7
|
+
If you did not request this message, you can ignore it.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<% content_for :title, "Check your email" %>
|
|
2
|
+
<h1 id="add_auth-title">Check your email</h1>
|
|
3
|
+
<p role="status">If this request is available for your account, we’ll send a link with the next step.</p>
|
|
4
|
+
<p class="<%= add_auth_class(:muted) %>">Check your spam folder too. Only the latest link for this request will work.</p>
|
|
5
|
+
<p><%= link_to "Back to sign in", "/sign-in" %></p>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<% content_for :title, "Delete your account" %>
|
|
2
|
+
<h1 id="add_auth-title">Delete your account</h1>
|
|
3
|
+
<p>Your account will be deleted and all sessions signed out. Verify it’s you, then return here to confirm.</p>
|
|
4
|
+
<% if @error %><p role="alert" class="<%= add_auth_class(:notice) %>"><%= @error %></p><% end %>
|
|
5
|
+
<%= button_to "Delete my account", "/account", method: :delete, class: add_auth_class(:button), form: {data: {turbo_frame: "_top"}} %>
|
|
6
|
+
<p><%= link_to "Keep my account", "/sessions" %></p>
|
|
@@ -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>
|