Fatih Erikli

Creating a user authentication with Cloudflare Workers

I have been trying serverless computing platforms in last years. Basically these platforms allows you to publish functions and execute them periodically, or by triggering it with an HTTP request.

What I have tried so far:

  • Amazon Lambda
  • CloudFlare
  • Azure
  • Serverless framework (This is not a platform)

In this blog post I will share an example in CloudFlare. Why so?

  • It's free
  • I find the control panel more user and developer friendly
  • They have a strong development community

We will create a user authentication by using Cloudflare workers.

Let's write down what we are going to build first. We will have three API endpoints.

  • Registration
  • Login
  • Auth

Registration and login is obvious. Auth endpoint will help us to authenticate user after the login process. We will store the auth_token provided us from the login or registration endpoint; and reuse them later (when the user refresh the page) to reauthenticate them again.

Persistance layer

First of all; we need to create a KV store; which is the database (or key-value store) of CloudFlare.

https://developers.cloudflare.com/workers/runtime-apis/kv

You can create a KV store on cloudflare panel; or you can create it with wrangler command line tool.

Let's start with Registration endpoint.

const sha256 = require('crypto-js/sha256')
const cryptoJs = require('crypto-js')
const jwt = require('jsonwebtoken')

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

/**
 * Simple authentication
 * @param {Request} request
 */
async function handleRequest(request) {
  const url = new URL(request.url)
  const { pathname } = url
  let response = { pathname }
  switch (pathname) {
    case '/register': {
      const { username, password } = await request.json()

      const user = await YOURKVSTORE.get(`user:${username}`)
      if (user) {
        response = {
          error: 'User exists.',
        }
        break
      }

      const hashedPassword = sha256(password).toString(cryptoJs.enc.Hex)
      // this is important
      await YOURKVSTORE.put(`user:${username}`, hashedPassword)

      const token = jwt.sign({ username }, TOKEN_KEY, {
        expiresIn: '2h',
      })

      await YOURKVSTORE.put(`user_token:${token}`, username)

      response = {
        token,
      }
      break
    }
  }
  return new Response(JSON.stringify(response), {
    headers: {
      'content-type': 'text/json'
    },
  })
}

This is how an event-driven computing-ready function looks like in any platform. It is not so different than creating a controller in a http or Rest API framework; we have a request body and we create a response with that.

Most important thing

You cannot store the user's password in your database. You need to hash them with a hashing algorithm; such as sha256, and save them.

When the user types username and password in login page; we need to hash the password provided by user; and match the hashed pairs, instead of raw password. This is very important.

After the hashing algorithm; we use JWT (JSON Web Token) to create a token that we can authenticate the user without username and password.

/**
 * Simple authentication
 * @param {Request} request
 */
async function handleRequest(request) {
  const url = new URL(request.url)
  const { pathname } = url
  let response = { pathname }
  switch (pathname) {
    case '/login': {
      const { username, password } = await request.json()
      const hashedPassword = sha256(password).toString(cryptoJs.enc.Hex)
      const storedPassword = await VECTORIAL.get(`user:${username}`)
      if (storedPassword === hashedPassword) {
        const token = jwt.sign({ username }, TOKEN_KEY, {
          expiresIn: '2h',
        })

        await VECTORIAL.put(`user_token:${token}`, username)

        response = {
          token,
        }
      } else {
        response = {
          error: 'Invalid credientials.',
        }
      }
      break
    }
    case '/auth': {
      const { token } = await request.json()

      let user

      try {
        user = jwt.verify(token, TOKEN_KEY)
      } catch (e) {
        response = {
          error: 'Invalid signature.',
        }
      }

      if (user) {
        const username = await VECTORIAL.get(`user_token:${token}`);
        if (!username) {
          response = {
            error: 'Invalid signature.',
          }
        } else {
          response = {
            username,
          }
        }
      }
      break
    }
    case '/register': {
      const { username, password } = await request.json()

      const user = await VECTORIAL.get(`user:${username}`)
      if (user) {
        response = {
          error: 'User exists.',
        }
        break
      }

      const hashedPassword = sha256(password).toString(cryptoJs.enc.Hex)
      await VECTORIAL.put(`user:${username}`, hashedPassword)

      const token = jwt.sign({ username }, TOKEN_KEY, {
        expiresIn: '2h',
      })

      await VECTORIAL.put(`user_token:${token}`, username)

      response = {
        token,
      }
      break
    }
  }
  return new Response(JSON.stringify(response), {
    headers: {
      'content-type': 'text/plain',
    },
  })
}

I wrote the code as clean as possible to not complicate things by explaining them with my English :)

CORS settings

We have created the API with workers. There's one thing needs to be done in the code in order to connect them via a browser; CORS (Cross Origin Resource Sharing) settings. Basically we need to whitelist a domain or url that we are going to connect to the API we have created.

async function handleRequest(request) {
  // ...
  return new Response(JSON.stringify(response), {
    headers: {
      'content-type': 'text/plain',
      'Access-Control-Allow-Origin': '*', // Whildcard allows all domains
      'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
      'Access-Control-Max-Age': '86400',
    },
  })
}

Yon can type the domain instead of the asteriks whildcard.

Deployment

I use wrangler to manage my workers. You can write and publish them on cloudflare dashboard as well. I find command-line tool more useful when you develop something more complicated.

$ my-cloudflare-worker % wrangler publish
✨  Built successfully, built project size is 213 KiB.
✨  Successfully published your script to
https://vectorial-cloudflare-worker.fatih-erikli.workers.dev

Let's try our endpoints.

I am going to register myself.

$ http post "https://1.fatih-erikli.workers.dev/register"
username=benfatih password=hello
HTTP/1.1 200 OK
Access-Control-Allow-Methods: GET,HEAD,POST,OPTIONS
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 86400
CF-RAY: 6b77697eab936b36-AMS
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/plain
Date: Thu, 02 Dec 2021 20:47:45 GMT
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Server: cloudflare
Transfer-Encoding: chunked
Vary: Accept-Encoding

{
    "token": "Wohoo yay, it worked. Of course I cropped my auth token. Its mine"
}

It worked :) We are going to authenticate ourselves with the token returned by our endpoint.

$ http post "https://1.fatih-erikli.workers.dev/auth" token=mytoken
HTTP/1.1 200 OK
Access-Control-Allow-Methods: GET,HEAD,POST,OPTIONS
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 86400
CF-RAY: 6b776bd4edcb1ead-AMS
Connection: keep-alive
Content-Length: 23
Content-Type: text/plain
Date: Thu, 02 Dec 2021 20:49:20 GMT
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Server: cloudflare
Vary: Accept-Encoding

{
    "username": "benfatih"
}

Yay. We are able to authenticate with an authentication token instead of username and password.

That's all :)

Happy hacking!