Tagged “authentication”

Passkeys: your friendly password replacement

Update: 04-26-24 - whooboy, so apparently I've missed some marketing nonsense around Passkeys. At some point, someone decided that Passkey === Resident Key and that apparently stuck? I wasn't aware of that, but I'm gonna link to a technical blog here where you can read more about the chaos.
As a result, this means the information below is a little...weird? Like, it's all still accurate except apparently "passkey" means "usernameless / discoverable key" in public parlance. Which is dumb? I don't like that. Yubikeys can only store up to 25 resident keys which makes them way less useful. Very handy in say, the Apple ecosystem still, but hey. I've added another sidebar in the context.

One of my absolute favorite topics to rant about continually is identity and access management (specifically authentication). One of my friends prompted me to write about this after needing some questions about passkeys answered.

Anyhow, one of the most thrilling innovations in identity managements in recent years that is finally seeing wider adoption is Passkeys. Put simply, Passkeys replace the need for passwords by shunting the authentication checks to a combination of something you have (a phone, or security key) with something you are (biometrics) to authenticate to a website or a service. No longer do you need to use a password manager to log in securely to a site, or remember a secret phrase, so long as you have your device and you remain you - you can access your content.

They’re unphishable, because you can’t just hand them over - you physically need to be in possession of the device. You can’t forget them. There’s nothing to remember. Stealing an entire database of public keys gets you basically nothing, you can’t really use those public keys to impersonate anyone - and because they’re website-specific, they’re immune to credential stuffing. They’re (usually) behind biometrics - so even if your device is stolen, the thief also must worry about faking your face or fingerprint. It makes the cost / time investment in stealing credentials much harder.

The way it does this is all behind-the-scenes and the end user doesn’t need to really understand how any of it works, but today we’re going to dive into that just a bit so you can peer behind the curtain and understand the limitations and risks of bad implementations.

So how does this passkey thing work anyhow?

Permalink to “So how does this passkey thing work anyhow?”

Easy! Public Key Cryptography! … wait that’s not easy? Right, okay, let’s start from the beginning. Passkeys are based on the concept of public key cryptography, in which a system generates a pair of keys on a device - a public key and a private key. The public key can encrypt data so that only the private key can decrypt it, and vice versa. Because the private key never leaves the device, you can hand out the public key so that folks can encrypt data so that only you (the device that holds the private key) can decrypt it. You can also create signatures which simply prove possession of a key without encrypting the data.

Passkeys, using a protocol called WebAuthN, take advantage of this by exchanging public keys back and forth so that when you register / log in, that you have your private key becomes your authentication proof.

The registration flow for a “brand new account” essentially works like this (I’ll be leaving out a fair amount of detail for the sake of understanding - if you want the full picture, the WebAuthN spec, Google, and Apple all have wonderfully detailed guides for implementing Relying Parties).

Okay, so for this section a "resident key" can also be usernameless, meaning the app can just straight sign you in without asking for an identifier - and apparently the wider passkey marketing means that's necessary. Which it's not actually necessary, but here we are.

  1. You visit a website and click “register”.
  2. For non-resident keys only: The website prompts you for some sort of identifier (most commonly email address, but could be username or anything, really) - or generates one for you.
  3. Your device prompts you to create a new passkey, and has you biometrically authenticate (face/touch ID, on iOS).
  4. For Resident Keys: Your device generates a new public / private key pair specific to that website’s domain name and sends the public key to the website. For Non-Resident Keys: a seed gets generated and stored against the identifier - that seed is what is used by the hardware token in conjunction with the private key resident on the token to ber unique per-site (still specific to the website's domain).
  5. The website stores your public key along with your identifier for future use; you are now registered.

This flow can also work for an existing legacy username/password account to add a new passkey to that old account - the only difference is the website already knows what account to associate the passkey to.

For login, the operation is very similar, but there’s an additional challenge mechanism in place.

  1. You visit the website, enter your username/email/whatever to identify which account to log into (the browser can also store this for you, so it knows you already).
  2. The website issues a challenge back to your device, basically: “I need you to prove you have this private key by using it to sign back this data I’m sending you”.
  3. Your device prompts you to use the stored passkey for this website and has you biometrically authenticate.
  4. Your device creates an attestation - which is the response to that challenge, but signed using the private key.
  5. The website uses the public key (or seed as necessary) to check that the assertion was signed correctly. If it was, then you are now logged in! If it wasn’t, the site rejects your credentials.

If you’re interested in the technical details, but do not want to read the spec, I recommend this guide on WebAuthN.guide, or the Google implementation Sandbox.

How is this different than using FaceID in an App?

Permalink to “How is this different than using FaceID in an App?”

It’s really not all that different - many of the same mechanisms that Apple uses for in-app FaceID also apply to Passkeys, they’re built on very similar tech.

Same deal for Google’s biometric implementations, a lot of the same plumbing powers the overall ecosystem.

Cool, so uh, what happens if I drop my phone in the lake?

Permalink to “Cool, so uh, what happens if I drop my phone in the lake?”

You may have noticed that there’s a potential problem if the identity is tied to a single device - how do you log in if you’ve lost the device? There are a couple of ways to handle this on both the device side and the provider / website side.

Apple and Google handle this by synching that private key to your cloud account on either platform - encrypted by the master password on your account. That way, you can use the same passkey on any device you own in the same ecosystem, protected by the same biometrics. For 95% of people, this is sufficient. Even if you lose your phone, so long as you can still get into Google / iCloud your credentials are safe. This means you still need one password - for your cloud account, but you can also protect that via something like a Yubikey stored in a lockbox or something.

On the provider side, they can offer you the ability to enroll multiple passkeys, so that you can keep one as backup on a physical security key. They can also offer robust backup options - currently this is typically 10 or so random “break glass” codes that you can print off and keep in a safe somewhere. They also still have the option of offering traditional SMS recovery, but your security is only as good as the weakest recovery method - and SMS is extremely insecure.

For those extremely paranoid, you can buy hardware authenticators and use those to generate passkeys, but you are responsible for backups at that point - you want to keep one somewhere safe in the event you drop your token in a gorge.

Note: Yubikeys support at most 25 resident "usernameless" keys.

Is this really the future of authentication?

Permalink to “Is this really the future of authentication?”

I think so. To put it extremely bluntly, passwords are awful. They can be stolen. You can forget them. We’ve been trying for years to get the less tech-savvy among us to adopt (and pay for!) password managers to create strong random passwords so that perhaps we can prevent credential stuffing. That hasn’t worked, because the barrier to entry is high and confusing.

The barrier to entry for passkeys is basically zero: “Do you want to let FaceID / TouchID manage this login for you?”. For most people, it will “just work”, and for the rest of us, our password managers or hardware tokens are for us to manage.

I don’t know if passwords will ever fully go away, but I sure hope that passkeys take out a huge chunk of them, and soon.

Auth Series 1: Sessions and Passwords

Auth Post part 1: Sessions and Passwords

Hello and welcome to the first part of a multipart post series where I'll dig through the depths of authentication knowledge and share all of that with you, including some deep lore of how we got from the early days of the internet to today (though, I'm a millennial, so I wasn't exactly a web professional back then. Neopets all the way).

😈 This guide is targeted primarily at web developers who want a deeper look at authentication implementations and some (but not all) of the considerations that go into them.

Part 1 will cover the following:

  1. How do web servers identify requests?
  2. Session storage and state management
  3. Passwords and how to store them as securely as possible
  4. Handling forgotten passwords
  5. Signing out and Account removal considerations

I've also created a repository on Gitlab with a couple of examples for session state: https://gitlab.com/cthonic-studios/authentication-examples

Now, there are going to be a lot of caveats to this topic - authentication is a deep topic and has a lot of gotchas. I'll cover some of them, but I'd need a full book to cover everything. That said, let's get right in!

😈 This post covers traditional multi-page apps being served by a webserver. We'll talk a lot more about Single Page Apps and their variants in a later post.

This post assumes some familiarity with web servers and web technologies. HTTP, cookies, methods of data storage, the concept of multiple web servers working in coordination. If you need some grounding in web servers, Digital Ocean has a good guide.

Web Servers are stateless. How do they identify requests? Sessions!

Permalink to “Web Servers are stateless. How do they identify requests? Sessions!”

So the very first thing you need to consider when dealing with web authentication is that web servers are stateless (more specifically, the HTTP protocol is stateless). Whenever your browser makes a request to a website, the server has no idea that one request is connected to another. There will be some telemetry, like your IP address or the browser you're using, but as far as the web server is concerned, each request is totally independent of any other request.

This is a problem if you want the server to understand a given series of requests are actually related to a given account. Without some unique identifier to pass along to the server, you can't associate a given page load with a given account, which makes persistent web applications kinda difficult.

To deal with this, we have the concept of a browser session. The very basic premise is that the browser will store some bit of information and send it along with every HTTP request that the server can then use to uniquely identify the "session" in question. There are several ways that one could do this so we're going to talk a bit about some of those techniques first and then talk about best practices.

I'm going to use PHP's session management as an example because it has a lot of built-in functions for managing session state, and they're quite illustrative of how it works under the hood. They even have a best practices page for proper session storage.

So, to start, PHP has a function called session_start() that will initiate a session for a given request, which allows you to store parameters in the $_SESSION global which will persist across requests. Just like magic, you've got a storage area where you can store information about the session in question. But how does this magic function work?

The very first thing PHP does when starting a session is generate a random session_id() for the session, and sends that back down to the browser in the response. How it sends it depends on the configuration. By default, it sets a cookie via the Set-Cookie response header, with the name of PHPSESSID. There are a number of configuration values for this, allowing you to disable session cookies, control their behavior, change the name of the session variable, and so on.

The generated session ID was a derived ID based on a number of factors until PHP 7.1, but now it's generated using random_bytes. Either way it results in an alphanumeric ID which isn't guessable in a reasonable amount of time given current computing power.

We'll talk about the cookie thing in a minute, but for the moment let's talk about what happens if you don't send a cookie back. How else might we get that session information? Well, if you disable the cookie setting, and enable session.use_trans_sid, the session information will be transparently appended to every anchor tag on the page so that ?PHPSESSID= will be appended to the target URL, thus preserving your session across requests through the power of $_GET variables. If you'd like to see this in action, I've included some simple examples in the aforementioned code repo.

Now, this isn't great, because that makes your session ID directly visible in the URL. GET params are encrypted over https:// but anyone who wants to look over your shoulder could take a picture of your session ID and then hijack your session simply by using the same ID. This is why basically all modern systems use Cookies to store your session identifier. We'll talk about GET parameters more when we discuss other auth systems (OIDC and SAML both use GET parameters for various things), but for now, just internalize "use Cookies to store session info".

😈 Why didn't we just always use Cookies? Why does this setting exist? Well, in the olden days, Cookies weren't always reliably available. We live in better times now, kinda. Wait until we talk about 3rd party cookies.

Okay! Now that we know how to start a session and how the server identifies a stateless request, what can we do with that? Well, now you can store information about that user in the session storage. In PHP, you can directly assign things to $_SESSION['example_key'] which will persist across requests. PHP's basic session storage is file-based (meaning it stores your session data on the hard drive of the server you're interacting with), which is generally unsuitable for distributed applications because you'll have to either ensure the user is pinned to the server where their session information is stored, or* you'll need to synchronize the file system across all web servers. Neither of those is ideal, but it's definitely a thing we used to do many years ago.

