Review cross-device authorization flow controls before rollout

John Burns

A QR code or a short code is easy to treat as proof that two devices belong to the same person. It is not. A television, kiosk, command-line client, or meeting-room display can show a code, while a phone signs in and approves the request. The code moves through a channel that is not authenticated between those devices. It can be copied and shown again in a different context.

RFC 10027, published as an IETF Best Current Practice in August 2026, calls this a cross-device flow and separates authorization from session transfer. Its central operational point is useful before an implementation reaches a usability test: a code does not establish why the user is being asked to approve access. The approval experience, the server-side lifetime, the resulting authorization, and the incident response path need an explicit review.

This guide creates a small local gate for that review. It is not an OAuth implementation and it does not certify RFC compliance. It reads a synthetic JSON review record and rejects a record that lacks an owner, a risk statement, a short lifetime, one-time use, consent context, or an incident-revocation path. The validation run used Python 3.13.5 on Linux AMD64. A complete fixture was accepted. Changing the lifetime to 900 seconds and removing one-time use and client identification made the same check return exit status 1. That paired result makes the gate suitable as a narrow pull-request or release input check.

Start by naming the flow and the asset

Cross-device authorization is not the same as moving an existing browser session onto another device. In an authorization flow, a consumption device asks for access and a separately trusted authorization device is used to authenticate and consent. The OAuth 2.0 Device Authorization Grant is one common protocol example. Session transfer has different consequences because it transfers an already established session and its state.

Write down which of those two jobs the feature performs before selecting a protocol or a user interface. This article is scoped only to cross-device authorization. The review record should name the resource being requested, the requesting client as the user will recognize it, and the consumption-device context. Avoid internal hostnames, device inventory identifiers, authorization URLs, QR payloads, or live user codes in a public record or a CI log.

A useful risk statement is short but specific. For example: a copied code could be placed in a misleading message, causing a user to approve access for a device or client they did not initiate. That is a consent and context problem, not merely a password problem. Multi-factor authentication can still succeed while the user approves the wrong authorization request.

RFC 10027 says implementers must perform a risk assessment and select mitigations for the identified risks. It also recommends avoiding cross-device flows when their risks cannot be adequately mitigated. That is why the local gate checks for a risk owner and summary rather than assuming a code format is a security decision.

Keep protocol mechanics separate from the review gate

The review record below uses generic fields. It is deliberately not a replacement for an authorization server’s client registration, token validation, rate limiting, audit logging, or OAuth library tests. Its job is to make important product and operations decisions reviewable before an endpoint is changed.

Save this as cross-device-review.json in the repository that owns the design review or deployment policy. The names and wording are illustrative; adapt them to the approved process.

{
  "flow_type": "authorization",
  "risk_owner": "identity-security",
  "risk_summary": "A copied code can move a consent request into a misleading context.",
  "consumption_context": "The device displays its product name and a short code.",
  "authorization_context": "The approval screen identifies the requesting client and requested resource.",
  "expiry_seconds": 180,
  "one_time_code": true,
  "confirmation_shows_client": true,
  "confirmation_shows_resource": true,
  "decline_path": "A visible decline control returns to the account without granting access.",
  "incident_revoke_runbook": "The incident runbook revokes the resulting authorization and sessions."
}

The 300-second maximum used below is a local review policy, not a value mandated by RFC 10027. Choose a lifetime that the authorization service can reliably enforce and that fits the use case. A short-lived, one-time code limits the period in which a copied value is useful, but it does not authenticate the display that originally showed it. The approval screen still needs enough context for the user to recognize the client, requested resource, and action.

A decline path belongs in the record because an unexpected approval request needs a safe outcome. It should not silently grant a reduced authorization, retry indefinitely, or force the user to abandon an account-recovery workflow. The runbook field establishes an operational owner for revoking the authorization and related sessions after a suspected consent-phishing report. Test that procedure separately in an authorized environment; this gate only checks that the review did not omit it.

Add a small, deterministic check

Save the following as cross_device_review.py. It accepts one JSON file, prints a concise result, and uses exit status 1 for a rejected review record. Exit status 2 means the input could not be read or parsed. Keeping those cases separate lets CI distinguish an unacceptable design record from a broken job.

