Complete the authentication flow prerequisites before requesting device codes.
Device Flow (CLIs and Mobile Apps)
The device authorization flow (RFC 8628) enables authentication on devices that don’t have a browser or have limited input capabilities, such as CLIs, IoT devices, or smart TVs.
How Device Flow Works
- Device requests a user code
- Device displays the code and verification URL to the user
- User visits the URL on another device (phone/computer)
- User enters the code and authenticates
- Device polls for the token
- Token is granted after user approves
CLI Implementation
import { SsoClient } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: 'https://sso.example.com'
});
async function loginCLI() {
// Step 1: Request device code
const deviceAuth = await sso.auth.deviceCode.request({
client_id: 'acme-cli',
org: 'acme-corp',
service: 'acme-cli'
});
console.log('\n=================================');
console.log('To authenticate, visit:');
console.log(deviceAuth.verification_uri);
console.log('\nAnd enter code:');
console.log(deviceAuth.user_code);
console.log('=================================\n');
// Step 2: Poll for token
const token = await pollForToken(deviceAuth.device_code, deviceAuth.interval);
if (token) {
// Save token to config file
console.log('Authentication successful!');
return token.access_token;
}
}
async function pollForToken(deviceCode: string, interval: number) {
const maxAttempts = 60; // 5 minutes with 5-second intervals
let attempts = 0;
while (attempts < maxAttempts) {
await sleep(interval * 1000);
attempts++;
try {
const tokenResponse = await this.sso.auth.deviceCode.exchangeToken({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: deviceCode,
client_id: 'acme-cli'
});
return tokenResponse;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.errorCode === 'DEVICE_CODE_PENDING') {
// User hasn't authorized yet, keep polling
process.stdout.write('.');
continue;
} else if (error.errorCode === 'TOO_MANY_REQUESTS') {
// Increase interval if requested
interval += 5;
continue;
} else if (error.errorCode === 'DEVICE_CODE_EXPIRED') {
console.error('\nDevice code expired. Please try again.');
return null;
} else if (error.isForbidden()) {
console.error('\nUser denied the request.');
return null;
}
}
throw error;
}
}
console.error('\nAuthentication timed out.');
return null;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Web Application: Device Code Verification
Your web application needs a route to handle device code verification (typically at /activate).
// Activation page component
import { useState } from 'react';
import { SsoClient } from '@drmhse/sso-sdk';
const sso = new SsoClient({
baseURL: process.env.REACT_APP_SSO_URL
});
export function DeviceActivationPage() {
const [userCode, setUserCode] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
// Verify the user code
const context = await sso.auth.deviceCode.verify(userCode.trim().toUpperCase());
// Redirect to OAuth flow with user_code parameter
const loginUrl = sso.auth.getLoginUrl('github', {
org: context.org_slug,
service: context.service_slug,
user_code: userCode.trim().toUpperCase(), // CRITICAL: pass user_code
redirect_uri: `${window.location.origin}/device-callback`
});
window.location.href = loginUrl;
} catch (err) {
if (err instanceof SsoApiError) {
setError(err.message);
} else {
setError('Failed to verify code');
}
}
};
return (
<div className="activation-page">
<h1>Device Activation</h1>
<p>Enter the code displayed on your device</p>
<form onSubmit={handleSubmit}>
<input
type="text"
value={userCode}
onChange={(e) => setUserCode(e.target.value)}
placeholder="XXXX-XXXX"
maxLength={9}
style={{ textTransform: 'uppercase' }}
/>
<button type="submit">Continue</button>
</form>
{error && <div className="error">{error}</div>}
</div>
);
}
Complete Device Flow: Full Example
// cli.ts - Complete CLI tool with device flow
import { SsoClient, SsoApiError } from '@drmhse/sso-sdk';
import * as fs from 'fs';
import * as path from 'path';
const CONFIG_PATH = path.join(process.env.HOME!, '.acme-cli', 'config.json');
class AcmeCLI {
private sso: SsoClient;
constructor() {
this.sso = new SsoClient({
baseURL: 'https://sso.example.com'
});
}
async login() {
console.log('Starting authentication...\n');
const deviceAuth = await this.sso.auth.deviceCode.request({
client_id: 'acme-cli',
org: 'acme-corp',
service: 'acme-cli'
});
console.log('To authenticate, visit:');
console.log(` ${deviceAuth.verification_uri}`);
console.log('\nAnd enter code:');
console.log(` ${deviceAuth.user_code}`);
console.log('\nWaiting for authentication', { flush: true });
const token = await this.pollForToken(
deviceAuth.device_code,
deviceAuth.interval
);
if (token) {
this.saveToken(token.access_token, token.refresh_token);
console.log('\nAuthentication successful!');
return true;
}
return false;
}
private async pollForToken(deviceCode: string, interval: number) {
const maxTime = 300; // 5 minutes
const startTime = Date.now();
while (Date.now() - startTime < maxTime * 1000) {
await this.sleep(interval * 1000);
try {
const tokenResponse = await this.sso.auth.deviceCode.exchangeToken({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code: deviceCode,
client_id: 'acme-cli'
});
return tokenResponse;
} catch (error) {
if (error instanceof SsoApiError) {
if (error.errorCode === 'authorization_pending') {
process.stdout.write('.');
continue;
} else if (error.errorCode === 'slow_down') {
interval += 5;
continue;
} else if (error.errorCode === 'expired_token') {
console.error('\n\nDevice code expired. Please try again.');
return null;
} else if (error.errorCode === 'access_denied') {
console.error('\n\nAuthentication denied.');
return null;
}
}
throw error;
}
}
console.error('\n\nAuthentication timed out.');
return null;
}
private saveToken(accessToken: string, refreshToken: string) {
const dir = path.dirname(CONFIG_PATH);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(
CONFIG_PATH,
JSON.stringify({ accessToken, refreshToken }, null, 2)
);
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// CLI entry point
const cli = new AcmeCLI();
cli.login();