So, you can also store your session information in some other system, like Memcached, or a database which is what modern systems tend to do. Indeed, if you're working in a language without a built-in session mechanism, this is what you're going to want to do. Let's talk about that a bit more.

Storing session state is a complicated topic and depends a lot on your server topology. If you just have a single web server, storing state on the file system is going to look "fine". If you have multiple web servers handling requests, you'll need your state to be available to them. There are several ways to handle this, and if you want a quick overview of the options Laravel's session storage page has a good series of the typical options. Including:

  • Storing data on the file system on a single server
  • Storing data on a networked file system (block storage on a cloud provider, typically)
  • A database, either the application database or a separate database
  • In-memory storage like Redis or Memcached
  • Directly in browser cookies, typically signed or encrypted.

The most common options I've seen implemented for session storage are either "just store it in the database" attached to the session identifier, or "store it in memcached/redis" also attached to a session identifier. Over the course of my career I've seen the progression from file → database → memory cache storage several times now, and that's directly correlated to scaling applications. File storage (even on a networked drive) is slower than the db which is slower than memory, so when you're wanting to squeeze more performance out, this is where you tend to go.

The other option which requires very little configuration is an encrypted session cookie. Most frameworks handle this for you because if you put session information in a cookie the client can tamper with it, which is usually something you don't want to have to worry about.

😈 You could also use file-based session storage and just ensure that a given session is always served by a given server with some load balancer trickery... but that's a topic for another day.

That said, you also generally want to minimize the amount of data you're storing in the session in the first place.

At minimum, for an authenticated user, you'll need to store their user_id or another primary key so you can identify that session to the user in question. We'll talk about that more in a second.

For anonymous sessions, you might do something like store a shopping cart (for e-commerce) or preferences, or any number of other things that you need persistent across page loads.

This can also be things like error or status messages that need to be shown on subsequent page loads, for example if there's an error on a POST request and you need to redirect back to the prior page with a message. You could include that in the URL, but it's commonly stored to session storage instead (because someone else could send you a link with a misleading message via that same URL).

This really depends on your application, but I'd recommend storing as little data in the session as you can get away with.

As the OWASP Session sheet calls out, you have a few different options:

  • Keep the session alive indefinitely, until the user takes an affirmative action (logging out)
  • Expire the session after a period of time.
  • Expire the session after the user has been inactive for a period of time.
  • Expire the session when the browser is closed.

Which one you'll choose depends on your application and how sensitive it is. I've encountered a lot of blanket "you must log the user out after 15 minute" corporate requirements, and I need to tell you that is less secure than you might think it is, especially if it isn't an idle timeout.

In general, I recommend:

  • Expire the session when the browser tab is closed (expiration time of 0 on the session cookie), but give the user the option to stay logged in across visits. In that case, set the expiration of the cookie to some sensible time in the future (weeks / months).

For more sensitive contexts:

  • Expire the session after a sensible idle timeout (yes, 15 minutes works)
  • Do some basic anomaly detection (IP addresses changing suddenly, etc), require re-authentication for sensitive operations, etc.

What about client side LocalStorage or IndexedDB or sessionStorage?

Permalink to “What about client side LocalStorage or IndexedDB or sessionStorage?”

A popular choice for storing ephemeral data on Single Page Apps (SPAs), which can be a good option for data that's tolerant to being modified by the client (meaning, the server cannot inherently trust that data, it has to be verified).

