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:
- 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
- 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.
- 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.
- 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.
- 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.
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.
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.
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.
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.