Writing a Passwordless Service With Go and Docker

Dominick Caponi
10 min readAug 11, 2021

You can’t forget your password if you don’t have one. Passwordless login is the next big thing in Customer Identity and Access Management (CIAM) which is fancy slang for how my app’s users log in. Passwordless login brings many benefits to the login experience for your users and yourself. Going passwordless means your users can’t lose their password, or have it stolen in a phishing attack, and you don’t have to worry about creating a sign up and login flow as they’re both the same experience at the surface. What’s even better is you can offload all the user management work to a IDaaS service like OneLogin. Using an IDaaS also enables us to get around creating a bunch of user management code which makes the service truly micro since we offload all the hard work to OneLogin.

In this article, I’ll walk you through how to set up a simple passwordless login service that offloads user management to OneLogin using their Go SDK. You’ll come away with a simple service you can modify and use in your own platform to your heart’s content. I’ll also go over some high level architecture decisions that should help you extend this to fit whatever your use case is. If you’re a first time Go user or someone who’s looking for ideas on how to roll a passwordless service, this is the article for you.

Passwordless Flow

Passwordless services are much simpler than you’d think. This implementation is a 2 endpoint HTTP service that just keeps track of who tried signing in and whether or not they complete the flow. Below is a sketch of the passwordless flow. I won’t focus on building the frontend here, but I can cover that in a future post — let me know in the comments if you want to see that and in what language/framework.

Basic Passwordless Flow Diagram

The flow is really straightforward. In detail it goes a little something like this:

  1. User goes to your site and enters an email to sign up or log in
  2. Your site sends the email to your passwordless service (what we’re building now)
  3. That service checks that a user exists or adds them to a directory / store. I’ll use OneLogin for this but you can use a database or other IDaaS service.
  4. The passwordless service caches a random guid token with the user’s email address for verification later. This example uses Redis and caches the email as the key with the token as its value.
  5. The service sends an email to a user. You can use what’s coded up now or I recommend using an email service for better reliability. The email contains a link back to your frontend with the email and token in the query params.
  6. The user clicks the link given in the email and the frontend forwards the query params to the passwordless service for validation
  7. If the email/token combo is valid, remove it from the cache and send a success response/session token letting the frontend know this user is valid. If the token is invalid, redirect to the login page.

Setup — Project

You can clone the project at dcaponi/pw_less if you don’t feel like coding along. Each step will show you the files you need to add and what to put in them. It’s not a very large project so this should go quick.

First you’ll want to create a root folder for the project

mkdir pw_less && cd pw_less

Setup — Root Folder (Go, Git, Env)

I built this with golang installed on my computer as it made fetching modules and checking compilation on my machine a little faster. You can of course run everything in a container that has go as well. Here are the commands I ran to get started.

go mod init 
git init
touch .env .gitignore main.go Dockerfile docker-compose.yml
echo "pw_less\n.env" >> .gitignore
echo "package main\n\nfunc main(){\n\n}" >> main.go

In your .env file add the following. We’ll go through OneLogin in detail later. I’m using a throwaway gmail account here, so you’ll want to replace your EMAIL_HOST with whatever email service you’re using if you have a different service.

.env

REDIS_ADDR=redis:6379 
EMAIL_FROM=<your email address>
EMAIL_PASSWORD=<your email password>
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
PORT=8000
ONELOGIN_CLIENT_ID=<your onelogin client id>
ONELOGIN_CLIENT_SECRET=<your onelogin client secret>
ONELOGIN_CLIENT_REGION=<your onelogin client region (us or eu)>

A Note on Using Personal Email for Prototyping

If you’re using a personal gmail address you need to temporarily allow “Less Secure Apps” to send messages on your behalf. This only works if you do not have 2-factor auth.

Typically this is O.K. for demonstration purposes only! I recommend you send emails from a throwaway account just to be sure. Also, this is why I recommend for production-ready apps, to move to a message sending service like Twilio.

