Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Device Authorization with Return Confirmation (DARC)

Plain RFC 8628 has a well-known weakness: the channel between the device and the user’s phone is unauthenticated. An attacker can start a device flow, send the displayed user_code to a victim (“enter this code to join the meeting”), and receive the victim’s tokens once the victim signs in and approves on the genuine verification page. MFA does not help: the victim is the one performing it. This is consent phishing, the dominant technique for bypassing MFA with the device grant since 2020.

DARC closes it with one extra step. After the user approves, the verification page shows a short confirmation code. The user types it on the device that started the flow, and the device sends it with its token request. The attacker never sees that page, so the attacker’s device never gets tokens unless the victim reads the code back to them — which the page explicitly tells them never to do.

DARC is a profile of RFC 8628: the same endpoints and grant type, a few new parameters, errors and metadata values, and no new cryptography.


Policy

Ahdapa decides whether a flow needs the confirmation; the client only states whether it can accept a code.

[device_flow]
confirmation                = "required"   # or "off" for plain RFC 8628
confirmation_exempt_clients = []           # rollout aid: client ids that run plain RFC 8628
confirmation_code_length    = 6            # 6–10
confirmation_charset        = "numeric"    # or "alphanumeric" (RFC 8628 base-20 set)
confirmation_code_ttl       = 180          # seconds after display, capped by expires_in
confirmation_max_attempts   = 3            # wrong codes before the session is denied

confirmation = "required" is the default. A client that cannot accept a code, and is not exempt, gets confirmation_unavailable: the server never silently continues without the confirmation.


Device authorization request

ParameterMeaning
confirmation_inputnumeric (digit keys), alphanumeric, or none. Absent means the client does not implement DARC.
login_hintThe account the device expects. Hint lock: only that account may approve; another account ends the request. Accepted only from authenticated (non-public) clients. Starting a request never cancels other pending requests: anyone able to start one for an account could otherwise cancel that user’s sign-in.
authorization_detailsRFC 9396 details, shown on the consent page as asserted by the client. Only clients listing the type in authorization_details_types may send it.
curl -s -X POST https://idp.example.com/device_authorization \
  -u "kdc-host:SECRET" \
  -d scope=openid \
  -d confirmation_input=numeric \
  --data-urlencode login_hint=alice@EXAMPLE.COM \
  --data-urlencode 'authorization_details=[{"type":"krb5_tgt","realm":"EXAMPLE.COM","principal":"alice@EXAMPLE.COM","armor":"anonymous"}]'
{
  "device_code": "…",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://idp.example.com/device",
  "expires_in": 600,
  "interval": 5,
  "confirmation_required": true,
  "confirmation_code_length": 6,
  "confirmation_code_charset": "numeric"
}

Show the input field for the confirmation code from the start, next to the user_code, so the user knows before visiting the page that a second step happens on the device.


Verification page

The page follows RFC 8628 up to consent. The consent screen labels every statement by its source:

  • Verified: signing in as alice@EXAMPLE.COM — the hint lock matched the signed-in user.
  • Asserted by client name: — the authorization_details. Known types are rendered as sentences, for example krb5_tgt as “Kerberos sign-in to EXAMPLE.COM as alice@EXAMPLE.COM from an unidentified terminal (anonymous armor)”.

After Allow, the page shows the code:

Almost done. Type this code on the device you are signing in to:

                 481 207

This code only works on the device in front of you.
Never read it out, paste it into another website, or send it to anyone —
no support agent, colleague or meeting host will ever ask for it.
[ I didn't start this — cancel and report ]

The code is generated at the moment of consent, stored only as a hash bound to the device code, never put in a URL, and the device pages are served with Cache-Control: no-store and Content-Security-Policy: frame-ancestors 'none'. The web UI (/ui/auth/device) shows the same step.


Session states

stateDiagram-v2
    [*] --> PENDING : device authorization
    PENDING --> APPROVED : user signed in and allowed (code shown)
    PENDING --> DENIED : deny, or another account on a hinted request
    APPROVED --> ISSUED : token request with the correct code
    APPROVED --> DENIED : attempts exhausted / cancel and report
    PENDING --> EXPIRED : expires_in elapsed
    APPROVED --> EXPIRED : code lifetime elapsed
    ISSUED --> [*]
    DENIED --> [*]
    EXPIRED --> [*]

There is exactly one transition to ISSUED, and it needs both the consent and the code arriving from the device.

Token endpoint

The device polls as in RFC 8628 and adds confirmation_code once the user has typed it. A code sent before approval is answered with authorization_pending and uses no attempt.

ConditionResponse
User has not approved yetauthorization_pending
Polling too fastslow_down
Approved, no code submittedconfirmation_required (keep polling, or prompt the user)
Approved, wrong code, attempts leftinvalid_confirmation
Wrong code with no attempts left; denied; cancelled and reported; another account tried a hinted requestaccess_denied
Request or code lifetime elapsedexpired_token
Device code already redeemedinvalid_grant
Approved, correct code200 with tokens

The ID token carries the approving session’s auth_time, acr and amr, so a client can map the strength of the user’s sign-in (for example to Kerberos authentication indicators).


Metadata and audit

Discovery advertises the capability, not the per-client policy:

"device_authorization_confirmation_supported": true,
"device_authorization_confirmation_charsets_supported": ["numeric", "alphanumeric"]

Audit events: device.approved, device.denied, device.reported, device.confirmation-failed, device.confirmation-exhausted and device.approved-unconfirmed. The last one — approval followed by no confirmation within the code lifetime — is the signature of an asynchronous consent-phishing lure.


What DARC does not stop

A victim coached in real time (“I’m from IT, read me the code”) can still hand the code over. DARC turns a mass email campaign into a live conversation per victim, contradicts the pretext on the page, and records the signals above; restricting high-value access to identified requesters (for example host-armored Kerberos requests) closes the rest.

The federation demo (contrib/demo/federation/run.sh, Step 10) walks through every case above with a KDC-like client and a user signing in through an upstream IdP.