Skip to content

Commit

Permalink
Magic Links
Browse files Browse the repository at this point in the history
Enable with `ActionAuth::Configuration.magic_links = true`

```
  # config/initializers/action_auth.rb
  config.magic_link_enabled = true
```
  • Loading branch information
kobaltz committed Aug 9, 2024
1 parent 26711cc commit 4a4f2fd
Show file tree
Hide file tree
Showing 15 changed files with 161 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
action_auth (1.0.0)
action_auth (1.1.0)
bcrypt (~> 3.1.0)
rails (~> 7.1)

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ ActionAuth.configure do |config|
config.webauthn_origin = "http://localhost:3000" # or "https://example.com"
config.webauthn_rp_name = Rails.application.class.to_s.deconstantize
config.verify_email_on_sign_in = true
config.magic_link_enabled = false
config.default_from_email = "[email protected]"
end
```
Expand All @@ -124,7 +125,7 @@ These are the planned features for ActionAuth. The ones that are checked off are
✅ - Passkeys/Hardware Security Keys
- Magic Links
- Magic Links
⏳ - OAuth with Google, Facebook, Github, Twitter, etc.
Expand Down Expand Up @@ -272,7 +273,7 @@ We can set the user to become a User record instead of an ActionAuth::User recor
class Current < ActiveSupport::CurrentAttributes
def user
return unless ActionAuth::Current.user
ActionAuth::Current.user.becomes(User)
ActionAuth::Current.user&.becomes(User)
end
end
```
Expand Down
20 changes: 20 additions & 0 deletions app/controllers/action_auth/magics/requests_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module ActionAuth
class Magics::RequestsController < ApplicationController
def new
end

def create
user = User.find_or_initialize_by(email: params[:email])
if user.new_record?
password = SecureRandom.hex(32)
user.password = password
user.password_confirmation = password
user.save!
end

UserMailer.with(user: user).magic_link.deliver_later

redirect_to sign_in_path, notice: "Check your email for a magic link."
end
end
end
15 changes: 15 additions & 0 deletions app/controllers/action_auth/magics/sign_ins_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module ActionAuth
class Magics::SignInsController < ApplicationController
def show
user = ActionAuth::User.find_by_token_for(:magic_token, params[:token])
if user
@session = user.sessions.create
cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true }
user.update(verified: true)
redirect_to main_app.root_path, notice: "Signed In"
else
redirect_to sign_in_path, alert: "Authentication failed, please try again."
end
end
end
end
7 changes: 7 additions & 0 deletions app/mailers/action_auth/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@ def email_verification

mail to: @user.email, subject: "Verify your email"
end

def magic_link
@user = params[:user]
@signed_id = @user.generate_token_for(:magic_token)

mail to: @user.email, subject: "Sign in to your account"
end
end
end
4 changes: 4 additions & 0 deletions app/models/action_auth/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class User < ApplicationRecord
password_salt.last(10)
end

generates_token_for :magic_token, expires_in: 20.minutes do
password_salt.last(10)
end

validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, allow_nil: true, length: { minimum: 12 }

Expand Down
21 changes: 21 additions & 0 deletions app/views/action_auth/magics/requests/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<h1>Sign up</h1>

<%= form_with(url: magics_requests_path) do |form| %>
<div class="mb-3">
<%= form.label :email, style: "display: block" %>
<%= form.email_field :email, required: true, autofocus: true, autocomplete: "email" %>
</div>

<div class="mb-3">
<%= form.submit "Request Magic Link", class: "btn btn-primary" %>
</div>
<% end %>

<div class="mb-3">
<%= link_to "Sign In", sign_in_path %> |
<%= link_to "Sign Up", sign_up_path %> |
<%= link_to "Reset Password", new_identity_password_reset_path %>
<% if ActionAuth.configuration.verify_email_on_sign_in %>
| <%= link_to "Verify Email", identity_email_verification_path %>
<% end %>
</div>
3 changes: 3 additions & 0 deletions app/views/action_auth/registrations/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@

