Skip to content

Commit

Permalink
Merge pull request #7 from kobaltz/have-i-been-pwned
Browse files Browse the repository at this point in the history
Have I Been Pwned
  • Loading branch information
kobaltz authored Aug 13, 2024
2 parents 6556cd9 + bc4673a commit 27a6fe5
Show file tree
Hide file tree
Showing 10 changed files with 100 additions and 27 deletions.
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

0 comments on commit 27a6fe5

Please sign in to comment.