Prefer Cloudflare Workers? There's a version for you here
Deploy a private, secure and serverless RESTful endpoint for sanely scoring users' new passwords using Dropbox's zxcvbn
library while (k-)anonymously querying Troy Hunt's haveibeenpwned
collection of +10 billion* breached accounts.
Example: handling results with VuetifyJS
* - probably a bajillion or something by now; I'm not keeping track
People seemed to think this concept was neat. An AWS Lambda
REST API was the best only first solution I could think of that's easy to
deploy, language/framework agnostic and, most importantly, https by default
🔒
-
Create an AWS profile with IAM full access, Lambda full access and API Gateway Administrator privileges.
-
Add the keys to your ~/.aws/credentials file:
[pwnage] aws_access_key_id = YOUR_ACCESS_KEY aws_secret_access_key = YOUR_ACCESS_SECRET
To use another profile, set it with
npm config set haveibeenpwned-zxcvbn-lambda-api:aws_profile some-aws-profile
(default:pwnage
) -
Copy/Rename
example.env.json
toenv.json
and edit as you see fit. Note that all entries must be strings, less we anger the Lambda gods.- (Optional) Define your AWS region of choice with
npm config set haveibeenpwned-zxcvbn-lambda-api:aws_region some-aws-region
(default:eu-central-1
) - (Optional) Define your API Gateway environment (aka version) with
npm config set haveibeenpwned-zxcvbn-lambda-api:aws_environment some-version
(default:development
)
- (Optional) Define your AWS region of choice with
-
Launch 🚀 with
npm run deploy
You can boot this API as an express development server like so:
- Copy/Rename
example.env.json
todev.env.json
and configure as you see fit. - Boot the development server with
npm run dev
Note: Development mode will add some random artificial latency to each request in a feeble attempt to simulate the wonky network conditions we encounter in the wild.
The following options are configurable via env.json
or dev.env.json
:
-
"ALLOW_ORIGINS"
: A comma-separated whitelist of origins for Cross Origin Resource Sharing. If none are provided, all origins are allowed (default:""
)- Example:
"ALLOW_ORIGINS": "https://secure.domain.lol,http://unsecure.domain.wtf"
- Example:
-
"CORS_MAXAGE"
: Value in seconds for theAccess-Control-Max-Age
CORS header (default:"0"
) -
"ALWAYS_RETURN_SCORE"
: Return thezxcvbn
score even if thepwnedpasswords
match value is > 0. See Response for details (default:"false"
) -
"DEV_SERVER_PORT"
: Port to use when running as a local server for development (default:"3000"
) -
"USER_INPUTS"
: Comma-separated list of words/phrases to be included in thezxcvbn
strength estimation dictionary. It's a good idea to include e.g. your company and/or application name here (default:""
) -
"RETURN_ZXCVBN_RESULT"
: Return the full result of thezxcvbn
strength estimation as ametadata
response key. Refer to the zxcvbn documentation for details on what that includes (default:"false"
)
Note that all env.json
values must be strings, less you anger the Lambda gods.
Update the Lambda API with any changes you make to the source by running npm run update
.
Update environment variables à la changes to env.json
by running npm run update-env
.
Lambda function and API Gateway configuration are fully automated using the cool-as-a-cucumber claudia.js - refer to the claudia.js docs to learn more about serverless voodoo magic.
Following a successful deployment or update, claudia.js
prints a configuration object for your freshly deployed Lambda function, which includes a secure url for immediate access to your function:
GET the warmup endpoint to verify access:
curl \
-X GET \
"https://$FUNCTION_ID.execute-api.$REGION.amazonaws.com/$ENVIRONMENT/$PREFIX/_up"
POST user password input as JSON to:
curl \
-X POST \
"https://$FUNCTION_ID.execute-api.$REGION.amazonaws.com/$ENVIRONMENT/$PREFIX/_score" \
-H 'content-type: application/json' \
-d '{ "password": "🍌📞bananaphone📞🍌" }'
Optionally, include an array of words or phrases to include in the zxcvbn dictionary:
curl \
-X POST \
"https://$FUNCTION_ID.execute-api.$REGION.amazonaws.com/$ENVIRONMENT/$PREFIX/_score" \
-H 'content-type: application/json' \
-d '{ "password": "roflcopters", "userInputs": ["MyCompanyName"] }'
POST user password input as JSON with field password
like so:
// pwned password
{
"password": "monkey123"
}
// stronger password
{
"password": "wonderful waffles"
}
// very strong password (technically), with supplementary dictionary
// NOTE: you'd be wise to test this client-side _before_ sending this request...
{
"password": "[email protected]",
"userInputs": ["somethingUserSpecific", "[email protected]"]
}
// very strong password, with supplementary dictionary
{
"password": "14HFF3vA8qremH9Fe3A9nsXw",
"userInputs": ["somethingUserSpecific", "[email protected]"]
}
The scoring function will gracefully terminate when an aborted request is detected, though you'll still incur a Gateway API call and a Lamdba function call for the respective request and invocation.
But Troy Hunt and Cloudflare offer us the pwnedpasswords
API for free - and that's pretty cool 😎 So please help keep overhead low by either cancelling open requests on new input, or governing requests on the frontend with something like lodash.debounce.
The Lambda gods will reply with an appropriate status code and a JSON body, with ok
indicating successful scoring and range search, a strength estimation score
of 0 through 4 per zxcvbn
, and pwned
matches, indicating the number times the input appears in the haveibeenpwned
database.
// pwned password: 'monkey123'
{
"ok": true,
"score": 0,
"pwned": 56491
}
// stronger password: 'wonderful waffles'
{
"ok": true,
"score": 3,
"pwned": 0
}
// password: '[email protected]'; matches supplementary dictionary entry...
{
"ok": true,
"score": 0,
"pwned": 0
}
// very strong password: '14HFF3vA8qremH9Fe3A9nsXw'
{
"ok": true,
"score": 4,
"pwned": 0
}
By default, if pwned
is greater than 0, then score
will always be 0. You can override this behavior by settings "ALWAYS_RETURN_SCORE"
to "true"
in env.json
If "RETURN_ZXCVBN_RESULT"
is configured "true"
, responses will also include a metadata
key with the complete zxcvbn
strength estimation result object.
Failure will return JSON to inform you that something's not ok
and a message
as to why.
{
"ok": false,
"message": "It went kaput 💩"
}
The health-check endpoint /_up
is included by default; this also serves as a handy means to warm-up a Lambda function container before your users start feeding you input.
src/index.js
is heavily commented, because I feel it's important that anyone
using this know damn well what's happening with users' passwords at every step.
Finally, it may seem strange that deploying will first delete
package-lock.json
, and then reinstall dependencies - it is a workaround for
claudia.js
weirdness that I cannot explain, but nonetheless occurs when retrying failed
deployments ¯\_(ツ)_/¯
I am not affiliated with Amazon, Troy Hunt, Dropbox, haveibeenpwned, good software development in general, or any combination thereof.
Handling user passwords is no laughing matter, so handle them with care and respect.
Just like your own users, assume in good faith that I have no idea what I'm doing.
REVIEW THE SOURCE, and use at your own risk 🙈
MIT