<div class="mb-3">
<%= link_to "Sign In", sign_in_path %> |
<% if ActionAuth.configuration.magic_link_enabled? %>
<%= link_to "Magic Link", new_magics_requests_path %> |
<% end %>
<%= link_to "Reset Password", new_identity_password_reset_path %>
<% if ActionAuth.configuration.verify_email_on_sign_in %>
| <%= link_to "Verify Email", identity_email_verification_path %>
Expand Down
3 changes: 3 additions & 0 deletions app/views/action_auth/sessions/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@

<div class="mb-3">
<%= link_to "Sign Up", sign_up_path %> |
<% if ActionAuth.configuration.magic_link_enabled? %>
<%= link_to "Magic Link", new_magics_requests_path %> |
<% end %>
<%= link_to "Reset Password", new_identity_password_reset_path %>
<% if ActionAuth.configuration.verify_email_on_sign_in %>
| <%= link_to "Verify Email", identity_email_verification_path %>
Expand Down
3 changes: 3 additions & 0 deletions app/views/action_auth/user_mailer/magic_link.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p>
Use this <%= link_to "link", magics_sign_ins_url(token: @signed_id) %> to sign in.
</p>
7 changes: 7 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@

resource :webauthn_credential_authentications, only: [:new, :create]
end

if ActionAuth.configuration.magic_link_enabled?
namespace :magics do
resource :sign_ins, only: [:show]
resource :requests, only: [:new, :create]
end
end
end
6 changes: 6 additions & 0 deletions lib/action_auth/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ class Configuration
attr_accessor :webauthn_origin
attr_accessor :webauthn_rp_name
attr_accessor :verify_email_on_sign_in
attr_accessor :magic_link_enabled
attr_accessor :default_from_email

def initialize
@webauthn_enabled = defined?(WebAuthn)
@webauthn_origin = "http://localhost:3000"
@webauthn_rp_name = Rails.application.class.to_s.deconstantize
@verify_email_on_sign_in = true
@magic_link_enabled = false
@default_from_email = "[email protected]"
end

def webauthn_enabled?
@webauthn_enabled.respond_to?(:call) ? @webauthn_enabled.call : @webauthn_enabled
end

def magic_link_enabled?
@magic_link_enabled.respond_to?(:call) ? @magic_link_enabled.call : @magic_link_enabled
end

end
end
2 changes: 1 addition & 1 deletion lib/action_auth/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module ActionAuth
VERSION = "1.0.0"
VERSION = "1.1.0"
end
43 changes: 43 additions & 0 deletions test/controllers/action_auth/magics/requests_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require "test_helper"

module ActionAuth
class Magics::RequestsControllerTest < ActionDispatch::IntegrationTest
include Engine.routes.url_helpers

test "should get new" do
get new_magics_requests_path
assert_response :success
end

# Test the 'create' action
test "should create user and send magic link" do
assert_difference('User.count', 1) do
post magics_requests_url, params: { email: '[email protected]' }
end

user = User.find_by(email: '[email protected]')
assert_not_nil user
assert_enqueued_emails 1
assert_redirected_to sign_in_path
end

test "should send magic link to existing user" do
existing_user = action_auth_users(:one) # assuming you have a fixture for this
assert_no_difference('User.count') do
post magics_requests_url, params: { email: existing_user.email }
end

assert_enqueued_emails 1
assert_redirected_to sign_in_path
end

test "should not create user with invalid email" do
assert_no_difference('User.count') do
post magics_requests_url, params: { email: '' }
end

assert_response :unprocessable_entity
end

end
end
24 changes: 24 additions & 0 deletions test/controllers/action_auth/magics/sign_ins_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require "test_helper"

module ActionAuth
class Magics::SignInsControllerTest < ActionDispatch::IntegrationTest
include Engine.routes.url_helpers

test "should sign in user with valid token" do
user = action_auth_users(:one)
valid_token = user.generate_token_for(:magic_token)
assert_difference("Session.count", 1) do
get magics_sign_ins_url(token: valid_token)
end
assert user.reload.verified
end

test "should not sign in user with invalid token" do
assert_difference("Session.count", 0) do
get magics_sign_ins_url(token: 'invalid_token')
end

assert_redirected_to sign_in_path
end
end
end

0 comments on commit 4a4f2fd

Please sign in to comment.