Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .air-cdk.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = "."
tmp_dir = "tmp"

[build]
bin = ""
cmd = './build.sh'
delay = 100
exclude_dir = ["tmp", "cdk"]
full_bin = "sam local start-api --port 8160 --template cdk/cdk.out/twosv-api-dev.template.json --env-vars cdk/env.json"
include_ext = ["go"]
kill_delay = "0s"
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,60 @@ to do with WebAuthn, but is the primary key for finding the right records in Dyn

### Delete one of the user's Webauthn credentials
`DELETE /webauthn/credential`

# Development

## Unit tests

To run unit tests, simply run "make test". It will spin up a Docker Compose environment and run the tests using
Docker containers for the API and for DynamoDB.

## Manual testing

Unit tests can be run individually, either on the command line or through your IDE. It is also possible to
test the server and Lambda implementations locally.

### Server

#### HTTP

If HTTPS is not needed, simply start the `app` container and exercise the API using localhost and the Docker port
defined in docker-compose.yml (currently 8161).

#### HTTPS

To use a "demo UI" that can interact with the API using HTTPS, use Traefik proxy, which is defined in the Docker
Compose environment. Traefik is a proxy that creates a Let's Encrypt certificate and routes traffic to the local
container via a registered DNS record. To configure this, define the following variables in `local.env`:

- DNS_PROVIDER=cloudflare
- CLOUDFLARE_DNS_API_TOKEN=<insert a valid Cloudflare token that has DNS write permission on the domain defined below>
- LETS_ENCRYPT_EMAIL=<insert your actual email address here>
- LETS_ENCRYPT_CA=production
- TLD=<your DNS domain>
- SANS=mfa-ui.<your domain>,mfa-app.<your domain>
- BACKEND1_URL=http://ui:80
- FRONTEND1_DOMAIN=mfa-ui.<your domain>
- BACKEND2_URL=http://app:8080
- FRONTEND2_DOMAIN=mfa-app.<your domain>

Create DNS A records (without Cloudflare proxy enabled) for the values defined in `FRONTEND1_DOMAIN` and
`FRONTEND2_DOMAIN` pointing to 127.0.0.1 and wait for DNS propagation. Once all of the above configuration is in place,
run `make demo`. The first time will take several minutes for all the initialization. You can watch Docker logs on the
proxy container to keep tabs on the progress.

### Lambda

To exercise the API as it would be used in AWS Lambda, run this command: `air -c .air-cdk.toml`. This will run a
file watcher that will rebuild the app code and the CDK stack, then run `sam local start-api` using the generated
Cloudformation template. This will listen on port 8160. Any code changes will trigger a rebuild and SAM will restart
using the new code.

Implementation notes:

- SAM uses Docker internally, which would make it complicated to run with Docker Compose.
- You will need to install CDK and SAM on your computer for this to work.
- It can use the DynamoDB container in Docker Compose, which can be started using `make dbinit`.
- The `make dbinit` command creates an APIKey (key: `EC7C2E16-5028-432F-8AF2-A79A64CF3BC1`
secret: `1ED18444-7238-410B-A536-D6C15A3C`)
- Some unit tests will delete the APIKey created by `make dbinit`.
13 changes: 13 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/env bash

set -e

set -x

go build -tags lambda.norpc -ldflags="-s -w" -o bootstrap ./lambda

(
cd cdk || exit
rm -rf cdk.out/asset.*
cdk synth -q
)
6 changes: 6 additions & 0 deletions cdk/cdk.context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"cli-telemetry": false,
"acknowledged-issue-numbers": [
34892
]
}
46 changes: 26 additions & 20 deletions cdk/cdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ func NewCdkStack(scope constructs.Construct, id string, props *CdkStackProps) aw
totpTable := getEnv("TOTP_TABLE", "totp")
webauthnTable := getEnv("WEBAUTHN_TABLE", "webauthn")
lambdaRoleArn := getEnv("LAMBDA_ROLE", "")
if lambdaRoleArn == "" {
panic("LAMBDA_ROLE environment variable must be set")
}

functionName := id

