Skip to content

Commit

Permalink
Updated web authentication methods in controller
Browse files Browse the repository at this point in the history
Refactored the controller to better handle web authentication. The changes introduce checks for web authentication support before allowing sign-in or sign-up actions. Also, error handling and event dispatching have been enhanced for more robust operation. Scripts related to data fetching and processing have been moved into private methods for improved code organization.
  • Loading branch information
Spomky committed Jul 15, 2024
1 parent 180f4f2 commit cc698c6
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 136 deletions.
24 changes: 20 additions & 4 deletions src/stimulus/assets/dist/controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ export default class extends Controller {
type: BooleanConstructor;
default: boolean;
};
requestHeaders: {
type: ObjectConstructor;
default: {
'Content-Type': string;
Accept: string;
mode: string;
credentials: string;
};
};
};
readonly requestResultUrlValue: string;
readonly requestOptionsUrlValue: string;
Expand All @@ -61,10 +70,17 @@ export default class extends Controller {
readonly residentKeyFieldValue: string;
readonly authenticatorAttachmentFieldValue: string;
readonly useBrowserAutofillValue: boolean;
connect(): void;
readonly requestHeadersValue: object;
connect: () => Promise<void>;
signin(event: Event): Promise<void>;
private _processSignin;
signup(event: Event): Promise<void>;
_dispatchEvent(name: string, payload: any): void;
fetch(method: string, url: string, body: string): Promise<XMLHttpRequest>;
_getData(): any;
private _dispatchEvent;
private _getData;
private _getPublicKeyCredentialRequestOptions;
private _getPublicKeyCredentialCreationOptions;
private _getOptions;
private _getAttestationResponse;
private _getAssertionResponse;
private _getResult;
}
180 changes: 113 additions & 67 deletions src/stimulus/assets/dist/controller.js
Original file line number Diff line number Diff line change
@@ -1,94 +1,89 @@
import { Controller } from '@hotwired/stimulus';
import { startAuthentication, startRegistration } from '@simplewebauthn/browser';
import { browserSupportsWebAuthnAutofill, browserSupportsWebAuthn, startAuthentication, startRegistration } from '@simplewebauthn/browser';

