RFC 6749 §4.1.3 in the wild: an OAuth code issued for a non-public hostname during Italy’s sovereign-cloud migration
- • Italy is moving its consular IT to Polo Strategico Nazionale, the national sovereign cloud. The first 48-hour migration window ran 13–14 April 2026, announced in advance on every consulate website.
- • During the window I observed the OAuth handover pointing at a PSN internal hostname ahead of the public-facing routing converging on it. The IdP (
iam.esteri.it) issued a standard authorisation code; the redirect target named a hostname that does not resolve on public DNS. - • OAuth 2.0 (RFC 6749 §4.1.3) is designed for exactly this kind of in-flight target change: authorisation codes are bound to the
redirect_urithe IdP used, not to whichever host the browser happens to be on. Returning to the canonicalprenotami.esteri.ithost completed the flow. - • A small detection shape for any OAuth deployment, included at the end — useful for any team that wants an early signal during planned cutovers.
Context: a national sovereign-cloud migration
Italy is in the middle of a serious piece of infrastructure work. The Farnesina announced a planned 48-hour window on 13–14 April 2026 to migrate SIFC (Sistema Integrato di Funzioni Consolari) — the consular services backend — onto the Polo Strategico Nazionale, Italy's national sovereign cloud. The window was published in advance on consulate sites worldwide (one example: Ambasciata Bangkok). A follow-up window is scheduled for 22–23 April.
The stack choices on display are sensible and modern: OAuth 2.0 with PKCE (code_challenge_method=S256), a dedicated IdP at iam.esteri.it, a separate relying party at prenotami.esteri.it, and a sovereign-cloud target. This is the correct shape for a system of this size.
What I observed
On 13 April 2026, during the announced migration window, the OAuth authorize flow initiated from the public login link on prenotami.esteri.it went through the usual hops and produced this URL in the address bar:
https://prenotami.psnesteri.loc/pingid ?code=T5Q7_... &iss=https%3A%2F%2Fiam.esteri.it%2Flogin%2Foauth2 &state=... &client_id=prenotami-fo
The host didn't resolve on public DNS — as expected, since .loc is a private TLD convention rather than a publicly-registered one (only .local is reserved, under RFC 6762). prenotami.psnesteri.loc appears to be an internal hostname for the PSN-hosted environment; the psn prefix plausibly corresponds to Polo Strategico Nazionale. (I have no internal visibility; this is inference from the public-facing shape.)
What the browser surfaced looks like a state in which the authorisation flow had been pointed at the new PSN-facing target ahead of the public-facing layer being ready to serve it. In a multi-stage migration where an IdP, application servers, DNS, and a reverse-proxy/CDN each converge on a new target, a short handover state like this is not unusual. OAuth 2.0 is designed to make flows robust to exactly that kind of in-flight change.
Editing the host in the address bar to prenotami.esteri.it while preserving the rest of the URL returned the browser to the canonical host, and the flow completed from there. The part worth writing up is why RFC 6749 makes that recovery work correctly.
Reading the URL
| Param | Meaning |
|---|---|
| host | Internal PSN hostname. No public DNS record. .loc is a private convention, not reserved by IANA (only .local is, under RFC 6762). |
| code | OAuth 2.0 authorization code (RFC 6749 §1.3.1). Short-lived, one-time. Exchanged by the relying party server-side for tokens. |
| iss | Issuer (RFC 9207). iam.esteri.it — the IdP host, resolving normally throughout the observed period. |
| state | CSRF token the relying party included in the authorization request; checked on return. |
| client_id | prenotami-fo. Front-office OAuth client for the public-facing consular-services portal. |
Why the flow completes on the canonical host — RFC 6749 §4.1.3
RFC 6749 §4.1.3 defines what the relying party sends when exchanging the authorisation code:
POST /token HTTP/1.1 Host: iam.esteri.it Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=T5Q7_... &client_id=prenotami-fo &client_secret=... &redirect_uri=<the one sent at step 1>
The IdP verifies the redirect_uriin the token request against the one used in the authorization request — not against whichever URL the browser happens to be viewing. So long as the relying party’s server-side configuration carries the same value the IdP expects, the exchange succeeds regardless of where the browser happened to land in between. This is the normative spec behaviour; it isn’t a quirk.
So when the browser returned to prenotami.esteri.it/pingid?code=..., the application server on the canonical host performed its backchannel POST /token, the IdP’s redirect_uricheck matched the value sent at authorization time, and the authenticated session established normally. Textbook RFC 6749 semantics — this post is a live illustration, not a new finding.
The IdP issued a valid code. The browser hit a host that didn't resolve. Correcting the host in the address bar recovered the flow because the code is bound to client_id and the redirect_uri the IdP used, not to the browser’s current domain.
Why hosts-file overrides don’t recover this flow
An intuitive first instinct from the user side is to map the non-public hostname to the canonical server’s IP in /etc/hosts. Three layers stop that from working, which is worth naming precisely because they are the same three layers that stop many well-intentioned “edit your hosts file” fixes on any modern HTTPS site.
First, SNI (RFC 6066 §3). The browser opens a TLS handshake to the mapped IP and sends prenotami.psnesteri.loc as the server name indication. The real server presents a certificate for *.esteri.it. The hostname doesn’t appear in any SAN on that certificate, the handshake fails, and the browser refuses the connection. There is no click-through path on a correctly-configured HSTS deployment.
Second, HTTP host-based routing. Even if TLS somehow completed, the HTTP Host: request header still carries the .loc hostname. The origin (reverse proxy or application server) routes requests by Host:; no virtual host configuration matches; the response is either a 404 or a default backend, not the intended relying party.
Third, HSTS (RFC 6797). Either preloaded policy for the parent zone, or a prior session’s own HSTS header, prevents the user from dismissing a certificate warning in the first place. Any approach that relies on “proceed anyway” is closed off by design.
Returning to the canonical hostname in the address bar sidesteps all three layers by putting the browser back on the name the certificates, routing tables, and HSTS policy all agree on.
Why this handover shape is easy to miss in standard OAuth testing
This is a universal observation, not specific to any one deployment. Most OAuth test coverage lives at the unit-test layer, where the IdP is mocked — mocks return whatever you tell them to, and never surface the real redirect_uri the client config currently points at. Staging integration tests hit the staging environment, whose IdP client is of course registered with the staging hostname, so the test passes cleanly. Production smoke tests typically hit a landing page and assert a 200; logging in requires credentials and 2FA, which CI usually doesn't have. The login redirect lives in a gap that most test harnesses don't cover.
The light-touch addition is a check on the shape of the redirect, without logging in. Click the login link, follow the 302, parse the redirect_uri parameter out of the authorize URL, assert it resolves publicly, assert the hostname suffix is canonical. All of that can be done unauthenticated, from a cron. Useful for any team running an OAuth deployment, especially during planned cutovers like a cloud migration.
A small monitor shape, for any OAuth deployment
A short Python probe, parameters at the top. It fetches the page that hosts the login link, parses the redirect_uriparameter out of the authorize URL, and asserts the host resolves publicly and ends with the expected suffix. Useful for any OAuth deployment — internal, open-source, or your own — not specific to any one operator.
# Adapt to your own OAuth deployment.
import re, socket, sys, urllib.parse, urllib.request
LANDING_PAGE = "https://app.example.com/" # page that contains the login link
AUTHORIZE_HOST = "idp.example.com" # your IdP host
EXPECTED_SUFFIX = ".example.com" # canonical suffix the callback must match
html = urllib.request.urlopen(LANDING_PAGE, timeout=10).read().decode("utf-8", "replace")
link = re.search(rf'href="([^"]*{re.escape(AUTHORIZE_HOST)}/[^"]*authorize[^"]*)"', html)
if not link: sys.exit("no login link found")
ru = re.search(r'redirect_uri=([^&]+)', link.group(1).replace("&", "&"))
if not ru: sys.exit("no redirect_uri in login link")
host = urllib.parse.urlparse(urllib.parse.unquote(ru.group(1))).hostname or ""
try: socket.gethostbyname(host)
except socket.gaierror: sys.exit(f"redirect_uri host does not resolve: {host}")
if not host.endswith(EXPECTED_SUFFIX): sys.exit(f"unexpected host: {host}")
print(f"ok: {host}")Run from a cron every 60 seconds and you catch this shape of transient inside a minute. The same check catches staging URLs leaked to prod, DNS misconfigurations on callback hosts, and mid-migration transients.
What I didn't check
I don't know how long the specific handover state was visible. The announced migration window was 48 hours; my own observation is a single point inside that window.
I didn't inspect the backchannel token exchange. The recovery behaviour described here is the standard RFC 6749 path and is consistent with how a Ping-style stack handles redirect_uri validation, but I don't have packet traces of the server-side call.
The second SIFC maintenance window (22–23 April 2026) is follow-up work on the same migration. I have no plans to probe it. How cleanly the second wave goes is up to the team running it — the point of a staged migration is exactly that each wave gets to learn from the one before.
Zooming out: migrating a service used by Italian citizens worldwide onto sovereign cloud, across a two-wave window announced weeks in advance, is a non-trivial engineering project. The stack choices are the right ones — OAuth 2.0 with PKCE, a dedicated IdP, a separate relying party, and a sovereign target. Observing a short handover state during a major cutover is, if anything, a sign that real infrastructure work is happening.
Primary source for the migration window: Ambasciata d'Italia Bangkok — SIFC temporary outage 13–14 April 2026. OAuth 2.0 semantics: RFC 6749; TLS SNI: RFC 6066 §3; HSTS: RFC 6797. The handover has since completed on the canonical host. The observation above was recorded from within a browser session I had initiated on my own device. The authorisation code referenced here was issued to that session and was corrected in my own browser. I did not attempt to access, enumerate, or retrieve any data or endpoints beyond what was returned to that session; I did not probe the system for additional issues; and I have not shared the authorisation code with anyone. Code values shown above were truncated before this post was written.