Expand All @@ -41,9 +38,7 @@ func NewCdkStack(scope constructs.Construct, id string, props *CdkStackProps) aw
RemovalPolicy: awscdk.RemovalPolicy_RETAIN, // Retain logs when stack is deleted
})

role := awsiam.Role_FromRoleArn(stack, jsii.String("Role"), jsii.String(lambdaRoleArn), nil)

function := awslambda.NewFunction(stack, jsii.String("Function"), &awslambda.FunctionProps{
functionProps := &awslambda.FunctionProps{
Code: awslambda.Code_FromAsset(jsii.String("../"), &awss3assets.AssetOptions{
// include only the bootstrap file
Exclude: jsii.Strings("**", "!bootstrap"),
Expand All @@ -52,16 +47,25 @@ func NewCdkStack(scope constructs.Construct, id string, props *CdkStackProps) aw
"API_KEY_TABLE": jsii.String(apiKeyTable),
"TOTP_TABLE": jsii.String(totpTable),
"WEBAUTHN_TABLE": jsii.String(webauthnTable),
"AWS_ENDPOINT": jsii.String(""),
},
FunctionName: &functionName,
Handler: jsii.String("bootstrap"),
LoggingFormat: awslambda.LoggingFormat_JSON,
LogGroup: logGroup,
MemorySize: jsii.Number(1024.0),
Role: role,
Runtime: awslambda.Runtime_PROVIDED_AL2023(),
Timeout: awscdk.Duration_Seconds(jsii.Number(5)),
})
}

if lambdaRoleArn != "" {
functionProps.Role = awsiam.Role_FromRoleArn(stack, jsii.String("Role"), jsii.String(lambdaRoleArn), nil)
} else {
functionProps.Role = awsiam.Role_FromRoleName(stack, jsii.String("Role"),
jsii.String("service-role/AWSLambdaBasicExecutionRole"), nil)
}

function := awslambda.NewFunction(stack, jsii.String("Function"), functionProps)

api := awsapigateway.NewRestApi(stack, jsii.String("API"), &awsapigateway.RestApiProps{
RestApiName: jsii.String(functionName),
Expand Down Expand Up @@ -93,19 +97,21 @@ func main() {
env = "dev"
}

NewCdkStack(app, "twosv-api-"+env, &CdkStackProps{
awscdk.StackProps{
Env: &awscdk.Environment{
Region: jsii.String(os.Getenv("AWS_REGION")),
},
Tags: &map[string]*string{
"managed_by": jsii.String("cdk"),
"itse_app_name": jsii.String("twosv-api"),
"itse_app_customer": jsii.String("shared"),
"itse_app_env": jsii.String(env),
},
props := awscdk.StackProps{
Tags: &map[string]*string{
"managed_by": jsii.String("cdk"),
"itse_app_name": jsii.String("twosv-api"),
"itse_app_customer": jsii.String("shared"),
"itse_app_env": jsii.String(env),
},
})
}

region := os.Getenv("AWS_REGION")
if region != "" {
props.Env = &awscdk.Environment{Region: &region}
}

NewCdkStack(app, "twosv-api-"+env, &CdkStackProps{props})

app.Synth(nil)
}
Expand Down
8 changes: 8 additions & 0 deletions cdk/env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Parameters": {
"AWS_ENDPOINT": "http://172.17.0.1:8000",
"API_KEY_TABLE": "ApiKey",
"TOTP_TABLE": "Totp",
"WEBAUTHN_TABLE": "WebAuthn"
}
}
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ services:
volumes:
- ./:/src
ports:
- 8080
- "8161:8080"
environment:
AWS_REGION: localhost
AWS_ENDPOINT: http://dynamo:8000
Expand All @@ -44,4 +44,4 @@ services:
volumes:
- ./demo-ui:/usr/local/apache2/htdocs
ports:
- 80
- "80"
3 changes: 1 addition & 2 deletions local.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@

# https proxy config
DNS_PROVIDER=cloudflare
CLOUDFLARE_EMAIL=
CLOUDFLARE_API_KEY=
CLOUDFLARE_DNS_API_TOKEN=
LETS_ENCRYPT_EMAIL=
LETS_ENCRYPT_CA=production
TLD=
Expand Down