While I don't recommend doing this (use a library if you can), the basics of how sessions work boil down to this:

  1. Create a session identifier which is unique and impractical to guess (ideally long and random) or cryptographically signed.
  2. Sending that session identifier securely to the browser (over TLS, encrypted, etc.) so that it cannot be intercepted in transit.
  3. Inducing the browser to send back that session ID to the server (also securely so it's not intercepted over the wire) ideally via cookies.
  4. Storing information associated with that session in some sort of storage that's accessible by all the web servers that could potentially serve the request.
  5. Deciding how long you want that session to last and removing the session when it expires.

There's a lot of nuance that I'm eliding over in those posts, but those are the core elements of a session storage mechanism. If there's enough interest we can do a roll-your-own example of session storage, but I'd never encourage you to do this yourself other than to understand the mechanics. Much like cryptography, there are a lot of footguns, and there are many good libraries that handle sessions.

That said, for more detailed information, check out the OWASP Session Cheat Sheet.

For an even deeper dive into proper session storage, NIST's SP-800-63B document is an extremely long technical document that covers a lot of information about authentication.

You should generally have the web server manage the session with a cookie, and unless you have a very good reason to allow JavaScript to read that cookie, you should send it with the HttpOnly flag (which prevents JS from accessing it).

Likewise, you should set SecureOnly so it will only be sent over HTTPS and the SameSite attribute so it will not be sent along to other domains on the same root domain (unless, of course, you want to do that and you know the trade-offs).

Session hijacking is a technique whereby an attacker gains access to the session for a given target user and then uses that to act on their behalf. OWASP covers that in this article, but the basics are "Gain token, use token". There's also a reverse attack where you use an XSS vulnerability to inject your own session token into another user's session and get them to do some target action.

Either way, you want to prevent that session token from leaking. Here are some steps you should take to prevent session hijacking:

  • Never send the session identifier over http://, only over https:// connections. If using cookies, ensure SecureOnly is set.
  • If you can help it, do not let JavaScript access the session identifier, like with HttpOnly cookies.
  • Ensure that your session ID is impractical to guess (length, randomness).
  • Change the default session identifier variable for your language (for PHP, don't use PHPSESSID). This'll make it harder for a malicious user to guess or target in bulk.
  • Implement Content Security Policies that prevent cross-site requests to prevent JS from sending session information.

Identifying the user with Username and Password

Permalink to “Identifying the user with Username and Password”


Song_about_summer @ Shutterstock #1794130912

Right, so we've now talked at length about how sessions work, but currently we still don't know anything about the person on the other end of the internet other than they initiated a session in a browser. We can reliably identify the browser that started the session is still the browser interacting with that session, But what happens if the human on the other side of the screen needs to change computers? Or they shut down their browser? Or they clear their cookies? How do we go past identifying the browser and identify the user?

Well, that's why usernames exist. Very basically, as a website operator, we're asking you directly to tell us who you are. So, when you provide registration, you'd ask a user who they are, and that most often is a memorable username chosen by the human who's signing up for the service.

But, if all we ask for is a username, then anyone with that username is able to log in as the person in question. Because usernames are typically also displayed publically, this would be very bad.

This is where passwords come in. Passwords are an ancient invention, you might have to utter a password to a guard at a door in ancient Rome to gain access to a place. Modern passwords are directly derived from that idea, you need to give a password that ideally only you know so that we, the service operator, know that you are who you claim to be. Or at least, that you have the requisite knowledge (which is where impersonation and multifactor authentication come in).

Now you want to collect an email address and use it for account recovery or for the username. How do you ensure that this email address is an email address? You can use a regular expression to validate that it's an email, right?

No! Email addresses are...remarkably complicated in how they can be formatted. There is a regular expression you can use and catch most of the cases you'd find in RFC 5322, but you're not going to catch everything. My advice is to use a simple regular expression to ensure the format is in the ballpark (ensuring there's a local and remote part with an @, for example) and then send an email to the provided email with a one-time code in it to verify delivery of the email address. There are also services like Neverbounce that do validation by sending test emails and maintaining giant lists, but you can get by with simply making the user validate their email address.

😈 Like names, you should accept that what the user is giving you is their email address and don't try to be clever. The best validation for emails is by sending an email.

You should also do this before allowing the user to change their email, and you should put that operation behind a second factor. If you haven't implemented MFA or the user hasn't turned it on, send a code to the existing email first (following the guidelines for password resets, presented later) and then again to the new email address to confirm the change.

So, the combination of username (or email address, commonly) and password are what basic applications use to identify users with the service. Great! We can just collect that and store it directly in the database, right?

Wrong! You may be tempted to store passwords in plain text in your database, how else will you send the password to the user when they forget it? This is a really bad idea. For one, if your database is compromised, you've now given malicious users every single user's password. Humans tend to reuse passwords across services (even though we've been trying to get folks to not do that for the entire history of application passwords), which means you might have compromised a bunch of user's security across any number of sites.

We don't want that. Okay, so maybe we'll encrypt those passwords in the database! Then, if you steal the database, you can't decrypt the passwords! This is also a bad idea, because in order to encrypt the password in the first place, your application server needs to be able to access the encryption key. Which means if your application is compromised, then you can still steal plain text passwords. It might be slightly harder, but it's still possible.

So, what do we do? You hash the passwords, with a one-way algorithm so that the server cannot get the plain text password back out of it. You'll be able to check that what the user has given you matches what you have on file when you run it through the hashing algorithm, but anyone with the database cannot just get that password back out.

In the early days, we'd often just use an algorithm like MD5 without any other mechanisms in place and call it good. As computing power has increased drastically, MD5 hashes can be checked in a short amount of time rendering them insufficient for modern security. Likewise, there are entire databases of hashes called "Rainbow tables" which correlate MD5 hashes to plaintext equivalents.

So, instead, we now have better algorithms and other techniques to slow down the checking of a hash. The most common mechanism I've seen in the wild is still bcrypt, while NIST recommends using PBKDF2 which is specifically for passwords. OWASP recommends using Argon2id. These algorithms increase security in several ways, the first is they each run through a number of iterations (which is configurable) to both increase the time taken to generate the hash and limiting the speed at which you can brute-force it (bcrypt is much better at this than PBKDF2) and to make it harder to do if you do not know how many iterations to use. Also, because of the rainbow tables we mentioned before, each of these algorithms includes a "salt" value, which is unique to the generated hash. This ensures you can't just build a giant database of hashes.

😈 Your framework should have a way to hash passwords built in. For example, Laravel's is Hash::make. It defaults to bcrypt with Argon2 as an option.

For a brief introduction to password cracking, check out this article by Matt Miller on Beyond Trust. You can also hop on over to TryHackMe's Crack the Hash room to try out password hash cracking for yourself to learn how to defend against it.

Man looking pretty stressed at a laptop
mapo_japan @ Shutterstock #2490729947

This is a thing we've been fighting for a long time. To forget things is human. In an ideal world (where passwords have to exist) everyone would be using a password manager, and they would never ever forget their password. And they'd always have access to that password manager. And they'd always be able to tell their loved ones how to access that password manager in the event of an accident.

😈 You're going to hear a lot about humans forgetting things across this series.

Okay, since none of those things are guaranteed out there in the world, we need to have a way for the user to get in if they don't have access to their password. How do we securely(ish) verify they are who they say they are when they request a password reset?

The most common way to handle this is by sending a one-time link or code to the email address or the phone number they gave us when they signed up that they can then click to prove they have control of that communications channel. We also generally assume that if the user's email address has been compromised, they have way bigger problems than a password reset on our service, so we'll let you in if you possess that one-time code. But! Critically, this requires us to have collected those communication channels up front. Which requires us to store more information about the user. This is why the username also tends to be the email address in services (but this is, IMO, a bad user experience because changing it often becomes problematic).

Related point, we could also send that code to SMS — most folks have a phone after all. But this is a fairly weak form of authentication because of sim swapping attacks. A sim swap, briefly, is where someone calls your phone company, pretends to be you, and then gets the phone company to activate their phone with your number. You lose access to your phone number, they gain it, and can intercept these kinds of codes (this is also true of multifactor). CISA recommends only using SMS or voice as a last-resort option for MFA, and that holds true for forgotten password requests (for the same reasons).

So, assuming we're going to send a reset request to some other communication channel for the user, what do we send them? Well, in order to be secure, it should have the following properties:

  1. Impossible to enumerate in a reasonable amount of time.
    1. This means that you should rate-limit the number of requests to the confirmation page.
    2. It should also be longer than a few characters, if you're using a PIN, at least 6 digits.
  2. Should only be valid for a short period of time.
    1. Generally between 5 and 30 minutes is acceptable. Use the shortest period of time that is reasonable for your user base.
  3. Should only be usable a single time. Once a reset code / pin / URL has been used successfully, it should be invalid for subsequent requests.

For more reading on the topic, I recommend OWASP's Forgot Password Cheat Sheet.

We should talk about multifactor authentication

Permalink to “We should talk about multifactor authentication”

Multifactor Authentication (MFA) is a large topic which I'll cover in more detail in a different post, but for its worth mentioning here: MFA both increases account security and the complexity of ensuring legitimate users don't lose access to their accounts.

So, stay tuned for the deeper dive on how to implement MFA in another post.

One thing that Banks in particular do that drives me to irrational anger is to ask "security" questions, like "What was the make and model of your first car?"

These questions are trying to give you something easy to remember as either a recovery method or a MFA factor, but they invariably ask things that are easy to find out in public or social engineer. Like, anyone can figure out what high school I went to. So instead of answering these in a memorable way, I use a password manager to generate random words to fill out the answers. That way you can't guess them, and I have a shot at remembering them (but otherwise just use my password manager). It's pointless, just give me a proper MFA factor and call it a day.

Stop using these, they're a bad idea.

Just ask NIST:

Verifiers and CSPs SHALL NOT prompt subscribers to use knowledge-based authentication (KBA) (e.g., “What was the name of your first pet?”) or security questions when choosing passwords. - NIST SP-800-63B

There's an old security requirement still floating around (especially in corporate settings) that you must force a password reset every {n} days. This is no longer a good requirement, and you should not force the user to change their password. This has been true for a long time now.

Verifiers SHOULD NOT require memorized secrets to be changed arbitrarily (e.g., periodically). However, verifiers SHALL force a change if there is evidence of compromise of the authenticator. - NIST SP-800-63B

To summarize, your baseline level of secure password storage:

  1. Do not store passwords in plaintext. Use Argon2id, scrypt, bcrypt, or PBKDF2. If you're using a framework it's probably doing this for you, but understand what the framework is doing and which hashing mechanism it has chosen.
    1. If you're going for a NIST certification, that will determine which algorithm you must use.
  2. Consider how you're going to reset the user's passwords when they lose access. This requires collecting an email, phone number, or some other mechanism up front.
  3. Resetting the password should require possession of something else (email, SMS, backup tokens).
  4. MFA adds additional security but also additional account lockout considerations.
  5. Please for the love of the gods do not ask for "security questions".

Alright! So we're getting close to the end of the topic for this post, but what happens if you want to log the user out. You've got a bunch of questions you should answer.

When the user clicks a "Log Out" button, you should do the following things:

  1. Delete their session storage (the cookie and any data you've got on the session server-side)
  2. Redirect them back to a public page.

You do not need to do things like remove their history, because your server should redirect to an authentication page if the session is missing. Most frameworks encapsulate best practices into a logout() method somewhere on your user or session model which will do these things for you.

As a matter of practice, you should offer your users the ability to delete their account. How you implement this depends on local regulations: you may need to anonymize the user's data and remove personal identifiers (but continue to store logs based on retention requirements - this is common in health care settings). However, if you do not have retention requirements, you should remove all related user data from your database that you do not require.

In practice, most applications will need to store a "stub" of the user record. For example, in an eCommerce application you'll want to retain archival records for your orders (which may be synced to other systems), but "what data do we need to retain" is a huge topic and industry-dependent.

Regardless, read up on GDPR or CCPA under the right to be forgotten for further guidelines on how you can delete user data.

Okay, this is getting long, and I'm sure there are some things I missed. If there are bits that you'd like to see me touch on I can update this post later, just leave me a comment below or ping me on Mastodon or Bluesky.

😈 Is this too long? Should it have been a two parter? Let me know.

Part 2 will be on Multifactor Authentication and the various methods for how that works, along with the why, how, and what to do when you drop your phone in a river.

Auth Series 2: Multifactor Authentication

Logos of Authy, Yubico, and a Fingerprint

Welcome to part 2 of the rambling journey through how to do authentication systems yourself (or at least understand how they work). This time it's an extension of part 1, where we learned how to do Username and Password Authentication. It's a topic that's pretty closely attached to that "now that I can log users in, what can I do to make their account resistant to nefarious multidimensional entities who wish to look at their weird game history?"

Okay, maybe you're not asking that exact question, but you do want to make accounts more secure. How do you do that? One way is by implementing a Multifactor authentication system, which is a fairly deep topic. It'll take up the entirety of this post.

I'll be linking off to various resources as we chat so you can find more information, but for this one I'll be quoting NIST and CISA several times because they have solid advice.

There's an adage in authentication circles, the Three Authentication Factors: Something you know, Something you have, and Something you are. (There are "more" sort-of. See this great StackExchange question).

😈 If you're feeling snarky: Something you'll forget, something you'll lose, something that can be impersonated. We'll get into that later in the post.

The first post in this series covered the "something you know" portion of Authentication, the humble password. Today we'll hit both the "something you have" and "something you are" portions. Combining two or more of the three factors is what makes it multifactor authentication.

That's it, that's all there is to it.

Aside: Just how many factors are we going to combine?

Permalink to “Aside: Just how many factors are we going to combine?”

For most commercial applications this is usually "two". One thing you want to do is balance how difficult it is to log in for the user with their willingness to jump through hoops to identify themselves. eCommerce, for example, naturally wants to reduce friction between you clicking on that shiny thing your lizard brain wants and getting through the checkout process to give up your money. I'm sure many of you have had just this conversation with stakeholders. There's not a "naturally correct" answer here. Like many things it really depends on your application and the level of security you want to foster.

A stock image of "authenticators"
SuPatMaN @ Shutterstock #2495179419

We're going to hit on the most common types of MFA for this post, in the order of least-to-most complicated to implement, and I'll talk about the implementation details in each section. Please note, some of the factors I'll mention are discouraged, and I'll put a big ☢️ symbol at the front of their section if you need to be wary of them along with why. I include them because sometimes you just can't get around it due to other factors, but I want to be very clear that there are better options.

Okay, that out of the way, let's go.

We talked about these in the context of a Forgot Password flow in the first post, but One-Time passcodes can also be used in a MFA context when you're sending them to some out-of-bands communication method. The main ways to do this are:

  • Email
  • ☢️ SMS
  • ☢️ Voice Calls

These all demonstrate possession of something, namely your email address or phone number is something you have. This factor assumes that you retain control over the channel in question.

You can do other things, like literally mailing a code to someone via physical mail (which is something the American Social Security office used to do and might still do), but for 99% of applications that's going to be incredibly impractical.

All the rules that apply for forgot password from the first post also apply to generating one-time passcodes, which I'll repeat here:

  1. Impossible to enumerate in a reasonable amount of time.
    1. This means that you should rate-limit the number of requests to the confirmation page.
    2. It should also be longer than a few characters, if you're using a PIN, at least 6 digits.
  2. Should only be valid for a short period of time.
    1. Generally between 5 and 30 minutes is acceptable. Use the shortest period of time that is reasonable for your user base.
  3. Should only be usable a single time. Once a reset code / pin / URL has been used successfully, it should be invalid for subsequent requests.

Then, you send that generated, expiring code to the channel the user has previously provided allowing them to enter the code to finish authenticating their session.

😈 You may also generate a token that you include in a URL parameter so that the user just needs to click to access. This is also how "magic login links" work

Okay, so why did I put the ☢️ symbol on SMS and voice calls? Well, CISA recommends only using SMS or voice as a last-resort option for MFA because of the rise in prevalence of SIM swapping attacks. That, very basically, is where someone calls your phone company, pretends to be you, and gets them to assign your phone number to a phone the attackers control. Here's a Lifehacker Article on the topic.

Anyhow, that leaves "email" as a viable channel, and the general consensus I've seen (this is my professional observation) is "if your email's been compromised you've got bigger problems". Which is both true and disconcerting.

So, if SMS is so bad, why do banks keep using it? I'd love for banks to stop using SMS for their MFA setups by default, but it mostly comes down (I believe) to access vs risk. They assume the majority of their customers will not fall victim to a SIM Swap and the increased complexity of something other than SMS isn't worth the hassle. Banks also have other protections in place to limit the blast radius of a leaked customer account (withdrawal limits, reconciliation checks, a dispute process in meat space, etc).

I don't work in the banking industry though, so that's all just informed speculation on my part. If anyone does and can provide an answer, please leave a comment below.

OTP Codes are able to be phished by an attacker. This is why codes you receive always have messages like no Cthonicbank employee will never ask you for this token, swear in blood that you won't give it out.

This is another "have" factor, in that you possess something that will generate a (usually) 6-digit code which changes every 30 seconds (though this is configurable if both sides know the interval). Most people will know this as the "Google Authenticator" method, but some of us old folks will remember physical RSA tokens (or the one Blizzard did in the earlier days of WoW). Now, there are some major security differences between modern TOTP approaches and the old tokens (namely, those tokens were not based on the modern TOTP algorithm) but the general idea is the same: the server and the client agree on a shared secret key which then feeds a random number generator. The random number generator uses that key (as a seed) and the current 30-second time slice to generate the code. If either side leaks the seed, you'd be able to guess the random numbers.

TOTP is based on an internet standard which you may read all about in RFC 6238, but Wikipedia outlines the basic algorithm.

To gloss over some complexity (if you want to implement this yourself, definitely read the RFC) this algorithm requires the server and the client (Google Authenticator, Authy, a physical token, etc) to establish a "shared secret" during the enrollment process. The key is usually a base32 encoded random string which is then passed into the HMAC-SHA1 (usually, but this is configurable) algorithm as part of the generation process. You also have to agree upon how often the codes are generated and a parameter to tell it how many times it should cycle through the algorithm before landing on a given number. This is all to make it impractical to guess a given number since you'd need to know all of those things (though there are defaults).

This blog post by Øyvind Stegard is a simple implementation of the algorithm in shell scripts, and it'll give you an understanding of the process end-to-end. The algorithm itself is actually quite straightforward (though it builds upon several concepts, like "what the hell is HMAC?").

So! When you enroll an "Authenticator" into a service, most services either provide you a QR Code to scan with all that information encoded, or a string that you should copy and paste into your authenticator. Then, you finish the enrollment by entering the next valid key for the time frame. Checking that token is extremely important because if you do not, you could lock the user out of their account. You do not want that. Always verify that the user can generate a valid code from your TOTP implementation.

😈 This is one of those things where there's very likely a library in your programming language that does this. I don't recommend rolling your own in production, but as far as authentication algorithms go this is one of the easier ones.

To summarize, the process for TOTP Registration is:

  1. Establish a Base32 encoded shared key during registration from the server and share that with the client along with any configurable options (like the token validity interval, number of cycles, hashing algorithm, etc).
    1. The server stores this shared key somewhere alongside the user record.
  2. The client stores that information and starts generating codes.
  3. The user must input a code from the authenticator before you mark the account as having 2FA turned on.
  4. The server validates any generated codes with the shared key from step 1 any time the user needs to input their 2FA token.

Part 2 of the Authentication Examples repo has a page which will let you enroll your TOTP token in an authenticator of your source and generate tokens. It uses a couple of PHP libraries to do this: symfony/lock and spomky-labs/otphp.

Just like the OTP method before, these can be phished, but since their duration is very short this kind of attack often relies on credentials being relayed in real-time through a proxy or some other mechanism for tricking the user to entering the code into a malicious page in real time.

Yubikeys! Or, hardware-based authenticators with WebAuthn

Permalink to “Yubikeys! Or, hardware-based authenticators with WebAuthn”


BestForBest @ Shutterstock #2395187395

Now we're getting into one of the more secure "have" factors. There are several hardware based authenticators on the market which support something called WebAuthn. WebAuthn is the "Web Authentication API" specification which enables using all kinds of hardware devices to authenticate with websites. It's in the same category of thing as a "smart card" that those of you in the government might have used to log into a physical computer. This can also be something like your phone, or a key stored in Bitwarden, but for simplicity’s sake I'm just going to talk about hardware keys in this section.

😈 It's also the basis for using Passkeys, which we'll be talking about at length in a future post.

I'm a big fan of WebAuthn, it's very neat when it's used as a second factor.

For the moment, I'm going to focus on FIDO U2F and FIDO2 (which covers more use cases and is tied up in the explanation of passkeys) but all of these things ultimately run through WebAuthn. Basically, the more modern process for using a hardware device all runs through the same API as passkeys, but for 2FA you simply use this key as a second factor rather than the primary factor.

How you do it winds up getting a little complex (okay maybe a lot complex), but I'll try to summarize the process from the server side of things as succinctly as possible. Let's start with the example from webauthn.guide:

const publicKeyCredentialCreationOptions = {
challenge: Uint8Array.from(
randomStringFromServer, c => c.charCodeAt(0)),
rp: {
name: "Duo Security",
id: "duosecurity.com",
},
user: {
id: Uint8Array.from(
"UZSL85T9AFC", c => c.charCodeAt(0)),
name: "lee@webauthn.guide",
displayName: "Lee",
},
pubKeyCredParams: [{alg: -7, type: "public-key"}],
authenticatorSelection: {
authenticatorAttachment: "cross-platform",
},
timeout: 60000,
attestation: "direct"
};

const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
});

First, you need to establish the website as a Relying Party (RP - this term will come up again in the post on OIDC / SAML). This kind of second factor is tied explicitly to the website in question so that you can't use common browser hijacking techniques to trick the user into giving you a valid key (unlike TOTP codes). In this example, the website is duosecurity.com and the name is Duo Security. The name can be whatever you want, but the id must match the site that you're presently authenticating to.

The second bit is establishing the user stanza. This is where things get a little weird. Let's look at it again:

user: {
id: Uint8Array.from(
"UZSL85T9AFC", c => c.charCodeAt(0)),
name: "lee@webauthn.guide",
displayName: "Lee",
},

The thing that probably sticks out to you is that id stanza. Where did they get UZSL85T9AFC? Why is it a Uint8Array? Yeah, you'll have to go to the spec for that:

The user handle of the user account. A user handle is an opaque byte sequence with a maximum size of 64 bytes, and is not meant to be displayed to the user.
To ensure secure operation, authentication and authorization decisions MUST be made on the basis of this id member, not the displayName nor name members. See Section 6.1 of [RFC8266].
The user handle MUST NOT contain personally identifying information about the user, such as a username or e-mail address; see § 14.6.1 User Handle Contents for details. The user handle MUST NOT be empty.

Okay, that's a mouth(eye?)full, but it's relatively simple. The ID must be a byte sequence (which is why it's a Uint8Array) and it must be opaque, not displayed to the user, and used for authorization decisions. Cool. Why's it seemingly random?

Couple of reasons, but the primary one being some token authenticators will make a discoverable (resident) key, which'll be visible on the device. As the Relying Party you have to both provide this id and store it with the user record. Frequently, I've seen this be the primary key for the User, either the auto_incrementing ID or a UUID, or a generated uniqid(). This is easy to do when you've already got a user record in a MFA context, but we'll talk a lot more about this in the Passkey segment.

Okay, cool. The only other stanza I think worth explaining here is the authenticatorSelection. There are a couple of options you can pass here. I'll cover them at length in the Passkeys post (I'm saying that a lot, aren't I?), but the one I want to call out right now is credentialProtectionPolicy.

credentialProtectionPolicy can be set to userVerificationOptional, userVerificationRequired, or userVerificationOptionalWithCredentialIDList. That last one requires some more explanation, so I'll leave it alone for the moment, but the first two control whether the authenticator is encouraged to ask you for a pin code or a biometric. In the case of something like the Yubikey 5c which has no biometrics, this will be a prompt to enter a pin into a browser-native popup. For things like an iPhone, it'll likely be FaceID. Note that userVerificationOptional doesn't mean that the device won't prompt you for a verification, some devices may always prompt for verification if they so choose. The credProtect extension documentation covers the various scenarios.

For MFA scenarios, this is usually set to "optional" because FIDO 2.0 did it that way, and it's less friction for the user who has already provided a password for the first factor.

😈 Some sites will still prompt for verification in a MFA scenario. I know Cloudflare and Zoho do this.

So, that was a lot of explanation. What happens after you initiate this request? Well, the user will authenticate with their WebAuthn token, and you'll get back a PublicKeyCredential. I want to quote webauthn.guide once again because I find this phrasing very amusing:

After the PublicKeyCredential has been obtained, it is sent to the server for validation. The WebAuthn specification describes a 19-point procedure to validate the registration data; what this looks like will vary depending on the language your server software is written in.

It's a lot. There are libraries that do this for you. Please use the libraries.

Anyhow, once you've validated this on the server side you store the public key you received attached to that userID (and the user record) you got before. The key will also generate an ID for itself, and you'll want to store this alongside your user for non-resident keys. You will want this to be a one-to-many relationship between the User and the Keys, because for physical tokens, this public key isn't portable (Apple and Google store keys in your keychain, as do some password managers, but you cannot assume that the key will be portable).

Okay. We've registered the token, how do we validate it when the user's logging in? From webauthn.guide:

const publicKeyCredentialRequestOptions = {
challenge: Uint8Array.from(
randomStringFromServer, c => c.charCodeAt(0)),
allowCredentials: [{
id: Uint8Array.from(
credentialId, c => c.charCodeAt(0)),
type: 'public-key',
transports: ['usb', 'ble', 'nfc'],
}],
timeout: 60000,
}

const assertion = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions
});

The main bits you'll want to focus on in that example is challenge and allowCredentials.id. The challenge is a Uint8Array that you generate on the server and provide to the credential to sign with its private key. The allowCredentials.id should match the we got back from the key in the registration step. You'll receive a PublicKeyCredential object back from this request, assuming the token is present. If you get an error, you'll want to parse the error and provide a button for the user to try again with the token (or to use an alternative method).

Here's how PublicKeyCredential looks:

PublicKeyCredential {
id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...',
rawId: ArrayBuffer(59),
response: AuthenticatorAssertionResponse {
authenticatorData: ArrayBuffer(191),
clientDataJSON: ArrayBuffer(118),
signature: ArrayBuffer(70),
userHandle: ArrayBuffer(10),
},
type: 'public-key'
}

There's yet more validation you need to do on this response, but ultimately you'll be checking to ensure that signature matches what you get if you use the public key you've stored for the user to sign that same challenge you produced before. If it does, you're good! If it doesn't.... well the key is bad or has been tampered with (if, for example, someone's made a key that responds to all credential requests or something).

Okay. That's it! We did it! WebAuthn 2FA!

😈 That is far from it, dear reader. There will be an entire post about WebAuthn for passkeys (which are basically the same procedure but there's a lot of detail to talk about).

It's worth doing this hands-on if you really want to get the concept, so Google has a great tutorial for doing this end-to-end along with a Glitch site with the code to it located here.

Right, this section is going to be relatively short because it's got a ton of overlap with the WebAuthn section because practical biometric auth for the web follows the same API patterns. Biometrics are the "something you are" factor, and are most commonly a fingerprint or a face identification. It can also encompass retinal scanners and other more... esoteric ideas, but you're likely to only ever interact with Fingerprint or Face.

😈 Genetic Marker testing for web here we come?!? (Gods I shouldn't give terrible people ideas)

Relatedly, on the web you'll likely not know which biometric authentication factor you've encountered or even if it's biometric at all. That's because (as far as I'm aware) the WebAuthn specification has no mechanism for saying "only allow devices with biometric attestation". The best you can do is set credentialProtectionPolicy: "userVerificationRequired" and hope the device in question is biometric. Yay!

Like I mentioned above, Apple and Google are going to use biometrics to authenticate, and some Yubikeys do biometric auth (with fingerprints) like this one.

So there are some other things worth mentioning (because we need to talk about recovery):

  • Pre-Shared Secrets
  • Lookup Secrets
  • Multiple Factor OTP Devices

Let's look at each of these just a little bit.

These are things that are shared between the server and the user. Passwords are a pre-shared secret. However, backup codes are also pre-shared secrets. We'll be talking about those in a minute, for recovery.

These are cool, and totally impractical. This is essentially a generated table of random whatevers which is arranged in a grid, so that you can prompt a user to lookup (for example) the word in column 2 row 24. This table should be random and unique per user.

The only thing I can think of where I've seen anything close to this is 2048 BIP-39 seed phrases which is commonly used for crypto wallets. That spec is just a giant list of possible words, arranged in order for a given cryptographic seed.

So, there's another cool thing you can do with Yubikeys. Yubikey makes a Yubico Authenticator app that lets you use your yubikey as an OTP device. So you have to have the Yubikey in order to generate the OTP. It's neat. I use this for sensitive things that don't support FIDO/WebAuthn.

Phone sitting precariously next to a gutter
monte_a @ Shutterstock #1784028497

Remember how I said that humans tend to forget things? Well, adding "something you have" factors to your authentication flow adds the possibility that they'll lose something in addition to forgetting it. This could be as simple as "I left my yubikey at home" or "I dropped my phone in the river". Yeah, troublesome.

This means you need to provide the user ways to break the glass to get back into their account. There are several ways you can handle this, but let's talk about the most common first. Backup codes!

If you've ever enrolled in a 2FA system yourself, you've likely encountered this. After you enroll the device you'll be presented with some number of "backup codes" in a window that warns you that if you lose your device you'll need those codes to get in. If you don't save them, you'll be locked out forever if you don't have it.

As the server operator, this is as simple as generating random impractical-to-guess codes and storing them attached to the user record. You'd then allow these codes to be used one time each as a bypass for the MFA device. Each time a code is used, remove it from the pool of valid codes you've made for the user.

The user, for their part, needs to store those codes somewhere safe, often printed or written down on a piece of paper. This is why recovery codes are often simply alphanumeric and relatively short (to allow for ease of entry).

You should also periodically remind users about their backup codes, and offer to let them regenerate them if it's been a long time since they were generated to reduce the chance that they lose those too.

Now, many services stop here and will not offer further recovery if the 2FA factors are all lost. Too bad, user, should have kept those codes. This is sensible for many applications, but not great for something like a bank.

Another option you have is a customer service based approach where you have the user use a customer service line where they have to prove their identity to a human in order to have MFA removed from their account.

Designing a customer service protocol around this is beyond the scope of this document (but if you want to talk about it, hop on over to the contact form, and we can set up some consulting time), but you want it to be robust and resistant to impersonation.

😈 Remember kids, customer service attacks are a big reason SMS hijacking is a thing.

Like forgot password considerations, you could allow an SMS or Email code be sent to the user to let them bypass 2FA, but I do not recommend this as it reduces the account to the weakest form of MFA and makes offering OTP / WebAuthn factors.

Okay, that was... a lot? Less words than on usernames and passwords, but still a fair amount of words to cover 2FA.

Stay tuned for part 3, where we'll be talking about single sign on (SSO) and associated protocols (SAML, OAuth/OIDC) along with the how / why you might want to do that.

Lemme know in the comments if there's something I missed or anything else you'd like to see.

Auth Series 3: SSO

Intro image showing the logos of SAML, OAuth, and OIDC

Right! Welcome back to the third post in the Authentication Post series and this time we're going to talk about single sign on (SSO) and other federated identity protocols.

I'll try to keep this post under the word count of the previous two, but like many auth things… this could be a lengthy topic.

In the previous posts in the series we covered how to authenticate users with a username and password on a single service, and how to add multifactor authentication to that account. What happens if you want to create another application? One option you can choose is to simply make a second user base for that application. That might be the right choice if you want those user bases to be completely separated from one another. But what if the two applications you've created are closely linked together? You could copy your existing user database to the new service, or link both services to the same database, but now you're having to manage the complexity of the user experience between two different sites. FIDO/U2F/Passkeys don't work cross-domain either, so you might be dealing with the complexities around that as well.

Plus, the users will have to enter their credentials on both sites any time they want to use them at the same time.

SSO solutions were built to handle these kinds of use-cases, allowing a user to log into multiple sites with a single set of credentials while minimizing the number of interactions the end user needs to perform. There have been many iterations of this over the years, so I'll give you a brief (non-exhaustive) history of some options and where they've gone.

One common option in the "olden days" was to have a single parent domain where you authenticated the user, and then used 3rd party cookies in order to trigger the authentication on multiple target sites by granting those sites access to that cookie. This was really common when you had a root parent domain (example.com) and your additional sites were all subdomains of that first site (service1.example.com, service2.example.com) because it was relatively easy to set a cookie that's readable on all those domains. You'd then wire the backend services to the parent domain for handling account actions (meaning you'd land on example.com to change your password, for example). The good news is you can still do that, so long as you set the domain property of the cookie to that parent domain, subdomains can still read that cookie.

But what if your authentication system is on a different domain as the services (one real life example of this is google.com vs youtube.com and how they do SSO on that is actually pretty fun)? Well, for a long while you could do something like an AJAX request to the backend service and have that service set a cookie that could be read on any domain. That's known as a 3rd party cookie. You might have heard a lot about those, because they're also used for surveillance capitalism and serving you ads. The browser vendors have been slowly blocking them by default for years (though Chrome apparently is walking back their plans because it'd impact their ad revenue). So that technique no longer works for auth because ad vendors are... well... I'll stop there.

😈 Fun fact, when you sign into Google it also issues a 302 redirect to Youtube to sign you in on Youtube to get around the cross-domain problem. (source)

Another fun element of the rise of social media platforms was the rise of using a social account to log into another service. Many services today offer the ability to log into their services using your Google, Facebook, (rip) Twitter, and etc. accounts in lieu of signing up for yet another website. This has some benefits for the end user, you don't need to remember yet another password for the service, you can just click on the "sign in with Google" button, asked to share some information, and then you're logged in! Easy.

The downside, of course, is you're locked into the social provider's whims, and service providers generally only bothered to support the biggest players (this was especially true before OAuth 2 / OIDC).

Anyhow, in this model you can't just rely on setting cookies willy nilly, that'd be insecure as all get out (you don't want every website on the internet having unrestricted access to your Google information — only Google Ads get to do that), so you need some sort of protocol to handle the exchange of information, first to ensure that the site in question is allowed to even ask you for your account information and a mechanism for securely sharing the bits you consent to sending back to the service.

That's OAuth (and now OIDC... I'll explain shortly)!

There are a bunch of other ways you can do authentication against parties you don't control, including:

  • SAML (We'll talk about this)
  • Kerberos
  • LDAP
  • RADIUS

A lot of these are context-dependent. For example, I've personally seen more SAML integrations with Universities than I've seen of the other protocols. Kerberos is pretty common in Enterprises (combined with Active Directory often alongside an LDAP system).

If you own the system, why use a protocol?

Permalink to “If you own the system, why use a protocol?”

So, like I mentioned above, using a combination of cookies and sorcery, you could build your own authentication systems with username and password authentication that works across a number of different properties you control. I'm going to recommend you do not do that. I strongly recommend that if you're going to build a centralized authentication system, you use a protocol (and I'm going to go further and say you use OAuth2 / OIDC). This is because it makes it a lot easier to implement across those sites. There will be libraries that you don't have to write yourself. The pathways are well-known. The cognitive overhead is smaller. If you want to let a third-party into your system you don't have to do something bespoke.

It'll save you a lot of headache after the initial headache of understanding how the heck these protocols work. Walk with me and I'll help you out.

SAML 2.0 - a versatile and complicated protocol

Permalink to “SAML 2.0 - a versatile and complicated protocol”

Carlo Toffolo @ Shutterstock #795590020
Carlo Toffolo @ Shutterstock #795590020

I don't want to spend a lot of time on SAML, but it's near and dear to my heart ever since I spent a full week reverse-engineering SimpleSAMLphp and reading the spec until my mind exploded. There are some similarities to how OAuth and OIDC handle the cross-site communications though, so it's worth discussing.

SAML is the "Security Assertion Markup Language", and by that it means it's XML. It's a lot of XML. Every bit of the communication protocol is sending large blobs of XML back and forth. The way this works is essentially this process for Service Provider (SP) initiated login:

😈 Before any of this can work the service provider and the identity provider must be configured to allow this. Namely, each side of the exchange must set up some configuration options to identify themselves to each other (otherwise any random site on the internet could try to trick you into signing into the identity provider).

Also, I'm going to use "service provider" through the rest of this post, but OAuth tends to call them "Relying Parties" or simply "the client".
  1. The SP, example.com, issues an <samlp:AuthnRequest> to the Identity Provider (IdP) greatlogins.test by a HTTP GET or HTTP POST request in the SAMLRequest param (this XML is deflated and base64 encoded to fit in that param).
    1. There's also an Artifact Binding which uses SOAP and a reference ID to allow a lookup rather than send the whole response but I've literally never seen this implemented in the wild. Not a single time. Have you?
  2. The IdP base64 decodes and inflates the AuthnRequest and validates it contains what it expects. Namely, that the SP is on its Allow List, and that it has been signed (and optionally encrypted) correctly by that SP and the request has not been tampered with.
    1. Yeah, setting up a SAML connection requires sharing certificates ahead of time. SAML Requests are signed, and optionally encrypted.
  3. The user is shown a login screen where they enter whatever credentials they need to (the SP doesn't need to care how this happens).
  4. The user is redirected back to the SP (again via a GET or a POST) which contains a <samlp:AuthnResponse> in the (you guessed it) SAMLResponse param. This too is deflated and base64 encoded.
  5. The SP validates the response in the same way the IdP did (ensuring the response has not been tampered with) and then pulls attributes out of the response to make authentication / authorization decisions based on that. These attributes are configurable, and are usually a thing you want to negotiate when doing the setup process.
😈 Why did I use a .test there? Because .test is a reserved TLD and there's no real website that could resolve to! (Like example.com)

Did I make that sound easy? Well there are a lot of things that can go wrong, but the good news is those are relatively predictable if you know how XMLSec works... Right. Yeah. Okay, so tl;dr it's usually a problem with configuring the shared secrets or missing response attributes. It could be other things, but it's there.

One major thing you're going to run into is there still aren't a lot of deep tutorials (that I'm aware of) about SAML. Here's a decent one.

That's all I want to say about SAML right now, but I did want to include it so you can see the similarities in...

Also known as "the precious". This is my most preferred SSO protocol these days, not least of which is because of breadth and depth. There are a number of software packages that support these two protocols. Doing simple implementations is very straightforward, but there's a lot of configuration and security features. There's even considerations for signing in to smart devices that don't have a web browser. There are definitely rough edges, but, it's the one that comes close to being the best we've got for the most people.

Like I mentioned in the history lesson, OAuth was originally created to allow users to grant 3rd parties access to a social account in order to do things on their behalf. This varied, but it was usually in the context of "allow this site to post as you on social media" to enable various kinds of experiences. While OAuth by itself grants authorization rather than authentication, a lot of folks were using it for both. Namely, if you're issued a token that can act on behalf of a social account, isn't that proof enough of authentication?

😈 No! But people were using it that way anyhow.

That's where OIDC comes into the picture. OIDC (or Open ID Connect) provides a series of protocols for authentication and it's often paired with OAuth 2.0 for authorization. This, I think, is why you hear folks use the terms basically interchangeably these days. I've been caught doing that very thing because for most audiences the distinction doesn't matter.

That out of the way, here's how a typical flow with OIDC works (try to spot the similarities to SAML). Now, there are several OAuth grant types and flows that you can and should be using which I will get into, but we'll start with the simplest one that's intended for end users: response_type=token (which is the deprecated Implicit grant). You should not use this grant type any longer, but it's the simplest version, and we'll use it to build your knowledge.

😈 The actual simplest one is client_credentials which involves sending a client_id and client_secret in exchange for a token. It's intended for machine-to-machine use cases, not for end users.

Like SAML, the IdP must be configured with knowledge of the SP (in the form of an "Application") which grants the SP a client_id and the SP tells the IdP what URLs it's allowed / expected to redirect you to.

😈 There's such a thing as client autoregistration, which is a thing that Mastodon uses, which allows you to just create a client on the fly. I... don't really like it but in some cases it's necessary. Like Mastodon.

The Implicit flow works like this:

  1. The SP redirects the user to the IdP's login endpoint (optionally autodiscovering this URL by querying /.well-known/openid-configuration on the IdP domain), including the client_id, response_type (token for this example), scopes (what information you want to request), state which is an identifier for your application to manage its "state", and the redirect_uri.
  2. The user is prompted to log into the IdP. Assume they do so.
  3. The user is shown a screen that shows the application that is requesting the user's information, along with an enumerated list of what information the application is requesting.
  4. Assuming the user says "yes go go go", the IdP redirects to the redirect_uri (assuming it's on the allowed list) along with a token parameter (and a refresh token to get a new one when that token expires).
  5. The Application can just use the token to then get information about the user from the IdP. Simple!

Now, the implicit flow is fairly vulnerable to access token leakage and token replay attacks, and they can't be bound to a given client. Instead, what you should be using is the Authorization Code flow, with PKCE. Let's build on the Implicit flow and add Authorization Code.

Here's how that looks:

  1. The SP redirects to the IdP in the same way as in the implicit flow, but instead response_type=authorization_code.
  2. Same
  3. Same
  4. The IdP redirects back with an authorization_code which is short-lived (usually 30 seconds).
  5. The SP makes a call to the token endpoint with that authorization_code from the backend along with its client_secret. Since an attacker won't have that secret, they cannot exchange the code for a token, preventing some leakage issues. The timeout prevents replay attacks.
    1. You can configure a public client that doesn't need the secret to do the exchange.
  6. The IdP returns the token to the SP, and marks the authorization code as used (though some providers allow you to configure how many times the code's valid to prevent weird race condition problems).

You might be noticing an issue here, if you're familiar with mobile apps. If you're authenticating directly from a device, the client must be public. There's no way to keep a secret a secret in a mobile app. Someone could decompile it, or otherwise coerce the information from the device (This is also true of Single Page Apps). So, how do we make those calls a bit more secure? PKCE!

😈 Sidebar: PKCE is also useful for apps with client secrets to prevent CSRF attacks.

Let's build upon our previous flow.

  1. The response type remains the same, but in this case the SP creates a code_verifier which to quote oauth.com: "This is a cryptographically random string using the characters A-Z, a-z, 0-9, and the punctuation characters -._~ (hyphen, period, underscore, and tilde), between 43 and 128 characters long."
  2. The SP makes a SHA-256 hash of the code_verifier which is then base64 encoded and sent along with the request. This is the code_challenge
  3. Proceed along until step 5 of the previous step - this time, instead of sending just the code, you also send the code_verifier along with the request.
  4. The IdP uses the code_verifier to generate a code_challenge and checks to see if that matches the code_challenge it received in step #2. If it does, it can be assured that the request hasn't been intercepted partway through since only the client had that information.
  5. The IdP returns tokens as established.

And that's PKCE in a nutshell.

vector zefirka @ Shutterstock #1936423573
vector zefirka @ Shutterstock #1936423573

Right, so I've said "the IdP returns tokens" a number of times now, and while that's accurate there's some more detail that we need to cover. What is a token?

It could be several things, turns out, but at it's most basic in OAuth parlance it's a "Bearer" token which allows access to resources on a 3rd party when sent along with the request. A lot of services will return an opaque token which is just an internal identifier to the IdP and means nothing outside of that. It could also be a JSON Web Token (JWT) that is an encoded and (usually) signed token that contains information about said token.

Here's an example of a (unsigned) JWT Access token from Auth0:

{
"iss": "https://my-domain.auth0.com/",
"sub": "auth0|123456",
"aud": [
"https://example.com/health-api",
"https://my-domain.auth0.com/userinfo"
],
"azp": "my_client_id",
"exp": 1311281970,
"iat": 1311280970,
"scope": "openid profile read:patients read:admin"
}
😈 For Access tokens Auth0 recommends treating them as opaque always, regardless of format. That is to say they expect you to call endpoints with the token, not introspect it.

There are also refresh tokens, which are how you keep a user logged in over an extended period of time by exchanging them for new access tokens (and ideally rotating them) and ID Tokens.

Let's look at the latter token.

It's worth calling out at this point that OIDC endpoints (remember how I mentioned that OIDC is concerned with authentication while OAuth is concerned with authorization?) can also return an id_token which will contain information about the user along with a series of claims (depending on which scopes you send along with the initial request).

In order to get this token, you have to pass the openid scope in the first request, and it'll return the id_token along with the access and refresh tokens. The id_token is always a JWT, and it should be cryptographically signed by the JSON Web Key (JWK) that's present in the /.well-known/openid-configuration endpoint I mentioned above.

Here's an example Auth0 ID Token (without the signature bits):

{
"iss": "http://my-domain.auth0.com",
"sub": "auth0|123456",
"aud": "my_client_id",
"exp": 1311281970,
"iat": 1311280970,
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"gender": "female",
"birthdate": "0000-10-31",
"email": "janedoe@example.com",
"picture": "http://example.com/janedoe/me.jpg"
}

You'll notice that there's a fair amount of information about the user in encoded in that token, but there are a couple of things I want to call out. First, everything from "name" onwards in that example isn't covered by the registered claim names from the spec. They're custom values Auth0 has added based on the scopes you're asking for. Second, the registered claims have special meanings which you'll want to take account of.

  • iss is the issuer field, this should be the IdP.
  • sub is the user's unique identifier, this will usually be an auto-incrementing id, or a UUID, or another unique id.
  • aud is the intended "audience" of the token, this is usually the client id of the app that requested the token.
  • exp and iat are the expiration time and issued at time, respectively in unixtimestamp format.

You can use any of the information in the token inside your application for displaying to the user or making local assertions but it is critically important that you validate the claims in the token have not been tampered with. Especially ensure that the iss and the aud claims are what you expect them to be and that the exp claim is in the future. It's not a huge deal if you're using ID tokens just for user data, but if you use them in lieu of access tokens (you shouldn't, more in a second), token forgery becomes a big problem.

For more information on validating ID tokens vs Access Tokens, please have a read of this excellent Auth0 Article

But is it really bad to use an ID token as an access token?

Permalink to “But is it really bad to use an ID token as an access token?”

Right. Okay. So there are, let's say, different opinions on this subject. Some folks, like Google think it's alright to use the claims in an ID token to establish the user's identity and use that as a proof of Authorization. If you have a highly coupled frontend and backend, this can be "okay" in that your aud claim should be something you generally expect from the backend.

Auth0 agrees with this statement in the article I linked, stating:

As said above, an ID token proves that a user has been authenticated. In a first-party scenario, i.e., in a scenario where the client and the API are both controlled by you, you may decide that your ID token is good to make authorization decisions: maybe all you need to know is the user identity.

If that's enough for you, so be it! But please for the love of the gods validate that the token isn't forged or expired. You still want to have a list of allowed aud claims as well, don't just allow any aud to pass.

😈 So there's also a cool thing called Demonstration of Proof-of-Possession (DPoP) which uses light cryptography to bind a JWT access token to a particular device. It exists so that if an access token leaks it's useless to an attacker since they don't have a private key needed to sign requests.

Relatedly, ID tokens lack scopes, so you won't be able to rely on them for granular authorization from the IdP. Meaning your backend will need to make authorization decisions some other way. You could configure the IdP to pass those as custom claims in the ID token, but at that point just use the access token, that's what it's for.

You're probably getting sick of hearing about tokens, but there are a few more things I should point out if you're digging into the world of OAuth/OIDC and want to avoid some footguns.

  1. Access tokens should be short-lived. This limits the damage should one be stolen. Auth0 defaults the lifetime to 24 hours. I'd recommend far less than that unless your application needs longer access token lifetimes.
    1. JWTs Cannot be revoked, by the way, so if a long-lived JWT access token leaks... you've got a problem on your hands.
  2. If you issue refresh tokens (via the offline_access scope), enable refresh token rotation. This way you can't reuse old refresh tokens.
  3. Limit the lifetime of the refresh token. This can be much longer than the access token, but it should be directly related to how long you want a user to be logged into your application. 7 days, 30 days, or the like are good ideas.
  4. Always validate your JWTs fully. There are libraries for this.
  5. Don't store sensitive data in ID Tokens. They're trivially decodable.
  6. Do not attempt to store client_secrets in a mobile app or a SPA. They will leak.

Right, so I've explained at length the how and the why of OAuth/OIDC, but none of that information actually solves for the "I want to silently log you in to multiple applications". You still need some sort of initiator for this to occur.

This is where OIDC silent login comes into play. You see, most IdPs support this protocol which allows you to skip the login prompt on your IdP if the user has logged in recently. How long between credential prompts is generally configurable, but usually I've seen it set to "the browser session". The way you do this is passing prompt=none when doing the initial redirect to the IdP. If the user's logged in already, the application just redirects them straight back to your site and you proceed with the token exchange as usual.

If they're not, then they're shown the authentication prompt as normal.

SSO Achieved! ... well, not quite. The user, in this scenario, still has to click "login". This is usually okay, since the process winds up being "click login, suddenly the app knows who I am", but sometimes that's unacceptable.

From that point, you have a couple of options:

  1. Combine this technique with the shared cookie thing from above where the cookie stores a trigger to auto-activate the login flow
  2. Redirect around your sites in a loop, hitting the authorization endpoint on each of them
    1. That can get unwieldy when you've got a lot of sites.
  3. Do what StackExchange did with LocalStorage on a shared domain.
  4. Cry, just a little. It's cathartic.

Relatedly, this is also a problem with logout.

Everything that applies to SSO also applies to SLO and the techniques also apply there. OIDC provides a mechanism for a redirect after hitting the logout endpoint, and you can use that to redirect them around and immediately end the session on all sites.

One other thing you can do is have a short-lived access token and revoke the refresh tokens on user logout. That'll ensure that when the access token expires they'll be signed out. So...yeah!

Likewise, if you're using that centralized cookie or local storage approach, you can just destroy those storage bits and force a logout on that end.

Well, I failed at making this post shorter than part 2. We'll try again in Part 4: Passkeys! Lots and lots of information about Passkeys.

So. Many. Passkeys.

Auth Series 4: Passkeys

Auth Series 4: Passkeys
I'm apparently obliged to tell you: The passkey icon is a trademark of FIDO Alliance, Inc

Here we are again! This time we're going to talk about one of my favorite subsets of this topic: Passkeys. You should mentally prepare yourself for this, it's going to be a lot of information and a fair amount of it is dense. I'll be using information from my other post on Passkeys so you don't need to read it first, but you can if you want.

😈 Guess what! "Passkey" doesn't actually have a real definition, it's a marketing term that a bunch of folks vied for control of. I think we've settled on a common definition, but keep that in mind.

That's as good a place to start as any, so let's gooooooo.

Okay, so, "passkeys" are rooted in the idea that passwords are bad, and we should securely eliminate them. As a reminder, passwords are "bad" because:

  • You have to remember them
  • People tend to reuse them across multiple websites so that they can remember them
  • Humans also tend to create insecure passwords (hunter2 comes to mind) that are easy to guess
  • Which means if they leak, the blast radius can be real big for a given individual
  • They can be phished relatively easily

The best advice that I can give to an individual in the absence of passkey technology is to get a password manager and have that autogenerate and fill secure passwords for you. Apple and Google both have password managers built in, but my preference would be Bitwarden or 1Password. This is going to be good advice until passkeys become reliably ubiquitous, if they ever do.

😈 I think they will, but if you want a more pessimistic view read this article.

Viewed with that lens, you want something that can be used in place of passwords that doesn't have the same security issues as passwords, but can accurately attest the identity of the user signing in to the service. That's what passkeys are. Technically, passkeys are a cryptographic private/public key pair tied to a particular service (they cannot be reused across services) which are stored somewhere that hopefully only the unique user in question has access to. This can be a bunch of things, which we'll talk about in a minute, but they're often also behind a biometric authentication mechanism as well. If you remember the second post in this series, this will be a combination of "something you have" and "something you are".

😈 BTW I'm not actually using the prevailing definition of passkeys here, I'll correct that later in the post. Stick with it.

Passkeys use asymmetric (public-key) cryptography to do their magic, so if you're unfamiliar, the Wikipedia page is a great resource to learn more. But, basically, when enrolling a passkey your device will generate a private key and a public key. The public key is given to the service and stored there. Because of how public key cryptography works, the public key and private key can each generate messages (signatures, or encryption) that you need the other side to verify. The private key stays with the user, the public key goes to the service.

So, when you as an end user start using passkeys to log into things, you'll begin to collect a pile of key pairs stored ... somewhere. Right, okay. So now we need to talk about the various things that can generate/store passkeys. I'll start from the end-user perspective and then move over to the developer perspective after that.

There's going to be a lot of "it depends" here. Also, a lot of "it's being worked on". I'll go ahead and say it now, the UX around passkeys still needs a lot of work (and folks are working on it).

This is probably the most confusing thing to the average user, I think, because the answer is "wide" but also the major platforms are trying to make it seamless to people who don't think about authentication all day every day (... I don't have a problem). So, the options you have right now are:

  • Apple Password Manager with Apple biometrics (Face ID or Fingerprints)
  • Google Password Manager with whatever biometrics your device supports
  • Windows Hello on certain devices (you need a secure element on the device, most modern machines will have this) with a pin or a biometric
  • Physical security keys, YubiKey being the most prominent, with a pin or a biometric (YubiKey sells a fingerprint reader fob)
  • Certain Password Managers like Bitwarden, KeePassX, 1Password
    • This requires an app or a browser plugin to be installed

Note that I listed the platform-specific ones up front, because that's where the "seamless migration" stuff is happening. Apple, Google, and Microsoft are all positioned to make the downsides of passkeys a bit less stabby to the end user. The other options require more effort and understanding to function...and there are some other sharp edges that I'll talk about shortly.

Anyhow, regardless of which option you use, there's a key pair associated with each service being generated on that device. What the prompt looks like when you generate a passkey depends a lot on the platform. Here's what my iPhone looks like when trying to register a new passkey on passkeys.io:

iPhone showing both Bitwarden and Passwords App
😈 This is actually a lot better than it used to be, because it now tells you where the passkeys get synced.

Because I have a saved passkey in my iCloud passwords, I can also use that same passkey on the mac. Here's how that UX looks:
Showing the same passkey on the Mac

So how did that work? Well, for Apple Password Manager with iCloud (and Google Passwords, and the password managers), when you generate a passkey it's synced to every device that's signed in to your iCloud account via the intertubes along with the rest of your password vault. All of that is encrypted with your device code so if someone manages to steal the backups they can't decrypt the keys (hopefully). If they didn't sync the passkeys across devices you wouldn't be able to access your account unless you had the specific phone or computer you used to create the passkey on. It'd also be a major problem if you lose your phone, because that passkey would be unrecoverable.

This is especially true of physical security keys. The private key, by design, cannot leave that device. If you lose the device, you lose the passkey. You might remember that constraint from the Multifactor post.

But wait, you might be wondering: What happens if I generate a bunch of passkeys in Apple's password manager, and then I decide to leave the Apple ecosystem? "We're working on it" is the answer to that question. The FIDO Alliance has released draft specifications for Secure Credential Exchange so you can migrate from one provider to another. At the time of this writing, though, the answer is "you don't". You'd have to go to each service and change out your passkey.

Passkey support is still a bit spotty in some browsers. Librewolf, for example, only reports partial passkey support so when browsers try to detect what UX it can show you, it fails and falls back to just showing a "please touch your device" popup with no further explanation. You could conceivably plug in a USB key after the prompt happens, but the messaging you get around that is very bad since you're now in the "edge case".

Right, fine, but how are Passkeys more secure?

Permalink to “Right, fine, but how are Passkeys more secure?”

Well, the first thing is you don't need to remember anything about them other than you used one to sign in to a service (and they can be autodiscovered, sometimes, we'll talk about that in the dev section). The second thing is they can't be phished. How?

Remember earlier I mentioned that passkeys are bound to a particular website / service? There are a couple of ways this is accomplished (which has a lot of overlap with the multifactor post on WebAuthn) is with the declaration of the Relying Party, the way browsers have implemented the communication channel, and the fact that those attestations are part of the authentication ceremony. While it's theoretically possible the FIDO alliance missed something critical, there are strong assurances built into the spec that the service prompting you for the passkey is the same service you registered with.

The final bit is stealing private keys is also difficult, depending on the storage mechanism. For password managers, it's as secure as your password service, typically encrypted with a thing you, the human, know. For Physical keys, the key cannot be extracted barring exotic attacks and physical access to the key.

😈 There was a fun Yubikey side channel discovered earlier this year, but it also requires knowing your pin or a valid service login.

So, in summary, passkeys are better than passwords because:

  • You can't tell someone a passkey, or have it stolen from a service's database.
  • It can't be phished, since it doesn't ever leave the device in question.
  • If you're using a syncing service, you also can't really lose them since they're backed up to an account.
    • But you could drop your phone and break it, or lose a YubiKey or the like.
  • Assuming the place storing the private key portion of the passkey is secure, it's quite difficult to steal them.

The Technical bits, how do you implement Passkey Authentication?

Permalink to “The Technical bits, how do you implement Passkey Authentication?”

Right. So here's where that "definition" of passkeys gets in the way of explaining this. There are two things that could conceivably be called passkeys: Resident and non-resident keys (also known as discoverable and non-discoverable).

Resident keys are generated when you register with a service and are stored on the device in question. Your browser (or other integrated software) can query the device for these keys to figure out if you've registered with a service. This allows you to use only the passkey to authenticate, no need for a service identifier like a username. In short, Resident keys can replace both usernames and passwords.

Non-resident keys use an identifier held by the service to deterministically generate the public / private key pair repeatably. This means the key in question does not need to store any information about the service its registered with, because it can just regenerate the key from that service identifier each time. This means that you still need a username or other account identifier to figure out what you need to send the key for it to generate the proper attestations. You cannot query the device for what services this has been done for, as the device does not store that information. Non-resident keys replace passwords, but not usernames.

😈 Remember how I said I think we've "settled" on a definition? As far as I can tell the common parlance for "passkeys" is now "Resident" or "Discoverable" keys (indeed, passkeys.dev uses this definition). This is a problem, for a few reasons, a key one being how they interact with hardware tokens like Yubikeys. I'll talk about that shortly.

Okay, so now that is out of the way, let's talk about how a Relying Party (service provider / website / etc.) can implement passkey authentication. We'll start with non-resident keys.

The steps to do this are basically as follows (and this will look very familiar to the 2FA post because it's exactly the same process). For code examples see Part 2 of the series.

For Registration:

  1. The user goes to a website and clicks on "register", providing a Username in the process.
  2. The site responds by requesting the public key from the device using navigator.credentials.create (providing a few values from the server).
  3. The authenticator generates a seed, which is sent back in the PublicKeyCredential.id field of the response. The site must store that ID alongside the username and will need to send it back when logging in. It also generates a public key which will be used for signature verification later. Store that as well.
    1. Make this a one-to-many relationship on your user record please.
    2. If you're curious how YubiKeys any program can query every key on the device. You do not need to know the relying party. There's an operation to just get it to dump the service list. Generally this requires a pin or a biometric to unlock do this Duo wrote up a blog post
  4. The server must verify the credential using the 19 point process outlined in the spec (just use a library).
  5. You now have a non-resident key stored for the user.

In snarky diagram format:

sequenceDiagram
    Participant Passkey Device
	Participant User
	Participant Browser
	Participant Server
	User->>+Browser: I would like to Register.
	Browser->>+User: Username please!
	User->>+Browser: It's cthos
	Browser->>+Server: New user coming in, please give me an id and a challenge key
	Server->>+Browser: Here you go. "e98fowfi" and "bacon".
	Browser->>+Passkey Device: Please give me a new key for user ID "e98fowfi" and sign this challenge: "bacon".
	Passkey Device->>+Browser: Okay, here's a `PublicKeyCredential` with the key and signature.
	Browser->>+Server: Here's the public key, Id "1d889" and stuff
	Server->>+Browser: It all checks out, let them in.

To Log the user in:

  1. The user goes to the website, clicks "login" and then provides their username.
  2. The server retrieves the key IDs that have been registered with the account (this could be multiple keys). Then, it uses navigator.credentials.get passing those key IDs along in the allowCredentials stanza.
  3. Using the credentialId list, the authenticator will loop through and attempt to re-create the private key from the input. If it can do so, it will sign the request with the private key and send back a PublicKeyCredential like in the registration step.
  4. The server must validate that response and that the signature matches what it was expecting.
sequenceDiagram
	Participant Passkey Device
	Participant User
	Participant Browser
	Participant Server
	User->>+Browser: I would like to Log in.
	Browser->>+User: Username please!
	User->>+Browser: It's cthos
	Browser->>+Server: Please give me the keys and challenge phrase for "cthos"
	Server->>+Browser: Here you go. Key ID "1d889" and "justicier".
	Browser->>+Passkey Device: Please sign this challenge: "justicier" using key "1d889".
	Passkey Device->>+Browser: Okay, here's a `PublicKeyCredential` with that signature.
	Browser->>Server: Here's the response, is it valid?
	Server->>Browser: It all checks out, let them in.

And that's it! For a given user ID, something like a YubiKey will only generate a single key pair, but you can use the same key for different users with a different generated key. Password managers and Apple/Google can operate in this mode in much the same way (though I believe they generate and store a key).

So what's an advantage of using key derivation over storing keys each time? Well, something like a YubiKey only has limited storage space. In fact, most resident-key-capable YubiKeys can only store 25 of them. Likewise, by virtue of not storing keys on the device, someone who steals your YubiKey can't figure out what sites you use by probing the key.

Right, so how are resident (discoverable) keys different?

These follow a very similar process, but instead of needing to provide a username, you can instead query the device for valid identities.

To Register:

  1. The user goes to a website and clicks on "register".
  2. The site responds by requesting the public key from the device using navigator.credentials.create. This time you pass requireResidentKey as true (if you want to force resident key creation).
  3. The authenticator generates a key pair, which is stored on the device associated with the Relying Party ID you sent in the request and the user.id you provided. Like before, you get a PublicKeyCredential response that includes an id and the public key.
    1. Store this along with your user record and whatever user.id you send along with the request, you'll need it later.
  4. The server must verify the credential using the 19 point process outlined in the spec (just use a library).
sequenceDiagram
Participant Passkey Device
	Participant User
	Participant Browser
	Participant Server
	User->>+Browser: I would like to Register.
	Browser->>+Server: New user coming in, please generate a new id and a challenge key
	Server->>+Browser: Here you go. "99209ed" and "bacon".
	Browser->>+Passkey Device: Please give me a new key for user ID "99209ed" and sign this challenge: "bacon".
	Passkey Device->>+Browser: Okay, here's a `PublicKeyCredential` with the key and signature. I've stored it under `99209ed` and your domain name.
	Browser->>Server: Here's the public key for user `99209ed`
	Server->>Browser: It all checks out, let them in.

To Authenticate:

  1. The user goes to a website and clicks on "Authenticate with a Passkey or something" (there's also an autofill API you can use).
  2. The server creates its secret, like before, but this time when calling navigator.credentials.get leave allowCredentials as an empty array, or leave it out entirely.
  3. The device will query itself based on the relying party ID and provide a list of credentials and the browser will prompt the user which one to use (if there are multiple).
  4. The device will send back a PublicKeyCredential which will include the key ID, an attestation, and the user.id it was provided during registration.
  5. On the server, query for that id, pull the public key associated with it, and validate the signature using the 19 point process.
  6. At this point you now know the user and their info! You're here. Hooray.
sequenceDiagram
Participant Passkey Device
	Participant User
	Participant Browser
	Participant Server
	User->>+Browser: I would like to Log in.
	Browser->>+Server: User login coming in hot, please give me a challenge.
	Server->>+Browser: Here you go: "lemonaide".
	Browser->>+Passkey Device: Please sign this challenge: "lemonaide" using any key you possess for my website.
	Passkey Device->>+Browser: Okay, here's a `PublicKeyCredential` with that signature, and the user.id `99209ed`.
	Browser->>Server: Here's the response, is it valid for user.id `99209ed`
	Server->>Browser: It all checks out, let them in.

So, you've gained the ability to authenticate the user without them needing to remember anything besides their device.

This is good, generally, but it's also bad when you consider security keys. Because of their limited memory if every service were to move to the resident key model you'd either need to carry an entire key ring of YubiKeys...or we're going to need a bigger storage YubiKey.

Likewise, because the keys are stored you can list them. Fortunately this usually requires you to enter a pin or use your biometrics to do so.

😈 Here's the point where I rant a little bit. I prefer non-resident keys because I prefer physical security keys. If we get a physical key with unlimited storage that doesn't cost a small fortune, I might change my tune.

Like the MFA post, I want to recommend a Google Developers post, which will walk you through how to do this in depth, from both the server and client side.

There are a lot of sharp edges, but I'm confident you too can implement passkeys.

Looks like this did manage to be a fair amount shorter than the previous posts in the series. Nice.

For the final post in the series, Part 5, I'm going to cover some oddball scenarios and other things that don't fit neatly into the rest of the posts like machine authentication and non-web identity systems.

See you all next time!

Auth Series 5: The Other Stuff

Header showing the Kerberos and JAMF logos

Welcome to the last post in my end-of-2024 Auth series. This post covers the "other" stuff that I didn't cover in the previous 4 posts. It's mostly just the fun wrap up post to hit some things you probably won't need to know about but might want to know more about.

So, here's the list:

  • OIDC/OAuth other flows (M2M and Device Pin)
  • Certificate/Ticket based SSO, like Kerberos
  • Mobile device management and certificates

Let's get right in!

Remote control facing a blurry TV
CeltStudio @ Shutterstock #2179196459

If you have a "smart" device without an easy way to type in your password, like an internet-connected TV, or refrigerator, or light bulb (or something) you might have seen the OIDC device flow.

Basically, when you request to log into a service, you give it your username or email, and then it will show you a short (usually) code. You're expected to go grab your phone or computer, log into the service over there, and enter the code to bind your device to your account.

Auth0 has a nice flow diagram of this flow. The things you want to note about this are:

  1. The URL you use to generate the code is different from the usual URL you'd redirect users to in interactive mode.
  2. Your device has to poll for access to /oauth/token, there's nothing in the standard that gives a push (but I suppose you could implement your own push solution).
  3. How you bind a device to the account is up to you, and will depend on the device in question. Your application could generate it itself and store that locally, or you could use a unique device ID from the platform as appropriate.

OAuth 2.0 Resource Owner Password Grant (ROPG)

Permalink to “OAuth 2.0 Resource Owner Password Grant (ROPG)”

Did you know there's an OAuth 2.0 grant where you collect the user's username and password directly and send it to the resource server yourself? Well there is! You probably shouldn't be using this grant for anything, but sometimes there's no other choice. It's pretty much only okay in a first party scenario where you control both ends, but I've seen it used as a shortcut in a lot of places rather than showing an OIDC style redirect.

😈 The number of arguments I've gotten into about "just let them enter their passwords in the app, the popup is ugly" have been notable.

The flow is really short:

  1. Prompt the user for their username and password.
  2. Send that directly to the oauth/token endpoint and get an access token back.

Now, it's worth noting that this flow has been removed in OAuth 2.1 because it's not nearly as secure as the other options.

The last one I want to touch on a bit more than I did in the SSO post is the client_credentials grant, which is designed for use in Machine-to-Machine scenarios. It exists outside the context of a user, so this grant type tends to either be very privileged or limited to a small number of scopes.

This requires the pre-sharing of secrets between the authorization server and the client and as such it requires a client_secret. Ideally you get this secret to the client securely because anyone in possession of it will be able to log in with this grant.

The flow is dead simple:

  1. Send the client_id and client_secret to the /oauth/token endpoint and get an access token back.

How long that token is good for is typically configurable at the authorization server. Auth0, for example, defaults this to 8 hours (and they charge based on how many times you do this).

You'd use this grant if you have some backend that needs to interact with protected resources, and the client can be restricted to requesting certain scopes.

Group of Wolves
David Dirga @ Shutterstock #358007024

😈 I've never used Kerberos professionally so this is going to be pretty light on details.

Like other things I've talked about, Kerberos uses cryptography to use its magic, and it can work with either symmetric or asymmetric keys.

For the symmetric flow, it works kinda like this:

  1. The user sits down to a computer and types in their username (or provides their identifier some other way).
  2. The Authentication server does a lookup to see if that client's registered, and if it is, it'll encrypt a session token with the user's password (or private key) and send that back to the client.
  3. The client enters their password which is used to try to decrypt that key.
  4. Assuming everything goes well that session token is used for any further communications with the server.

There a few different ways to do this, all of which I'm only familiar with on the end-user side of things, but you'll find this a lot in corporate environments, and you usually won't have to think about it or even realize it's happening. One example is being able to log into corporate Wi-Fi and have any internal applications already know who you are. I've seen this implemented using a CBA solution, where a certificate is pushed to the device in question and that certificate is then used to authenticate the device to the network.

CBAs require you to set up a Certificate Authority (CA) that can issue certificates and is trusted by the device (which you can force via the next section).

😈 This is also how HTTPS works, but the CA is trusted by...general consensus that the CA is safe.

Basically, the way that this works is each certificate can contain information about the client encoded in it, which is signed with a private key that only the CA holds, so you can use that certificate to validate who the client is and that their client certificate has not been tampered with.

How this works in practice is that when you're authenticating with something, you can send that certificate and have your backend verify the Certificate with the CA and then make authentication decisions based on that. But, to get this to work with websites magically... you have to do some interception shenanigans. For example, having the device use that certificate instead of ... regular SSL certificates.

And how might one do that? Let's talk about Mobile Device Management.

If you're working at a big company and your work computer was issued by an IT department, you probably have some sort of MDM installed on your device. MDMs allow a centralized server to do all sorts of things to that device, like install applications, remotely wipe the machine (in the case of loss or theft), or identify the machine to the network. We're going to focus on that last part.

One major MDM vendor is jamf, and it's the one I've got the most experience as an victim end-user of. On Mac, it uses the same certificate process to gain control of the device (and shows you exactly what it can do in that certificate).

😈 Your machine almost certainly also includes some sort of monitoring solution to look for policy violations. Assume anything you do on a work machine is being monitored.

So, remember the section right before this where you can install a trusted CA on a device? Using an MDM is a way you can do this (you can also image the machine before sending it to the end user, but then you have to worry about updates somehow). So, here's how an MDM facilitates the magical auth from the section above.

  1. Have the MDM solution identify a user via whatever auth mechanism you want (installing an MDM profile and then having them log in for credentials is what I've seen).
  2. The MDM and the CA create a certificate for the user and device and pushes that certificate to the device.
  3. The MDM configures the client machine to both trust and use that CA for all of its traffic.
  4. Additional shenanigans are in place to configure say a browser to use that certificate essentially as a man-in-the-middle to intercept traffic and present that certificate to a website or internal application to do auto-logins.

That does mean that the traffic going to those (or all) websites is encrypted by the custom certificate meaning the company can see encrypted traffic to those websites. For an example of someone figuring this out, check out this Stack Exchange post. Because your certificate is also unique to you, they can introspect that traffic.

😈 BTW big companies are probably doing telemetry on this traffic, but in my experience they're not going to proactively go poking around in the giant piles of data unless they have a reason to.

But yeah, it is pretty magical not having to type in your credentials to business applications.

Right, we're done here. I hope you enjoyed this series. If you'd like to see more long-form deeper dives on topics, let me know on the socials or here in the comments.