Quickly add authentication to your command-line application.
CLI Auth enables your command-line applications to authenticate users through the web via your WorkOS app. Based on the OAuth 2.0 Device Authorization Flow, this flow is optimized for devices that lack a web browser or have limited input capabilities.
With CLI Auth, your command-line app requests a device authorization from WorkOS, which includes a code for the user and a code for your app. After the user confirms the code, your app can exchanges its device code for tokens.
To begin the authentication flow, your CLI application makes a request to the /authorize/device
endpoint to obtain the necessary codes and URLs for user authentication.
const response = await fetch( 'https://api.workos.com/user_management/authorize/device', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: 'client_123456789', }), }, ); const data = await response.json();
{ "device_code": "71azDp28ToiCscGDvLxnXLkuuFRMrnd4V7rdsjIlBPXuy13j8GOzU0aZHb46tsz3", "user_code": "RRGQ-BJVS", "verification_uri": "https://<subdomain>.authkit.app/device", "verification_uri_complete": "https://<subdomain>.authkit.app/device?user_code=ABCD-EFGH", "expires_in": 300, "interval": 5 }
After you get a response, your app can provide next steps to the user.
Your application should display the user_code
from the response, along with the verification_uri
in the terminal. If you offer the ability to open in a browser easily like in this screenshot, we suggest using the verification_uri_complete
for that.
Never display the device_code
to the user. That is only for the device to poll the token endpoint.
If the user navigates to the verification_uri
, they’ll be presented with a form to enter the code manually. If they are not logged in they’ll be prompted to do that first and then returned to the code entry screen.
If the user goes to the verification_uri_complete
, (for example, https://<authkit_domain>/device?user_code=ABCD-EFGH
, they’ll instead need to confirm that the code matches what is displayed in the terminal.
While the user is completing authentication in their browser, your CLI application should poll the token endpoint to check for authorization completion.
Make requests to the token endpoint using device_code
from the authorization response from step 1:
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function pollForTokens({ clientId, deviceCode, expiresIn = 300, interval = 5, }) { const timeout = AbortSignal.timeout(expiresIn * 1000); while (true) { const response = await (async () => { try { return await fetch( 'https://api.workos.com/user_management/authenticate', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: deviceCode, client_id: clientId, }), signal: timeout, }, ); } catch (error) { if (error.name === 'TimeoutError') { throw new Error('Authorization timed out'); } throw error; } })(); const data = await response.json(); if (response.ok) { // Success! Return tokens for further processing. return data; } switch (data.error) { case 'authorization_pending': // Wait before polling again await sleep(interval * 1000); break; case 'slow_down': // You shouldn't see this if you're only polling every 5 seconds. // In this example, we're increasing the interval by 1 second when this happens. interval += 1; await sleep(interval * 1000); break; // Terminal cases case 'access_denied': case 'expired_token': throw new Error('Authorization failed'); default: throw new Error('Authorization failed'); } } }
{ "user": { "object": "user", "id": "user_01JYHX0DW7077GPTAY8MZVNMQX", "email": "grant.mccode@workos.com", "email_verified": true, "first_name": "Grant", "last_name": "McCode", "profile_picture_url": null, "last_sign_in_at": "2025-06-25T19:16:35.647Z", "created_at": "2025-06-25T01:20:21.355Z", "updated_at": "2025-06-25T19:16:35.647Z", "external_id": null }, "organization_id": "org_01JYHNPKWTD5DRGPJHNYBB1HB8", "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkdyYW50IE1jQ29kZSIsImFkbWluIjp0cnVlLCJpYXQiOjEzMzcsInBhc3N3b3JkIjoiaHVudGVyMiJ9.kcmTbx7M89k-3qUXN1UVcy9us6xdPZkDOqQ0UeY3Bws", "refresh_token": "RSzR4ngmJROKFJZQEpp5fNF4y", "authentication_method": "GoogleOAuth" }
slow_down
errors by increasing your polling intervalaccess_denied
or expired_token
errorsCLI Auth is available for Connect applications, allowing you and third-party developers to build CLI tools that integrate with your app’s credentials.
The flow is the same but uses Connect endpoints:
https://<authkit_domain>/oauth2/device_authorization
https://<authkit_domain>/oauth2/token
Since command-line applications are distributed to end users, you should avoid embedding the client secret in the app. To make this work, set up your Connect app as a public client.
Third-party Connect applications will require users to grant consent to the third-party app.