Command Line Interfaces (CLI) have become integral tools for developers looking to streamline their workflow, but how does it actually work?
Written by
James Perkins
Published on
Command Line Interfaces (CLI) have become integral tools for developers looking to streamline their workflow. Some of the biggest names in tech, including Vercel, GitHub, Netlify, and Planetscale, offer CLIs that provide a powerful way to interact with their services.
The question then comes up: How does CLI authentication work? How does a developer link this separate service with their authentication provider of choice? Let's delve into the world of CLI auth.
Suppose you are getting ready to authenticate with a CLI—you might use a command like npx vercel login
. But what exactly happens under the hood?
Check out the demo used in this blog post here
Before we look at any code, let us look at a diagram that explains the process.
The process begins when a user invokes the CLI, which starts a server on a free port that listens for an incoming request. This will also result in the user's web browser opening up to a specific URL for CLI authentication, let's say unkey.dev/cli-auth
, and it will include a unique code that the user will need to confirm the authentication process.
Once the web application has launched, the user is prompted to log in using their authentication method of choice. This could be a username, password, social login, or two-factor authentication (2FA) method. After the user has successfully authenticated, the web application will display a unique code that the CLI generated to the user. The user must then confirm the authentication process by entering the unique code into the web application.
The web application will then generate a new API key or token and return it to the CLI. The CLI will then store the token securely on the user's machine and use it for future requests to the service. The user will then be shown a message that the authentication process was successful, and they can return to the CLI.
User ---> CLI ---> Auth Web Page ---> Auth Confirmation ---> Token Generation ---> CLI token storage
The token is now securely stored on the user's machine, and each future CLI request will use this token to authenticate with the service without the user needing to re-authenticate each time.
To better understand the process, let's walk through a real-world example. We'll use the @unkey/cli-demo
package, a simple CLI demonstrating the authentication process. The package is available on npm, and you can use it by running the following command:
1npx @unkey/cli-demo login
Executing the command will create the local server and open a browser window that asks you to log in using Clerk. At the same time, the CLI will display a unique code that you will need to enter into the web application. Once you have entered the code, the web application will generate a new API key and send it back to the CLI. The CLI will then store the token securely on your machine and use it for future requests for a service.
To see the file that was created, you can run the following command:
1ls -a ~/
You'll find .unkey
, which holds your API key.
We can break down the code into the CLI and the web application. The CLI is a simple Node server that creates a file named .unkey
and stores your freshly created API key. The web application is a simple Next app that interacts with Unkey to generate a new API key.
The CLI code has some boilerplate code that sets up a server and has a command named login
that will start the server and open a browser window to the web application. The server will listen for a request from the web application and store the API key in a file.
Below is a promise waiting to be resolved when the server is started. The promise will resolve when the user has confirmed the authentication process and the API key has been generated and returned from the web, or the user cancels the request.
1// set up HTTP server that waits for a request containing an API key
2// as the only query parameter
3const authPromise = new Promise<ParsedUrlQuery>((resolve, reject) => {
4 server.on("request", (req, res) => {
5 // Set CORS headers for all responses
6 res.setHeader("Access-Control-Allow-Origin", "*");
7 res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
8 res.setHeader(
9 "Access-Control-Allow-Headers",
10 "Content-Type, Authorization",
11 );
12
13 if (req.method === "OPTIONS") {
14 res.writeHead(200);
15 res.end();
16 } else if (req.method === "GET") {
17 const parsedUrl = url.parse(req.url as string, true);
18 const queryParams = parsedUrl.query;
19 if (queryParams.cancelled) {
20 res.writeHead(200);
21 res.end();
22 reject(new UserCancellationError("Login process cancelled by user."));
23 } else {
24 res.writeHead(200);
25 res.end();
26 resolve(queryParams);
27 }
28 } else {
29 res.writeHead(405);
30 res.end();
31 }
32 });
33});
The web application code is a Next.js application that uses Clerk to authenticate the user, generate a new API key, and send it back to the CLI. When the user successfully confirms the code in the web application, it will make a request to Unkey to generate a new API key associated with the user's account and send it back to the CLI.
Below is the verification function that is called when the user confirms the code in the web application. The function will request an endpoint called /api/unkey
to generate a new API key.
1async function verify(opts: { code: string | null; redirect: string | null }) {
2 setLoading(true);
3 try {
4 const req = await fetch("/api/unkey", {
5 method: "POST",
6 body: JSON.stringify(opts),
7 headers: {
8 "Content-Type": "application/json",
9 },
10 });
11
12 if (!req.ok) {
13 throw new Error(`HTTP error! status: ${req.status}`);
14 }
15
16 const res = await req.json();
17
18 try {
19 const redirectUrl = new URL(res.redirect);
20 redirectUrl.searchParams.append("code", res.code);
21 redirectUrl.searchParams.append("key", res.key);
22
23 await fetch(redirectUrl.toString());
24
25 setLoading(false);
26 setSuccess(true);
27 } catch (_error) {
28 console.error(_error);
29 setLoading(false);
30 toast.error("Error redirecting back to local CLI. Is your CLI running?");
31 }
32 } catch (_error) {
33 setLoading(false);
34 toast.error("Error creating Unkey API key.");
35 }
36}
The /api/unkey
endpoint will generate a new API key, which is associated with the user's account. Below is the code for the endpoint.
1import { Unkey } from "@unkey/api";
2import { NextResponse } from "next/server";
3
4export async function POST(request: Request) {
5 const { id, redirect, code } = await request.json();
6 if (!process.env.UNKEY_ROOT_KEY || !process.env.UNKEY_API_ID) {
7 return NextResponse.json({
8 statusCode: 500,
9 message: "Unkey root key and API ID must be provided.",
10 });
11 }
12 const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY });
13
14 const { result, error } = await unkey.keys.create({
15 apiId: process.env.UNKEY_API_ID,
16 prefix: "cli_demo",
17 ownerId: id,
18 });
19
20 if (error) {
21 return NextResponse.json({
22 statusCode: 500,
23 message: "Error creating key – please ensure apiId is valid.",
24 });
25 }
26
27 return NextResponse.json({ ...result, code, redirect });
28}
As you can see, we are using Unkey's owner to associate the end user with the new API key, which makes it easy to find and revoke the key if needed.
You can dive deeper into the CLI demo code in our GitHub repository and see how the CLI and web application work together to authenticate a user and generate a new API key. The CLI and web application are simple examples of how to authenticate a user and generate a new API key using Unkey. You can use the same principles to create your CLI with minimal effort.
2500 verifications and 100K successful rate‑limited requests per month. No CC required.