Skip to main content
A sign-in with a QR code can be forwarded: an attacker starts a sign-in in their own browser and talks an employee into confirming it. An internal application — an intranet, a back office, an admin panel — has no reason to accept sign-ins from outside the company. When a sign-in can start only from the office network or over the VPN, an outside attacker has no QR code to forward. Employees still confirm the sign-in in their messaging app, and the phone stays on mobile data. Neither Veriqa nor your application changes: you set the restriction on the reverse proxy, and it works with the current version.

What you build yourself

  1. List your networks. The office ranges and the VPN address pool, both IPv4 and IPv6. Include only networks where you control who gets in: a managed device, a VPN sign-in, a building pass.
  2. Restrict /connect/authorize on the proxy. Only this route starts a sign-in. An NGINX example with documentation address ranges, to add alongside the configuration from Behind a reverse proxy:
    Veriqa accepts this route regardless of case and with a trailing slash, so the block matches it the same way: ~* ignores case, and /? allows the slash. An exact location = would let /CONNECT/AUTHORIZE through the general location /. The query string does not affect the match. If Veriqa runs under a path prefix, include the prefix in the expression and let it repeat: ^(/auth/veriqa)+/connect/authorize/?$. When your general block trims the prefix, /auth/veriqa/auth/veriqa/connect/authorize reaches Veriqa as /auth/veriqa/connect/authorize, and Veriqa still treats it as this route. NGINX checks regular expressions in the order they appear, so put this block before the other location ~ blocks. Make sure no location ^~ or location = block covers this path: NGINX then skips regular expressions. In this block proxy_pass forwards the path unchanged, because NGINX does not allow a URI in proxy_pass inside a regular-expression location. Veriqa still accepts the prefixed path even if your general block trims the prefix. Use the same address as your general block, for example the same upstream, so that these requests reach your instances the same way as all others.
  3. Leave the other routes open. The phone opens the pages under /auth/ and /r/ over mobile data, the messaging platforms call the webhooks under /api/channels/, and your backend calls /connect/token from wherever it runs. The browser that has already passed the check opens the sign-in page and its WebSocket. The phone also uses the routes under /connect/authorize/callback, for example to submit the form that asks for a missing profile field, so do not restrict them. The block above does not match them.
  4. Check the client’s address. NGINX checks the address of whoever connects to it. If a load balancer or a CDN sits in front of NGINX, restore the client address from its header first with the set_real_ip_from and real_ip_header directives of the realip module; otherwise every request comes from the load balancer, and they all pass or all fail together. On the Veriqa side, configure trust in the proxy itself in the ForwardedHeaders section — see self-hosted, step 6.
  5. Tell the person why. Outside your networks the browser gets a 403. Serve your own page for that response (error_page 403 in the same location) saying that sign-in works only from the office network or over the VPN.

Why this works

A forwarded-QR-code attack starts when the attacker’s own browser opens the sign-in page. From outside your networks that browser gets a 403 instead of the page, and there is no QR code to send. To start a sign-in, the attacker needs a device inside the network — the RFC puts it as “the attacker needs to have access to a device on the network”. In BCP 247 terms, this is the Trusted Networks mitigation (RFC 10027 §6.1.8). The list lives in your proxy rather than in Veriqa, which is why Veriqa, BCP 247 and the threat model marks the row as not covered in the core and points here for what you can do today. The same list of networks can also pick the surface in the Place and identity scenario.

What to keep in mind

  • The rule applies to every application on the instance. They share the /connect/authorize route. The proxy can tell which application starts a sign-in only from the client_id query parameter, and the same parameter can be written in several ways, so a proxy rule based on it is easy to get around. If one Veriqa instance serves both an internal and a public application, give the internal one an instance of its own, with its own host name and issuer.
  • It does not suit consumer applications. Your customers sign in from networks you do not control; the RFC itself notes this limitation.
  • The network is only as trusted as the way into it. Guest Wi-Fi in the same range, a personal device on the VPN, or malware on an office computer is already inside. The RFC advises combining this control with others (§6.1.8, SHOULD), for example with the Passkeys alongside Veriqa scenario.
  • Remote work goes through the VPN. An employee outside the office signs in only with the VPN on, and a VPN outage stops their sign-ins too.