From c6c55130ba9aa6b04e250ada617200207ac4922e Mon Sep 17 00:00:00 2001 From: Nixon Date: Tue, 8 Oct 2024 00:10:58 -0300 Subject: [PATCH 1/4] new version --- CHANGELOG.md | 2 ++ Gemfile.lock | 2 +- README.md | 6 ++++++ lib/authentication_zero/version.rb | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf5ef2..d49dd9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +## Authentication Zero 4.0.0 ## + ## Authentication Zero 3.0.2 ## * Fix bug where token is not expired/invalid diff --git a/Gemfile.lock b/Gemfile.lock index dc2603d..ae24507 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - authentication-zero (3.0.2) + authentication-zero (4.0.0) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index 98f6800..ea87fb8 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,12 @@ The purpose of authentication zero is to generate a pre-built authentication sys $ bundle add authentication-zero ``` +If you are using Rails < 8, you must use version 3. + +``` +$ bundle add authentication-zero --version "~> 3" +``` + If you are using Rails < 7.1, you must use version 2. ``` diff --git a/lib/authentication_zero/version.rb b/lib/authentication_zero/version.rb index 933bf49..1246d68 100644 --- a/lib/authentication_zero/version.rb +++ b/lib/authentication_zero/version.rb @@ -1,3 +1,3 @@ module AuthenticationZero - VERSION = "3.0.2" + VERSION = "4.0.0" end From 576e86ea12875c528fb13a1cf85a265878129514 Mon Sep 17 00:00:00 2001 From: Nixon Date: Tue, 8 Oct 2024 00:12:38 -0300 Subject: [PATCH 2/4] Remove system tests --- .github/workflows/CI.yml | 1 - CHANGELOG.md | 2 ++ .../authentication_generator.rb | 2 -- .../application_system_test_case.rb.tt | 15 ---------- .../system/identity/emails_test.rb.tt | 26 ---------------- .../identity/password_resets_test.rb.tt | 28 ----------------- .../test_unit/system/passwords_test.rb.tt | 18 ----------- .../test_unit/system/registrations_test.rb.tt | 14 --------- .../test_unit/system/sessions_test.rb.tt | 30 ------------------- 9 files changed, 2 insertions(+), 134 deletions(-) delete mode 100644 lib/generators/authentication/templates/test_unit/application_system_test_case.rb.tt delete mode 100644 lib/generators/authentication/templates/test_unit/system/identity/emails_test.rb.tt delete mode 100644 lib/generators/authentication/templates/test_unit/system/identity/password_resets_test.rb.tt delete mode 100644 lib/generators/authentication/templates/test_unit/system/passwords_test.rb.tt delete mode 100644 lib/generators/authentication/templates/test_unit/system/registrations_test.rb.tt delete mode 100644 lib/generators/authentication/templates/test_unit/system/sessions_test.rb.tt diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 244cd64..40093fa 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -50,7 +50,6 @@ jobs: run: | cd test-app bin/rails test - bin/rails test:system test_api: name: 🧪 Run API Tests diff --git a/CHANGELOG.md b/CHANGELOG.md index d49dd9e..c0fe0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Authentication Zero 4.0.0 ## +* Remove system tests + ## Authentication Zero 3.0.2 ## * Fix bug where token is not expired/invalid diff --git a/lib/generators/authentication/authentication_generator.rb b/lib/generators/authentication/authentication_generator.rb index c9953a3..a28fd7a 100644 --- a/lib/generators/authentication/authentication_generator.rb +++ b/lib/generators/authentication/authentication_generator.rb @@ -222,9 +222,7 @@ def add_routes def create_test_files directory "test_unit/controllers/#{format}", "test/controllers" directory "test_unit/mailers/", "test/mailers" - directory "test_unit/system", "test/system" unless options.api? template "test_unit/test_helper.rb", "test/test_helper.rb", force: true - template "test_unit/application_system_test_case.rb", "test/application_system_test_case.rb", force: true unless options.api? end private diff --git a/lib/generators/authentication/templates/test_unit/application_system_test_case.rb.tt b/lib/generators/authentication/templates/test_unit/application_system_test_case.rb.tt deleted file mode 100644 index f3db1f4..0000000 --- a/lib/generators/authentication/templates/test_unit/application_system_test_case.rb.tt +++ /dev/null @@ -1,15 +0,0 @@ -require "test_helper" - -class ApplicationSystemTestCase < ActionDispatch::SystemTestCase - driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] - - def sign_in_as(user) - visit sign_in_url - fill_in :email, with: user.email - fill_in :password, with: "Secret1*3*5*" - click_on "Sign in" - - assert_current_path root_url - user - end -end diff --git a/lib/generators/authentication/templates/test_unit/system/identity/emails_test.rb.tt b/lib/generators/authentication/templates/test_unit/system/identity/emails_test.rb.tt deleted file mode 100644 index c681a77..0000000 --- a/lib/generators/authentication/templates/test_unit/system/identity/emails_test.rb.tt +++ /dev/null @@ -1,26 +0,0 @@ -require "application_system_test_case" - -class Identity::EmailsTest < ApplicationSystemTestCase - setup do - @user = sign_in_as(users(:lazaro_nixon)) - end - - test "updating the email" do - click_on "Change email address" - - fill_in "New email", with: "new_email@hey.com" - fill_in "Password challenge", with: "Secret1*3*5*" - click_on "Save changes" - - assert_text "Your email has been changed" - end - - test "sending a verification email" do - @user.update! verified: false - - click_on "Change email address" - click_on "Re-send verification email" - - assert_text "We sent a verification email to your email address" - end -end diff --git a/lib/generators/authentication/templates/test_unit/system/identity/password_resets_test.rb.tt b/lib/generators/authentication/templates/test_unit/system/identity/password_resets_test.rb.tt deleted file mode 100644 index 1c254ab..0000000 --- a/lib/generators/authentication/templates/test_unit/system/identity/password_resets_test.rb.tt +++ /dev/null @@ -1,28 +0,0 @@ -require "application_system_test_case" - -class Identity::PasswordResetsTest < ApplicationSystemTestCase - setup do - @user = users(:lazaro_nixon) - @sid = @user.generate_token_for(:password_reset) - end - - test "sending a password reset email" do - visit sign_in_url - click_on "Forgot your password?" - - fill_in "Email", with: @user.email - click_on "Send password reset email" - - assert_text "Check your email for reset instructions" - end - - test "updating password" do - visit edit_identity_password_reset_url(sid: @sid) - - fill_in "New password", with: "Secret6*4*2*" - fill_in "Confirm new password", with: "Secret6*4*2*" - click_on "Save changes" - - assert_text "Your password was reset successfully. Please sign in" - end -end diff --git a/lib/generators/authentication/templates/test_unit/system/passwords_test.rb.tt b/lib/generators/authentication/templates/test_unit/system/passwords_test.rb.tt deleted file mode 100644 index 486fdf9..0000000 --- a/lib/generators/authentication/templates/test_unit/system/passwords_test.rb.tt +++ /dev/null @@ -1,18 +0,0 @@ -require "application_system_test_case" - -class PasswordsTest < ApplicationSystemTestCase - setup do - @user = sign_in_as(users(:lazaro_nixon)) - end - - test "updating the password" do - click_on "Change password" - - fill_in "Password challenge", with: "Secret1*3*5*" - fill_in "New password", with: "Secret6*4*2*" - fill_in "Confirm new password", with: "Secret6*4*2*" - click_on "Save changes" - - assert_text "Your password has been changed" - end -end diff --git a/lib/generators/authentication/templates/test_unit/system/registrations_test.rb.tt b/lib/generators/authentication/templates/test_unit/system/registrations_test.rb.tt deleted file mode 100644 index da7c0db..0000000 --- a/lib/generators/authentication/templates/test_unit/system/registrations_test.rb.tt +++ /dev/null @@ -1,14 +0,0 @@ -require "application_system_test_case" - -class RegistrationsTest < ApplicationSystemTestCase - test "signing up" do - visit sign_up_url - - fill_in "Email", with: "lazaronixon@hey.com" - fill_in "Password", with: "Secret6*4*2*" - fill_in "Password confirmation", with: "Secret6*4*2*" - click_on "Sign up" - - assert_text "Welcome! You have signed up successfully" - end -end diff --git a/lib/generators/authentication/templates/test_unit/system/sessions_test.rb.tt b/lib/generators/authentication/templates/test_unit/system/sessions_test.rb.tt deleted file mode 100644 index 640c626..0000000 --- a/lib/generators/authentication/templates/test_unit/system/sessions_test.rb.tt +++ /dev/null @@ -1,30 +0,0 @@ -require "application_system_test_case" - -class SessionsTest < ApplicationSystemTestCase - setup do - @user = users(:lazaro_nixon) - end - - test "visiting the index" do - sign_in_as @user - - click_on "Devices & Sessions" - assert_selector "h1", text: "Sessions" - end - - test "signing in" do - visit sign_in_url - fill_in "Email", with: @user.email - fill_in "Password", with: "Secret1*3*5*" - click_on "Sign in" - - assert_text "Signed in successfully" - end - - test "signing out" do - sign_in_as @user - - click_on "Log out" - assert_text "That session has been logged out" - end -end From b9887b6e99248e5295bfd1263d61c8758440362a Mon Sep 17 00:00:00 2001 From: Nixon Date: Tue, 8 Oct 2024 00:14:05 -0300 Subject: [PATCH 3/4] Use native rate_limit for lockable --- CHANGELOG.md | 1 + .../authentication/authentication_generator.rb | 2 +- .../controllers/api/application_controller.rb.tt | 10 ---------- .../api/identity/password_resets_controller.rb.tt | 2 +- .../controllers/html/application_controller.rb.tt | 10 ---------- .../html/identity/password_resets_controller.rb.tt | 2 +- .../html/sessions/passwordlesses_controller.rb.tt | 2 +- 7 files changed, 5 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0fe0e6..32fd41f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Authentication Zero 4.0.0 ## * Remove system tests +* Use native rate_limit for lockable ## Authentication Zero 3.0.2 ## diff --git a/lib/generators/authentication/authentication_generator.rb b/lib/generators/authentication/authentication_generator.rb index a28fd7a..9570954 100644 --- a/lib/generators/authentication/authentication_generator.rb +++ b/lib/generators/authentication/authentication_generator.rb @@ -259,7 +259,7 @@ def sudoable? end def redis? - options.lockable? || options.ratelimit? || sudoable? + options.ratelimit? || sudoable? end def importmaps? diff --git a/lib/generators/authentication/templates/controllers/api/application_controller.rb.tt b/lib/generators/authentication/templates/controllers/api/application_controller.rb.tt index ccddca2..4a8dc8c 100644 --- a/lib/generators/authentication/templates/controllers/api/application_controller.rb.tt +++ b/lib/generators/authentication/templates/controllers/api/application_controller.rb.tt @@ -17,14 +17,4 @@ class ApplicationController < ActionController::API Current.user_agent = request.user_agent Current.ip_address = request.ip end - <%- if options.lockable? %> - def require_lock(wait: 1.hour, attempts: 10) - counter = Kredis.counter("require_lock:#{request.remote_ip}:#{controller_path}:#{action_name}", expires_in: wait) - counter.increment - - if counter.value > attempts - render json: { error: "You've exceeded the maximum number of attempts" }, status: :too_many_requests - end - end - <%- end -%> end diff --git a/lib/generators/authentication/templates/controllers/api/identity/password_resets_controller.rb.tt b/lib/generators/authentication/templates/controllers/api/identity/password_resets_controller.rb.tt index f4b2166..9b3cbe0 100644 --- a/lib/generators/authentication/templates/controllers/api/identity/password_resets_controller.rb.tt +++ b/lib/generators/authentication/templates/controllers/api/identity/password_resets_controller.rb.tt @@ -2,7 +2,7 @@ class Identity::PasswordResetsController < ApplicationController skip_before_action :authenticate <%- if options.lockable? -%> - before_action :require_lock, only: :create + rate_limit to: 10, within: 1.hour, only: :create <%- end -%> before_action :set_user, only: :update diff --git a/lib/generators/authentication/templates/controllers/html/application_controller.rb.tt b/lib/generators/authentication/templates/controllers/html/application_controller.rb.tt index 9a996ec..e8fcb95 100644 --- a/lib/generators/authentication/templates/controllers/html/application_controller.rb.tt +++ b/lib/generators/authentication/templates/controllers/html/application_controller.rb.tt @@ -15,16 +15,6 @@ class ApplicationController < ActionController::Base Current.user_agent = request.user_agent Current.ip_address = request.ip end - <%- if options.lockable? %> - def require_lock(wait: 1.hour, attempts: 10) - counter = Kredis.counter("require_lock:#{request.remote_ip}:#{controller_path}:#{action_name}", expires_in: wait) - counter.increment - - if counter.value > attempts - redirect_to root_path, alert: "You've exceeded the maximum number of attempts" - end - end - <%- end -%> <%- if sudoable? %> def require_sudo unless Current.session.sudo? diff --git a/lib/generators/authentication/templates/controllers/html/identity/password_resets_controller.rb.tt b/lib/generators/authentication/templates/controllers/html/identity/password_resets_controller.rb.tt index 70dbe75..e452e0a 100644 --- a/lib/generators/authentication/templates/controllers/html/identity/password_resets_controller.rb.tt +++ b/lib/generators/authentication/templates/controllers/html/identity/password_resets_controller.rb.tt @@ -2,7 +2,7 @@ class Identity::PasswordResetsController < ApplicationController skip_before_action :authenticate <%- if options.lockable? -%> - before_action :require_lock, only: :create + rate_limit to: 10, within: 1.hour, only: :create, with: -> { redirect_to root_path, alert: "Try again later" } <%- end -%> before_action :set_user, only: %i[ edit update ] diff --git a/lib/generators/authentication/templates/controllers/html/sessions/passwordlesses_controller.rb.tt b/lib/generators/authentication/templates/controllers/html/sessions/passwordlesses_controller.rb.tt index 60d5db6..67d72e5 100644 --- a/lib/generators/authentication/templates/controllers/html/sessions/passwordlesses_controller.rb.tt +++ b/lib/generators/authentication/templates/controllers/html/sessions/passwordlesses_controller.rb.tt @@ -2,7 +2,7 @@ class Sessions::PasswordlessesController < ApplicationController skip_before_action :authenticate <%- if options.lockable? -%> - before_action :require_lock, only: :create + rate_limit to: 10, within: 1.hour, only: :create, with: -> { redirect_to root_path, alert: "Try again later" } <%- end -%> before_action :set_user, only: :edit From 3369d85f841551a69326226abc72b4518872478c Mon Sep 17 00:00:00 2001 From: Nixon Date: Tue, 8 Oct 2024 00:58:29 -0300 Subject: [PATCH 4/4] Copy web_authn_controller.js --- CHANGELOG.md | 1 + .../authentication_generator.rb | 6 +- .../javascript/controllers/application.js | 11 -- .../controllers/web_authn_controller.js | 111 ++++++++++++++++++ 4 files changed, 115 insertions(+), 14 deletions(-) delete mode 100644 lib/generators/authentication/templates/javascript/controllers/application.js create mode 100644 lib/generators/authentication/templates/javascript/controllers/web_authn_controller.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 32fd41f..a135e19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Remove system tests * Use native rate_limit for lockable +* Copy web_authn_controller.js instead of depend on stimulus-web-authn ## Authentication Zero 3.0.2 ## diff --git a/lib/generators/authentication/authentication_generator.rb b/lib/generators/authentication/authentication_generator.rb index 9570954..264e21d 100644 --- a/lib/generators/authentication/authentication_generator.rb +++ b/lib/generators/authentication/authentication_generator.rb @@ -123,9 +123,9 @@ def create_controllers def install_javascript return unless webauthn? - copy_file "javascript/controllers/application.js", "app/javascript/controllers/application.js", force: true - run "bin/importmap pin stimulus-web-authn" if importmaps? - run "yarn add stimulus-web-authn" if node? + copy_file "javascript/controllers/web_authn_controller.js", "app/javascript/controllers/web_authn_controller.js" + run "bin/importmap pin @rails/request.js" if importmaps? + run "yarn add @rails/request.js" if node? end def create_views diff --git a/lib/generators/authentication/templates/javascript/controllers/application.js b/lib/generators/authentication/templates/javascript/controllers/application.js deleted file mode 100644 index 1d92631..0000000 --- a/lib/generators/authentication/templates/javascript/controllers/application.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Application } from "@hotwired/stimulus" -import WebAuthnController from "stimulus-web-authn" - -const application = Application.start() -application.register("web-authn", WebAuthnController) - -// Configure Stimulus development experience -application.debug = false -window.Stimulus = application - -export { application } diff --git a/lib/generators/authentication/templates/javascript/controllers/web_authn_controller.js b/lib/generators/authentication/templates/javascript/controllers/web_authn_controller.js new file mode 100644 index 0000000..29acdde --- /dev/null +++ b/lib/generators/authentication/templates/javascript/controllers/web_authn_controller.js @@ -0,0 +1,111 @@ +import { Controller } from "@hotwired/stimulus" +import { create, get, supported } from "@github/webauthn-json" +import { FetchRequest } from "@rails/request.js" + +export default class WebAuthnController extends Controller { + static targets = [ "error", "button", "supportText" ] + static classes = [ "loading" ] + static values = { + challengeUrl: String, + verificationUrl: String, + fallbackUrl: String, + retryText: { type: String, default: "Retry" }, + notAllowedText: { type: String, default: "That didn't work. Either it was cancelled or took too long. Please try again." }, + invalidStateText: { type: String, default: "We couldn't add that security key. Please confirm you haven't already registered it, then try again." } + } + + connect() { + if (!supported()) { + this.handleUnsupportedBrowser() + } + } + + getCredential() { + this.hideError() + this.disableForm() + this.requestChallengeAndVerify(get) + } + + createCredential() { + this.hideError() + this.disableForm() + this.requestChallengeAndVerify(create) + } + + // Private + + handleUnsupportedBrowser() { + this.buttonTarget.parentNode.removeChild(this.buttonTarget) + + if (this.fallbackUrlValue) { + window.location.replace(this.fallbackUrlValue) + } else { + this.supportTextTargets.forEach(target => target.hidden = !target.hidden) + } + } + + async requestChallengeAndVerify(fn) { + try { + const challengeResponse = await this.requestPublicKeyChallenge() + const credentialResponse = await fn({ publicKey: challengeResponse }) + this.onCompletion(await this.verify(credentialResponse)) + } catch (error) { + this.onError(error) + } + } + + async requestPublicKeyChallenge() { + return await this.request("get", this.challengeUrlValue) + } + + async verify(credentialResponse) { + return await this.request("post", this.verificationUrlValue, { + body: JSON.stringify({ credential: credentialResponse }), + contentType: "application/json", + responseKind: "json" + }) + } + + onCompletion(response) { + window.location.replace(response.location) + } + + onError(error) { + if (error.code === 0 && error.name === "NotAllowedError") { + this.errorTarget.textContent = this.notAllowedTextValue + } else if (error.code === 11 && error.name === "InvalidStateError") { + this.errorTarget.textContent = this.invalidStateTextValue + } else { + this.errorTarget.textContent = error.message + } + this.showError() + } + + hideError() { + if (this.hasErrorTarget) this.errorTarget.hidden = true + } + + showError() { + if (this.hasErrorTarget) { + this.errorTarget.hidden = false + this.buttonTarget.textContent = this.retryTextValue + this.enableForm() + } + } + + enableForm() { + this.element.classList.remove(this.loadingClass) + this.buttonTarget.disabled = false + } + + disableForm() { + this.element.classList.add(this.loadingClass) + this.buttonTarget.disabled = true + } + + async request(method, url, options) { + const request = new FetchRequest(method, url, { responseKind: "json", ...options }) + const response = await request.perform() + return response.json + } +}