Skip to content

Building Fargate - Part Three - Sign-In with Cloudflare

Published: at 03:10 PM

tldr;

Fargate has no login code. Cloudflare Access handles sign-in with an emailed one-time code, and the app checks the token Cloudflare attaches to each request.

If you’re just joining, the prelude shows how all the pieces fit together.

Why Cloudflare Access?

The whole point of this project was to avoid building a login system. I didn’t want to store passwords, send login emails, or handle password resets for a site used by a small group of close friends.

Cloudflare Access does all of that for me. A friend enters their email on Cloudflare’s sign-in page, Cloudflare emails them a one-time code, and once they enter it they’re in. Cloudflare only sends codes to addresses on the allowlist, and it tells everyone “a code has been emailed” either way. So someone poking at the sign-in page can’t use it to find out who’s on the list.

The allowlist itself lives in Terraform, which I covered in the last article. Adding a friend never touches the app.

I did consider the AWS-native route of Cognito’s email codes behind CloudFront. I nixed it because the app would then own the login redirect, the callback, and the session handling. That’s exactly the app-managed auth I set out to avoid.

Trust, but verify

If you’ve read my JWT series, this part will look familiar. When Cloudflare lets a request through, it attaches a signed JWT to it. It would be really easy to see that header and assume the request is fine. Don’t do that. A header is just a header.

The app verifies the token on every protected request. It checks the signature against Cloudflare’s published public keys, and it checks the issuer, the audience, and the expiry. Anything that fails gets a 403. The library I used, jose, caches Cloudflare’s keys and fetches new ones when it sees a key it doesn’t recognize, so when Cloudflare rotates its keys there’s nothing for me to do.

There’s also a development mode so I can work locally without Cloudflare in the way. It signs everyone in as a fake local user. The app refuses to start in that mode if it detects that it’s been deployed, so a bad setting fails the deploy instead of opening the site up.

Why there’s a public welcome page

You might remember the 16-bit welcome page from part one and wonder why it exists at all, since Cloudflare already has a sign-in page.

The answer is that Cloudflare’s sign-in page is pretty limited in what you can customize. You get a logo, a name, some header and footer text, and a background color. That’s it. I wanted something with a bit more personality. So the welcome page lives on the site, outside of Access, and everything else sits behind it.

Because it’s public, it says nothing about what’s inside. There’s the name, a Sign in button, and “Available by invitation only.” If you’re already signed in, it sends you straight to the library.

Closing the back door

Putting Cloudflare in front of an app only helps if nobody can go around it. If a bot found the AWS address behind Cloudflare, it could skip Cloudflare entirely. The app would reject every one of those requests, but each one would still cost a Lambda run and an API Gateway request. At this scale that’s fine. If a bot gets enthusiastic then that costs me money and that’s a no-no.

So the AWS side only accepts requests that actually came through Cloudflare. Anything else gets turned away before Lambda ever runs. On top of that, Cloudflare’s free plan has bot protection and firewall rules, so I turned those on too.

Wrapping up the series

That’s Fargate. A tiny TypeScript app that runs on Lambda, a private bucket that doubles as the database, Terraform for everything around it, and Cloudflare handling sign-in. My friends get a list of guides behind an emailed code, and I get to publish a file with one command. The whole thing should cost close to nothing, and if it ever doesn’t, a budget alert will let me know.


Next Post
Building Fargate - Part Two - Infrastructure with Terraform