Setup — Docker

Make sure you have Docker installed and running. Then drop this in your Dockerfile.

Dockerfile

# Dockerfile for app that gets built in the container 
# Likely won't work for private repos
FROM golang:latest
# Standard copy from host to container stuff
WORKDIR /go/src/pw_less
COPY ./ /go/src/pw_less
# Download the modules we import
RUN go mod download
# Using this to trigger rebuild on code change
RUN go get github.com/githubnemo/CompileDaemon
# Fire it up!
ENTRYPOINT CompileDaemon --build="go build -o ./pw_less main.go" --command=./pw_less

Next add this to your docker-compose.yml which will stand up the app, Redis, and a connection between them.

docker-compose.yml

version: "3" services: 
pw_less:
build: .
tty: true
stdin_open
: true
volumes
:
- ./:/go/src/pw_less
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- redis
redis:
image: redis
container_name: redis
entrypoint: redis-server --appendonly yes
restart: always
ports:
- "6379:6379"
env_file:
- .env
volumes:
- ../data/redis:/data
volumes:
redis:

Setup — OneLogin

If you don’t have one already, head over and get yourself a free OneLogin developer account. Once you have that, head to the administration portal (sign in as the account owner and click Administration in the top right corner).

Once there, notice you only have 1 user (yourself) in Users. In the top nav bar head over to Developers > API Credentials and create a new API credential.

Copy/Paste the API Client ID and Client Secret and replace them in the .env where you see <your onelogin client id> and <your onelogin client secret> respectively. Also update <your onelogin client region> with us if you’re in the US or eu if you’re in Europe.

Let’s GO!

Now that all our setup is done, we can start getting into the actual code. As with all Go programs, we start with main.go For such a small service, main.go is mostly going to consist of initializations and starting the HTTP server. Details aside your main file should look like this.

main.go

You’ll notice 3 setup steps here.

  1. Initialize the Redis connection. We called out a bunch of Redis details in the .env and docker-compose.yml files. This is where we plug those details into a redis client.
  2. Initialize the email client. I’m using the vanilla SMTP client in Go for this. I have the Redis and email clients wrapped in adapter code to make them swappable for testable mocks that can be injected into the controller and repository.
  3. Initialize the OneLogin client — this comes from the onelogin go sdk and contains all the logic for calling the OneLogin APIs to manage users.

Then we create a new user repository that will use OneLogin as the storage medium via the oneloginClient. The repository gets injected along with a cache and messenger client into the userController which gets passed to the userHandler which handles all calls coming to /users. Finally you’ll see the familiar ListenAndServe call to start accepting requests.

Wrappers & Adapters

Photo by Call Me Fred on Unsplash

I won’t get into too much detail as architecture is beyond the scope of this (let me know if you want to see a deep dive into clean architecture in Go). We’re going to create 2 go modules (folders with go files in them basically) for our Redis client and our SMTP client. These files are just adapters for configuring our clients with the environment variables we established at the beginning. The key things to note are the interfaces. Structuring our code like this will make it easier to create drop in replacements later that can just return dummy test data without being concerned with sending real emails or hooking up to a cache server.

mkdir cache email 
touch ./cache/cache.go ./email/email.go
echo "package cache" >> ./cache/cache.go 4echo "package email" >> ./email/email.go

cache.go

cache.go is responsible for taking our cache config variables address password and others we may need for other implementations like Memcached - and establishing a connection with a client, and returning a handle to that client.

Here I put the redis.Client client handle in the RedisCache struct which implements the Cache interface. That makes it possible to mock the cache object later with any struct that implements the 4 Cache interface methods without needing to connect to an actual cache server.

While it is possible to simply return an interface func NewRedisCache(c RedisCacheConfig) (Cache, error){} in Go the general idiom is to typically accept interfaces & return structs. This pattern complies with that best practice.

email.go

