A Serverless API for registering and validating Multi-Factor Authentication methods.
Currently supports Time-based One Time Passwords (TOTP) and FIDO U2F devices (YubiKeys).
For details about the various API endpoints, see the RAML file.
POST
to /api-key
, providing an email
address.
/api-key
with API Gateway's built-in api keys, which
must be provided as an x-api-key
header in the HTTP request.make list-dev-api-keys
to see it.POST
to /api-key/activate
, providing the email address
and the API Key.POST
to /totp
, providing their API Key and API Secret
in the headers.POST
to /totp/{uuid}/validate
, providing
their API Key and API Secret in the headers and the 6-digit code in the body.POST
to /u2f
, providing their API Key and API Secret
in the headers as well as the appId
in the JSON body.u2f.register()
call.u2f.register()
javascript API call in the browser.u2f.register()
call which should pass the response object to the consumer's service, which in turn
can make a PUT
call to /u2f/{uuid}
with a JSON body including a property named
signResult
with a value of the object returned from the U2F device.keyHandle
and publicKey
encrypted by
the consumer's API Secret and respond with a success or error message.POST
to /u2f/{uuid}/auth
, providing their API Key and API Secret
in the headers.u2f.sign()
call.u2f.sign()
call which should pass the response object to the consumer's service, which in turn
can make a PUT
call to /u2f/{uuid}/auth
with a JSON body including a property named
signResult
with a value of the object returned from the U2F device.appId
used in the registration process must match the URL of the page where the user is authenticating and it
must be HTTPS. It does not need the full path though, so https://myapp.com is sufficient if the page is at
https://myapp.com/auth/login.While DynamoDB supports On Demand backups as well as Continuous Backups with Point-in-time Recovery (PITR), both of these methods restore to a new table rather than restoring to the existing DynamoDB table. While turning on Point-in-time Recovery is certainly not a bad idea, we have ended up using an alternate approach to make restores easier.
The shevchenkos/DynamoDbBackUp software sets up Lambda functions that are triggered each time the associated DynamoDB table is changed, and it backs up the records to an S3 bucket. We used it to set up automated backups for each of the DynamoDB tables used by this repo. We also forked it (to https://github.com/silinternational/DynamoDbBackUp) in case the original "shevchenkos/DynamoDbBackUp" repo is ever deleted, but if the original repo is available use it, as it will more likely be up-to-date.
For the shevchenkos/DynamoDbBackUp software to be able to make the necessary
changes in your AWS account, you will need to set up an IAM user with an Access
Key and Secret and with a policy similar to the following. Note that you will
need to replace YOUR IP ADDRESS BLOCK CIDR
with a real value (for the IP
address or range of addresses from which you want the following commands to
allowed). You may also want to narrow down the breadth of permissions granted
here, further restrict statements by IP CIDR, restrict S3 paths, etc.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "iam:CreateRole",
"Resource": "arn:aws:iam::*:role/*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "iam:*",
"Resource": "arn:aws:iam::*:role/LambdaBackupDynamoDBToS3",
"Condition": {
"IpAddress": {
"aws:SourceIp": "YOUR IP ADDRESS BLOCK CIDR"
}
}
},
{
"Effect": "Allow",
"Action": [
"cloudformation:*"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"dynamodb:*"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"iam:AttachRolePolicy",
"iam:CreatePolicy",
"iam:CreateRole",
"iam:DeleteRole",
"iam:DeleteRolePolicy",
"iam:GetRole",
"iam:PassRole",
"iam:PutRolePolicy"
],
"Resource": [
"arn:aws:iam:::*",
"arn:aws:iam:::role/*"
]
},
{
"Effect": "Allow",
"Action": [
"lambda:*"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:*"
],
"Resource": [
"arn:aws:logs:*::log-group:*:log-stream:"
]
},
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::*",
"arn:aws:s3:::*/*"
]
}
]
}
Once you have the shevchenkos/DynamoDbBackUp software set up on your computer, you can use commands like the following to set up your automated backups.
Create the S3 bucket:
gulp deploy-s3-bucket \
--s3bucket yourorg.backups.dynamodb.mfa-api \
--s3region us-east-1
(Note: Some of these commands, including the one above, had to be run twice to get past an apparent race condition.)
If actually setting up these backups for your use of this library, you will need
to run the following commands once for each of the production tables (currently mfa-api_prod_api-key
, mfa-api_prod_totp
, and mfa-api_prod_u2f
)
Set up the Lambda function for backing up changes:
gulp deploy-lambda \
--s3bucket yourorg.backups.dynamodb.mfa-api \
--s3prefix mfa-api_prod_api-key \
--s3region us-east-1 \
--dbregion us-east-1 \
--lName backup_dynamodb_mfa-api_prod_api-key \
--lRegion us-east-1 \
--lAlias active \
--lRoleName LambdaBackupDynamoDBToS3 \
--lTimeout 60
Set up the event to trigger the Lambda function when a specific DynamoDB table is changed:
gulp deploy-lambda-event \
--dbtable mfa-api_prod_api-key \
--dbregion us-east-1 \
--lName backup_dynamodb_mfa-api_prod_api-key \
--lRegion us-east-1 \
--lAlias active
Do an initial full backup:
gulp backup-full \
--s3bucket yourorg.backups.dynamodb.mfa-api \
--s3prefix mfa-api_prod_api-key \
--s3region us-east-1 \
--dbtable mfa-api_prod_api-key \
--dbregion us-east-1
If you want to do a restore to a specific point in time (in this example, Thu, 25 Jan 2018 22:10:00 GMT), you would run the following:
gulp restore \
--s3bucket yourorg.backups.dynamodb.mfa-api \
--s3prefix mfa-api_prod_totp \
--s3region us-east-1 \
--dbtable mfa-api_prod_totp \
--dbregion us-east-1 \
--restoretime 1516918200000
(Note: The restore time is a JavaScript timestamp, in milliseconds.)
To run this locally (such as for development)...
make dynamodb-tables
make dev-server
POST
to https://localhost:8080/prod/api-key with a JSON body
like the following:
{ "email": "local@example.com" }
It should return a 204 No Content
response.
make list-dev-api-keys
, and copy the "value" parameter's value.POST
to https://localhost:8080/prod/api-key/activate, with a JSON
body like the following:
{
"email": "local@example.com",
"apiKey": "the-value-parameter-from-the-dynamo-db-table"
}
It should return a 200 OK
with a JSON body containing an apiSecret that
you will need. When copying that value, make sure you include any trailing
equals signs (=
).
Put the apiSecret returned (including any trailing =
signs) and the apiKey
value you used in the JSON body into your local idp-in-a-box code's
/docker-compose/broker/local.env
file, both for the MFA_TOTP_*
and
MFA_U2F_*
environment variables, something like this (but using YOUR
values for the apiKey and apiSecret entries, not these dummy/sample values):
MFA_TOTP_apiBaseUrl=http://localhost:8080/
MFA_TOTP_apiKey=347a15dc60f014bdd93e4fc59aab607b022c8e19
MFA_TOTP_apiSecret=za3c5Op8XgQcWNK16Rg6Th3ndmJ2ZTGL4uEldAJxDes=
MFA_U2F_apiBaseUrl=http://localhost:8080/
MFA_U2F_apiKey=347a15dc60f014bdd93e4fc59aab607b022c8e19
MFA_U2F_apiSecret=za3c5Op8XgQcWNK16Rg6Th3ndmJ2ZTGL4uEldAJxDes=
idp-in-a-box
repo. See that repo's README.md for instructions.To start a local container for development of Serverless configuration:
docker compose run --rm dev bash
terraform taint module.serverless-user.aws_iam_access_key.serverless
API Key
: A hex string used to identify calls to most of the endpoints on
this API. We store a copy of this in the database.API Secret
: A base-64 encoded random value used to encrypt/decrypt the
consumer's data. We store a salted, stretched hash of this (as we
would a password) for validating later calls that provide an API Secret.TOTP Key
: The secret used for generating TOTP values. This is provided to
the consumer of this API for them to show as a string / QR Code to their end
user. We store an encrypted copy of this (encrypted using the API Secret) so
that when we need to verify given 6-digit code, we can do so.