This Week #16: Authenticating from the CLI with OAuth 2.0

As we’ve discussed in previous issues of our Devlog, we’ve been developing a new service for securely managing environment variables and secrets alongside your Devbox shell. As part of this service, we needed a way to securely authenticate the Devbox CLI so that it can access protected resources and secrets.

In this post, we’ll describe how we developed an auth flow for the CLI using the OAuth 2.0 Standard. Hopefully this post will be helpful for other dev tools developers who need to integrate an authentication flow with their command line tools.

Defining the Goals for our Authentication Flow

When designing the Authentication Flow for the Devbox CLI, we had a few criteria we wanted to follow:

  1. Use an established vendor:  Authentication is not our specialty, and it seemed a poor use of our time to build our owns solutions from scratch
  2. Multi-tenant support: Our product requires us to support multiple, isolated organizations/teams, so the vendor we went with needed to support creating and managing new teams on the fly.
  3. Adhere to an official OAuth/OpenID standard: To ensure security and compliance, we wanted our solution to match an official standard published by OAuth or a similar organization.
  4. Create an intuitive auth experience: We wanted the authentication workflow to be easy for users to follow. We wanted to minimize manual steps like copy/pasting tokens or passwords.
  5. Avoid Hardcoding Secrets: We want to avoid distributing any client secrets or tokens along with the CLI, since these could be extracted from the binary and used to steal credentials.

Going through these criteria, we decided to go with Stytch as our provider, largely due to their excellent support for multi-tenant + B2B workflows.

Choosing an OAuth 2.0 Standard

Once we’d chosen our vendor, we now needed to decide which standard to adopt. Early on there were 3 contenders that we needed to choose between:

Authorization Code

The Authorization Code flow is probably the most popular and common OAuth flow. In this flow, the application exchanges a Client ID + Secret and Authorization Code for an access token to the secure resources. You’ve probably used this flow when using OAuth in your browser.

The standard Authorization Code OAuth Flow. Note that this flow requires the CLI to provide a hardcoded Client Secret to identify itself, which introduces some security risks. 

Unfortunately, since this flow requires the client application to store and transmit a Client ID and Secret, it fails one of our most important requirements.

Device Grant

The first standard we investigated was OAuth 2.0 Device Grant. This flow should be familiar to any use who has authorized a device or TV for a streaming service.

The (likely) familiar Device Authorization flow from Netflix. Developers enter a code displayed on their TV when authenticating in the browser. 

In this flow, the Device or app displays a unique code, and begins polling the authentication server for a response. The unique code is provided to the Authentication Server via a web browser (see the Netflix example above). Once the code is entered, the device receives an access token and can access protected resources.

This was our preferred option, since it doesn’t require us to securely store a Client Secret in the CLI. Unfortunately, we could not find a vendor that supported the Device Grant for multi-tenant use cases, meaning we couldn’t support teams and organizations with this method. Since we also didn’t want to build our own authentication server, this option was ruled out.

Our Choice: Authorization Code with PKCE

After ruling out the Device Grant, we decided on the Proof Key for Code Exchange (PKCE) variant of the Authorization Code flow. This flow is typically used in Native apps or Single Page Apps that need to authenticate, but can’t privately store a client secret.

Authorization Code with PKCE introduces a PKCE Verifier and Challenge that the Server can use to prevent CSRF and other attacks. The Verifier and Challenge let the server confirm that both requests come from the same source. 

Instead of a single Client Secret, our CLI generates a one time Code Verifier, and then uses it to generate a Code Challenge. The CLI sends the challenge along with the initial request to the auth server, and then sends the verifier when requesting the access token for the Jetpack Cloud API. The auth server can use the verifier and challenge to verify that the requests are coming from the same source, preventing a malicious user from intercepting the auth tokens.

Implementing PKCE from the Command Line

The main challenge with implementing the PKCE was that we needed a way for the CLI to receive tokens from the Authorization server. Native apps or SPAs provide URLs that the auth server can redirect the browser to, but our CLI didn’t have anything for this out of the box.

Our solution was to include an embedded callback server in our CLI (easy to do, since we use Go) that spins up and listens on a localhost address when the developer authenticates. The auth server can then redirect the browser to the server's localhost URL when responding with tokens.

Our CLI starts the authentication session, and then spins up a local callback server on the user's machine. The Authorization server can then redirect to the browser to our server on localhost to transmit the authentication and access tokens

This call back server lets us implement PKCE with a more efficient "push" mechanism, and avoid the polling required in official Device flow.

We plan to bundle this callback server, PKCE implementation, and other auth features into a small open source go packager for other developers to use. Look for the announcement in future devlogs!

Stay up to Date with Jetpack

If you're reading this, we'd love to hear from you about how you've been using Devbox for your projects. You can follow us on Twitter, or chat with our developers live on our Discord Server. We also welcome issues and pull requests on our Github Repo.