Even simpler than cache.go email.go abstracts the setup and send logic allowing you to mock this module in testing without sending actual emails, and lets you swap out the email client logic without impacting the rest of your codebase.

Clean Apps are Like Onions — They Have Layers

In main.go we added a line that looks like this user.NewHandler(user.NewController(user.NewRepo(*oneloginClient), cache, gmailer)). You’ll notice theres another module called user which we are going to make now. This is effectively our “user resource” or “user service” and is built in layers. Working from the outermost layer in toward the user model at the core, it looks like this.

  1. Handler — responsible for parsing the HTTP request into just the information that the user business logic cares about.
  2. Controller — where the business logic around users exists and is influenced by data from the request and data stored in the service.
  3. Repository — logic for storing / retrieving the user lives here. This is where you’d have stored database procedures defined, retry logic, or regular HTTP requests.
  4. Model (Not Shown) — defines what a user looks like. We’re leaning on the OneLogin Go SDK for this.

We’ll start by creating a user module.

mkdir user 
touch user/handler.go user/controller.go user/repository.go
echo "package user" >> user/handler.go
echo "package user" >> user/controller.go
echo "package user" >> user/repository.go

Our passwordless service has 2 endpoints — one POST for inserting or checking the user, and the other GET for validating the token. Those 2 endpoints, their methods, and request parsers live in user/handler.go

handler.go

The handler just sets up the route for the requests http.Handle(“/users”, http.HandlerFunc(uh.handleUsers)) and defines the functions that unpack requests and pack responses.

Controller.go

This is where all the fun happens. There are 2 functions CreateUser and ValidateToken. CreateUser uses the injected userRepository to call out to the user store and see if the user exists. If the user exists, we check the injected cache client to see if the user has a token. If the user does not exist, we create an entry in the cache with the key being the email and the token being a random Guid. Then we email the user with the injected email client a link that looks like so.

https://callback.com/users?email=user@email.com&token=randomGuid1234

Once the user clicks the link, the frontend should have made a GET request to /users with the query parameters given and assuming user@email.com is in the cache and the token matches, the service responds with a 200 — otherwise it returns an error. If this is the user’s first time logging in, it will persist the user in the store.

Start it Up!

Photo by Ashutosh Dave on Unsplash

At long last we are now able to start the app and take it for a spin. You can start everything in one go with docker-compose up and you should see the app and a Redis container come online. Look out in the logs for the indication that the Redis connection was established and you should be ready to go.

Like we discussed earlier, if you’re using a personal gmail, you’ll need to temporarily disable less safe app protection to have your gmail account send emails on your behalf.

Finally we can pretend to be our frontend and send requests to the passwordless service. First request will create a user as if they are signing up.

curl -X POST 'localhost:8000/users' -d '{"email": "test@email.com"}'

If everything was successful, you should see the user come back in the response and an email lands in your email inbox. At this point, your OneLogin account will NOT show the new user. We have to validate it first. We’re not paying to store users who don’t validate their email.

If you click the link in your inbox you should be redirected to http://localhost:8000/users?email=test@email.com&token=123 which initiates a GET to that same URL. Our ValidateUserToken function takes over and checks the cache for that email. If it exists and the token matches, you should see the user in your browser (as JSON - I didn’t spend a lot of time on this part 😆) and if you check your OneLogin account, you’ll see a newly minted user.

Wrapping Up

You should now have a solid idea of what a passwordless service looks like from an implementation standpoint and a few ideas for how to roll your own. We saw that a passwordless service can be implemented in a 2 endpoint microservice and requires no database or anything more complicated than a key/value store like Redis. You also should have an idea as to how helpful it can be to offload all (or most of) the user management logic to a service like OneLogin letting you focus on the important things like your user experience.

If you have any questions or would like to see more content reach out to me here in the comments or on LinkedIn or on the OneLogin Slack. We’d love to hear your feedback on how we can improve our content.

--

--