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_SECRETTo use another profile, set it with
npm config set haveibeenpwned-zxcvbn-lambda-api:aws_profile some-aws-profile(default:pwnage) -
Copy/Rename
example.env.jsontoenv.jsonand 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.jsontodev.env.jsonand 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-AgeCORS header (default:"0") -
"ALWAYS_RETURN_SCORE": Return thezxcvbnscore even if thepwnedpasswordsmatch 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 thezxcvbnstrength 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 thezxcvbnstrength estimation as ametadataresponse 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": "emailAddress@of-this.user",
"userInputs": ["somethingUserSpecific", "emailAddress@of-this.user"]
}// very strong password, with supplementary dictionary
{
"password": "14HFF3vA8qremH9Fe3A9nsXw",
"userInputs": ["somethingUserSpecific", "emailAddress@of-this.user"]
}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: 'emailAddress@of-this.user'; 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
