Skip to content

How to set up device based auth via code based linking

View as Markdown

Let a device, CLI or TV app with no easy browser call Port OS as a user. Your client asks Port OS for a short code. The user types the code on a Port OS web page from any other device and approves. Your client polls until the approval arrives, then receives tokens. You need a registered OAuth client that is allowed to use the device code grant first; see Set up an OAuth client for Port OS.

Every request in this flow carries a DPoP proof, and the tokens are bound to the key that signed it. Keep that key for as long as you keep the tokens.

Connection details

SettingValue
Device authorization endpointhttps://auth.portoperatingsystem.com/auth/device-authorization
Token endpointhttps://auth.portoperatingsystem.com/auth/token
Grant typeurn:ietf:params:oauth:grant-type:device_code
Proof of possessionA DPoP header on every request
Code lifetime30 minutes (expires_in is 1800)
Polling interval5 seconds (interval)

The client must have the device code grant registered. The OAuth clients page on the website creates authorization code clients only, so create a device client with the create client operation and put urn:ietf:params:oauth:grant-type:device_code in grantTypes. Anonymous dynamic registration clients cannot use this flow.

1. Create a DPoP key

Create an asymmetric key pair once and keep the private key on the device. Port OS accepts ES256, ES384, ES512, RS256, RS384, RS512 and EdDSA proofs. This guide uses ES256.

Each proof is a signed JWT whose header carries your public key as jwk. Its claims name the request:

ClaimValue
htmThe HTTP method, such as POST
htuThe full request URL, without query or fragment
iatThe current time in seconds. It must be within 60 seconds of the server clock
jtiA new random value for every proof. A repeated jti is rejected
JavaScript
const b64u = (input) => {
  const bytes = typeof input === 'string' ? new TextEncoder().encode(input) : new Uint8Array(input);
  return btoa(String.fromCharCode(...bytes)).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
};

const keys = await crypto.subtle.generateKey(
  { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign', 'verify']);
const jwk = await crypto.subtle.exportKey('jwk', keys.publicKey);

async function dpopProof(method, url) {
  const header = { typ: 'dpop+jwt', alg: 'ES256', jwk: { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y } };
  const claims = { htm: method, htu: url, iat: Math.floor(Date.now() / 1000), jti: crypto.randomUUID() };
  const signingInput = b64u(JSON.stringify(header)) + '.' + b64u(JSON.stringify(claims));
  const signature = await crypto.subtle.sign(
    { name: 'ECDSA', hash: 'SHA-256' }, keys.privateKey, new TextEncoder().encode(signingInput));
  return signingInput + '.' + b64u(signature);
}

Build a new proof for every request. Never reuse one.

2. Ask for a device code

POST a form with your client_id and optionally a space-separated scope.

Terminal
curl -X POST https://auth.portoperatingsystem.com/auth/device-authorization \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "DPoP: $DPOP_PROOF" \
  --data-urlencode "client_id=YOUR_CLIENT_ID" \
  --data-urlencode "scope=endpoint:read message:send"
JavaScript
const deviceUrl = 'https://auth.portoperatingsystem.com/auth/device-authorization';
const deviceResponse = await fetch(deviceUrl, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    DPoP: await dpopProof('POST', deviceUrl),
  },
  body: new URLSearchParams({
    client_id: 'YOUR_CLIENT_ID',
    scope: 'endpoint:read message:send',
  }),
});
const device = await deviceResponse.json();

The response:

JSON
{
  "device_code": "DEVICE_CODE_KEEP_PRIVATE",
  "user_code": "ABCD-EFGH",
  "verification_uri": "https://verification.example/oauth/device/verify",
  "verification_uri_complete": "https://verification.example/oauth/device/verify?user_code=ABCD-EFGH",
  "expires_in": 1800,
  "interval": 5
}

Show the user the user_code and the verification address, or render verification_uri_complete as a QR code. Never show the device_code.

Known issue: the service currently returns a verification_uri on a retired host, not on portoperatingsystem.com. The Port OS verification page is /oauth/device/verify on portoperatingsystem.com. Until the service is corrected, tell users to open https://portoperatingsystem.com/oauth/device/verify and type the code there.

3. The user approves

On the verification page the user signs in to Port OS, enters the code, reviews the permissions your client asked for and clicks Allow. They can uncheck permissions, so the grant may carry fewer scopes than you asked for.

4. Poll for the tokens

Until the user answers, poll the token endpoint no faster than interval seconds. Sign a new proof for each poll.

Terminal
curl -X POST https://auth.portoperatingsystem.com/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "DPoP: $DPOP_PROOF" \
  --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
  --data-urlencode "device_code=DEVICE_CODE" \
  --data-urlencode "client_id=YOUR_CLIENT_ID"
JavaScript
const tokenUrl = 'https://auth.portoperatingsystem.com/auth/token';
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

let interval = device.interval;
const deadline = Date.now() + device.expires_in * 1000;
let tokens;
while (Date.now() < deadline) {
  await sleep(interval);
  const poll = await fetch(tokenUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      DPoP: await dpopProof('POST', tokenUrl),
    },
    body: new URLSearchParams({
      grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
      device_code: device.device_code,
      client_id: 'YOUR_CLIENT_ID',
    }),
  });
  const body = await poll.json();
  if (poll.ok) { tokens = body; break; }
  if (body.error === 'authorization_pending') continue;
  if (body.error === 'slow_down') { interval += 5; continue; }
  throw new Error('Device login stopped: ' + body.error); // access_denied or expired_token
}

When the user approves, the response carries the tokens:

JSON
{
  "access_token": "ACCESS_TOKEN_STORE_PRIVATELY",
  "refresh_token": "REFRESH_TOKEN_STORE_PRIVATELY",
  "token_type": "DPoP",
  "expires_in": 86400,
  "scope": "endpoint:read message:send"
}

The access token lasts 24 hours and the refresh token 30 days. Both are bound to your DPoP key.

5. Call Port OS

Send the access token with the DPoP scheme and a fresh proof for the request, with the request's own method and URL.

Terminal
curl https://api.portoperatingsystem.com/endpoints \
  -H "Authorization: DPoP ACCESS_TOKEN" \
  -H "DPoP: $DPOP_PROOF_FOR_THIS_REQUEST"

6. Refresh the tokens

Refresh with the same key. A proof from a different key is rejected.

JavaScript
const refreshed = await fetch(tokenUrl, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    DPoP: await dpopProof('POST', tokenUrl),
  },
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: tokens.refresh_token,
    client_id: 'YOUR_CLIENT_ID',
  }),
});
const next = await refreshed.json(); // Replace the stored refresh token.

Refresh tokens rotate, so store the new refresh_token. When the refresh token expires or is revoked, run the whole flow again from step 2.

If something goes wrong

ProblemWhat to do
authorization_pendingThe user has not answered yet. Keep polling at the interval.
slow_downYou polled too fast. Add 5 seconds to your interval.
access_deniedThe user clicked Deny. Stop and tell them.
expired_tokenThe code expired after 30 minutes. Request a new one in step 2.
invalid_client or unauthorized_clientThe client ID is wrong or the client does not have the device code grant. Create a device client as described above.
invalid_dpop_proof or a 400 about the proofCheck htm and htu match the request exactly, iat is within 60 seconds, jti is new, and the public key is in the header as jwk.
Refresh rejectedSign the proof with the same key that was used to get the tokens.

References

  1. Set up an OAuth client for Port OS
  2. Website based auth via authorization code and PKCE
  3. Device authorization operation
  4. Token operation
  5. Create client operation