CLI Tool Needs Login? Use Device Code Flow

Published on:

Command-line tools often need to act for a signed-in user.

But a CLI usually cannot receive a normal browser redirect.

For this kind of app, the practical pattern is:

Device Code Flow.

This pattern is used when:

  • a user is present
  • the app runs in a terminal
  • the app cannot safely store a client secret
  • the app cannot rely on a local browser callback
  • authentication is handled by Microsoft Entra ID
  • the app needs delegated access to an API

In this example:

  • CloudTrips CLI is the command-line client
  • CloudTrips API is the protected resource
  • Employee is the signed-in user

Authentication is done with OAuth 2.0 Device Code Flow. Authorization is controlled with delegated permissions.

How This Differs from the SPA PKCE Flow

The previous trip used Authorization Code Flow with PKCE for a browser-based SPA.

That SPA could redirect the same browser to Microsoft Entra ID and receive the authorization code at /auth/callback.

A CLI is different.

It runs in a terminal. It may be used over SSH, inside a container, on a server, or in an environment where opening and listening for a browser callback is awkward.

Instead of redirecting back to the app, Device Code Flow splits the sign-in across two places:

  • the CLI requests a temporary device code from Microsoft Entra ID
  • the CLI shows a short user code and verification URL
  • the user opens the verification URL in any browser
  • the user enters the code and signs in
  • the CLI polls the token endpoint until sign-in completes
  • Microsoft Entra ID returns delegated tokens to the CLI

The shape is:

CLI asks for device code -> user signs in in browser -> CLI polls -> CLI receives tokens -> CLI calls API

The important idea is that the CLI never handles the user’s password.

Architecture

Employee -> CloudTrips CLI -> Microsoft Entra ID -> CloudTrips API

The CLI starts the login.

The browser completes the login.

The CLI receives an access token for the API after Microsoft Entra ID confirms that the user completed the device code challenge.

No client secret is used.

Create CloudTrips API App

First, create the resource application.

Go to:

Entra ID > App registrations > New registration

Create an application:

Name: CloudTrips-API

This app registration represents the API that will receive and validate access tokens.

Microsoft Entra app registration creation page with CloudTrips API entered as the application name

Expose API

Inside CloudTrips-API, open:

Expose an API

Set the Application ID URI:

api://<cloudtrips-api-application-id>

This identifier becomes the audience for access tokens issued to the CloudTrips API.

CloudTrips API App registration on the Expose an API page with the Application ID URI configured

Create Delegated Scope

Inside CloudTrips-API, add a delegated scope:

Expose an API > Add a scope

Example:

Scope name: Trips.Read
Who can consent: Admins and users
Admin consent display name: Read CloudTrips trips
User consent display name: Read your CloudTrips trips
State: Enabled

The CloudTrips API can later check whether the incoming token contains this scope.

Add a scope form for CloudTrips API showing Trips.Read and consent settings

Create CloudTrips CLI App

Now create the public client application.

Go to:

Entra ID > App registrations > New registration

Create an application:

Name: CloudTrips-CLI
Supported account types: Single tenant
Redirect URI: leave empty

The CLI does not need a web redirect URI for Device Code Flow.

Microsoft Entra app registration creation page with CloudTrips CLI entered as the application name

Enable Public Client Flow

Inside CloudTrips-CLI, open:

Authentication

Find:

Advanced settings > Allow public client flows

Set it to:

Yes

Do not create a client secret for the CLI.

A CLI using Device Code Flow is a public client. A secret shipped with a local tool can be copied.

CloudTrips CLI Authentication page showing Allow public client flows enabled

Add API Permission

Go to:

App registrations > CloudTrips-CLI > API permissions

Then select:

Add a permission

Choose:

My APIs > CloudTrips-API > Delegated permissions > Trips.Read

This allows the CLI to request an access token for the CloudTrips API on behalf of the signed-in user.

CloudTrips CLI API permissions page showing the CloudTrips API delegated Trips.Read permission selected

