/projects/dietbox-b2c

← All projects

Dietbox B2C

One login for all of the product’s systems.

Senior Software Engineer, then Head of Technology · 2021–2024

Azure AD B2CIdentity Experience FrameworkXMLOpenID ConnectOAuth 2.0.NET 6HTMLCSSAzure DevOps
WebsitePrivate
Dietbox B2C screenshot

Overview

One account gets a person into every part of the product. The nutritionist signs in once and reaches her mobile app, the web product and the checkout with the same credentials; the patient signs in once and reaches the Android app, the iOS app and the web product. Five clients across three platforms, over two audiences that share nothing but the login — a nutritionist subscribing and paying, and a patient arriving by invitation from the one treating her. Three years of custom sign-in journeys, federated providers, silent migration off the legacy store, and session revocation that reaches every open browser.

What I did

This is the most of me there is anywhere in the Dietbox estate: I wrote half the commits over three years, across both audiences’ sign-in journeys.

  • The two policy sets — one for the practitioner, one for the patient — each its own sign-up, sign-in and password-reset journey.
  • Federation with Google, Facebook and Apple, each mapped through its own exchange profile into a common subject claim.
  • The first-sign-in migration that moves a legacy-store user into the directory during the same journey they log in with.
  • Session revocation: a stamp on the user compared against the token’s issue time, so a password change or an admin revoke signs the account out everywhere.
  • The custom sign-in pages, one set per audience, served and filled in at runtime.

The problem

A hosted login gives a product one journey. This one needed several: a subscriber signing up and paying, a patient arriving by invitation with no password to set, an academy student, and a receptionist acting on someone else’s behalf — all over one directory, without four separate user stores to keep in sync.

By the numbers

The line count is a plain line count over the committed policy files; the commit share comes from the repository.

~7.2klines of policy XMLacross two policy sets
~730commitsmine, of ~1.5k total

Architecture

Two independent policy sets sit above one directory, and everything downstream trusts the tokens they issue.

  1. ClientsTwo patient apps, the nutritionist’s app, the web product and the checkout — every one of them starts here.
  2. Practitioner policiesSign-up, sign-in, subscriber and academy journeys for the nutritionist audience.
  3. Patient policiesSign-up and sign-in for the patient audience, invited rather than self-registering.
  4. DirectoryOne user store beneath both policy sets, holding local and federated accounts alike.
  5. Auth serviceValidates the tokens this system issues. Reads and writes against the directory itself go through a shared gateway package that several services in the platform take a dependency on.
  6. Custom UI pagesStatic markup, one set per audience, served by the identity platform and filled in at runtime.

The sign-in journey

Every step below corresponds to a technical profile that exists in the policy — this is the orchestration as written, not a simplification of it.

  1. Sign-inLocal credentials, or a federated provider — Google, Facebook or Apple — exchanged into a common subject claim.
  2. Legacy checkIs this a legacy-store user who has not yet been migrated?
  3. MigrationIf so, the account is written into the directory with an alternative security identifier linking it back to the legacy credential — in the same journey as the sign-in, not a separate step.
  4. EntitlementIs the account enabled, and does it belong to a gated journey — subscriber, academy — that requires an active entitlement?
  5. TokenA token is issued, stamped with the time the user’s security record was last valid from.

What it does

  • Sign-up and sign-in for each audience, on its own policy and its own branded pages.
  • Two further gated sign-ins for the nutritionist on top of the ordinary one — subscribers, and academy students — each a policy of its own.
  • Password reset, password change and profile edit, each an entry point of its own, per audience.
  • A direct credential exchange for the native apps, alongside the browser redirect the web clients use — same directory, same rules, two shapes.
  • Refresh-token redemption as a journey of its own, one per entitlement, so a renewed token is re-checked rather than assumed still valid.
  • Federated sign-in with three providers, each exchanged into a common subject claim.
  • Silent migration off the legacy store during the user’s own sign-in journey.
  • Per-audience branded pages, one set for the practitioner and one for the patient.
  • Entitlement gates for subscriber and academy journeys, enforced inside the sign-in flow rather than after it.

Engineering decisions

  • Custom policies instead of a hosted login

    A hosted login gives one journey. This product needed a subscriber signing up and paying, a patient arriving by invitation, an academy student, and a receptionist — over one directory, without four user stores to keep in sync. Writing the policy directly was the only way to get gated journeys and a first-sign-in migration without forking the user base.

  • Migration as a side effect of signing in

    Nobody was asked to reset a password or re-register. The account is written into the directory and linked back to the legacy credential during the same sign-in the user was already doing.

  • Revocation that reaches open sessions

    Revoking an account has to end the sessions that are already open, not only stop the next token from being issued. The policy compares the token’s issue time against a stamp on the user record, so moving that stamp stops an open session working.

  • Two ways in, because a phone cannot open a redirect

    Three of the five clients are native apps, and a native app signing a user in through a browser redirect is a bad experience and a worse one to recover from. So the same directory answers two shapes of request: the redirect journey the web product and the checkout use, and a direct credential exchange the apps use, each with its own refresh-token redemption. The entitlement checks and the migration behaviour live in the policy, not in the client, so the two shapes cannot drift into two different sets of rules.

  • One directory, several journeys

    Separate policies per audience over one shared user store, rather than one policy branching on audience or several stores that would have to be reconciled. The audiences share an identity, not a form.