Building an API MVP from the Ground Up: Part 1 - Introduction and User Authentication

Like many of you, I have come up with (what I thought were) great API business ideas in the past. After a bit of digging, I would always get turned off by the number of necessary features. Or I would get overwhelmed by the amount of options for everything from billing to email to deployment.

My intention is to document my efforts shipping FavoritesAPI, and will be a multi-part series covering everything from initial user authentication to actual production code deployment. I’ve also made my scaffolding code available on Github, so you can extend the skeleton code to fit your own needs. Keep in mind that I am in no way claiming my approaches outlined here are the “right” way to do it - I am merely documenting what has worked for me (and hopefully will help some others along the way).

Here we go.

What we'll be building

It'll be helpful to start with a high-level overview of what we want to achieve with this project. Rather than rattling off general terms, let's try and think about about our goals in terms of what our users will experience.

First, our users will hit our landing page. It'll contain some copy to explain the product, but more importantly - it will contain a sign up form. This will be a form where they input their email and then are redirected to Stripe checkout.

Next, we will need the billing code to tie together our user information and their purchasing decision. For our purposes, this will be a simple webhook endpoint that Stripe can hit to notify us of checkouts.

One the user has completed their purchase, they are redirected to the app page. From there, they will be able to see API key information. They can then start making API requests, which we'll need to authenticate.

What if the user wants to come back to their app page to view their API keys again? We'll need a way for them to sign back in.

Lastly, we'll need a deployment setup in order to push out future API code.

With all that boilerplate, we should have everything we need for a simple API MVP.

Authentication

If you were paying attention earlier, you might have noticed that we need two kinds of authentication. One will authenticate users to the API portal, while the other will authenticate API requests. To keep things simple, we'll create separate data models for both cases. User information will be stored in the User model, while API access information will be stored in the API Key (more on this later).

Initial Setup

There are 100 different approaches to user authentication. I personally like being able to touch my own test data, so I opted to spin up a local docker Postgres instance during initial development. The setup is really straightforward - it’s just the Postgres docker image and necessary environment variables. You can find the setup here.

Once you have docker and docker-compose set up, starting the database should be as easy as docker-compose up -d . Since I am using Postgres, I would then check on the database with psql -h localhost -d db_name -U username. It shouldn’t have anything in it, but the practice gives you a good gut check.

Now, we’ll move on to the actual population of user/API key data. I am using sequelize as my ORM, because it is well-used and well-documented. Using an ORM is incredibly helpful because it prevents you from having to right the messy SQL commands yourself (been there, wouldn’t recommend it). Regardless of which ORM you choose, this will be the part when you initialize the project and create your User and API Key models. For now, let’s just set up the User model with an email field, and the API Key model with public and private key fields. The API key model will also have a foreign key reference to the User, since each user can have multiple keys. If you’re feeling like an overachiever, you can also add a boolean activity status field to track whether the API key can be used.

Run the following setup commands:

npm install -g sequelize-cli # We'll need this to run the init command

yarn add sequelize 

sequelize init # will create a bunch of scaffolding code. If you don't like the default structure, you can move things around and let sequelize know in .sequelizerc

And create model migrations in the following form:

User{
    "id": 1,
    "email": "test@email.com",
    "createdAt": "2020-07-21 00:26:15.105+00",
    "updatedAt": "2020-07-21 00:26:15.105+00"
}

APIKey{
    "id": 1,
    "publicKey": "myPublicKey",
    "privateKey": "myPrivateKey",
    "userId": 1,
    "isActive": false,
    "createdAt": "2020-07-21 00:26:15.105+00",
    "updatedAt": "2020-07-21 00:26:15.105+00"
}

After that’s done, we’ll create the DB and and run the necessary migration to add these tables to it. Set up the database with sequelize db:create, and run the migrations with sequelize db:migrate. Now that we’ve got our two main models set up, we can move on to the actual authentication piece.

User Authentication