#!/usr/bin/env python3
import json
import sys
from pathlib import Path

REQUIRED = (
    "flow_type", "risk_owner", "risk_summary", "consumption_context",
    "authorization_context", "expiry_seconds", "one_time_code",
    "confirmation_shows_client", "confirmation_shows_resource",
    "decline_path", "incident_revoke_runbook",
)

def problems(record):
    errors = []
    missing = [key for key in REQUIRED if not record.get(key)]
    if missing:
        errors.append("missing required review fields: " + ", ".join(missing))
    if record.get("flow_type") != "authorization":
        errors.append("this gate is scoped only to cross-device authorization")
    if isinstance(record.get("expiry_seconds"), int) and record["expiry_seconds"] > 300:
        errors.append("expiry_seconds exceeds this review policy's 300-second limit")
    if record.get("one_time_code") is not True:
        errors.append("one_time_code must be true")
    if record.get("confirmation_shows_client") is not True:
        errors.append("confirmation_shows_client must be true")
    if record.get("confirmation_shows_resource") is not True:
        errors.append("confirmation_shows_resource must be true")
    return errors

def main():
    if len(sys.argv) != 2:
        print(f"usage: {Path(sys.argv[0]).name} REVIEW.json", file=sys.stderr)
        return 2
    try:
        record = json.loads(Path(sys.argv[1]).read_text())
    except (OSError, json.JSONDecodeError) as exc:
        print(f"review record unavailable: {exc}", file=sys.stderr)
        return 2
    errors = problems(record)
    if errors:
        print("REJECT: " + "; ".join(errors))
        return 1
    print("ACCEPT: review record has the local cross-device authorization controls")
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

Run it before the change is deployed:

python3 cross_device_review.py cross-device-review.json

A clean record produces this result:

ACCEPT: review record has the local cross-device authorization controls

Do not make the job pass with || true. An unreadable file, invalid JSON, or an omitted policy field needs attention. If the code lives in a repository, pin the interpreter major version in the CI image or record it in the build output so a future runtime change is not confused with a policy failure.

Prove that the gate rejects a weak record

A positive result alone does not show that a gate protects anything. Make a disposable negative fixture by changing only three controls:

{
  "expiry_seconds": 900,
  "one_time_code": false,
  "confirmation_shows_client": false
}

Merge those fields into an otherwise complete copy of the review record, then run the same command. The validation run rejected the fixture with exit status 1:

REJECT: missing required review fields: one_time_code, confirmation_shows_client; expiry_seconds exceeds this review policy's 300-second limit; one_time_code must be true; confirmation_shows_client must be true

The duplicate-looking messages are intentional in this compact example: one reports absent or false required data and the other states the policy decision. A production gate can refine that output, but it should keep the decisive failure visible to a reviewer. The test did not contact an authorization server, decode a QR code, or process a real account. All values were synthetic and the test directory was removed from the publication tree after capture.

Follow the check with service-level evidence

Passing this record does not prove that a real flow is safe. Before rollout, use an approved non-production tenant to verify the authorization server enforces the selected lifetime and one-time use. Capture redacted evidence that the authorization device displays the recognizable client and resource before consent, that declining grants nothing, and that the incident procedure actually revokes the resulting authorization. Do not copy access tokens, refresh tokens, authorization codes, device identifiers, tenant names, or private callback URLs into the test artifact.

For native applications, RFC 8252 requires using an external user-agent such as the user’s browser for OAuth requests and describes protected redirect choices. RFC 9700 adds broader OAuth security best practices. Those standards address properties this JSON gate cannot inspect, including redirect URI handling, code protection, and client behavior. Use the gate to ensure the cross-device decisions are visible, then use the protocol’s supported server configuration and an authorized end-to-end test to establish the rest.

The valuable outcome is not a longer checklist. It is a review record that fails when the consent context or response plan has been left implicit. A short-lived code can be copied; a user-facing approval and a practiced revocation path are the controls that make that fact operationally manageable.

Sources