Skip to content

Creating a new validator

Patrick Sachs edited this page Sep 22, 2018 · 12 revisions

Basic Setup

Validators in react-formilicious are essentially just simple functions that return/throw an error if the validation failed, or not if it passed.

Let's walk through the creation of an somewhat advanced password validator that will match the password entered by the user against the haveibeenpwned API of breached/common passwords.

Let's get started by creating the validator file:

// my-own-validators/pwned.js
const pwned = options => value => {

};

export default pwned;

An important detail of this is that the validator is a function returning a function. The first function is the function that the user calls to pass some options to the validator(We'll see how this works in a bit), and the second function is the one called by Formilicious with the actual value in the form(There are actually a few more parameters in the second function, but we won't get into them in this tutorial).

Interestingly enough this is already a valid validator, we can plug into a form:

// index.js
import pwned from './my-own-validators/pwned';

<Form
  data={{}}
  elements={[
    {
      type: TextField,
      mode: "password",
      key: "password",
      name: "πŸ”‘ Password",
      placeholder: "πŸ”‘ Your super secret password here!",
      validator: pwned()
    }
  ]} />

This validator wouldn't be of much use though, as it would simply pass all values(Validators returning undefined count as valid). See this page for all values a validation may return: Valid Validator Validation Values

Just for the sake of it, let's change the validator to always fail:

// my-own-validators/pwned.js
const pwned = options => value => {
  return `'${value}'? What kind of password is that supposed to be?`;
};

export default pwned;

While the general assumption that the user entered a bad password is probably true, they won't be overly happy with this logic. Let's get started for real.

Contacting the API

Since sending passwords in plain text is a bad idea the haveibeenpwned API accepts the first 5 characters of the sha-1 of the password.

It then returns a list with multiple lines, each containing the rest of the sha-1 characters, followed by a colon and the number of accounts that use this password.

// sha1("password") -> "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8"
// GET https://api.pwnedpasswords.com/range/5BAA6
1D2DA4053E34E76F6576ED1DA63134B5E2A:2
1D72CD07550416C216D8AD296BF5C0AE8E0:10
1E2AAA439972480CEC7F16C795BBB429372:1
1E3687A61BFCE35F69B7408158101C8E414:1
1E4C9B93F3F0682250B6CF8331B7EE68FD8:3533661
20597F5AC10A2F67701B4AD1D3A09F72250:3
20AEBCE40E55EDA1CE07D175EC293150A7E:1
20FFB975547F6A33C2882CFF8CE2BC49720:1
22158C3C153B18E085F0AE99105605AA1F3:3
2288B6F854BCD5B01DA45F2246939330D04:3
22ED852E72B423F8D5537C9093C5254C285:3
237B9E2165C9704F834C9ADAB8B4138967F:2
[....]

Fair enough, let's implement the hashing and splitting. Since writing our own sha1 algorithm would be a slight overkill for this tutorial let's just use the js-sha1 library for the hashing.

// my-own-validators/pwned.js
import sha1 from "js-sha1";

const splitAt = (string, at) => [string.substring(0, at), string.substring(at)];

const pwned = options => async value => {
  const [requestSha, matchSha] = splitAt(sha1(value).toUpperCase(), 5);
  const apiResultHttp = await fetch(`https://api.pwnedpasswords.com/range/${requestSha}`);
  const apiResult = await apiResultHttp.text();

  return apiResult;
};

export default pwned;

Truly gorgeous, but we can improve the output a bit.

However, we can already see the support for async validators in react-formilicious: We make a HTTP request, parse it and then return our validation result! This allows for some very powerful validators.

If multiple async validators are running at the same time, only the result of the last started validator will be used.

Don't drink too much cool-aid!

If an async validator takes too much time to validate, it will be considered as failed by Formilicious. This is not to troll and annoy library authors, but to ensure that the form stays responsive to the user and not be locked into a (seemingly) endless validation cycle.

By default, this time is 3000ms. You can adjust it by passing the timeout in ms to the fieldTimeout prop of the form. Pass a negative value to disable the timeout. (This timeout is shared by the validator and the field itself)

Parsing the API result

Let's use the magical power of regex to find the column for our password, and use a capture group to to get the amount of breaches for the password.

// my-own-validators/pwned.js
import sha1 from "js-sha1";

const splitAt = (string, at) => [string.substring(0, at), string.substring(at)];

const pwned = options => async value => {
  const [requestSha, matchSha] = splitAt(sha1(value).toUpperCase(), 5);
  const apiResultHttp = await fetch(`https://api.pwnedpasswords.com/range/${requestSha}`);
  const apiResult = await apiResultHttp.text();

  const regex = new RegExp("^" + matchSha + ":(\\d+)$", "im");
  const exec = apiResult.match(regex);
  const breachCount = exec ? parseInt(exec[1], 10) : 0;

  return `Oh Jolly! This password has been used by ${breachCount} other people.`;
};

export default pwned;

That's actually pretty cool. But this validator still has a small flaw:

Since we have to adjust our logic anyways, let's allow users to use passwords that have been breached less than 5 times, but still display a warning in that case.

// my-own-validators/pwned.js
if (breachCount > 5) return `Oh Jolly! This password has been used by ${breachCount} other people.`;
if (breachCount > 0) return {
  validated: "hint",
  message: `So - You could use this password, like the other ${breachCount} people who did and regretted it, or just choose a more secure one.`
};

It's working!

As a final touch, let's look into adding an option for users to customize the error threshold:

// my-own-validators/pwned.js
-const pwned = options => async value => {
+const pwned = ({ errorThreshold = 5 } = {}) => async value => {

-if (breachCount > 5) return `Oh Jolly! This password has been used by ${breachCount} other people.`;
+if (breachCount > errorThreshold) return `Oh Jolly! This password has been used by ${breachCount} other people.`;
// index.js
elements={[
  {
    type: TextField,
    mode: "password",
    key: "password",
    name: "πŸ”‘ Password",
    placeholder: "πŸ”‘ Your super secret password here!",
    validator: pwned({ errorThreshold: 50000 })
  }
]} 

We're done! πŸŽ‰

We now have a validator that matches the given password against a database of known vulnerable passwords. Take a look at some of the included default validators under /src/validators for some more advanced concepts and some information about which parameters are available for paramters.

If some things didn't work out for you at some point or you'd like to toy around with this field a bit, take a look at the source code of the "validator-pwned": https://github.com/PatrickSachs/react-formilicious/tree/master/packages/validator-pwned

Clone this wiki locally