Skip to content

Commit

Permalink
Copy web_authn_controller.js
Browse files Browse the repository at this point in the history
  • Loading branch information
lazaronixon committed Oct 8, 2024
1 parent b9887b6 commit 3369d85
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ##

Expand Down
6 changes: 3 additions & 3 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

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
}
}

0 comments on commit 3369d85

Please sign in to comment.