class default_1 extends Controller {
connect() {
var _a, _b;
const options = {
requestResultUrl: this.requestResultUrlValue,
requestOptionsUrl: this.requestOptionsUrlValue,
requestSuccessRedirectUri: (_a = this.requestSuccessRedirectUriValue) !== null && _a !== void 0 ? _a : null,
creationResultUrl: this.creationResultUrlValue,
creationOptionsUrl: this.creationOptionsUrlValue,
creationSuccessRedirectUri: (_b = this.creationSuccessRedirectUriValue) !== null && _b !== void 0 ? _b : null,
constructor() {
super(...arguments);
this.connect = async () => {
var _a, _b;
const options = {
requestResultUrl: this.requestResultUrlValue,
requestOptionsUrl: this.requestOptionsUrlValue,
requestSuccessRedirectUri: (_a = this.requestSuccessRedirectUriValue) !== null && _a !== void 0 ? _a : null,
creationResultUrl: this.creationResultUrlValue,
creationOptionsUrl: this.creationOptionsUrlValue,
creationSuccessRedirectUri: (_b = this.creationSuccessRedirectUriValue) !== null && _b !== void 0 ? _b : null,
};
this._dispatchEvent('webauthn:connect', { options });
const supportAutofill = await browserSupportsWebAuthnAutofill();
if (supportAutofill && this.useBrowserAutofillValue) {
const optionsResponseJson = await this._getPublicKeyCredentialRequestOptions({});
if (!optionsResponseJson) {
return;
}
this._processSignin(optionsResponseJson, true);
}
};
this._dispatchEvent('webauthn:connect', { options });
}
async signin(event) {
if (!browserSupportsWebAuthn()) {
this._dispatchEvent('webauthn:unsupported', {});
return;
}
event.preventDefault();
const data = this._getData();
this._dispatchEvent('webauthn:request:options', { data });
const resp = await this.fetch('POST', this.requestOptionsUrlValue, JSON.stringify(data));
const respJson = await resp.response;
const asseResp = await startAuthentication(respJson, this.useBrowserAutofillValue);
const verificationResp = await this.fetch('POST', this.requestResultUrlValue, JSON.stringify(asseResp));
const verificationJSON = await verificationResp.response;
this._dispatchEvent('webauthn:request:response', { response: asseResp });
if (verificationJSON && verificationJSON.errorMessage === '') {
this._dispatchEvent('webauthn:request:success', verificationJSON);
if (this.requestSuccessRedirectUriValue) {
const optionsResponseJson = await this._getPublicKeyCredentialRequestOptions(null);
if (!optionsResponseJson) {
return;
}
this._processSignin(optionsResponseJson, false);
}
async _processSignin(optionsResponseJson, useBrowserAutofill) {
try {
const authenticatorResponse = await startAuthentication(optionsResponseJson, useBrowserAutofill);
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
const assertionResponse = await this._getAssertionResponse(authenticatorResponse);
if (assertionResponse !== false && this.requestSuccessRedirectUriValue) {
window.location.replace(this.requestSuccessRedirectUriValue);
}
}
else {
this._dispatchEvent('webauthn:request:failure', verificationJSON.errorMessage);
catch (e) {
this._dispatchEvent('webauthn:assertion:failure', {});
return;
}
}
async signup(event) {
event.preventDefault();
const data = this._getData();
this._dispatchEvent('webauthn:creation:options', { data });
const resp = await this.fetch('POST', this.creationOptionsUrlValue, JSON.stringify(data));
const respJson = await resp.response;
if (respJson.excludeCredentials === undefined) {
respJson.excludeCredentials = [];
}
const attResp = await startRegistration(respJson);
this._dispatchEvent('webauthn:creation:response', { response: attResp });
const verificationResp = await this.fetch('POST', this.creationResultUrlValue, JSON.stringify(attResp));
const verificationJSON = await verificationResp.response;
if (verificationJSON && verificationJSON.errorMessage === '') {
this._dispatchEvent('webauthn:creation:success', verificationJSON);
if (this.creationSuccessRedirectUriValue) {
try {
if (!browserSupportsWebAuthn()) {
this._dispatchEvent('webauthn:unsupported', {});
return;
}
event.preventDefault();
const optionsResponseJson = await this._getPublicKeyCredentialCreationOptions(null);
if (!optionsResponseJson) {
return;
}
const authenticatorResponse = await startRegistration(optionsResponseJson);
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
const attestationResponseJSON = await this._getAttestationResponse(authenticatorResponse);
if (attestationResponseJSON !== false && this.creationSuccessRedirectUriValue) {
window.location.replace(this.creationSuccessRedirectUriValue);
}
}
else {
this._dispatchEvent('webauthn:creation:failure', verificationJSON.errorMessage);
catch (e) {
this._dispatchEvent('webauthn:attestation:failure', {});
return;
}
}
_dispatchEvent(name, payload) {
this.element.dispatchEvent(new CustomEvent(name, { detail: payload, bubbles: true }));
}
fetch(method, url, body) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.responseType = 'json';
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr);
}
else {
reject({
status: xhr.status,
statusText: xhr.statusText,
});
}
};
xhr.onerror = function () {
reject({
status: xhr.status,
statusText: xhr.statusText,
});
};
xhr.send(body);
});
}
_getData() {
let data = new FormData();
try {
this.element.reportValidity();
if (!this.element.checkValidity()) {
return;
}
data = new FormData(this.element);
}
catch (e) {
Expand All @@ -107,6 +102,51 @@ class default_1 extends Controller {
authenticatorAttachment: data.get(this.authenticatorAttachmentFieldValue),
});
}
async _getPublicKeyCredentialRequestOptions(formData) {
return this._getOptions(this.requestOptionsUrlValue, formData);
}
async _getPublicKeyCredentialCreationOptions(formData) {
return this._getOptions(this.creationOptionsUrlValue, formData);
}
async _getOptions(url, formData) {
const data = formData || this._getData();
if (!data) {
return false;
}
this._dispatchEvent('webauthn:options:request', { data });
const optionsResponse = await fetch(url, {
headers: Object.assign({}, this.requestHeadersValue),
method: 'POST',
body: JSON.stringify(data)
});
if (!optionsResponse.ok) {
this._dispatchEvent('webauthn:options:failure', {});
return false;
}
const options = await optionsResponse.json();
this._dispatchEvent('webauthn:options:success', { data: options });
return options;
}
async _getAttestationResponse(authenticatorResponse) {
return this._getResult(this.creationResultUrlValue, 'webauthn:attestation:', authenticatorResponse);
}
async _getAssertionResponse(authenticatorResponse) {
return this._getResult(this.requestResultUrlValue, 'webauthn:assertion:', authenticatorResponse);
}
async _getResult(url, eventPrefix, authenticatorResponse) {
const attestationResponse = await fetch(url, {
headers: Object.assign({}, this.requestHeadersValue),
method: 'POST',
body: JSON.stringify(authenticatorResponse)
});
if (!attestationResponse.ok) {
this._dispatchEvent(eventPrefix + 'failure', {});
return false;
}
const attestationResponseJSON = await attestationResponse.json();
this._dispatchEvent(eventPrefix + 'success', { data: attestationResponseJSON });
return attestationResponseJSON;
}
}
default_1.values = {
requestResultUrl: { type: String, default: '/request' },
Expand All @@ -122,6 +162,12 @@ default_1.values = {
residentKeyField: { type: String, default: 'residentKey' },
authenticatorAttachmentField: { type: String, default: 'authenticatorAttachment' },
useBrowserAutofill: { type: Boolean, default: false },
requestHeaders: { type: Object, default: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'mode': 'no-cors',
'credentials': 'include'
} },
};

export { default_1 as default };
Loading

0 comments on commit cc698c6

Please sign in to comment.