AWRA OpsHub Search

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.

Integrations & Data AWRA OpsHub Team 11 min read

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.

The address is configuration held by somebody else. Everything works around that being fixed.
The address is configuration held by somebody else. Everything works 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.

  1. 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.

  2. 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.

  3. 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.

Built in

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.

Built in

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.

Built 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.

Built in

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.

Built 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.

Built in

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.

Built in

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.

Built in

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 systems

Frequently 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.

Help Center

Need a quick answer while you read?

Run inventory, procurement, assets, sales, and field work with approved AWRA guidance for setup, migration, integrations, security, pricing, and support.

Search all approved AWRA public help articles.

Open Help Center