One Callback, Three Journeys
Signing in, signing up and signing in from a phone are three different journeys that must all come back to one address, because that address was typed into a provider console by hand. What tells them apart is a marker, and every property of that marker is a decision.
Every sign-on integration has a constraint nobody designs around until they meet it: the address a provider returns to is registered in advance, by a person, in a console. It is not something the code chooses per request. So every journey that starts a handshake ends up at the same place, and something has to remember which journey it was. That something turns out to have a surprising number of ways to be wrong.
Three journeys, one destination
| Journey | Starts at | Should end at |
|---|---|---|
| Signing in | The sign-in screen | The dashboard |
| Signing up | The registration screen | The signup form, identity in hand |
| Signing in from the app | The mobile application | Back inside the app |
All three come back to one address, because registering a second one means somebody editing a provider console — for every provider, in every environment, whenever a journey is added.
The address is configuration held by somebody else. Everything else has to work around that being fixed.
Where the marker cannot live
There is an obvious-looking place to put it. The handshake carries a value out and back — a parameter intended to survive the round trip.
It is already in use. That parameter is how the framework detects a forged callback: it puts an unpredictable value in, and checks the same value comes back. Adding your own data to it means either replacing that check or entangling with it, and the check is the only thing standing between you and accepting a handshake somebody else initiated.
So the marker lives in the browser session instead — set when the journey starts, read when the callback arrives. A field that already has a security purpose is not spare capacity, however convenient its shape.
Three ways a marker goes wrong
Each of these is handled explicitly, and each would otherwise be a real defect.
-
It belongs to the wrong provider
Start a signup with one provider, abandon it, then sign in with another. The marker is still there. It is stored with the provider it belongs to and ignored when they do not match.
-
It is stale
An abandoned attempt leaves a marker behind. A later browser sign-in clears it unconditionally, so it cannot divert somebody into a journey they did not start.
-
It was never legitimate
The app journey needs more than a parameter in an address — it needs a valid challenge issued for that handoff, so a crafted link cannot make a browser sign-in behave like an app one.
The second is the one worth sitting with. A stale marker does not fail loudly. It sends somebody who signed in on a laptop to a destination meant for a phone — a link the browser cannot open — and they are left looking at a page that never loads, having successfully signed in.
No error, no log line, and a support conversation that begins with "it just hangs" and has nothing in it about an attempt abandoned two days earlier.
Clearing unconditionally rather than carefully
The fix for that is worth naming as a pattern, because the tempting version is worse.
A web sign-in clears the marker every time — not when it looks stale, not when a timestamp suggests it should be gone, not when a heuristic decides it is safe. Always.
Conditional clearing requires knowing when a marker stops being relevant, which means a rule, which means a case the rule gets wrong. Unconditional clearing needs no rule, because a web sign-in is by definition not an app journey and the marker cannot be needed.
It is the same instinct as regenerating a session identifier on every sign-in rather than when a risk seems present: where the cheap action is always safe, do it always and delete the question.
Abandoning a journey is a route
One small thing that shows somebody drove the product rather than only building it.
A half-finished signup can be abandoned — the marker is cleared and the ordinary registration form returns. Without it, somebody who started with a provider and changed their mind is stuck: the form keeps offering to continue with a provider they no longer want, and the only escape is to complete a journey they have rejected.
It is reachable by a plain link rather than a submitted form, and the reasoning is recorded: a form there would mean two forms on a page that already has one, with the nesting problems that brings. The change it makes is clearing a marker in the person's own session, which is not the sort of state that needs protecting from itself.
That is a judgement rather than a rule, and writing down which judgement was made is what stops it being reversed by somebody applying the rule.
What the marker is not allowed to decide
Worth being explicit, because a value in a session that steers a sign-in deserves suspicion.
It decides where somebody is sent afterwards. It does not decide whether they are let in, who they are, or what they may do. Those come from the provider's verified identity and from the gate chain every sign-in method passes.
So the worst a corrupted marker achieves is sending somebody to the wrong page — annoying, visible, and recoverable — rather than admitting them to something. Keeping routing state and authorisation state apart is what makes the routing state ordinary, and it is the reason a session value can be trusted with this job at all.
What is in place, layer by layer
Three journeys through one callback
One registered address per provider
Every journey returns to the address configured by hand in the provider console, so adding a journey never means editing a console.
The marker kept out of the handshake parameter
That field carries the framework's forgery check, and a field with a security purpose is not spare capacity.
The provider stored with the marker
A marker left by one provider is ignored on another's callback, so an abandoned attempt cannot divert an unrelated sign-in.
Cleared unconditionally on a web sign-in
Always, rather than when a rule judges it stale — because a rule has a case it gets wrong and this one costs somebody a page that never loads.
The app journey requires a real challenge
A parameter in an address is not enough; a valid handoff challenge is required, so a crafted link cannot redirect a browser sign-in.
A way to abandon a half-finished signup
The marker is cleared and the ordinary form returns, so somebody who changed their mind is not stuck completing a journey they rejected.
Routing kept apart from authorisation
The marker decides where somebody goes afterwards and never whether they are let in, which is what makes a session value adequate for it.
App failures returned to the app
A failed app journey redirects into the application with a reason rather than leaving somebody on a browser page they did not expect.
The registered callback address is configuration held in somebody else's console. Every design decision here follows from that being fixed and expensive to change.
Three positions held on purpose
- A field with an existing security purpose is never repurposed, however well its shape fits. The forgery check on a handshake is the only thing distinguishing your callback from one somebody else started.
- State is cleared unconditionally where clearing is always safe. A conditional rule needs to know when something stops mattering, and the case it gets wrong here is a person left staring at a page that never loads.
- Routing state and authorisation state are kept apart. A marker that decides only where somebody is sent can be an ordinary session value; one that decided whether they were let in could not.
Five questions about sign-on callbacks
How many callback addresses do you register?
A good answer sounds like
One per provider.
What ours actually is
One, reused by every journey, so adding a journey never means editing a provider console.
Where is the journey remembered?
A good answer sounds like
Not in the handshake parameter.
What ours actually is
In the session, because that parameter carries the framework's forgery check.
What if somebody abandons a signup?
A good answer sounds like
It does not follow them.
What ours actually is
A web sign-in clears the marker unconditionally, and the marker is tied to the provider that set it.
Can a crafted link change where a sign-in goes?
A good answer sounds like
No.
What ours actually is
The app journey needs a valid handoff challenge, not merely a parameter in an address.
Can the marker affect who gets in?
A good answer sounds like
No — it is routing only.
What ours actually is
Admission comes from the provider's verified identity and the gate chain every method passes.
Our take
This is the last of sixty-six posts written by reading the code for thirty-three connectors, and the same thing kept turning up. The interesting part is almost never the feature. It is the constraint the feature had to be built around — a callback address somebody typed into a console, a provider that sends no verification claim, a spreadsheet that evaluates formulas, a number you have to choose yourself. None of those is in a product description, all of them decide how the thing behaves, and every one of them was found by reading rather than by asking. If there is one thing worth taking from the series, it is that the answer to "how does this actually work" is always in the source, and it is usually more interesting than the answer you would have been given.
Thirty-three connectors, written from the code
Every post in this series names the files and the lines it was checked against. If you want to know how a connector behaves before you rely on it, that is the series to read.
Talk through connecting your systemsFrequently asked questions
Why is there only one callback address per provider?
Because it is registered by hand in the provider's console, per provider and per environment. Adding a second for every new journey would mean somebody editing a console each time, so every journey reuses the one and a marker records which journey it was.
What happens if somebody abandons a sign-up half way?
A later web sign-in clears the marker unconditionally, and the marker is stored with the provider that set it — so an abandoned attempt with one provider cannot divert a sign-in with another. There is also an explicit way to abandon it and return to the ordinary registration form.
Could a crafted link send our sign-in somewhere unexpected?
No. The mobile journey requires a valid handoff challenge rather than merely a parameter in the address, so a link alone cannot make a browser sign-in behave like an app one.
Does the marker affect whether somebody is allowed in?
No — it decides only where they are sent afterwards. Admission comes from the provider's verified identity and from the gate chain that every sign-in method passes, which is what keeps a session value adequate for this job.
Why not put the journey in the handshake state parameter?
Because that parameter already carries the framework's check against forged callbacks — an unpredictable value sent out and compared on return. Repurposing it means either replacing that check or entangling with it, and it is the only thing distinguishing your callback from one somebody else initiated.