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,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
class AccountsController < ::ApplicationController
|
|
5
|
+
include Rails::AuthenticationPages
|
|
6
|
+
|
|
7
|
+
skip_before_action :require_authentication, raise: false
|
|
8
|
+
layout "add_auth/authentication"
|
|
9
|
+
helper AddAuth::SignInsHelper
|
|
10
|
+
before_action :require_feature
|
|
11
|
+
before_action :private_response
|
|
12
|
+
before_action :require_add_auth_authentication, only: %i[edit_email change_email edit_password change_password confirm_deletion destroy]
|
|
13
|
+
protect_from_forgery with: :exception
|
|
14
|
+
rescue_from AddAuth::Error, "ActiveJob::EnqueueError", with: :service_unavailable
|
|
15
|
+
|
|
16
|
+
def new = page("register")
|
|
17
|
+
def check_email = page("check_email")
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
admitted = intake(:register)
|
|
21
|
+
return intake_failure(admitted) if admitted.is_a?(Symbol)
|
|
22
|
+
result = Rails::Runtime.accounts.register(identifier: admitted, password: params[:password], profile: registration_profile)
|
|
23
|
+
if result.success?
|
|
24
|
+
redirect_to "/account/check-email", status: :see_other
|
|
25
|
+
else
|
|
26
|
+
@error = "Check your email and choose a password that meets this application's requirements."
|
|
27
|
+
page("register", status: :unprocessable_entity)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def request_form
|
|
32
|
+
@purpose = purpose
|
|
33
|
+
page("request")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def request_proof
|
|
37
|
+
@purpose = purpose
|
|
38
|
+
admitted = intake(:account_request)
|
|
39
|
+
return intake_failure(admitted) if %i[challenge_rejected challenge_unavailable].include?(admitted)
|
|
40
|
+
Rails::Runtime.enqueue_account_request(identifier: admitted, purpose: @purpose) unless admitted.is_a?(Symbol)
|
|
41
|
+
redirect_to "/account/check-email", status: :see_other
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def proof
|
|
45
|
+
@purpose = purpose
|
|
46
|
+
result = Rails::Runtime.accounts.preview(token: params[:token], purpose: @purpose)
|
|
47
|
+
page(result.success? ? "proof" : "invalid_proof")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def consume
|
|
51
|
+
@purpose = purpose
|
|
52
|
+
admitted = Rails::Runtime.intake.anonymous(ip: request.remote_ip, action: :account_consume)
|
|
53
|
+
return intake_failure(admitted) if admitted.is_a?(Symbol)
|
|
54
|
+
result = Rails::Runtime.accounts.consume(token: params[:token], purpose: @purpose, password: params[:password])
|
|
55
|
+
if result.success?
|
|
56
|
+
redirect_to "/sign-in", status: :see_other, notice: "Your account has been updated. Sign in to start a new session."
|
|
57
|
+
else
|
|
58
|
+
@error = "This request could not be completed. Check the password requirements or request a new link."
|
|
59
|
+
page("proof", status: :unprocessable_entity)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def edit_email = page("email")
|
|
64
|
+
def edit_password = page("password")
|
|
65
|
+
def confirm_deletion = page("delete")
|
|
66
|
+
|
|
67
|
+
def destroy
|
|
68
|
+
result = Rails::Runtime.accounts.delete_account(user: Current.user, session: Current.session)
|
|
69
|
+
if result.success?
|
|
70
|
+
terminate_session
|
|
71
|
+
redirect_to "/sign-in", status: :see_other, notice: "Your account was deleted."
|
|
72
|
+
elsif result.reason == :elevation_required
|
|
73
|
+
redirect_to "/reauthenticate?purpose=delete_account", status: :see_other
|
|
74
|
+
else
|
|
75
|
+
@error = "Your account could not be deleted. Contact support if this continues."
|
|
76
|
+
page("delete", status: :unprocessable_entity)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def change_password
|
|
81
|
+
result = Rails::Runtime.accounts.change_password(user: Current.user, session: Current.session, password: params[:password])
|
|
82
|
+
if result.success?
|
|
83
|
+
terminate_session
|
|
84
|
+
redirect_to "/sign-in", status: :see_other, notice: "Your password was changed. Sign in with your new password."
|
|
85
|
+
elsif result.reason == :elevation_required
|
|
86
|
+
redirect_to "/reauthenticate?purpose=change_password", status: :see_other
|
|
87
|
+
else
|
|
88
|
+
@error = "This password cannot be used. Check the password requirements and try again."
|
|
89
|
+
page("password", status: :unprocessable_entity)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def change_email
|
|
94
|
+
result = Rails::Runtime.accounts.change_email(user: Current.user, session: Current.session, identifier: params[:email_address])
|
|
95
|
+
if result.success?
|
|
96
|
+
redirect_to "/account/check-email", status: :see_other
|
|
97
|
+
elsif result.reason == :elevation_required
|
|
98
|
+
redirect_to "/reauthenticate?purpose=change_email", status: :see_other
|
|
99
|
+
else
|
|
100
|
+
@error = "This email address cannot be used. Check it and try again."
|
|
101
|
+
page("email", status: :unprocessable_entity)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# Host controllers may override with an explicit Rails parameter allowlist.
|
|
108
|
+
# The default does not forward arbitrary account/role parameters.
|
|
109
|
+
def registration_profile = {}
|
|
110
|
+
|
|
111
|
+
def require_feature
|
|
112
|
+
head :not_found unless AddAuth.configuration.lifecycle.enabled
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def purpose
|
|
116
|
+
Core::AccountLifecycle::PURPOSES.include?(params[:purpose]) ? params[:purpose] : "confirm"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def intake(action)
|
|
120
|
+
Rails::Runtime.intake.call(identifier: params[:email_address], ip: request.remote_ip, action: action, challenge_token: challenge_token)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def authentication_partial(partial) = "add_auth/accounts/#{partial}"
|
|
124
|
+
|
|
125
|
+
def service_unavailable
|
|
126
|
+
@error = "Account requests are temporarily unavailable. Please try again shortly."
|
|
127
|
+
response.set_header("Retry-After", "60")
|
|
128
|
+
page("unavailable", status: :service_unavailable)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def intake_failure(reason)
|
|
132
|
+
@error = "This request could not be verified. Please try again shortly."
|
|
133
|
+
status = {rate_limited: :too_many_requests, challenge_unavailable: :service_unavailable}.fetch(reason, :unprocessable_entity)
|
|
134
|
+
response.set_header("Retry-After", "300") if %i[too_many_requests service_unavailable].include?(status)
|
|
135
|
+
page("unavailable", status: status)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -20,7 +20,8 @@ module AddAuth
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def boot
|
|
23
|
-
source = 'if (!window.Turbo) await import("/add_auth/turbo.js");
|
|
23
|
+
source = Rails::Runtime.config.turbo_enabled ? 'if (!window.Turbo) await import("/add_auth/turbo.js");' : ""
|
|
24
|
+
source += ' await import("/add_auth/challenge.js");'
|
|
24
25
|
source += ' await import("/add_auth/passkey.js");' if Rails::Runtime.config.passkeys.enabled
|
|
25
26
|
render body: source, content_type: "text/javascript"
|
|
26
27
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AddAuth
|
|
4
|
+
class ExternalIdentitiesController < ::ApplicationController
|
|
5
|
+
include Rails::AuthenticationPages
|
|
6
|
+
|
|
7
|
+
skip_before_action :require_authentication, raise: false
|
|
8
|
+
before_action :private_response
|
|
9
|
+
before_action :require_feature
|
|
10
|
+
before_action :require_add_auth_authentication
|
|
11
|
+
protect_from_forgery with: :exception
|
|
12
|
+
helper AddAuth::SignInsHelper
|
|
13
|
+
layout "add_auth/authentication"
|
|
14
|
+
rescue_from AddAuth::Error, with: :unavailable
|
|
15
|
+
|
|
16
|
+
def index
|
|
17
|
+
load_identities
|
|
18
|
+
page("index")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def destroy
|
|
22
|
+
elevation = Rails::Runtime.sessions.with_elevation(user: Current.user, session: Current.session,
|
|
23
|
+
purpose: Core::ExternalIdentities::UNLINK, policy: Rails::Runtime.step_up_policy)
|
|
24
|
+
result = service.unlink(user: Current.user, session: Current.session, identity_id: params[:id], grant: elevation.credential)
|
|
25
|
+
if result.success?
|
|
26
|
+
redirect_to "/account/external-identities", status: :see_other
|
|
27
|
+
elsif result.reason == :elevation_required
|
|
28
|
+
redirect_to "/reauthenticate?purpose=unlink_external_identity", status: :see_other
|
|
29
|
+
else
|
|
30
|
+
@error = (result.reason == :last_credential) ? "Add another usable sign-in method before removing this one." : "This sign-in method could not be changed. Try again."
|
|
31
|
+
load_identities
|
|
32
|
+
page("index", status: :unprocessable_entity)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def service = Rails::Runtime.external_identities
|
|
39
|
+
|
|
40
|
+
def require_feature
|
|
41
|
+
head :not_found unless Rails::Runtime.config.external_identities.enabled
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def authentication_partial(partial) = "add_auth/external_identities/#{partial}"
|
|
45
|
+
|
|
46
|
+
def load_identities
|
|
47
|
+
@identities = ::AddAuthExternalIdentity.where(user_id: Current.user.id, revoked_at: nil).order(:linked_at)
|
|
48
|
+
@providers = Rails::Runtime.config.external_identities.providers
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unavailable
|
|
52
|
+
@error = "External sign-in is temporarily unavailable."
|
|
53
|
+
page("unavailable", status: :service_unavailable)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -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>
|