Skip to content

Commit

Permalink
Merge pull request #118 from lazaronixon/v4
Browse files Browse the repository at this point in the history
V4
  • Loading branch information
lazaronixon authored Oct 8, 2024
2 parents 0eb2ba9 + 3369d85 commit 647e3db
Show file tree
Hide file tree
Showing 19 changed files with 132 additions and 174 deletions.
1 change: 0 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ jobs:
run: |
cd test-app
bin/rails test
bin/rails test:system
test_api:
name: 🧪 Run API Tests
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Authentication Zero 4.0.0 ##

* 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 ##

* Fix bug where token is not expired/invalid
Expand Down
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:
authentication-zero (3.0.2)
authentication-zero (4.0.0)

GEM
remote: https://rubygems.org/
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

```
Expand Down
2 changes: 1 addition & 1 deletion lib/authentication_zero/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module AuthenticationZero
VERSION = "3.0.2"
VERSION = "4.0.0"
end
10 changes: 4 additions & 6 deletions lib/generators/authentication/authentication_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -261,7 +259,7 @@ def sudoable?
end

def redis?
options.lockable? || options.ratelimit? || sudoable?
options.ratelimit? || sudoable?
end

def importmaps?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
}
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

0 comments on commit 647e3db

Please sign in to comment.