Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Have I Been Pwned #7

Merged
merged 1 commit into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ group :test do
end

# Add these gems for WebAuthn support
gem "webauthn", "~> 3.1"
gem "webauthn"

# Add these gems for pwened password support
gem "pwned"
8 changes: 5 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ GEM
cbor (0.5.9.8)
childprocess (5.1.0)
logger (~> 1.5)
concurrent-ruby (1.3.3)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
cose (1.3.0)
cose (1.3.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
crass (1.0.6)
Expand Down Expand Up @@ -152,6 +152,7 @@ GEM
public_suffix (6.0.1)
puma (6.4.2)
nio4r (~> 2.0)
pwned (2.4.1)
racc (1.8.1)
rack (3.1.7)
rack-session (2.0.0)
Expand Down Expand Up @@ -249,10 +250,11 @@ DEPENDENCIES
letter_opener
minitest-stub_any_instance
puma
pwned
simplecov
sprockets-rails
sqlite3 (~> 1.7)
webauthn (~> 3.1)
webauthn

BUNDLED WITH
2.4.22
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ user experience akin to that offered by the well-regarded Devise gem.
- [Routes](#routes)
- [Helper Methods](#helper-methods)
- [Restricting and Changing Routes](#restricting-and-changing-routes)
5. [WebAuthn](#webauthn)
6. [Within Your Application](#within-your-application)
7. Customizing
5. [Have I Been Pwned](#have-i-been-pwned)
6. [WebAuthn](#webauthn)
7. [Within Your Application](#within-your-application)
8. Customizing
- [Sign In Page](https://github.com/kobaltz/action_auth/wiki/Overriding-Sign-In-page-view)
7. [License](#license)
8. [Credits](#credits)
9. [License](#license)
10. [Credits](#credits)

## Breaking Changes

Expand Down Expand Up @@ -130,6 +131,8 @@ These are the planned features for ActionAuth. The ones that are checked off are

⏳ - OAuth with Google, Facebook, Github, Twitter, etc.

✅ - Have I Been Pwned Integration

✅ - Account Deletion

⏳ - Account Lockout
Expand Down Expand Up @@ -206,13 +209,15 @@ versus a user that is not logged in.
end
root to: 'welcome#index'

## WebAuthn
## Have I Been Pwned

ActionAuth's approach for WebAuthn is simplicity. It is used as a multifactor authentication step,
so users will still need to register their email address and password. Once the user is registered,
they can add a Passkey to their account. The Passkey could be an iCloud Keychain, a hardware security
key like a Yubikey, or a mobile device. If enabled and configured, the user will be prompted to use
their Passkey after they log in.
[Have I Been Pwned](https://haveibeenpwned.com/) is a way that youre able to check if a password has been compromised in a data breach. This is a great way to ensure that your users are using secure passwords.

Add the `pwned` gem to your Gemfile. That's all you'll have to do to enable this functionality.

```ruby
bundle add pwned
```

## Magic Links

Expand All @@ -236,6 +241,13 @@ will want to style this to fit your application and have some kind of confirmati
<%= button_to "Delete Account", action_auth.users_path, method: :delete %>
</p>
```
## WebAuthn

ActionAuth's approach for WebAuthn is simplicity. It is used as a multifactor authentication step,
so users will still need to register their email address and password. Once the user is registered,
they can add a Passkey to their account. The Passkey could be an iCloud Keychain, a hardware security
key like a Yubikey, or a mobile device. If enabled and configured, the user will be prompted to use
their Passkey after they log in.

#### Configuration

Expand Down
11 changes: 11 additions & 0 deletions app/controllers/action_auth/identity/password_resets_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module ActionAuth
module Identity
class PasswordResetsController < ApplicationController
before_action :set_user, only: %i[ edit update ]
before_action :validate_pwned_password, only: :update

def new
end
Expand Down Expand Up @@ -41,6 +42,16 @@ def user_params
def send_password_reset_email
UserMailer.with(user: @user).password_reset.deliver_later
end

def validate_pwned_password
return unless ActionAuth.configuration.pwned_enabled?

pwned = Pwned::Password.new(params[:password])
if pwned.pwned?
@user.errors.add(:password, "has been pwned #{pwned.pwned_count} times. Please choose a different password.")
render :edit, status: :unprocessable_entity
end
end
end
end
end
11 changes: 11 additions & 0 deletions app/controllers/action_auth/passwords_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module ActionAuth
class PasswordsController < ApplicationController
before_action :set_user
before_action :validate_pwned_password, only: :update

def edit
end
Expand All @@ -22,5 +23,15 @@ def set_user
def user_params
params.permit(:password, :password_confirmation, :password_challenge).with_defaults(password_challenge: "")
end

def validate_pwned_password
return unless ActionAuth.configuration.pwned_enabled?

pwned = Pwned::Password.new(params[:password])
if pwned.pwned?
@user.errors.add(:password, "has been pwned #{pwned.pwned_count} times. Please choose a different password.")
render :new, status: :unprocessable_entity
end
end
end
end
25 changes: 20 additions & 5 deletions app/controllers/action_auth/registrations_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module ActionAuth
class RegistrationsController < ApplicationController
before_action :validate_pwned_password, only: :create

def new
@user = User.new
end
Expand All @@ -23,12 +25,25 @@ def create
end

private
def user_params
params.permit(:email, :password, :password_confirmation)
end

def send_email_verification
UserMailer.with(user: @user).email_verification.deliver_later
def user_params
params.permit(:email, :password, :password_confirmation)
end

def send_email_verification
UserMailer.with(user: @user).email_verification.deliver_later
end

def validate_pwned_password
return unless ActionAuth.configuration.pwned_enabled?

pwned = Pwned::Password.new(params[:password])

if pwned.pwned?
@user = User.new(email: params[:email])
@user.errors.add(:password, "has been pwned #{pwned.pwned_count} times. Please choose a different password.")
render :new, status: :unprocessable_entity
end
end
end
end
6 changes: 3 additions & 3 deletions app/views/action_auth/identity/password_resets/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@

<%= form.hidden_field :sid, value: params[:sid] %>

<div>
<div class="mb-3">
<%= form.label :password, "New password", style: "display: block" %>
<%= form.password_field :password, required: true, autofocus: true, autocomplete: "new-password" %>
<div>12 characters minimum.</div>
</div>

<div>
<div class="mb-3">
<%= form.label :password_confirmation, "Confirm new password", style: "display: block" %>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password" %>
</div>

<div>
<%= form.submit "Save changes" %>
<%= form.submit "Save changes", class: "btn btn-primary" %>
</div>
<% end %>
9 changes: 7 additions & 2 deletions lib/action_auth/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,28 @@ def initialize
@allow_user_deletion = true
@default_from_email = "from@example.com"
@magic_link_enabled = true
@pwned_enabled = defined?(Pwned)
@verify_email_on_sign_in = true
@webauthn_enabled = defined?(WebAuthn)
@webauthn_origin = "http://localhost:3000"
@webauthn_rp_name = Rails.application.class.to_s.deconstantize
end

def allow_user_deletion?
@allow_user_deletion.respond_to?(:call) ? @allow_user_deletion.call : @allow_user_deletion
@allow_user_deletion == true
end

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

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

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

end
end
12 changes: 10 additions & 2 deletions test/controllers/action_auth/registrations_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,23 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
test "should sign up" do
assert_difference("ActionAuth::User.count") do
email = "#{SecureRandom.hex}@#{SecureRandom.hex}.com"
post sign_up_path, params: { email: email, password: "123456789012", password_confirmation: "123456789012" }
post sign_up_path, params: { email: email, password: email, password_confirmation: email }
end
assert_response :redirect
end

test "should not sign up" do
assert_no_difference("ActionAuth::User.count") do
email = "#{SecureRandom.hex}@#{SecureRandom.hex}.com"
post sign_up_path, params: { email: email, password: "1234567890AB", password_confirmation: "123456789012" }
post sign_up_path, params: { email: email, password: email, password_confirmation: "123456789012" }
end
assert_response :unprocessable_entity
end

test "should not sign up with pwned password" do
assert_no_difference("ActionAuth::User.count") do
email = "#{SecureRandom.hex}@#{SecureRandom.hex}.com"
post sign_up_path, params: { email: email, password: "Password1234", password_confirmation: "Password1234" }
end
assert_response :unprocessable_entity
end
Expand Down
6 changes: 6 additions & 0 deletions test/mailers/action_auth/user_mailer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,11 @@ class UserMailerTest < ActionMailer::TestCase
assert_equal "Verify your email", mail.subject
assert_equal [@user.email], mail.to
end

test "magic_link" do
mail = ActionAuth::UserMailer.with(user: @user).magic_link
assert_equal "Sign in to your account", mail.subject
assert_equal [@user.email], mail.to
end
end
end
Loading