Like I said earlier, there are a ton of different ways to handle user authentication. The common approach is to authenticate the user based on their username and password. Password authentication comes with some extra complexity. For instance, you need to make sure the password is securely stored (no plaintext). You'll also usually want to provide a way for the user to reset their password in case they lose track of it.

The other approach (that I have opted to go for) is passwordless authentication. Rather than having them enter their info, my approach would email the user a temporary login link. A big benefit of passwordless login is that you have one less sensitive data point to keep track of. You also don't need to build out a "forgot password" flow, since they no longer need a password. The main drawback is that it can be an annoying user experience, since it is higher friction than a traditional login. Since API clients shouldn't have to login often, we have a strong case for passwordless.

The actual email code we'll get to in another section, so for now we'll just be implementing the login token flow.

To get moving, we'll need to create a new model for the login token, and check against it in tha auth route. The login token itself is a randomly generated string associated with a user - think {"token": "myRandomString", userId: 1}.

You'll create the login token code, and then authenticate against it like so:

router.get('/login', async (req, res) => {
    const { token } = req.query;
    try {
        await AuthController.validateLoginToken(token);
        
        // ten day cookie keeps them logged in
        res.cookie('login_token', token, { expires: new Date(Date.now() + TEN_DAYS_IN_MS) });
        res.redirect(`/`);
    } catch(err) { handleError(err, res) }
});

validateLoginToken is just checking to see if the generated auth token exists in the database.

Now we've got a way for users to login - a login token is created, and we authenticate against that token. Let's move on to API key authentication.

API Authentication

First, we’ll need to set up our basic authentication middleware. In express, this would look like:

const express = require ('express');
const router = express.Router();

router.use(async (req, res, next) => {
    // here is where we will check the bearer token to make sure they are a valid user
    
    next();
});

router.get('/', (req, res) => res.send('You made it past'))

Obviously, our middleware isn't helping us out much, so we'll need to add in that authentication logic. Before we do that, we should talk about bearer token validation.

Bearer Token Auth

Bearer token authentication is an authentication strategy in which the provider checks the Authorization header for some kind of string token, and uses that to extrapolate which valid user (if any) is requesting access. For instance, let's say you have one user for your service, and you really couldn't care less about security. Let's call your one user Jim. Jim could pass Bearer Jim in the Authorization header, and every time you see that token, you know that a valid user is requesting access. If Toby were to snoop on Jim's API call, he would easily see what was happening, and could gain access to your system by sending Bearer Jim.

Actual bearer authentication is a little more clever than that, but comes with the same risks. Clients will need to be using HTTPS, and should only be sending a request from their servers rather than in-browser.

Our implementation will append the public and private keys together to create a cryptographic hash. The authentication code above will now become:

const isValidBearerToken = (bearerToken) => {
    if (!bearerToken || !bearerToken.includes('Bearer)) {
        return false;
    }
    
    // take out "Bearer" piece
    const token = bearerToken.split('Bearer').pop();

    // base64 decoding to public:private
    const bearerTokenStringDecoded = Buffer.from(token, 'base64').toString('utf8');

    // split by colon
    const keyArr = bearerTokenStringDecoded.split(':');
    const publicKey = keyArr[0] ? keyArr[0].trim() : null;
    const privateKey = keyArr[1] ? keyArr[1].trim() : null;
    return publicKey && privateKey;
}

router.use(async (req, res, next) => {
    const { authorization: bearerToken } = req.headers;
    if (isValidBearerToken(bearerToken)) {
        // next, check if API key is active
        next();
    } else {
        res.status(401).send('Nope');
    }
});

We would also want to check that the API key is active before we let it through to our API routes. Since our routing code is getting a bit cluttered, now would also be a good time to break out the authentication logic into a separate module.

If you're still following along, we now have a general-purpose way of authenticting API clients and users.

That should wrap it up for the time being. Next time, I'll be talking about how to start collecting payments using Stripe.


If you've found this to be a little too high-level, check out the Github repo to see how authentication is being implemented. If you have any questions or suggestions for the posts to come, you can reach me at stephen.huffnagle@gmail.com.

Show Comments