#What I set out to build
Guardian was an authentication server in Rust: OAuth, sessions, email templates, the standard set. I wrote it to learn the domain properly rather than to ship a product, and the thing it taught me was structural rather than cryptographic.
#Every method is a different shape
Password authentication needs a hashing configuration, a password policy, a reset flow with tokens and email, and a history table if you enforce reuse rules. Its storage is a hash and some metadata.
OAuth needs client registration, redirect URI validation, scopes, and one configuration block per provider . Its storage is provider identities linked to local accounts, and its routing includes a callback endpoint per provider.
Passkeys need a relying party identifier, attestation preferences, and a challenge store with a short lifetime . Its storage is credential identifiers and public keys.
Those have almost nothing in common beyond producing a session at the end. I tried to express them as configuration options on one server, and the resulting configuration surface had options that were meaningless in combination and no way to say so.
#The cost of carrying what you do not use
A server that supports every method contains the code for every method, whether or not a deployment uses it. That is a maintenance cost and, more importantly, an attack surface cost. Economy of mechanism is on Saltzer and Schroeder's list for exactly this reason : a simpler mechanism has fewer places to be wrong, and code that is present is reachable if something else goes wrong.
An application that will never use SAML should not ship SAML parsing.
#The design that came out of it
Years later I built the same domain again with methods as plugins. Core identity, users, sessions, organisations, roles and devices, sits in the middle. Each authentication method is a separate module that registers its own routes, its own storage and its own configuration, and you compile in the ones you use.
This is Parnas applied to a security domain . The module boundary follows the decision being hidden, and the decisions each method hides really are independent.
#The thing I still find hard
Account linking cuts across every method. If a user signs up with a password and later signs in with Google using the same email address, what happens is a policy question with security consequences, and it cannot live inside either plugin.
It sits in the core, and it is the part of the design I am least happy with, because it is the one place where the plugins have to know about each other.
References
- [1]D. Hardt (ed.), “The OAuth 2.0 Authorization Framework”, IETF RFC 6749, 2012doi:10.17487/RFC6749 ↗
- [2]Jerome H. Saltzer, Michael D. Schroeder, “The Protection of Information in Computer Systems”, Proceedings of the IEEE, vol. 63, no. 9, pp. 1278-1308, 1975doi:10.1109/PROC.1975.9939 ↗
- [3]World Wide Web Consortium, “Web Authentication: An API for Accessing Public Key Credentials”, W3C Recommendationhttps://www.w3.org/TR/webauthn-2/ ↗
- [4]D. L. Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules”, Communications of the ACM, vol. 15, no. 12, pp. 1053-1058, 1972doi:10.1145/361598.361623 ↗