skip to content

Why must an authorization server not answer its login form POST with an HTTP 307 redirect?

level: seniorimportance: nice to knowfreq 20%

answer

  1. which request is in flight here
  2. the login body belongs to the server
  3. 307 repeats the method and the body
  4. the client ends up holding the password
  5. 303 fetches with GET, no body

basics

~20 s

A 307 tells the user agent to repeat the request unchanged - same method, same body. After the login form POST, that body holds the resource owner's credentials, so they are replayed to the client. RFC 9700 section 4.12 says use 303.

solid answer

~50 s

At the authorization endpoint the resource owner authenticates to the **authorization server**, typically by POSTing a form. When authentication succeeds the server redirects the browser to the client's redirection endpoint with the authorization response. If that redirect is a 307, the user agent is being told to reissue the same request at the new location without changing the method or dropping the body - so it POSTs the login form, username and password included, to the **client**. The client now holds the resource owner's credentials for the authorization server, which is precisely what the delegation model exists to prevent. RFC 9700 section 4.12 names 303 as the answer: it is the one redirect status that unambiguously instructs the user agent to fetch the new location with GET and no body. A 302 is rewritten to GET by user agents in practice, but the specification permits keeping the method, so it is not a guarantee.

code

http · 14 lines
http
POST /login HTTP/1.1
Host: as.example.net
Content-Type: application/x-www-form-urlencoded

username=j.okafor&password=correct-horse-staple

HTTP/1.1 307 Temporary Redirect
Location: https://claims.example.rail/oauth/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj

POST /oauth/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj HTTP/1.1
Host: claims.example.rail
Content-Type: application/x-www-form-urlencoded

username=j.okafor&password=correct-horse-staple

go deeper

for a junior

Recall that redirect status codes differ in whether the user agent repeats the method and body, and that 307 repeats both.

for a middle

Explain which request is in flight at that step - the resource owner's login POST to the authorization server - and therefore what a method-preserving redirect hands to the client.

for a senior

Show where this comes from in practice: a framework redirect helper whose default preserves the method, and the client-side detection of a POST arriving at a redirection endpoint.

for a principal

Position it as a boundary question: your authorization server's job includes making sure no relying party can ever receive a user's password, even by accident, and status-code choice is one of the places that guarantee is made or lost.

## What is happening at that moment The flow runs like this. The rail operator's claim service sends the passenger's browser to the ticket retailer's **authorization endpoint**. The passenger is not signed in, so the authorization server shows its own login page and the browser POSTs credentials **to the authorization server** - not to the claim service, which is the entire point of delegated authorization. Authentication succeeds, consent is given, and now the authorization server needs to get the browser back to the client with the authorization response. The response to that POST is a redirect. Which status code it uses decides what the browser does next with the request body it just sent. ## Why 307 is different ```http POST /login HTTP/1.1 Host: as.example.net Content-Type: application/x-www-form-urlencoded username=j.okafor&password=correct-horse-staple HTTP/1.1 307 Temporary Redirect Location: https://claims.example.rail/oauth/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj POST /oauth/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj HTTP/1.1 Host: claims.example.rail Content-Type: application/x-www-form-urlencoded username=j.okafor&password=correct-horse-staple ``` The third message is the defect. A 307 means *reissue this request at the new location, unchanged*. The user agent obeys: same method, same body. The body is the login form. The new location belongs to the client. So the client receives the resource owner's authorization-server credentials in a request it did not solicit and cannot refuse to receive. The damage does not require a malicious client, though a malicious one is the worst case. An honest client with ordinary request logging writes that body to a log file. The password is now in a third party's log retention, and nobody involved did anything they would describe as wrong. ## The status codes, precisely | Status | What it tells the user agent | Fit for this redirect | |---|---|---| | 302 | Historically ambiguous: agents rewrite to GET in practice, but the specification allows keeping the method | Usually harmless, not a guarantee | | 303 | Fetch the new location with GET, and do not carry the body | The correct choice | | 307 | Reissue the request unchanged, method and body included | Replays credentials at the client | This is the reasoning behind RFC 9700 section 4.12: the authorization server should use 303 for this redirect, because 303 is the only one of the three that *states* the behaviour everyone assumes. Relying on 302 is relying on user-agent convention rather than on what the message means. ## What to check in a real deployment - **The status code on the redirect that follows the login POST**, specifically. Other redirects in the flow are frequently GETs already and are not where this bites. - **Framework defaults.** The trap is usually not a decision anyone made; it is a helper whose default preserves the method, chosen so that API redirects behave sensibly. - **Anything in front of the authorization server** that rewrites status codes on the way out. - **On the client's side**, treat a POST arriving at the redirection endpoint as an anomaly: the authorization response is expected as a GET, so a POST there means something upstream is misbehaving. Refuse it, alert, and be careful that the alert does not log the body you are complaining about. ## Why this is a good interview question and a rare bug It is rare because most deployments land on 302 or 303 by accident and never notice. It is a good question because answering it requires holding three things at once: **who** the credentials belong to and who is allowed to see them, **what** a redirect status actually instructs a user agent to do, and **where** in the flow that redirect sits. A candidate who knows only that "307 preserves the method" has half of it; the other half is realising which body is in flight at that exact step, and that the party it is about to be handed to is the one party the protocol is designed to keep it away from.

  • Is answering with 302 instead safe?
    In practice user agents rewrite a 302 to a GET and drop the body, so it usually behaves like 303. But the specification permits an agent to keep the method, so a 302 relies on convention rather than on what the message says. RFC 9700 section 4.12 names 303 because 303 states the required behaviour outright.
  • What should a client do if a POST arrives at its redirection endpoint?
    Treat it as an anomaly and refuse it. The authorization response is expected as a GET, so a POST there means an upstream party is redirecting in a way it should not. Alert on it - and make sure the alert does not persist the request body, which may be exactly the credentials you are trying not to hold.
  • Does the damage require the client to be malicious?
    No. An honest client with ordinary request logging writes the replayed body to its logs, so the resource owner's authorization-server password ends up in a third party's retention with nobody making a bad decision. A malicious client simply keeps it deliberately.

A 303 says "go and knock on that other door yourself, empty-handed". A 307 says "take this sealed envelope you were about to hand me and deliver it to that address instead" - and the envelope has the passenger's password in it.

saying these in an interview costs you the question

  • Thinks every redirect status makes the user agent switch to GET
  • Says 307 is safe because the response is encrypted in transit
  • Cannot say whose credentials are in the login form body
  • Treats 302 and 303 as interchangeable in specification terms
  • Assumes only a malicious client could be harmed by the replay