Designing an Identity System
59.0 What this chapter gives you#
- You will be able to list the eleven decisions that fix the shape of any identity system, put them in the order that makes each one cheapest to get right, and say what each one costs to reverse once the system is carrying real people.
- You will be able to choose the internal identifier for a person and defend the choice on three separate grounds - opacity, uniqueness and reassignment - naming the specific failure that each ground prevents.
- You will be able to set an identity proofing level using the risk process in NIST SP 800-63-4, published July 2025, name the evidence you will accept at each level, and write down in the same document who you have just excluded.
- You will be able to fill in an authenticator decision table for your own populations, matching the authentication assurance levels of SP 800-63B-4 to the risk of the transaction and to the devices people actually hold.
- You will be able to choose between a server-side session and a self-contained token with the revocation delay, the storage cost and the failure mode written down as numbers rather than as preferences.
- You will be able to pick an authorization model deliberately - roles, attributes or relationships - before the first conditional statement in the codebase picks one for you, and say where the decision point lives and who may change a policy.
- You will be able to run a build, buy or federate comparison with the real cost drivers on both sides, using published list prices as of August 2026 and an honest estimate of engineering effort marked as an estimate.
- You will be able to write a migration plan that moves an existing user base onto a new system without a mass password reset, including a plan for the long tail of accounts that never come back.
- You will be able to name the metrics an identity system must emit in order to be operable at all, give an alert threshold for each, and separate the ones that measure harm to the service from the ones that measure harm to users.
- You will be able to draw your own reference architecture with every trust boundary marked, and say for each boundary what crosses it, what is checked as it crosses, and what the system does when the check fails.
This book has spent fifty-eight chapters taking identity apart. It has looked at signatures and seals, at passwords and one-time codes, at certificates and assertions, at directories and federations, at law and at failure. This chapter puts the pieces back together, because at some point somebody has to build the thing, and building it means making a finite number of decisions in a particular order and living with them for a decade.
The claim of this chapter is that the number is small. An identity system - any identity system, for a bank or a hospital or a bus network or a game - is determined by eleven decisions. Everything else is implementation. That is a strong claim and it is meant to be. The eleven are not a checklist of good practices; they are the joints of the machine. Two systems that answer the eleven the same way will behave the same way in production even if one is written in Java and one in Go, and two systems that answer them differently will not, no matter how similar their login screens look.
The second claim is that the decisions are not equally reversible, and that most teams take them in exactly the wrong order. The decisions that are almost impossible to reverse - what a subject is, what names it, how permission is expressed - are usually made accidentally, in the first week, by whoever wrote the first database table. The decisions that are easy to reverse - which vendor, which multi-factor method, which session length - are argued about for months in meetings. This chapter reverses that. It spends its weight on the joints that weld shut.
A word about what this chapter is not. It is not a survey of failures; chapter 58 handles the breaches that taught the industry each control, and chapter 60 handles what breaks and what each failure costs in money and in harm. Where this chapter needs a failure to explain a decision, it gets one line and a pointer. This chapter is about the moment before any of that: the blank page, the eleven questions, and the discipline of answering them on purpose.
The plain version#
A club that has not opened yet#
Imagine a town has built a new sports and swimming centre. It is finished. The pool is full, the lights work, the doors lock. Nobody has joined yet, and nobody has been through the door. Before it opens, somebody has to sit down and decide how membership will work, and that person has more power over the next ten years of the building’s life than anyone will ever realize.
Here is the first thing they must decide, and it sounds trivial. What is a member. Is a member a person, or a household, or a company that buys twenty passes for its staff, or a child who comes with a parent, or a visitor from another town who wants one swim. Every one of those is somebody who will come through the door, and if the answer is “a member is a person”, then the family of four is four members and the company is twenty, and if the answer is “a member is an account that can pay”, then the family is one and the child does not exist in the records at all. There is no right answer. There is only an answer, and everything else in the building will be built on top of it.
The second decision is the number on the card. Every member gets a number. It seems like a small thing until you ask three questions about it. Does the number tell you anything about the member - the year they joined, the branch that signed them up, their date of birth encoded in the middle. Is there exactly one number per member, and exactly one member per number. And the third question, the one nobody asks: when a member leaves, does their number ever get given to somebody else.
That third question is the one that ruins clubs. Suppose the number is short and the club is tidy, so when a member leaves after ten years their number goes back in the pot and is issued to a new member the following spring. Now the old member’s locker is assigned to the new member. The old member’s swimming records appear in the new member’s file. A letter arrives about an unpaid bill from 2019 addressed to a person who joined last March. Every one of those is the same bug, and the bug was created on the day somebody decided that numbers could be reused, probably to save space in a filing cabinet.
The third decision is how much proof you ask for at the desk. This is not one decision but a family of them, because different members are asking for different things. A person who wants to swim once and pay cash needs to prove nothing at all; they hand over money and get a wristband. A person who wants a monthly membership paid by direct debit has to prove they control a bank account. A person who wants the half-price rate for people over sixty-five has to prove they are over sixty-five, and now the club is in the business of examining documents. A person who wants to be a swimming instructor and be alone with children has to prove a great deal more, in person, with their face compared to a photograph by a member of staff who has been trained to do it.
Notice that these are four different levels of proof for four different privileges in the same building, and that a club which sets one level for everybody will get it wrong in both directions. If it asks everyone for a passport, the person who wanted one swim goes to a different pool. If it asks nobody for anything, the half-price rate is claimed by people who are thirty.
The fourth decision is what the member carries to get in each day. A plastic card. A code they tap in. Their face, read by a camera at the turnstile. A key fob. A paper ticket. This is a different question from the third one, and confusing the two is the single most common mistake in the subject. Proving who you are when you join is one act, done once, carefully, by a human being. Getting in on a Tuesday morning is another act, done thousands of times, quickly, by a machine. The proof at the desk decides whose name is on the card. The thing in your pocket decides whether the door opens.
The fifth decision is the one everybody forgets, and it is what happens when a member loses the thing in their pocket. They come to the desk with no card, no fob and no phone, and they say they are Margaret Ellis and they would like to swim. What does the club do. Whatever the club does in that moment is the club’s real security, because a burglar will not attack the turnstile - the burglar will come to the desk and say they are Margaret Ellis. A club with an unbreakable turnstile and a receptionist who issues replacement cards to anyone who knows a member’s date of birth has the security of the receptionist, not the turnstile.
The day pass and the rooms#
The sixth decision is about time. When a member comes in on Tuesday, how long does that count for. Until they leave. Until midnight. For a fortnight, so they can come and go without showing anything. Until somebody at the desk cancels it.
There is a real trade here and it has two sides. If the pass lasts a long time, the turnstile is fast and the member is happy, because they wave once and it works all week. But if the club decides on Wednesday that a member must be barred, the pass in that member’s pocket still opens the door until it expires, and there is nothing the turnstile can do about it, because the turnstile is not checking with the desk. If instead the turnstile telephones the desk on every single entry, then a bar takes effect the instant it is entered, and the price is that the turnstile is now only as fast and as available as the telephone line to the desk.
Nobody escapes this trade. The only choices are how much delay you accept, and for which doors.
The seventh decision is about rooms. The building has a pool, a gym, a creche, a staff office, a plant room with the chemicals in it and a safe. Who is allowed in which. There are three honest ways to write this down and they lead to very different buildings.
You can write it by job. Members go in the pool and the gym. Instructors go everywhere except the safe. Managers go everywhere. This is tidy, it fits on one page, and it works beautifully until the day somebody needs to be an instructor on Tuesdays and a member on Saturdays, and you find yourself inventing a job called “instructor who is also a member” and then another and another until there are ninety jobs and nobody understands any of them.
You can write it by facts about the person and the room. Anyone who has a current first-aid certificate may enter the pool when it is unstaffed. Anyone over eighteen may use the gym. Anyone whose chemical training is less than two years old may enter the plant room. This bends more gracefully, and the price is that you now need a reliable answer to “is this person’s first-aid certificate current” at the moment the door is being opened, which means the door depends on a filing system.
Or you can write it by relationships. This child belongs to that parent, so the parent may see the child’s swimming records. This instructor is assigned to that class, so they may mark that class’s register and no other. This is the most expressive of the three and by far the hardest to compute, because answering “may this person open this door” now means walking a chain of relationships that may be long.
Most real buildings use all three, and the mistake is not using all three. The mistake is using all three by accident, with the roles in one system, the facts scattered through the code, and the relationships buried in whatever the developer wrote on the Thursday afternoon when the request came in.
Making the cards, buying them, or accepting somebody else’s#
The eighth and ninth decisions are about who does the work. Membership cards, turnstiles, the desk software - the club can build all of that itself, or it can pay a company that sells exactly this to leisure centres, or it can accept a card issued by somebody else entirely. That third option is worth pausing on, because it is the one people find strangest and it is increasingly the normal one. The club could simply say: if you can prove to us that you hold an account with the town library, that is enough for us. The library did the work of checking who you are. The club trusts the library.
Each of the three has a cost that shows up at a different time. If you build it, you pay in people, every month, for as long as the club exists, and you own every problem. If you buy it, you pay a fee that grows with the number of members, and on the day the supplier changes its prices or its terms you discover how much of your building is now shaped around their product. If you federate - if you accept the library’s card - you pay almost nothing and you have handed a stranger the power to lock out every member you have, on an afternoon when their system is down and there is nothing whatever you can do about it.
The tenth decision is about what you write down and keep. Every proof you accept leaves a residue. If a member shows a passport to claim the over-sixty-five rate, does the club keep a photograph of that passport, and for how long, and where. A club that keeps everything has built a small treasure chest that it did not intend to build and does not know how to guard. A club that keeps nothing cannot answer a complaint six months later.
The eleventh decision is the book by the door. What gets written down every time somebody comes in, goes out, is refused, or asks for a replacement card. This sounds like an afterthought and it is the difference between a system somebody can run and a system nobody can run. When the turnstile starts refusing one member in twenty at eight in the morning, the only question that matters is whether there is a record that lets you find out why, and that record either exists because somebody designed it or it does not exist at all.
The thing we will actually build#
We will carry one example through the whole chapter. Meridian Transit runs the buses, trams and suburban trains for a city of about three million people. It is not a real operator; it is a worked example, and every figure attached to it is either arithmetic you can check or a published number from a named source, and the invented figures are labelled as the example’s own so that nobody quotes them as a benchmark.
Meridian has 4.2 million registered passenger accounts and 11,400 staff. It is replacing a fare and account system built in 2009. The old system stores passwords as MD5 hashes with a per-user salt, uses the passenger’s email address as the primary key in every table, and has no concept of a role beyond a column called is_admin. Nothing about that is unusual. It is what a system built in 2009 by competent people looks like in 2026.
Of the 4.2 million accounts, about 3.10 million use the mobile app, about 0.74 million use only the website, and about 0.36 million never use either and are managed at a station counter or over the telephone. That last group matters more than its size suggests, because it contains most of the concession holders - the older passengers and disabled passengers who travel at reduced fares - and any design that assumes a smartphone has quietly decided to exclude them.
Of the 11,400 staff, 7,900 are drivers and station staff who share terminals, 2,600 are office staff with their own machines, 610 are engineers working from vehicles, and 290 hold powers that move money: refunds, fare adjustments and account merges. Those 290 are, from a security point of view, the entire staff population that matters, and they are two and a half per cent of it.
Where the plain version stops being true#
The eleven are not independent, and the order is not free#
The plain version presented eleven decisions as if they were eleven separate questions you could answer in any order. They are not. They form a graph, and several of the edges are one-way.
The identifier decision constrains the federation decision: if you have chosen to key your entire estate on email address, you cannot later accept assertions from an identity provider that refuses to release email, and there are several that refuse on privacy grounds. The proofing decision constrains the authorization decision: you cannot enforce a policy that says “only verified account holders may request a refund over fifty units” if you never recorded, per account, what was verified and when. The authorization decision constrains the session decision: if permissions can change in the middle of a working day, a token that carries permissions inside it and lives for an hour has just made your permission changes take up to an hour to bite.
The honest version: the eleven are a dependency graph, not a list, and the useful ordering is by reversibility. Decide first the things that are welded shut by the first row of production data - the subject model, the identifier, the shape of a permission. Decide last the things you can change on a Wednesday afternoon - session lengths, alert thresholds, which of two equally good authenticators you offer first.
An opaque identifier is not a private identifier#
The plain version said the membership number should not tell you anything about the member. That is right, and the reason usually given for it is wrong.
The reason usually given is privacy: if the number encodes the date of birth then anyone who sees the number learns the date of birth. True, and minor. The real reason is coupling. A number that encodes the joining branch will be parsed. Somebody will write code that reads characters three and four of the identifier to decide which regional database to query, and that code will be in production for eleven years, and on the day you close a branch or open a hundred, the identifier format becomes a migration project rather than a configuration change. Meaning inside an identifier is a promise about the future that you have no way to keep.
The honest version: opacity buys you the freedom to change your organization without changing your primary key. It does not buy you privacy. An opaque identifier that is stable, unique and shared with several parties is a perfect correlation handle - it is precisely the thing that lets two companies work out that their customer is the same person. If privacy across relying parties is the goal, opacity is not enough, and you need a different identifier per relying party. NIST SP 800-63C-4 calls these pairwise pseudonymous identifiers and describes them in section 3.4.1.
Proofing level is not a security dial#
The plain version treated proof at the desk as a dial you turn up when the risk is higher. That framing is standard and it is dangerously incomplete, because the dial has a second scale on the back of it, and the second scale is exclusion.
Every increase in required evidence removes people. Requiring a passport removes everyone who does not hold one. Requiring a driving licence removes people who do not drive, which correlates strongly with age, income, disability and city living. Requiring a document with a photograph removes people whose appearance has changed. Requiring a smartphone camera for a face comparison removes people without a smartphone, and the automated comparison then performs unevenly across faces in ways that have been measured and published. Requiring a fixed address removes people who do not have one, and the people who do not have one are frequently the people who most need the service.
The honest version: proofing level is a joint decision about fraud and exclusion, and you have not made the decision until you have written down both numbers. In the design document for Meridian we will not write “concession accounts require IAL2”. We will write “concession accounts require IAL2, which we estimate removes between three and six per cent of eligible applicants at the first attempt, and here is the assisted route for those applicants, staffed at these hours”. A proofing level without a named alternative route is a decision to exclude that has not been admitted.
The recovery path is the real assurance level#
The plain version said that a club with an unbreakable turnstile and a soft receptionist has the security of the receptionist. That is true and it is worth stating in its stronger form, because the stronger form is unpopular.
A system’s authentication assurance level is the minimum over every path to a live session, not the level of the path you are proud of. If you issue hardware security keys to 290 privileged staff, and your help desk can reset any of those staff members’ credentials after a telephone call in which the caller states their employee number and their manager’s name, then your privileged staff are protected by an employee number and a manager’s name. The keys are decoration. Chapter 58 covers the specific incidents that made this lesson expensive; here the point is only structural.
The honest version: the recovery path is not an operational detail to be worked out after launch by the support team. It is decision five of eleven, it belongs in the design, and the correct question is not “how do we help people who are locked out” but “what is the strongest thing this system will ever do, and does the recovery path match it”. Where it cannot match it - and sometimes it genuinely cannot, because a person really has lost everything - the honest design says so, and puts the reduced-assurance session behind reduced powers rather than pretending.
Self-contained tokens do not remove state, they relocate it#
The plain version described the trade between a fast turnstile and an instant bar. In the technical world this is the argument between a server-side session and a self-contained token, and the argument is usually conducted dishonestly by both sides.
The claim made for self-contained tokens is that they are stateless: the resource server can verify the signature and needs no database. This is true of the signature and false of everything else. Revocation is state. Token replay detection is state. Refresh token rotation, which is the thing that makes long-lived access survivable, is state - it requires the authorization server to remember which refresh token was issued last so that it can detect the reuse of an old one. Session binding to a device is state. What a self-contained token actually does is move the state from the request path to the issue path, and reduce the number of components that need to see it from all of them to one.
The honest version: you are not choosing between state and no state. You are choosing where the state lives and how stale a copy of it you will tolerate at the edge, and the number you are choosing is the maximum time between “this access should stop” and “this access stops”. Write that number down in seconds. If you cannot write it down, you have not made the decision.
Buying does not move the risk, it moves the work#
The plain version said that if you buy, you pay a fee and you shape your building around a supplier’s product. There is a further point that vendors do not volunteer and that buyers regularly get wrong.
Buying an identity platform moves engineering work. It does not move accountability, and under most data protection law it cannot: the organization that decides why and how personal data is processed remains responsible for it, whoever runs the servers. If your supplier has an outage, your users cannot log in and your users are angry with you. If your supplier is breached, your users’ data was breached, and your regulator will write to you. If your supplier changes its pricing model, your budget changes.
The honest version: build, buy and federate differ in who does the work, when you pay, and how hard it is to leave. They differ much less than people assume in who carries the risk. The exit cost is the number that should decide it, and the exit cost is almost never in the business case.
The reference architecture is a lie about time#
The last honest correction is about the diagram we will draw. Every architecture diagram in every identity project shows boxes and arrows in a steady state, as though all the boxes exist at once and always have. Real systems are two systems for a long time: the old one and the new one, running together, with data flowing both ways, for a period that is always longer than planned.
The honest version: a reference architecture is a picture of the destination and it is not a plan. The plan is the migration, and the migration is where the security holes are, because the migration is the only period during which two systems both believe they are authoritative about the same person.
The technical version#
The eleven decisions, written out#
Here is the full list, in reversibility order, with the artefact that welds each one shut. This ordering, and the framing of exactly eleven, is the author’s own, drawn from building and rebuilding these systems at KedByte; it is not taken from a standard and should not be cited as one. The individual decisions map onto the standards named against each.
| No. | Decision | Welded by | Reversal |
|---|---|---|---|
| 1 | Subject model | First schema | Very high |
| 2 | Identifier | First insert | Very high |
| 3 | Permission shape | First check | Very high |
| 4 | Data and retention | First store | High |
| 5 | Federation posture | Contracts | High |
| 6 | Sourcing | Procurement | High |
| 7 | Proofing level | Policy, law | Medium |
| 8 | Authenticators | Enrolment | Medium |
| 9 | Recovery path | Support desk | Medium |
| 10 | Session strategy | Client code | Medium |
| 11 | Observability | Log format | Low |
In words, so that the table is not the only record of them.
Decision one, the subject model, is what gets an identity at all. A human being. A staff member, who may also be a customer. A device. A service account. An organization. A delegated relationship, such as a carer acting for a passenger. Each of these is a subject type, and the mistake is to model only the first and then bolt the others on as flags.
Decision two, the identifier, is the string that names a subject internally, forever.
Decision three, the permission shape, is the grammar in which “may Ada do this” is written: role, attribute, relationship, or a mixture named on purpose.
Decision four, data and retention, is which attributes you hold, for how long, in which jurisdiction, and which of them you deliberately refuse to hold.
Decision five, federation posture, is who you accept assertions from and who accepts yours, and at which federation assurance level. SP 800-63C-4 defines FAL1 in section 2.3, FAL2 in section 2.4 and FAL3 in section 2.5. FAL2 requires that the assertion be audience-restricted to a single relying party and that the trust agreement be established in advance; FAL3 additionally requires the relying party to verify that the subscriber controls an authenticator bound to the assertion, not merely that the assertion arrived.
Decision six, sourcing, is build, buy or federate, taken per component rather than for the whole system.
Decision seven, the proofing level, is the identity assurance level per population and the evidence you will accept.
Decision eight, authenticators, is the authentication assurance level per transaction class and the specific authenticator types issued to each population.
Decision nine, the recovery path, is what a person with nothing does, and what powers they get back.
Decision ten, session strategy, is what a successful authentication produces, how long it lives, and the maximum delay between revocation and effect.
Decision eleven, observability, is what the system emits so that a human can operate it.
Decision two: the identifier#
Three properties, three failures.
Opacity means the identifier carries no meaning that can be parsed. The failure it prevents is coupling: code elsewhere that reads part of the identifier and therefore depends on the organizational fact it encodes. Every meaningful identifier eventually becomes a migration.
Uniqueness means one live subject has exactly one identifier in the namespace, and one identifier denotes exactly one subject. The failure it prevents is the merged record, in which two people’s data ends up in one account, and its mirror the split record, in which one person has two accounts and can see only half of their own history.
Non-reassignment means an identifier retired from one subject is never issued to another. OpenID Connect Core 1.0, in the version incorporating errata set 2 dated 15 December 2023, defines the sub claim in section 2 as “a locally unique and never reassigned identifier within the Issuer for the End-User”. That is a specification requirement, not a convention, and it exists because the failure it prevents is the worst one available: a new person inheriting an old person’s access.
Reassignment is not a theoretical worry. Yahoo announced on 13 June 2013 that it would free up user identifiers that had been inactive for over twelve months and reissue them, and the recorded consequence was exactly the predicted one - password reset messages for third-party services arriving at an address now controlled by a different human being. Telephone numbers are reassigned continuously as a matter of routine; the United States Federal Communications Commission adopted its Reassigned Numbers Database Order on 13 December 2018 and the database became operational for paid subscribers on 1 November 2021, precisely so that callers could find out whether a number had changed hands. By contrast, the United States Social Security Administration has never reused a number, which is why it works as a lookup key and, for the same reason, why it is such a poor secret.
Here is the identifier decision for Meridian, made explicitly.
| Candidate | Opaque | Reassigned | Verdict |
|---|---|---|---|
| Email address | No | Yes | Never key |
| Phone number | No | Yes | Never key |
| Sequential integer | No | Sometimes | No |
| Random UUIDv4 | Yes | No | Acceptable |
| UUIDv7 | Partly | No | Chosen |
| National number | No | No | Refused |
UUID version 7 is defined in RFC 9562, published May 2024, which obsoletes RFC 4122. Its layout is a 48-bit Unix millisecond timestamp in the most significant bits, then a 4-bit version field set to 0111, then twelve bits that may be a counter or random, then a 2-bit variant field, then sixty-two further bits of randomness or counter. The timestamp prefix makes newly minted identifiers sort in creation order, which matters a great deal to database index locality and therefore to insert cost at four million rows and beyond.
That timestamp is also a small, deliberate leak: a UUIDv7 tells an observer when the account was created, to the millisecond. For Meridian’s passenger accounts that is acceptable and is written down as accepted. For a system where account creation time is itself sensitive - a domestic abuse support service, a whistleblowing platform - it is not, and UUIDv4 is the right answer despite the index cost. The decision is contextual and the point is to take it consciously rather than to inherit whatever the framework generates.
The internal identifier is not the only one. A working system has several, and confusing them causes most identifier bugs. Meridian’s set:
{
"subject_id": "018f2c6e-9b31-7a42-93de-4d1c7a5e0b88",
"subject_type": "passenger",
"external_ids": {
"legacy_2009_pk": "a.okonkwo@example.net",
"fare_card_serial": "MTC-0044-819273",
"support_reference": "P-8F3K2Q"
},
"rp_pairwise": {
"cycle-hire-partner": "b7f4...c219",
"city-museum-partner": "3a91...ee07"
},
"login_hints": ["a.okonkwo@example.net", "+44...7714"],
"created_at": "2026-03-04T09:11:52Z",
"identifier_policy": "opaque-v7-never-reassigned"
}
Five distinct roles, and each has different rules. subject_id is the internal primary key: opaque, never reassigned, never shown to a user, never used in a URL that a user can see. external_ids are the keys of other systems, including the old one, held as a map so that a new one can be added without a schema change. rp_pairwise holds a different identifier per federation partner so that two partners cannot collude to link a passenger across services. login_hints are the human-facing strings a person types to begin a login - email addresses and phone numbers - and these are the ones that change, get reassigned and must be verified before use. support_reference is the short, pronounceable, deliberately non-secret string a person reads out on the telephone. It is not a credential and must never be treated as one.
The rule that makes this work is one line long and should be enforced by review: nothing except the identity service itself may join on a login hint. Every other table joins on subject_id. Meridian’s 2009 system violated this everywhere, which is why its migration is a project rather than a script.
Decision seven: proofing level and the evidence you accept#
NIST SP 800-63-4, published July 2025 as the replacement for Revision 3 of June 2017, sets out a five-step digital identity risk management process: define the online service in section 3.1, conduct the initial impact assessment in section 3.2, select initial assurance levels in section 3.3, tailor and document in section 3.4, and continuously evaluate and improve in section 3.5. The impact assessment scores each of a minimum set of categories as low, moderate or high: degradation of mission delivery, damage to trust or reputation, unauthorized release of sensitive information, financial loss or liability, and harm to human safety, health or the environment.
The structural feature of Revision 4 that matters most for design is that the three assurance levels are selected separately. There is no single overall level. You choose an identity assurance level for proofing, an authentication assurance level for the login, and a federation assurance level for assertions, and they need not match. A service can quite properly require strong authentication of a weakly proofed account - a pseudonymous account protected by a passkey is a coherent and common design.
SP 800-63A-4 describes the proofing levels. There is a no-proofing tier with no requirement to link the applicant to a real-life person. IAL1, in section 4.1, supports the real-world existence of a claimed identity with basic assurance; core attributes may be derived from evidence or self-asserted and validated against authoritative sources, and it is aimed at limiting highly scalable attacks. IAL2, in section 4.2, requires additional evidence and more rigorous validation and verification, and is aimed at scaled and targeted attacks including evidence falsification and social engineering. IAL3, in section 4.3, requires a trained representative of the credential service provider to interact directly with the applicant in an attended on-site session and requires collection of at least one biometric characteristic. Evidence itself is graded in three strengths - FAIR, STRONG and SUPERIOR - by how rigorous the issuing process was, what the evidence contains, and how it can be validated.
Meridian’s proofing decision, written as a table with the exclusion consequence beside it rather than in a separate document nobody reads.
| Tier | Population | Proofing | Excludes |
|---|---|---|---|
| Anonymous | Cash and tap | None | Nobody |
| Named | 4.2 m accounts | Self-asserted | Nobody |
| Concession | 610 k claims | IAL2 | 3 to 6 pc |
| Staff | 11,400 | IAL2 attended | Nobody |
| Privileged | 290 | IAL2 plus vetting | Nobody |
The exclusion percentages in that table are the example’s own estimate, not a published figure, and in a real design they would be replaced by measured first-attempt completion from a pilot.
The evidence list must be written down explicitly, because “acceptable identity documents” left to the discretion of an operations team drifts within months. Meridian’s concession tier accepts any one of: a national identity card or passport, machine-readable and cryptographically checkable, which is STRONG or SUPERIOR evidence; a driving licence validated against the issuing authority, which is STRONG; or, on the assisted route, a combination of a benefits award letter and a utility bill validated against the issuer, which is two pieces of FAIR evidence and is accepted at a station counter with a trained member of staff present. That third route exists precisely because the first two exclude people, and the design records that it is a deliberate reduction in evidence strength compensated by attendance.
Two honest notes. First, evidence validation is only as good as the authoritative source you check it against, and for several document types no online authoritative source exists, in which case what you are validating is the document’s security features and not the issuance. Second, the automated face comparison used in remote unattended proofing - which SP 800-63A-4 describes in section 2.1.3 as fully automated with no proofing agent involved - has measured performance differences across demographic groups, and a service that offers only that route has made an exclusion decision whether or not it has admitted it.
Decision eight: authenticators, by risk and by population#
SP 800-63B-4 defines three authentication assurance levels. AAL1 permits single-factor or multi-factor authentication using passwords, look-up secrets, out-of-band devices, single-factor one-time-password devices or cryptographic authenticators. AAL2 requires two distinct factors, either as a multi-factor authenticator or as a password or biometric combined with a physical authenticator, and requires that a phishing-resistant option be available. AAL3 requires public-key cryptographic authentication with private keys that cannot be exported from the authenticator, and requires phishing resistance.
The reauthentication limits are specific numbers and are worth having to hand, because they are the first constraint on the session decision.
| AAL | Overall limit | Inactivity limit |
|---|---|---|
| AAL1 | 30 days | None stated |
| AAL2 | 24 hours | 1 hour |
| AAL3 | 12 hours | 15 minutes |
The password rules in SP 800-63B-4 section 3.1.1.2 are also specific, and they overturn a generation of habit. A minimum of fifteen characters is required where a password is the only factor, with eight the floor when it is used as one factor of several. Verifiers should permit at least sixty-four characters. Composition rules - requiring a mixture of character types - shall not be imposed. Periodic forced change shall not be required, though a change shall be forced on evidence of compromise. OWASP ASVS states the same policy from the other direction: in version 5.0.0, released May 2025, requirement 6.2.1 asks that user-set passwords be at least eight characters with fifteen strongly recommended, 6.2.9 that at least sixty-four characters be permitted, and 6.2.5 that passwords of any composition be allowed with no rules limiting character types. Note the renumbering, because a great deal of internal documentation still points at the old chapters: authentication was chapter V2 and session management chapter V3 in ASVS 4.0.3 of October 2021, and in 5.0.0 they became V6 Authentication and V7 Session Management, with V8 Authorization, V9 Self-contained Tokens and V10 OAuth and OIDC split out alongside them.
Now the decision table. Its columns are the ones that actually determine the answer: how many people, what they hold, and what the worst transaction they can perform is worth.
| Population | Count | Primary | AAL |
|---|---|---|---|
| Rider, app | 3.10 m | Passkey | AAL2 |
| Rider, web only | 0.74 m | Password, TOTP | AAL2 |
| Rider, assisted | 0.36 m | Password, posted | AAL1 |
| Staff, shared till | 7,900 | Card and PIN | AAL2 |
| Staff, office | 2,600 | Passkey | AAL2 |
| Staff, mobile | 610 | Passkey on phone | AAL2 |
| Staff, privileged | 290 | Security key | AAL3 |
And the fallback and recovery row, which is the part that decides the real level.
| Population | Fallback | Recovery |
|---|---|---|
| Rider, app | Emailed code | Self-serve, 24 h hold |
| Rider, web only | Emailed code | Self-serve, 24 h hold |
| Rider, assisted | None | Station counter, ID |
| Staff, shared till | Supervisor card | Line manager, in person |
| Staff, office | TOTP | Line manager, in person |
| Staff, mobile | TOTP | Line manager, in person |
| Staff, privileged | Second key | Two managers, in person |
Two design choices in that table deserve their reasons. The privileged 290 are issued two hardware security keys at enrolment, not one, and the second is the entire recovery path. There is no help desk route to reset a privileged credential, because a help desk route would define the assurance level of the whole tier downwards, and 290 people is a small enough number that issuing a spare key each is cheap. The shared-till population uses a physical card plus a PIN rather than a passkey because 7,900 people share about 1,400 terminals across shift patterns, and a per-device credential model does not fit a per-shift human model. That is a real constraint, and pretending otherwise produces a design that gets defeated by staff writing PINs on the terminal.
On the evidence for these choices. Google, with New York University and the University of California San Diego, published results on 17 May 2019 showing that on-device prompts blocked 100 per cent of automated bot attacks, 99 per cent of bulk phishing and 90 per cent of targeted attacks, while codes sent by SMS to a recovery phone blocked 100 per cent, 96 per cent and 76 per cent respectively; the same research reported that no user who exclusively used security keys fell victim to targeted phishing during the investigation. A Microsoft study published in May 2023 reported that multi-factor authentication reduced the risk of compromise by 99.22 per cent across the whole population studied and by 98.56 per cent in cases where credentials had already leaked. Those are established measurements from named organizations, and they say something narrow and useful: the gap between no second factor and any second factor is enormous, and the gap between a phishable second factor and a phishing-resistant one is real but smaller. A design that gets 3.1 million people onto passkeys and leaves 0.36 million on a password with a posted code has done far more good than a design that insists on hardware keys and is therefore not adopted.
Note the standing of each claim. The numbers above are established fact from published research. The proposition that passkeys will eliminate passwords is a marketing claim. The proposition that syncable passkeys are appropriate at AAL2 is a standards position taken in SP 800-63B-4, and it is one on which practitioners genuinely disagree, because a passkey that syncs through a cloud account inherits the security of that cloud account. Both sides of that argument are reasonable: one says the sync provider’s security is better than the average user’s device backup discipline, the other says it introduces a dependency that the relying party cannot inspect.
Decisions ten: sessions and tokens#
A successful authentication produces something. What it produces is a design decision with three numbers attached: the maximum revocation delay, the per-request cost, and the blast radius if the artefact leaks.
| Strategy | Revoke lag | Per-request cost |
|---|---|---|
| Server session | Under 1 s | 1 store read |
| JWT, 15 min | Up to 15 min | Signature only |
| JWT, 60 min | Up to 60 min | Signature only |
| JWT, introspected | Under 1 s | 1 network call |
| JWT plus deny list | Seconds | 1 cache read |
The honest framing is that the middle two rows are the same architecture with a different number in it, and the number is the whole decision. Fifteen minutes of stale authorization is acceptable for reading a timetable and unacceptable for a refund queue.
Meridian’s answer, stated as a policy rather than a preference:
Passenger web and app
artefact : opaque session ref in a cookie
cookie : Secure; HttpOnly; SameSite=Lax; Path=/
entropy : 128 bits from a CSPRNG
idle timeout : 30 minutes
absolute : 12 hours, then silent re-auth via passkey
revoke lag : under 1 second (store read per request)
Internal service to service
artefact : JWT access token, RFC 9068 profile
lifetime : 300 seconds
audience : one resource server, exact match
binding : DPoP, RFC 9449
revoke lag : up to 300 seconds, accepted and recorded
Refund and fare-adjust API (the 290)
artefact : JWT plus introspection, RFC 7662
lifetime : 300 seconds
revoke lag : under 1 second
extra : step-up to AAL3 within the last 15 minutes
The SameSite=Lax choice is worth one line of justification because it is the kind of thing that gets flipped later by someone who does not know why it was set. Lax blocks the cookie on cross-site sub-requests but allows it on top-level navigations, which is what a passenger following a link from an email needs. Strict would break that flow; None would remove a cross-site request forgery defence and require compensating controls.
OWASP ASVS 5.0 does not prescribe timeout values, and that is deliberate. Requirement 7.3.1 asks you to verify that an inactivity timeout exists such that re-authentication is enforced according to risk analysis and documented security decisions, and 7.3.2 asks the same of an absolute maximum session lifetime. The standard’s demand is that the number be chosen and written down, not that it be any particular number. Requirement 7.1.2 asks that documentation define how many concurrent sessions are allowed per account and what happens when the limit is reached - a question most systems answer by accident, with the answer “unlimited, and nothing”.
The token rules come from RFC 9700, “Best Current Practice for OAuth 2.0 Security”, published January 2025 as BCP 240. Its load-bearing requirements: public clients must use PKCE and confidential clients should; redirect URIs must be validated by exact string matching, with a narrow exception for loopback ports in native applications; the implicit grant and any response type that issues an access token from the authorization endpoint should not be used; the resource owner password credentials grant must not be used; and authorization and resource servers should use sender-constraining mechanisms such as mutual TLS or DPoP so that a stolen token is not usable by the thief. As of August 2026 the consolidated OAuth 2.1 document is still an Internet-Draft - revision 15 is dated 2 March 2026 and the working group milestone targets submission to the IESG in December 2026 - so the correct citation for a design document today is RFC 6749 as amended by BCP 240, not “OAuth 2.1”.
Refresh token rotation deserves its own sentence because it is the mechanism that makes long-lived access survivable. Each use of a refresh token issues a new one and invalidates the old; presentation of an already-used refresh token is treated as evidence of theft and revokes the entire chain. This requires the authorization server to keep state, which returns us to the honest point made earlier: the stateless token is a stateless verifier attached to a stateful issuer.
Decision three: the authorization model#
Authorization is the decision that codebases make for themselves if designers do not, and the mechanism is always the same. A requirement arrives on a Thursday: managers should be able to see refunds. Someone writes a conditional that tests a boolean column. Six months later there are four hundred such conditionals in eleven services, no two of them consistent, and there is no possible answer to the question “who can see refunds” other than reading all four hundred.
The theory is old and settled. Butler Lampson’s paper “Protection”, presented at the fifth Princeton Conference on Information Sciences and Systems in 1971 and reprinted in ACM Operating Systems Review in January 1974, gave the access matrix: subjects down the side, objects along the top, permitted operations in the cells. Every model since is a way of not storing that matrix, because the matrix is enormous and mostly empty. Jerome Saltzer and Michael Schroeder’s “The Protection of Information in Computer Systems”, in the Proceedings of the IEEE in September 1975, gave the design principles that still decide whether an authorization system is any good - least privilege, fail-safe defaults, economy of mechanism, complete mediation, open design, separation of privilege, least common mechanism, and psychological acceptability. Complete mediation is the one that identity systems break most often: every access to every object must be checked, and a system with a cached decision or an unchecked internal path has failed it.
Three practical grammars, with the honest boundary of each.
| Model | Fits when | Breaks when |
|---|---|---|
| Roles | Jobs are stable | Exceptions multiply |
| Attributes | Rules are general | Attributes go stale |
| Relationships | Data is a graph | Depth is unbounded |
Role-based access control was formalized by David Ferraiolo and Rick Kuhn at the 15th National Computer Security Conference in October 1992, standardized as ANSI/INCITS 359-2004 adopted on 11 February 2004, and revised as INCITS 359-2012. It fits a workforce with named jobs. Its characteristic failure is role explosion: each exception becomes a new role, and the count grows until the model is less comprehensible than the four hundred conditionals it replaced.
Attribute-based access control is described in NIST SP 800-162, “Guide to Attribute Based Access Control (ABAC) Definition and Considerations”, published January 2014 and last updated 2 August 2019. Its definition is worth quoting because it names all four inputs: authorization is determined by evaluating attributes of the subject, the object, the requested operations and, in some cases, environment conditions, against policy or rules. Its characteristic failure is attribute staleness - the door depends on a filing system, and when the filing system is eleven minutes behind, so is the door.
Relationship-based access control is what you need when the question is about a graph. Google’s Zanzibar paper, “Zanzibar: Google’s Consistent, Global Authorization System” by Ruoming Pang and colleagues at the 2019 USENIX Annual Technical Conference in July 2019, is the reference description: trillions of access control lists, millions of authorization requests per second, 95th-percentile latency under ten milliseconds and availability above 99.999 per cent over three years of production use. Those figures are Google’s published numbers for Google’s system and are not a target for anyone else; what transfers is the model, not the scale.
Where the decision is made is a second, separate choice, and the vocabulary comes from XACML. The eXtensible Access Control Markup Language version 3.0 became an OASIS Standard on 22 January 2013 and defines the policy enforcement point, which performs access control by making decision requests and enforcing decisions; the policy decision point, which evaluates applicable policy and renders a decision; the policy administration point, which creates policy; and the policy information point, which supplies attribute values. XACML’s XML syntax has largely fallen out of use; its architecture has not, and those four names are the standard vocabulary for describing any authorization deployment, including ones built on entirely modern tooling.
As of August 2026 there is a current interoperability standard for the wire format between enforcement and decision points: the OpenID Foundation’s AuthZEN working group published Authorization API 1.0 as a final specification on 11 January 2026. An access evaluation request carries a subject, an action, a resource and an optional context; the response carries a boolean decision and an optional context that can convey reasons, obligations, or a demand for step-up authentication. Meridian’s refund check, in that shape:
{
"subject": {
"type": "staff",
"id": "018f2c6e-9b31-7a42-93de-4d1c7a5e0b88",
"properties": {
"aal": "AAL3",
"aal_age_seconds": 214,
"depot": "north"
}
},
"action": { "name": "refund.issue" },
"resource": {
"type": "fare_transaction",
"id": "FT-2026-08-17-0099421",
"properties": { "amount_minor": 4250 }
},
"context": { "channel": "back_office", "ip_asn": 8075 }
}
And the answer, showing the pattern that makes step-up work properly - the decision point does not merely deny, it says what would change the answer:
{
"decision": false,
"context": {
"reason": "amount over depot limit",
"policy": "refund-limits@2026-07-01",
"required": {
"approval": "second_staff_subject",
"aal": "AAL3",
"aal_max_age_seconds": 300
}
}
}
Meridian’s answer to decision three is a deliberate mixture, written down as such: roles for the 11,400 staff, because jobs on a transport network genuinely are stable and rostered; attributes for the environmental conditions, because “during your rostered shift, from a depot address” is an attribute rule and not a role; and relationships for passenger data, because “may this carer see this passenger’s journey history” is a graph question and nothing else. What makes the mixture safe rather than chaotic is that it is one policy language evaluated at one decision point, not three systems.
Decision six: build, buy or federate#
The comparison must be run per component, not for the whole system, and the honest version has four columns because there are four cost families and they arrive at different times.
| Driver | Build | Buy | Federate |
|---|---|---|---|
| Year one effort | Highest | Medium | Lowest |
| Ongoing licence | None | Per user | None |
| Ongoing staff | 2 to 4 FTE | 1 FTE | Under 1 FTE |
| Exit cost | None | Very high | Medium |
Now the real numbers, with their provenance marked, because this is the section where books usually go vague.
Published list prices, as of August 2026. Microsoft Entra ID P1 is listed at 7.00 US dollars per user per month on an annual commitment and Entra ID P2 at 10.00 US dollars, with the Entra Suite at 12.00; Entra External ID core features are free for the first 50,000 monthly active users, with pricing above that not published. Auth0 lists a free tier up to 25,000 monthly active users, an Essentials plan at 35 US dollars a month and a Professional plan at 240 US dollars a month, both quoted for 500 monthly active users, with larger volumes described as custom tiers.
Apply those to Meridian’s workforce, which is the straightforward half. Put 11,110 staff on P1 and the 290 privileged staff on P2:
11,110 x 7.00 x 12 = 933,240 USD per year
290 x 10.00 x 12 = 34,800 USD per year
-----------
workforce licence 968,040 USD per year
Just under one million US dollars a year, at list, before any negotiated discount, for a workforce of 11,400. That number is checkable arithmetic on published prices and it is the single most useful thing to put in front of a decision meeting, because it converts an architectural argument into a budget line.
Now the passenger half, and here the honest answer is that you cannot compute it from published prices at all. Meridian has roughly 2.6 million monthly active passenger accounts. Both vendors publish prices only up to a threshold far below that and refer larger volumes to sales. That is itself a cost driver and belongs in the comparison: at consumer scale, the price of the buy option is not knowable without entering a negotiation, it is not stable across renewals, and it is frequently structured so that the thing you are most trying to encourage - more people logging in - is the thing that increases the bill.
The build side. The following is the author’s own operating experience at KedByte and is offered as experience, not as a benchmark: a first production-quality consumer identity service - enrolment, login, recovery, session management, an administrative console, an audit trail and the operational tooling to run all of it - takes a team of five to seven engineers between nine and fifteen months, and the steady state afterwards never drops below two full-time engineers, because the specifications move, the client platforms move, and the abuse moves. Anyone who tells you it is a two-month project has either built it before with the same team or has not yet met account recovery.
The federate side has a cost that is not money. If Meridian accepts a national electronic identity for concession proofing, it removes an entire proofing operation, and it acquires a dependency it cannot fix. When the national service is unavailable, concession enrolment stops, and Meridian’s status page will say so while its passengers blame Meridian. The correct design response is not to avoid federation but to require a fallback route that does not depend on the partner, which means the assisted station-counter route is not merely an inclusion measure but also the continuity plan. Two problems, one solution, which is the sign of a decision taken properly.
Meridian’s sourcing decision, per component: buy the workforce directory and workforce single sign-on, because it is a solved commodity and the staff estate is already on commodity productivity software; build the passenger identity service, because it is at consumer scale where the buy price is unpredictable and because passenger identity is entangled with fares, concessions and ticketing in ways that no product models; federate concession proofing to the national scheme with an assisted fallback; and buy the proofing vendor rather than building document inspection, because building document inspection well is a different company.
Migration: 4.2 million people, no mass reset#
A mass password reset is the design of last resort. It is expensive - every reset is a support contact waiting to happen - and it is dangerous, because it trains an entire user base to expect unsolicited messages telling them to change their password, which is the exact shape of the attack you spend the rest of your life defending against.
The alternative is to migrate credentials lazily, on successful login, and to accept a long tail. Meridian’s old system stores MD5 with a per-user salt, which is unacceptable by any modern standard. The technique is to wrap rather than to break:
Legacy record
h_old = MD5(salt1 || password)
Wrap, offline, without ever seeing a password
h_wrap = Argon2id(salt2, h_old, m=19456, t=2, p=1)
store: {v:2, salt1, salt2, h_wrap}
delete: h_old
On a successful login with password P
inner = MD5(salt1 || P)
verify Argon2id(salt2, inner) == h_wrap
if ok:
h_new = Argon2id(salt3, P, m=19456, t=2, p=1)
store: {v:3, salt3, h_new}
delete: salt1, salt2, h_wrap
The Argon2id parameters there are the OWASP Password Storage Cheat Sheet’s stated minimum configuration - 19 MiB of memory, two iterations, one degree of parallelism - and the same cheat sheet’s guidance on legacy upgrades is exactly this pattern: when the user enters their password, that input is re-hashed using the new algorithm.
Two honest warnings about the wrap, because it is more dangerous than it looks. First, layering a modern hash over a weak one does not undo the weak one if the weak one has already leaked. The attack is called password shucking: given a leaked MD5 list, an attacker cracks the cheap MD5 first and then tests only the surviving candidates against the expensive outer layer, which collapses the cost of the outer layer to almost nothing. If there is any possibility that the 2009 hash set has ever been exposed, the wrap buys you time and nothing more, and the migration must be treated as urgent rather than gradual. Second, OWASP warns specifically that pre-hashing before bcrypt is hazardous because of null bytes in the hash output and because of shucking, and recommends a keyed construction with a separately stored pepper if pre-hashing is unavoidable. Argon2id does not share bcrypt’s 72-byte input truncation, which is why the scheme above uses it, but the shucking argument applies to any wrap.
The migration also has to move the identifier, which is the harder half. Meridian’s old system keys on email address. The new one keys on subject_id. The sequence that avoids a flag day:
| Phase | Weeks | What happens |
|---|---|---|
| 0 Prepare | 1 to 4 | Mint subject_id for all |
| 1 Shadow | 5 to 10 | New service reads only |
| 2 Dual write | 11 to 16 | Both systems written |
| 3 Cut reads | 17 to 20 | New service authoritative |
| 4 Cut writes | 21 to 24 | Old system read-only |
| 5 Tail | 25 to 78 | Lazy rehash continues |
| 6 Retire | 79 | Old credentials deleted |
Phase 0 mints a subject_id for every existing account and writes the old email into external_ids.legacy_2009_pk, so that both keys resolve from day one. Phase 1 runs the new service in shadow: it answers no traffic, but every login through the old system is replayed against it and the answers compared, which finds the disagreements before any user does. Phase 2 writes both. Phase 3 flips reads. Phase 4 makes the old system read-only. Phase 5 is the long tail.
The tail is where the plan must be honest. In this example, the lazy rehash reaches 18 per cent of accounts in the first week, 41 per cent by week four, 63 per cent by week twelve, 74 per cent by week twenty-six and 81 per cent by week fifty-two, leaving roughly 800,000 accounts that have never logged in again. Those figures are the example’s own, chosen to be realistic in shape rather than quoted from a study, and every real migration should replace them with its own measurements from the first fortnight, because the shape of the curve is knowable early and the tail size is the number that decides the endgame.
What happens to the 800,000 is a decision, not an accident, and there are only three honest options. Delete the credentials and require those accounts to go through recovery when they next appear, which is safe and produces a support spike concentrated on people who have been away a long time. Keep the wrapped hashes indefinitely, which keeps a weak hash set alive forever and is the option most organizations choose by not choosing. Or set a date, notify, and delete on that date, accepting that most of the notifications will go to abandoned mailboxes. Meridian’s decision is the third, with the deletion date at week seventy-nine, and the account itself preserved so that a returning passenger recovers rather than re-registers and loses their travel history.
One more migration rule, learned expensively and offered as experience rather than as a standard: never migrate the recovery path and the primary authenticator in the same week. If passwords move in one release and the password reset flow moves in another, a failure in either is diagnosable. If they move together, and a user reports being unable to log in or to reset, no one can tell which system is wrong.
Decision eleven: observability#
An identity system that cannot be observed cannot be operated, and most of them emit either almost nothing or an undifferentiated stream of “user logged in” lines that answers no question anyone actually asks at three in the morning.
The organizing idea is that identity telemetry splits in two, and the second half is the half that gets left out. Some metrics measure harm to the service: attacks, abuse, load. Some measure harm to users: people who could not get in, people who gave up, people who were wrongly refused. A dashboard with only the first half will show a healthy green system on a morning when tens of thousands of legitimate passengers cannot buy a ticket, because from the service’s point of view refusing them is working correctly.
| Metric | Split by | Alert when |
|---|---|---|
| Auth success rate | Method, population | Drops 2 pc in 15 min |
| Auth attempt rate | Method, ASN | Trebles in 5 min |
| Enrolment completion | Step, population | Any step drops 5 pc |
| Recovery rate | Population | Doubles week on week |
| Recovery duration p50 | Population | Over 10 minutes |
| Step-up pass rate | Reason | Under 85 pc |
| Revocation lag p99 | Artefact type | Over stated budget |
| Refresh reuse events | Client | Any, ever |
| Authz decision p99 | Policy | Over 25 ms |
| Authz deny rate | Policy, action | Changes 1 pc in a day |
| Directory replica lag | Region | Over 5 seconds |
| Locked-account count | Population | Rises 3x in an hour |
Several of those thresholds are the example’s own operating choices rather than published standards; the discipline is that each metric has a written threshold at all, because a metric with no threshold is a graph nobody looks at.
Three of those rows carry most of the value. Authentication success rate split by population is the single best early warning available, because almost every identity incident - a bad deploy, an expired certificate, a partner outage, a botched policy change - shows up first as one population’s success rate falling while the others hold steady. Revocation lag at the 99th percentile is the measurement of the promise made in the session decision; if you wrote down “under one second” and never measured it, you wrote down a wish. And refresh token reuse is the only metric in the list whose correct threshold is zero: a reused refresh token means either a bug or a theft, and both need a human.
The log record matters as much as the metrics, and the rule for its contents is short. Enough to reconstruct a decision, never enough to repeat it.
event : authn.completed
ts : 2026-08-17T07:41:09.213Z
subject_id : 018f2c6e-9b31-7a42-93de-4d1c7a5e0b88
subject_type : passenger
population : rider_app
method : webauthn.platform
aal_achieved : AAL2
phishing_res : true
credential_id : sha256:6b1f...9ac4 (hash, not the id)
session_ref : sha256:41d0...77be (hash, not the ref)
client_id : mtc-passenger-ios
ip : 203.0.113.0/24 (truncated)
asn : 8075
device_new : false
risk_score : 12
outcome : success
policy_ver : authn-policy@2026-06-14
duration_ms : 640
The choices there are all deliberate. The credential identifier and session reference are stored as hashes so that the log can be joined against live records without the log itself becoming a set of usable credentials. The IP address is truncated because the operational question is nearly always “which network” rather than “which house”, and truncation reduces what a log leak costs. The policy version is present so that when the deny rate moves you can tell whether the world changed or you did. The risk_score is recorded, not just acted upon, because a score you never store is a decision you can never audit or improve.
OWASP ASVS 5.0 collects logging requirements in chapter V16, Security Logging and Error Handling, and the governing principle across them is the one above: log the fact and the context, never the secret. A log that records a submitted password, a one-time code, a session token or a full assertion has converted a monitoring system into a credential store, and monitoring systems are read by more people, retained for longer, and shipped to more third parties than any database.
Finally, service level objectives. The vocabulary comes from Google’s Site Reliability Engineering book, published in 2016, and the useful part is the error budget: you set a target, the gap between the target and one hundred per cent is a budget you are allowed to spend, and when it is spent you stop shipping features and fix reliability. For identity the interesting subtlety is that the objective must be written on the user-visible thing. “The authentication service was available 99.98 per cent of the time” is compatible with a morning in which every one of 0.36 million assisted-route passengers was locked out. “99.9 per cent of legitimate authentication attempts by each named population succeeded within three seconds” is a promise about people.
The reference architecture, with the boundaries marked#
A diagram earns its place only if it marks the boundaries, because the boundaries are the design. Everything else is deployment.
ZONE 0 UNTRUSTED - hardware we do not control
+------------------------------------------------------+
| browser | mobile app | ticket machine | staff till |
+------------------------------------------------------+
| | | |
+------------+------+------+------------+
|
== T1 == public network boundary
|
ZONE 1 EDGE - ours, reachable by everyone
+------------------------------------------------------+
| TLS terminate | WAF | rate limit | bot score | CDN |
+------------------------------------------------------+
|
== T2 == edge to application
|
ZONE 2 APPLICATION - relying parties
+---------------------------+ +-----------------------+
| passenger fare + account | | staff back office |
+---------------------------+ +-----------------------+
| ^ | ^
| | == T3 == OIDC boundary
v | v |
ZONE 3 IDENTITY - the authority
+------------------------------------------------------+
| authn service | session store | authorization server |
+------------------------------------------------------+
| | | |
== T4 == == T5 == == T6 == == T7 ==
| | | |
+-----------+ +-------------+ +-----------+ +---------+
| credential| | subject | | policy | | audit |
| store | | directory | | decision | | sink |
| (HSM key) | | | | point | | (WORM) |
+-----------+ +-------------+ +-----------+ +---------+
|
== T8 == external federation
|
ZONE 4 OUTSIDE PARTIES - contracts, not code
+------------------------------------------------------+
| national eID | proofing vendor | SMS gw | partner RP |
+------------------------------------------------------+
Eight boundaries, and for each one the same three questions: what crosses, what is checked, what happens when the check fails.
T1, the public network boundary. What crosses: everything a user sends. What is checked: TLS 1.3 with HSTS, request shape, rate limits per network and per account, and a bot score. What happens on failure: refuse with a generic response that does not reveal whether the account exists, and count it. The critical design rule at T1 is that nothing beyond it may assume the client is honest, which sounds obvious and is violated every time a client sends a field the server trusts, such as a device identifier or a claimed risk level.
T2, edge to application. What crosses: HTTP requests that survived the edge. What is checked: mutual TLS between edge and application, and the absence of any public route that bypasses the edge. What happens on failure: the request is dropped and an alert fires, because a request arriving at the application without an edge marker means either a misconfiguration or someone inside the perimeter.
T3, the OIDC boundary between relying parties and the identity provider. What crosses: authorization codes, identity tokens, access tokens. What is checked: exact redirect URI matching, PKCE, audience restriction to a single relying party, issuer, signature, expiry and nonce. What happens on failure: the authentication is abandoned and logged with the specific failed check, not a generic error, because “which check failed” is the difference between a five-minute diagnosis and a five-hour one. This is the boundary at which the federation assurance level is realized: audience restriction to a single relying party is the FAL2 requirement in SP 800-63C-4 section 2.4.
T4, the credential store boundary. What crosses: verification requests only. What is checked: that the caller is the authentication service, that the operation is verify rather than read, and that the signing and wrapping keys never leave the hardware security module. What happens on failure: refuse and page a human. The rule at T4 is the strictest in the architecture: no service other than the authentication service ever reads this store, no administrator ever reads a credential, and there is no export path. If there is a support tool that displays a credential, T4 does not exist.
T5, the directory boundary. What crosses: subject records and attributes. What is checked: authorization per attribute, not per record, because “may this service read this subject” is the wrong granularity - a fare engine needs a concession flag and has no business reading a date of birth. What happens on failure: the attribute is omitted rather than the request refused, and the omission is logged, so that over-broad callers are discovered by their logs rather than by their outages.
T6, the policy decision boundary. What crosses: access evaluation requests and decisions, in the AuthZEN shape shown earlier. What is checked: the identity of the calling enforcement point, the policy version in effect, and the freshness of any attributes used. What happens on failure: deny. This is Saltzer and Schroeder’s fail-safe defaults principle, and the tempting alternative - allow on timeout, so that a policy outage does not stop the business - converts an availability incident into a security incident, which is a bad trade in every direction.
T7, the audit boundary. What crosses: log records, one way. What is checked: that the sink is append-only and that the identity system holds no delete permission on it. What happens on failure: buffer, alert, and never silently discard. The one-way property is what makes the audit trail evidence rather than an opinion; a log that the compromised system can edit tells you only what the attacker wanted you to know.
T8, the external federation boundary. What crosses: assertions from national schemes and proofing vendors, and messages to gateways. What is checked: signatures against a pinned key set, issuer, freshness, and the specific claims you agreed to receive - and nothing else is stored, because an assertion that arrives with fourteen attributes when you contracted for three has just handed you a data protection problem. What happens on failure: fall back to the assisted route rather than fail the user, which is only possible because that route was designed in.
There is a ninth boundary that does not appear on the diagram because it does not appear in any diagram, and it is the one that gets crossed. It is the administrative boundary: the console, the runbook, the database client on an engineer’s machine, the support tool that can act as a passenger. Every system with the eight boundaries above and an unguarded ninth has the security of the ninth. The design rule is that administrative access is a subject like any other - it is proofed, it is authenticated at AAL3, its sessions are short, its actions cross T7 into the audit sink, and impersonation of a user, if it exists at all, requires that user’s live consent and is logged as impersonation rather than as the user.
Writing the decisions down#
The last piece of design work is the artefact, and there is a standard one to use. SP 800-63-4 section 3.4 requires that the tailoring step be recorded in a Digital Identity Acceptance Statement. That name is worth adopting even outside a United States federal context, because it is a document with a defined job: it states the assurance levels chosen, the impact assessment that led to them, any tailoring away from the baseline, the compensating controls, and the residual risk that has been accepted and by whom.
DIGITAL IDENTITY ACCEPTANCE STATEMENT
service : Meridian passenger account
version : 3.0
date : 2026-08-17
owner : Director of Customer Systems
impact assessment (SP 800-63-4 sec 3.2)
mission : moderate
reputation : moderate
info release : moderate
financial : moderate
safety : low
selected levels (sec 3.3)
IAL : 1 general, 2 for concession claims
AAL : 2 all accounts, 3 for refund powers
FAL : 2 for the national eID connection
tailoring (sec 3.4)
assisted enrolment at station counters is
accepted at reduced evidence strength with
attendance as the compensating control
accepted residual risk
posted-code recovery for 0.36 m assisted
accounts is below AAL2; refund and payee
change are blocked on such sessions
review : 2027-02-17 or on incident
Two features of that document do the work. It records the tailoring, which means the place where you departed from the baseline is written down with its reason rather than discovered later as a mystery. And it names an accepted residual risk with a compensating control, which is the difference between a considered decision and an oversight. The reduced-assurance recovery path for assisted accounts is not hidden; it is stated, and the powers available on such a session are reduced to match.
Alongside it, keep an architecture decision record per decision - a page each for the eleven, written when the decision is made, saying what was chosen, what was rejected, and what would have to change for the decision to be revisited. The last of those three is the one that pays. A record that says “we chose UUIDv7 and would revisit if account creation time became sensitive” lets a future engineer reopen the question correctly. A record that says only “we chose UUIDv7” is a fact, and facts without reasons get overturned by whoever feels strongest in the meeting.
59.98 Common wrong ideas#
Wrong: The first decision is which identity vendor to use. Right: Sourcing is the sixth of the eleven decisions in reversibility order, and the four above it - subject model, identifier, permission shape, and data retention - are welded shut by the first production data, whereas a vendor can be changed, painfully but possibly, at any time. Choosing the vendor first means letting the vendor’s data model make your four irreversible decisions for you.
Wrong: An email address is a perfectly good primary key, since everyone has one and they are unique. Right: Email addresses are user-editable, are reassigned by providers, are frequently shared within households, are sometimes lost entirely, and are the exact string a person types when trying to log in, which makes them a login hint rather than an identity. Key on an opaque never-reassigned identifier and treat email as a verified, changeable attribute pointing at it.
Wrong: Higher identity proofing is always the safer choice, so ask for the strongest evidence you can. Right: Proofing level is simultaneously a fraud control and an exclusion control, and raising it removes real eligible people in numbers that can be measured. The safe choice is the level justified by the documented impact assessment, plus a named alternative route for the people that level excludes, with both the level and the exclusion estimate written in the same document.
Wrong: Using self-contained tokens makes the system stateless and removes the need for a session store. Right: It makes verification stateless and leaves issuance, revocation, refresh rotation and replay detection stateful; what you have chosen is not the absence of state but a maximum staleness at the edge. The design is only complete when you have written down the maximum number of seconds between a revocation and its taking effect, and measured it in production.
Wrong: Buying an identity platform transfers the risk to the supplier. Right: It transfers engineering work and some operational burden. Accountability to users and to regulators stays with the organization that decided to process the data, outages are still your outages in the eyes of your users, and the number that should decide the purchase - the cost of leaving in three years - is almost never in the business case.
Wrong: Migrating to modern password hashing means forcing every user to reset their password. Right: Wrap the old hash inside a modern memory-hard hash offline, verify through both layers on the next successful login, and replace with a clean modern hash at that moment. Reserve the forced reset for the tail that never returns, and treat the tail as a decision with a date on it rather than as a residue you keep forever.
Wrong: Role-based access control is the safe default, so start with roles and add complexity later. Right: Roles fit stable job structures and fail by multiplying into hundreds of near-identical roles as exceptions accumulate; relationship questions such as “may this carer see this passenger’s journeys” cannot be expressed as roles at all without inventing a role per pair. Choose the mixture deliberately, express it in one policy language at one decision point, and write down which questions each part answers.
Wrong: We have observability because every login is written to the log. Right: Observability means a small set of named metrics with written thresholds, split by population and method, covering both harm to the service and harm to users - and a log record designed to reconstruct a decision without being able to repeat it. A stream of undifferentiated login lines with no success-rate metric by population will show green during an outage that locks out an entire user group.
Wrong: Multi-factor authentication can be added later, once the product has users. Right: The authenticator decision determines the enrolment flow, the recovery path, the session policy and the support model, and retrofitting it means re-enrolling an existing user base, which is the most expensive operation in identity. More seriously, the assurance level of a system is the minimum over every path to a live session, so adding strong authentication without redesigning recovery raises nothing at all.
Wrong: The architecture diagram is documentation to be produced at the end. Right: The diagram is the enumeration of trust boundaries, and each boundary is a place where you must state what crosses, what is checked and what happens when the check fails. Produced at the end it is decoration; produced first it is the design, and the boundary that never appears on anyone’s diagram - administrative and support access - is the one that most often decides the outcome.
59.99 Chapter summary in 20 lines#
- An identity system is determined by eleven decisions, and everything else about it is implementation detail that can be changed later.
- The eleven should be taken in order of reversibility, because four of them are welded shut by the first row of production data and are effectively permanent.
- The subject model comes first: decide what gets an identity - person, staff member, device, service, organization, delegated relationship - before writing a schema.
- The internal identifier must be opaque so that no code couples to its meaning, unique so that records neither merge nor split, and never reassigned so that no person inherits another’s access.
- OpenID Connect Core 1.0, errata set 2 of 15 December 2023, requires in section 2 that the subject claim be locally unique and never reassigned, which makes non-reassignment a standard rather than a preference.
- Email addresses and telephone numbers are login hints, not identities, because both are routinely reassigned, as Yahoo’s 13 June 2013 identifier recycling and the United States Reassigned Numbers Database of 1 November 2021 both demonstrate.
- NIST SP 800-63-4 of July 2025 sets a five-step risk process and requires the identity, authentication and federation assurance levels to be selected separately rather than as one overall grade.
- Proofing level is an exclusion dial as well as a fraud dial, and a proofing decision is incomplete until the excluded population and its alternative route are written in the same document.
- Authenticator choice must be made per population against the devices those people actually hold, because a policy that 0.36 million assisted-route passengers cannot follow is not a policy but a lockout.
- SP 800-63B-4 sets concrete reauthentication limits - thirty days at AAL1, twenty-four hours with one hour of inactivity at AAL2, twelve hours with fifteen minutes at AAL3 - and forbids composition rules and periodic password change.
- The recovery path sets the true assurance level of a system, because assurance is the minimum over every path to a live session and not the level of the best path.
- Self-contained tokens do not remove state; they relocate it to the issuer and introduce a revocation delay, and the design is incomplete until that delay is written down in seconds and measured.
- RFC 9700, published January 2025 as BCP 240, is the current citable OAuth security baseline, since OAuth 2.1 remained an Internet-Draft as of August 2026.
- The authorization model must be chosen deliberately as roles, attributes, relationships or a named mixture, or the first conditional statement in the codebase will choose it silently and permanently.
- The XACML 3.0 vocabulary of enforcement point, decision point, administration point and information point survives its XML syntax, and the OpenID AuthZEN Authorization API 1.0, final on 11 January 2026, gives a current wire format for it.
- Build, buy and federate differ in who does the work, when the money is spent and how hard it is to leave, and they differ much less in who carries the accountability.
- At consumer scale the buy option cannot be costed from published prices at all, which is itself a decisive cost driver, whereas a workforce of 11,400 can be costed exactly from list prices in one line of arithmetic.
- A user base can be migrated without a mass reset by wrapping legacy hashes offline and rehashing on successful login, provided the shucking risk is acknowledged and the untouched tail is given a deletion date.
- An identity system must emit metrics that measure harm to users as well as harm to the service, each with a written threshold, or its dashboard will show green during a lockout.
- Every trust boundary in the architecture needs three written answers - what crosses, what is checked, what happens on failure - and the administrative boundary, which appears on no diagram, needs them most.
Chapter sources: NIST SP 800-63-4, Digital Identity Guidelines, July 2025, sections 3.1 to 3.5 for the digital identity risk management process, the impact categories and the Digital Identity Acceptance Statement; NIST SP 800-63A-4, Identity Proofing and Enrollment, July 2025, sections 2.1.3, 4.1, 4.2 and 4.3 and the FAIR, STRONG and SUPERIOR evidence strengths; NIST SP 800-63B-4, Authentication and Authenticator Management, July 2025, section 2 for AAL1 to AAL3, section 3.1.1.2 for password length, composition and change rules, and the AAL reauthentication limits of thirty days, twenty-four hours with one hour inactivity, and twelve hours with fifteen minutes inactivity; NIST SP 800-63C-4, Federation and Assertions, July 2025, sections 2.3 to 2.5 for FAL1 to FAL3 and section 3.4.1 for pairwise pseudonymous identifiers; NIST SP 800-63-3, June 2017, as the superseded revision; OWASP Application Security Verification Standard 5.0.0, May 2025, chapters V6 Authentication (requirements 6.2.1, 6.2.4, 6.2.5, 6.2.9), V7 Session Management (7.1.2, 7.3.1, 7.3.2), V8 Authorization, V9 Self-contained Tokens, V10 OAuth and OIDC and V16 Security Logging and Error Handling, together with ASVS 4.0.3 of October 2021 in which authentication and session management were chapters V2 and V3; the OWASP Password Storage Cheat Sheet for the Argon2id minimum of 19 MiB, two iterations and one degree of parallelism, the bcrypt work factor floor of 10 and 72-byte input limit, the PBKDF2-HMAC-SHA256 figure of 600,000 iterations, the warnings on pre-hashing and password shucking, and the rehash-on-login upgrade pattern; RFC 9562, Universally Unique IDentifiers, May 2024, obsoleting RFC 4122, for the UUIDv7 layout; RFC 9700, Best Current Practice for OAuth 2.0 Security, January 2025, published as BCP 240; RFC 9068 for the JWT profile for OAuth access tokens; RFC 9449 for DPoP; RFC 7662 for token introspection; RFC 6749 for the OAuth 2.0 framework; draft-ietf-oauth-v2-1 revision 15 of 2 March 2026 for the current status of OAuth 2.1; OpenID Connect Core 1.0 incorporating errata set 2, 15 December 2023, section 2 for the subject claim; OpenID Foundation Authorization API 1.0 (AuthZEN), final specification of 11 January 2026; OASIS eXtensible Access Control Markup Language Version 3.0, OASIS Standard of 22 January 2013, for the PEP, PDP, PAP and PIP vocabulary; NIST SP 800-162, Guide to Attribute Based Access Control, January 2014, updated 2 August 2019; David Ferraiolo and Richard Kuhn, Role-Based Access Controls, 15th National Computer Security Conference, October 1992, with ANSI/INCITS 359-2004 adopted 11 February 2004 and revised as INCITS 359-2012; Butler Lampson, Protection, 5th Princeton Conference on Information Sciences and Systems, 1971, reprinted in ACM Operating Systems Review, January 1974; Jerome Saltzer and Michael Schroeder, The Protection of Information in Computer Systems, Proceedings of the IEEE, September 1975; Ruoming Pang and others, Zanzibar: Google’s Consistent, Global Authorization System, USENIX Annual Technical Conference, July 2019, for the reported trillions of access control lists, millions of requests per second, sub-ten-millisecond 95th-percentile latency and availability above 99.999 per cent over three years; Google, New York University and the University of California San Diego, published 17 May 2019, for the account hygiene figures of 100, 99 and 90 per cent for on-device prompts and 100, 96 and 76 per cent for SMS codes; the Microsoft study of May 2023 reporting a 99.22 per cent reduction in compromise risk from multi-factor authentication and 98.56 per cent for leaked credentials; Niels Provos and David Mazieres, A Future-Adaptable Password Scheme, USENIX Annual Technical Conference, 1999, for bcrypt; the United States Federal Communications Commission Reassigned Numbers Database Order of 13 December 2018, with the database operational for paid subscribers on 1 November 2021; Yahoo’s identifier recycling announcement of 13 June 2013 with a twelve-month inactivity threshold; Regulation (EU) 2024/1183 of 11 April 2024, in force 20 May 2024, amending Regulation (EU) No 910/2014 to establish the European Digital Identity Framework; the Google Site Reliability Engineering book of 2016 for service level objectives and error budgets; published list prices as of August 2026 from the Microsoft Entra pricing page (Entra ID P1 at 7.00 US dollars and P2 at 10.00 US dollars per user per month on annual commitment, Entra Suite at 12.00, External ID free to 50,000 monthly active users) and the Auth0 pricing page (free to 25,000 monthly active users, Essentials at 35 US dollars and Professional at 240 US dollars per month at 500 monthly active users); and the author’s own operating experience at KedByte Technologies Private Limited, marked as such in the text, for the eleven-decision framing, the build effort estimates, the Meridian Transit worked example and its invented figures, and the rule against migrating credentials and recovery in the same release.