If your tenant requires administrator approval, click:

Grant admin consent

CloudTrips CLI API permissions page after admin consent is granted

Request a Device Code

At runtime, the CLI calls the device code endpoint:

POST https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/devicecode

Request body:

client_id=CLOUDTRIPS_CLI_CLIENT_ID
scope=openid profile offline_access api://<cloudtrips-api-application-id>/Trips.Read

Microsoft Entra ID returns values like:

{
  "user_code": "ABCD-EFGH",
  "device_code": "long-device-code-for-the-cli",
  "verification_uri": "https://microsoft.com/devicelogin",
  "expires_in": 900,
  "interval": 5,
  "message": "To sign in, use a web browser to open..."
}

The CLI shows the user_code and verification_uri to the user.

The CLI keeps the device_code for polling.

Terminal output showing the device code response with verification URL, user code, device code, expiration, and polling interval

The user opens:

https://microsoft.com/devicelogin

Then the user enters the short code shown by the CLI.

After entering the code, the user signs in with Microsoft Entra ID.

If consent is required, the user or administrator approves the delegated permission.

CLI Polls the Token Endpoint

While the user signs in, the CLI polls the token endpoint:

POST https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token

Request body:

client_id=CLOUDTRIPS_CLI_CLIENT_ID
grant_type=urn:ietf:params:oauth:grant-type:device_code
device_code=DEVICE_CODE_FROM_DEVICECODE_RESPONSE

The CLI should wait between polling attempts using the interval value from the device code response.

Before the user finishes sign-in, Microsoft Entra ID can return:

{
  "error": "authorization_pending"
}

That is normal.

After the user completes sign-in, Microsoft Entra ID returns tokens.

Terminal or safe log showing token polling with grant_type device_code and a successful token result

Access Token Claims

The CLI receives an access token for the CloudTrips API.

Decode the access token.

Important claims:

Claim Meaning
aud API that should accept the token
scp Delegated permissions granted to the CLI
sub Stable user identifier for this app
tid Tenant ID
iss Token issuer

For this trip, the important proof is:

{
  "aud": "api://<cloudtrips-api-application-id>",
  "scp": "Trips.Read"
}

Decoded access token showing CloudTrips API audience and Trips.Read delegated scope

Call the Protected API

The CLI sends the access token to the CloudTrips API:

GET https://api.cloudtrips.example/trips
Authorization: Bearer ACCESS_TOKEN

The API validates:

  • token signature
  • issuer
  • audience
  • expiration
  • delegated scope

If the token contains scp=Trips.Read, the API returns the trips.

Terminal output showing the CloudTrips CLI calling the protected API and receiving a successful trips response

Key Difference from Other Flows

Topic Server-side web app SPA with PKCE CLI with Device Code
Client type Confidential Public Public
Client secret Yes No No
User interaction Browser redirect Browser redirect Separate browser code entry
Token exchange Server side Browser/public client CLI polling
Protection Client secret PKCE verifier Device code plus user completion
Common use Web apps Browser apps CLI tools, devices, SSH sessions

Enterprise Note

Device Code Flow is convenient, but it is still interactive user sign-in.

It is not a replacement for workload identity.

Use it for tools where a human operator is present.

For unattended automation, use managed identity, workload identity federation, certificate credentials, or client credentials flow instead.

In enterprise environments, consider Conditional Access impact. Some tenants restrict or monitor device code sign-in because it can be abused in phishing scenarios.

Use separate app registrations per environment when API audiences, permissions, or Conditional Access boundaries differ.

Example:

CloudTrips-CLI-DEV
CloudTrips-CLI-TEST
CloudTrips-CLI-PROD

CloudTrips-API-DEV
CloudTrips-API-TEST
CloudTrips-API-PROD

This provides:

  • clearer consent boundaries
  • safer production access
  • independent troubleshooting
  • cleaner audit logs
  • no accidental cross-environment token use