Dev.to Security 🔐 Cybersecurity 👁 0 📖 41 min read

Authentication From First Principles: How Login Actually Works, From Google Sign-In to Passkeys

Understanding authentication, OAuth, PKCE, sessions, cookies, and passkeys — one problem at a time. Think about how many websites you use every day. You watch videos on YouTube, browse Instagram, send messages, or work

Understanding authentication, OAuth, PKCE, sessions, cookies, and passkeys — one problem at a time.

Think about how many websites you use every day.

You watch videos on YouTube, browse Instagram, send messages, or work on your favorite applications.

Most of the time, you don't even think about authentication. You open a website, and somehow, it already knows who you are.

Your profile is there. Your settings are there. Your saved content is there.

But have you ever wondered:

How does a website actually know it's you?

And more importantly, how does it know that someone else isn't pretending to be you?

That's where authentication comes in.

When I started exploring authentication, I realized something interesting. We use login systems almost every day, but there's quite a lot happening behind that simple Sign In button.

Google Sign-In, OAuth, PKCE, cookies, sessions, passkeys — you've probably heard some of these terms.

But why do we need so many things just to log someone in?

Instead of memorizing definitions, let's build an authentication system in our heads, starting with the simplest possible solution.

Every time we encounter a problem, we'll figure out why it happens and introduce the mechanism designed to solve it.

By the end, we'll understand not just how authentication works, but why it works that way.

1. Why Do We Need Login in the First Place?

Imagine you're building a website like YouTube.

People can visit your website and watch videos.

So far, so good.

But now someone wants to save videos to a playlist, subscribe to a channel, or receive personalized recommendations.

That creates a problem.

How do you know which playlist belongs to which person?

Suppose two people are using your application.

One person has saved 20 videos, and the other has saved 50.

Your application needs to recognize them so it can show each person the correct data.

You might think:

"That's simple. I'll just ask them for their username."

Okay, but what stops me from entering someone else's username?

Nothing.

If your application simply trusts whatever name someone enters, anybody could pretend to be anybody.

So knowing someone's name isn't enough.

We need a way for the user to prove their identity.

And that's the fundamental problem authentication solves.

Authentication vs. Authorization

Before going further, we need to understand two concepts that sound similar but solve different problems.

Let's use a driving license as an example.

Imagine someone asks you to prove who you are.

You show them your driving license.

They check your identity.

That's similar to authentication.

Authentication answers one question:

Who are you?

Now imagine you want to drive a heavy commercial vehicle.

Your license might prove your identity, but does it mean you're allowed to drive every type of vehicle?

Of course not.

Your license may permit you to drive a car but not a heavy truck.

That's where authorization comes in.

Authorization answers a different question:

What are you allowed to do?

The same thing happens inside an application.

Suppose you log in to YouTube.

The website recognizes your account. You're authenticated.

But that doesn't mean you can delete someone else's videos or modify their account settings.

You're authenticated, but you aren't authorized to perform those actions.

Authentication proves who you are. Authorization determines what you can access or do.

And here's something important: authentication alone doesn't make an application secure.

Even after a user logs in successfully, the backend must check whether that user is authorized to access the requested resource.

Now that we understand why login exists, let's figure out how to implement it.

2. How Do We Actually Authenticate Someone?

There are several ways to authenticate users.

We could ask them to create a password. We could let them sign in using Google. Or we could use something called a passkey.

All three can help establish someone's identity, but they work differently.

Let's start with something almost everyone recognizes:

Continue with Google.

You've probably clicked that button hundreds of times.

But what actually happens when you click it?

Our First Attempt: Just Ask Google

Imagine we're building our own application.

Instead of managing passwords ourselves, we want Google to authenticate users for us.

Our first idea might look like this:

Our Application
       |
       v
     Google
       |
       v
User signs in
       |
       v
Google sends a token
       |
       v
Our Application

It sounds reasonable.

Google already knows the user, so why not let Google verify their identity and send us proof?

But there's a problem.

How should Google send that proof back to our application safely?

To understand the solution, we need to look at two protocols that frequently work together.

OAuth 2.0 vs. OpenID Connect

You've probably heard that Google Sign-In uses OAuth.

That's true, but there's an important distinction.

OAuth 2.0 is primarily about delegated authorization.

For example, imagine an application wants permission to read certain information from your Google Calendar.

You shouldn't have to give that application your Google password.

Instead, Google can ask whether you want to grant the application specific permissions.

The application can then receive an access token for those permitted operations.

But remember our original problem?

We're not necessarily trying to access the user's Google Calendar.

We're trying to figure out who the user is.

That's where OpenID Connect (OIDC) comes in.

OIDC adds an identity layer on top of OAuth 2.0.

It allows an application to authenticate a user and receive an ID token, containing information about the authenticated identity.

So when we talk about Google Sign-In, we're usually talking about an OpenID Connect authentication flow built on OAuth 2.0.

Don't worry about remembering the terminology yet.

The important distinction is:

An access token is intended for accessing protected resources. An ID token is intended to communicate information about authentication and identity to the client application.

Now let's return to our application.

We know Google can authenticate the user.

But how do we safely retrieve the result?

3. The Authorization Code: Why Not Send Tokens Directly?

Imagine a user clicks Continue with Google.

Our application redirects their browser to Google.

They sign in, and Google needs to send the result back.

Our first thought might be:

"Why doesn't Google simply put the token in the redirect URL?"

Something like:

https://ourapp.com/callback?token=...

But URLs can end up in places we don't want sensitive credentials to appear, including browser history, logs, and other systems.

We don't want to unnecessarily expose important tokens during browser redirects.

So how can Google tell our application that authentication succeeded without putting the final tokens in the redirect?

The Solution: An Authorization Code

Instead of sending the final tokens through the redirect, Google sends a temporary authorization code.

The flow becomes:

User clicks "Continue with Google"
               |
               v
       Redirect to Google
               |
               v
        User authenticates
               |
               v
 Google redirects back with
      Authorization Code
               |
               v
   Our application exchanges
   the code directly with Google
               |
               v
       Tokens are returned

The authorization code is short-lived and intended for a limited exchange.

Our backend sends it to Google's token endpoint, together with the other required information, to obtain tokens.

This is much better than putting the final tokens in the browser redirect.

But notice something.

We've introduced a new credential: the authorization code.

And that creates another question.

What if someone tries to interfere with the login callback?

4. The State Parameter: Did We Actually Start This Login?

Let's imagine our application receives a request:

https://ourapp.com/callback?code=xyz123

There's an authorization code in the URL.

But how does our application know that this callback belongs to a login attempt started by the current user?

What if someone tries to trick another person's browser into completing an authentication flow that the person didn't initiate?

This is a form of login-related CSRF risk.

We need a way to connect an incoming callback to the original authorization request.

The Solution: state

Before sending the user to Google, our application generates a random value.

Let's call it:

state = random_value_123

Our application securely associates this value with the user's authentication attempt.

Then it includes that value in the authorization request sent to Google.

When Google redirects the browser back, it returns the same state.

Our application checks:

State we originally created
            ==
State returned by Google

If they match, the response is associated with the login flow we started.

If they don't match, we reject the callback.

In a real implementation, state must be unpredictable and bound to the correct browser-initiated flow. Simply trusting any returned value would defeat its purpose.

So state helps us protect the integrity of the login flow.

But we're still not finished.

Because state answers one question:

Does this callback belong to a login flow we started?

It doesn't, by itself, solve every problem involving a stolen authorization code.

And that brings us to PKCE.

5. PKCE: What If Someone Steals the Authorization Code?

We introduced authorization codes because we didn't want to expose the final tokens during the redirect.

But an authorization code is still valuable.

Suppose an attacker somehow obtains a valid authorization code.

What stops them from trying to exchange that code for tokens before the legitimate application does?

That's the problem PKCE helps solve.

PKCE stands for Proof Key for Code Exchange.

The name sounds complicated, but the idea is surprisingly simple.

Instead of allowing the authorization code alone to be sufficient for redemption, we make the client prove that it possesses a secret value associated with the original authorization request.

Step 1: Create a Secret

Before redirecting the user to Google, the client generates a cryptographically random value.

This is called the code verifier.

Think of it as a secret the client must remember during this particular login attempt.

For example:

Code Verifier = long_random_secret

The client doesn't send this verifier in the initial authorization request.

Instead, it derives another value from it.

Step 2: Create the Code Challenge

Using the recommended S256 method, the client hashes the code verifier with SHA-256 and applies the required URL-safe encoding.

This produces the code challenge.

Conceptually:

Code Verifier
      |
      v
SHA-256 + URL-safe encoding
      |
      v
Code Challenge

Here's the interesting part.

It's easy to create the challenge from the verifier.

But knowing the challenge doesn't practically allow someone to recover a properly generated random verifier.

The client sends the challenge to Google when starting authorization.

Google associates it with that authorization request.

Step 3: Exchange the Authorization Code

After the user signs in, Google redirects the browser back with the authorization code.

Now the client wants to exchange that code for tokens.

But this time, the request must also include the original code verifier.

Google derives the challenge from that verifier and compares it with the challenge associated with the authorization code.

If they match, the PKCE check passes.

Otherwise, the code exchange is rejected.

Why Does This Help?

Imagine someone steals the authorization code but doesn't have the corresponding code verifier.

The code alone is no longer enough.

They need the verifier associated with the original authorization request.

That's the protection PKCE adds.

Of course, if an attacker compromises the verifier too, PKCE cannot magically stop them. It also doesn't replace HTTPS, client authentication where required, or other OAuth security checks.

But it closes an important gap.

Now we have three separate pieces that make much more sense together.

The authorization code keeps the final tokens out of the authorization redirect.

The state parameter helps connect the callback to the login attempt our application initiated.

And PKCE makes a stolen authorization code insufficient for redemption without the associated verifier.

Each one solves a different problem.

Now let's say all these checks pass.

Google successfully authenticates the user.

What happens next?

6. Google Knows Who You Are. But How Does Our Application Know?

At this point, Google has authenticated the user, and our application has successfully exchanged the authorization code.

For an OpenID Connect login, Google returns an ID token.

But should our backend immediately trust whatever identity information it receives?

Of course not.

The backend first needs to validate the ID token.

This includes checking its cryptographic signature, whether it was issued by the expected provider, whether it was intended for our application, and whether it has expired.

Depending on the authentication flow, we may also need to verify other values, such as a nonce, which helps bind the ID token to the authentication request.

Once the token passes the required checks, our application can trust the verified identity claims.

An important claim is sub, which is Google's stable identifier for that user.

Why is that useful?

Because email addresses can change, and they're not always a reliable primary identifier for an external identity.

We can associate the Google account's verified identifier with a local user record in our database.

For example:

Google authenticates user
           |
           v
Our backend verifies ID token
           |
           v
Read Google's user identifier
           |
           v
Find existing application user
       or create one

Now our application knows which local account belongs to the authenticated Google user.

Great!

But here's another problem.

Does the User Have to Sign In with Google on Every Request?

Imagine you're watching YouTube.

You open the homepage, search for a video, open a channel, and then save something to a playlist.

Does YouTube have to send you through the complete Google authentication process for every action?

That would be incredibly inconvenient.

And for our application, repeatedly starting a new external login flow would be unnecessary.

We already verified the user's identity.

Now we need a way to remember that authentication across subsequent requests.

That's where sessions come in.

7. Sessions: How Does a Website Remember You're Logged In?

Let's think about the problem.

When you visit a website, your browser sends an HTTP request.

The server processes that request and returns a response.

Later, you click another page or perform another action.

Your browser sends another request.

But HTTP requests don't automatically remember that you authenticated earlier.

So how does the server connect all those requests to the same logged-in user?

The Solution: Create a Session

After successfully authenticating someone, our backend creates a session.

Think of a session as a record that says:

"This user successfully authenticated, and this login is currently valid."

A simplified session record might look like this:

Session
-------------------------
Session ID: random_abc123
User ID:    user_42
Created:    10:00 AM
Expires:    11:00 AM

In practice, the session identifier must be generated using a secure random number generator and should be much harder to guess than our example.

The backend can store the session in a database or a session store such as Redis.

But there's a new problem.

The backend has created a session.

How does the browser know which session belongs to it?

Cookies: The Browser's Session Identifier

Our backend can send a cookie containing an unpredictable session identifier.

For example:

Set-Cookie: session=abc123;
            HttpOnly;
            Secure;
            SameSite=Lax;
            Path=/

The browser stores the cookie.

Now, whenever the browser makes an appropriate request to our application, it automatically includes that cookie.

The backend can look up the corresponding session and determine which user is authenticated.

The process looks like this:

Browser
   |
   | Request + Session Cookie
   v
Backend
   |
   | Look up session
   v
Database / Redis
   |
   | Session belongs to user_42
   v
Backend identifies user
   |
   | Check authorization
   v
Return requested data

Notice something important.

The browser usually doesn't need to store the user's full information inside the session cookie.

For this server-side session design, it only needs to hold an opaque identifier.

The actual session state stays on the server.

That's useful because the server can expire or revoke the session independently of the browser.

Also, storing a session in Redis rather than a database doesn't fundamentally change how authentication works.

It's a storage and performance decision.

At this point, our user can browse the application without signing in again on every request.

Problem solved?

Not completely.

Because we've just introduced a new credential.

The session cookie.

And if someone gets access to a valid session cookie, they may be able to use it to impersonate the user.

So how do we protect it?

8. HttpOnly: What If JavaScript Tries to Steal the Cookie?

Imagine you're logged into our application.

Your browser contains a valid session cookie.

Now suppose an attacker manages to inject malicious JavaScript into one of our pages.

This could happen because of an XSS vulnerability.

XSS stands for Cross-Site Scripting.

Depending on the vulnerability, malicious JavaScript may be able to read information from the page or act within the user's authenticated browser session.

If our session cookie is accessible through JavaScript, an attacker might try to read it.

For example:

document.cookie

We don't want ordinary page scripts to have access to our session credential.

The Solution: HttpOnly

We can mark the session cookie with the HttpOnly attribute.

Set-Cookie: session=abc123; HttpOnly

This tells the browser not to expose the cookie through normal JavaScript APIs such as document.cookie.

That's helpful.

But here's a question I had when learning about this:

If JavaScript can't read the cookie, how does the application send it to the backend?

The answer is that JavaScript doesn't need to read it.

The browser handles cookies.

When a request matches the cookie's scope and applicable browser rules, the browser can attach the cookie automatically.

So HttpOnly restricts JavaScript access without preventing normal cookie-based authentication.

However, there's an important limitation.

HttpOnly doesn't completely solve XSS.

Malicious JavaScript running on our site may still be able to perform actions as the authenticated user, even if it cannot directly read the session cookie.

We still need proper XSS prevention.

But the cookie is now better protected against direct theft through JavaScript.

Great.

Unfortunately, that automatic cookie behavior creates another problem.

9. CSRF: Can Another Website Use My Logged-In Session?

Remember how the browser automatically attaches cookies?

That's convenient for us.

But what if another website tricks the browser into sending a request to our application?

Let's use a simple banking example.

Imagine you're logged into:

https://bank.com

Your browser has a valid session cookie for that website.

Now you visit a completely different website.

The second website contains malicious content that tries to submit a request to bank.com.

The attacker cannot normally read your bank's HttpOnly cookie.

But under certain conditions, they may be able to make your browser send a request that includes it.

If the bank accepts that request without appropriate protections, it might perform an action you never intended.

And here's the important part:

The attacker didn't need to steal your cookie. Your browser supplied it.

This is the fundamental problem behind Cross-Site Request Forgery, or CSRF.

So how do we prevent unwanted requests from using our authenticated session?

First Defense: SameSite Cookies

Cookies can use a SameSite attribute.

This tells the browser when to include them in cross-site situations.

There are three commonly used values.

SameSite=Strict

The browser won't send the cookie with cross-site requests.

This provides strong cross-site restrictions, but it can sometimes make legitimate navigation less convenient.

SameSite=Lax

This is often a practical middle ground.

It generally prevents cookies from being included in cross-site POST requests while allowing them on certain top-level navigations using safe HTTP methods, such as GET.

SameSite=None

This allows cookies in cross-site contexts, subject to browser policies.

It requires the Secure attribute.

Applications using cross-site cookies need appropriate additional protections.

For many traditional web applications, SameSite=Lax offers useful protection.

But should we rely on SameSite alone for every sensitive operation?

Not necessarily.

Second Defense: CSRF Tokens

Imagine an attacker can cause your browser to send a request to the bank.

But what if the bank requires something additional?

Something that the legitimate application can include, but a malicious website cannot normally obtain.

That's the idea behind a CSRF token.

The application generates a secure, unpredictable value and makes it available to its legitimate frontend through an appropriate mechanism.

For protected actions, the frontend must send that value with the request.

The server validates it.

Conceptually:

Session cookie + Valid CSRF token
                |
                v
       Request can continue

But:

Session cookie + Invalid CSRF token
                |
                v
          Reject request

A malicious website might cause a browser to attach cookies, but browser security rules normally prevent that website from reading the legitimate application's CSRF token.

That's why this additional value can help protect sensitive requests.

Applications may also use Origin checks and other CSRF defenses depending on their architecture.

And one more rule matters:

Never make a state-changing operation depend on an unsafe use of GET.

A request that deletes an account or changes a password shouldn't be triggered simply by visiting a URL.

What About the Secure Cookie Attribute?

There's another important cookie setting:

Secure

This tells the browser to send the cookie only over secure HTTPS connections.

So now the three attributes make sense.

HttpOnly restricts JavaScript access.

Secure restricts transmission to HTTPS.

SameSite controls cross-site cookie sending.

They don't solve the same problem.

Each protects a different part of our authentication system.

Now that our session cookie is better protected, let's return to the user's experience.

Because there's another problem we haven't addressed.

How long should a session remain valid?

10. Session Expiration: Should a User Stay Logged In Forever?

Imagine you're watching YouTube.

You've been watching videos, exploring different channels, and saving videos to your playlists.

Then you try to like a video.

Suddenly, you're asked to sign in again.

That's frustrating.

You were actively using the website. Why did it suddenly forget who you were?

In a real application, an expired login wouldn't necessarily interrupt public video playback, but it could interrupt account-specific actions such as commenting, saving playlists, or managing subscriptions.

Now imagine you're building that application.

You need to decide how long a session should remain valid.

Let's say every session expires after one hour.

That sounds simple.

But what happens when a user is still actively using the website after that hour?

If we force them to log in again every time, the experience becomes annoying.

So we need something better.

Sliding Expiration: Extend Active Sessions

What if we extend the session while the user is actively using the application?

That's called sliding expiration.

Imagine a session has a one-hour inactivity timeout.

The user authenticates at 10 AM.

Initially, the session expires at 11 AM.

But at 10:30 AM, the user interacts with the application.

Our server can renew the session's inactivity expiration.

Now it might expire at 11:30 AM.

If the user continues interacting with the application, the expiration can move forward again.

The idea is simple:

Keep active users signed in, while allowing inactive sessions to expire.

The backend must update the relevant server-side session expiration and, when necessary, renew the browser cookie too.

We don't necessarily need to perform this update on every request. Applications can renew periodically to avoid unnecessary database writes.

This improves the user experience.

But can you see the problem?

What If the User Keeps Coming Back Forever?

Imagine someone uses YouTube every day.

Every time they return, the session gets extended.

One day becomes a week.

A week becomes a month.

And a month becomes a year.

Should that session remain valid forever?

Probably not.

Especially if someone else has managed to obtain the session credential.

That's where another concept comes in.

Absolute Session Expiration

An absolute expiration places a maximum lifetime on a session, regardless of activity.

For example, an application might use:

Inactivity timeout: 1 hour

Absolute lifetime: 30 days

These are illustrative values, not universal security recommendations.

A session can be renewed while it's active, but it cannot extend beyond the absolute expiration.

Once the maximum lifetime is reached, the application requires the appropriate fresh authentication before establishing a new fully authenticated session.

This is the balance we're trying to achieve.

Sliding expiration improves convenience.

Absolute expiration limits how long the same authentication can remain valid.

But here's something I initially wondered:

"Why not just create a new session automatically when the old one reaches its absolute expiration?"

After all, that would avoid interrupting the user.

It sounds like a clever solution.

Until we consider what happens if someone steals the session.

11. What If Someone Steals Your Session Cookie?

Imagine your session identifier is:

session = ABC123

Your browser sends that cookie to the server.

The backend checks the session store.

It finds the session, confirms that it's still valid, and identifies your account.

Everything works.

But now imagine someone steals a copy of that cookie.

They send:

session = ABC123

The backend checks the same session store.

The session exists.

It hasn't expired.

It hasn't been revoked.

So the backend may accept it.

Now we have a serious problem.

Both the real user and the attacker can present the same valid session credential.

How does the server know who's actually using it?

Based on the session cookie alone, it generally can't.

That's one of the most important things to understand about session-based authentication.

A valid cookie proves possession of a session credential. It doesn't necessarily prove that the person presenting it is the legitimate account owner.

Why Not Just Create a New Session Token?

Suppose we regularly replace the old session identifier with a new one.

This is called session rotation.

Rotation is useful. For example, replacing a session identifier after successful authentication helps protect against session fixation attacks.

But does rotating a stolen session automatically solve session hijacking?

Not necessarily.

If an attacker can use the same session credential as the real user, simply issuing another credential doesn't tell the backend which person is legitimate.

In some situations, the attacker might even obtain the newly issued credential.

So rotation is a security tool, not a guaranteed method of detecting impersonation.

We need something different when the risk is high.

The Solution: Reauthentication

Let's return to YouTube.

Imagine you're watching a video and click the Like button.

Should YouTube ask you to verify your identity again?

That would be ridiculous.

Now imagine you're trying to change your password, remove an important security method, or delete your account.

Those actions are much more sensitive.

What if the application asks for fresh proof that you're the legitimate account owner before allowing them?

That's reauthentication, sometimes called step-up authentication when stronger or additional authentication is required.

The application might ask you to authenticate again using a password, passkey, or another suitable method.

The exact choice depends on the application's risk and the available authentication methods.

The important distinction is that we're not merely checking whether the existing session is valid.

We're asking for new proof of identity.

That helps reduce the risk of someone who possesses only a stolen session cookie performing highly sensitive operations.

It isn't perfect protection against every compromised-device attack, but it's much stronger than blindly trusting the old cookie.

What Happens When the User Logs Out?

Let's consider another everyday situation.

You use a website on a shared computer and click Log Out.

Should the server simply delete the cookie from that browser?

No.

Imagine someone had copied the cookie before you logged out.

Clearing the cookie from your browser wouldn't invalidate their copy.

The server must also revoke or delete the corresponding session.

Then even if someone presents the old identifier, the backend rejects it.

What If You're Logged In on Multiple Devices?

You might be signed in on your phone, laptop, and tablet.

Ideally, each device has its own session.

That gives the application more control.

If you lose your laptop, you may want to revoke that device's session without logging out everywhere else.

Or you may want a Log Out of All Devices feature that invalidates every active session.

Both are easier when the server tracks sessions individually.

What If the Cookie Expires but the Session Still Exists?

There's one more detail worth understanding.

Browser cookie expiration and server-side session expiration are not the same thing.

Imagine your browser deletes its session cookie because the cookie expired.

But the corresponding session record still exists in Redis or your database.

The browser no longer has the identifier needed to use that session.

So even though the server-side record exists, the user may appear logged out.

The backend cannot simply recognize that browser as the old session without another valid credential.

Now imagine thousands of abandoned sessions sitting in the database.

We don't want those records accumulating forever.

Applications can periodically delete expired sessions, use TTL-based expiration where supported, or combine those approaches.

This keeps session storage manageable.

At this point, we've built a fairly complete session system.

Our application can authenticate someone, remember their identity, maintain active sessions, expire old ones, revoke access, and request fresh authentication for sensitive actions.

But notice something.

We're still depending on a user having some way to prove their identity initially or when reauthentication is required.

Google Sign-In is one option.

A traditional password is another.

And passwords introduce a whole new collection of problems.

What if we could solve those too?

12. Passwords: Why Is Something So Common Also So Problematic?

Let's forget Google Sign-In for a moment.

Imagine you're registering on a new website.

It asks you to create a password.

Okay, no problem.

You enter one.

Tomorrow, you register on another website.

Another password.

The day after that, you register on three more websites.

Three more passwords.

Now multiply that by every application you use.

Email, social media, shopping, streaming, banking, developer tools, and dozens of other services.

Are you really going to remember a different, strong password for every website?

Most people won't.

And that's where the trouble begins.

Problem 1: Reusing Passwords

Imagine you use the same password for five websites.

It seems convenient.

You only need to remember one password.

But what happens if one of those websites suffers a data breach and your password is exposed?

An attacker may try that same password on your other accounts.

Suddenly, a breach at one website threatens accounts on completely different websites.

This is one reason password reuse is dangerous.

Problem 2: Weak Passwords

Okay, maybe we'll use different passwords.

But remembering all those passwords is difficult.

So some people choose things that are easy to remember.

Their name.

Their birthday.

A familiar word.

A simple pattern.

Unfortunately, those passwords may also be easier for attackers to guess.

Attackers can attempt password guessing, use leaked password lists, or try automated attacks against poorly protected authentication systems.

Of course, websites should implement protections such as rate limiting and detection of suspicious login activity.

But weak passwords remain a problem.

Problem 3: Even a Strong Password Can Be Compromised

Now suppose you do everything correctly.

You generate a long, random password.

You don't reuse it anywhere.

You don't share it.

Surely you're safe?

You're much safer, but there's still a problem.

The website itself could be compromised.

Websites shouldn't store plaintext passwords. They should store passwords using appropriate salted password-hashing algorithms, such as Argon2id.

However, attackers who steal a password database may attempt offline guessing against those hashes.

Strong, unique passwords are much harder to recover, but a breach still creates risk.

And passwords can also be stolen through phishing.

Imagine a fake website that looks exactly like Google's login page.

You enter your password because everything looks familiar.

But you've just given the password to an attacker.

A strong password doesn't protect you if you willingly enter it into the wrong website.

So now we have several problems.

Passwords are difficult to remember, tempting to reuse, sometimes easy to guess, and vulnerable to phishing.

Even with good password storage and a strong password, security still depends on keeping a shared secret protected.

Can We Improve This?

What if we could authenticate someone without asking them to remember a password?

What if every website had its own cryptographic credential that wasn't based on a memorable phrase?

And what if a website could verify our identity without storing a password or any equivalent secret that could be used to impersonate us?

That sounds much better.

And this brings us to passkeys.

13. Passkeys: What If We Didn't Need Passwords?

Let's start with the problem we're trying to solve.

Traditional password authentication depends on a secret.

You know the password, and the website stores information that lets it check whether you entered the correct password.

Passkeys approach authentication differently.

Instead of inventing a password, your device or credential manager creates a pair of cryptographic keys.

One is called the private key.

The other is called the public key.

Together, they're known as a key pair.

Now here's the important part.

The private key remains protected by your authenticator or passkey manager.

The public key is shared with the website.

For example:

Your Device / Passkey Manager
-----------------------------
Private Key
(Kept protected)


Website Database
-----------------------------
Public Key
(Used for verification)

At first, that might sound strange.

Why are we giving the public key to the website?

And how can the website verify us without knowing the private key?

To understand that, let's use a simple analogy.

Think of a Unique Cryptographic Signature

Imagine you have a special stamp.

Only you can use it to produce a particular kind of signature.

Other people have a way to check whether a signature was created using your stamp.

But being able to check the signature doesn't mean they can create a valid one themselves.

That's roughly how digital signatures help with passkey authentication.

The private key produces a cryptographic signature.

The corresponding public key verifies that signature.

Importantly, the public key doesn't decrypt the signature.

It checks its mathematical validity.

Here's the idea:

Private Key
     |
     | Signs a message
     v
Digital Signature
     |
     | Verified using
     v
Public Key

The website doesn't need your private key.

It only needs the corresponding public key to verify that the expected private key was used.

And generating the public key from the private key doesn't mean that someone can practically reverse the process and recover the private key.

This is the foundation of passkey authentication.

What Problems Have We Solved?

Think back to our password problems.

First, the user no longer needs to invent and memorize a password for the website.

Second, because cryptographic credentials are generated rather than chosen as memorable phrases, we avoid the problem of someone selecting their birthday or name as their password.

Third, passkeys are created for a particular website or service. We're not reusing the same password across unrelated websites.

Fourth, the server stores a public key rather than a password or password hash.

Even if someone steals that public key from the database, they cannot use it alone to generate valid signatures.

That doesn't make a database breach harmless. Other information in the database could still be sensitive.

But it avoids exposing a reusable password credential.

Pretty interesting, right?

We've replaced the need to remember a secret with the ability to prove possession of a private key.

But now we have a new question.

How does the website know which public key belongs to which user?

14. Creating a Passkey: How Does the Website Know It's Yours?

Imagine you're creating an account on a new website.

You click:

Sign Up with Passkey

What should happen?

Our first thought might be:

"Generate a key pair, store the public key in the database, and keep the private key on the user's device."

That sounds correct.

But we need to think a little more carefully.

Suppose you're registering using an email address.

What's stopping someone else from entering your email address and registering their own passkey?

They would have a perfectly valid private key.

And the website would have the matching public key.

The cryptography would work.

But that doesn't mean they own your email address or your existing account.

That's an important distinction.

A valid key pair can prove possession of the private key.

It doesn't automatically prove ownership of an email address or permission to control an existing account.

First, Establish Which Account Owns the Passkey

For an existing account, the website should normally require appropriate authentication, and sometimes fresh reauthentication, before allowing a new passkey to be added.

For a new account, the website can create the account through a passkey-first registration flow.

If that new account claims ownership of an email address, the website should verify the email address before trusting the claim.

This is separate from the passkey cryptography.

Now we know which account the new credential should belong to.

But how do we create and register it securely?

That's where WebAuthn comes in.

15. WebAuthn: How Does Passkey Registration Actually Work?

WebAuthn stands for Web Authentication.

It's a web standard that lets applications use authenticators for public-key authentication.

Passkeys are based on this technology.

Let's imagine you're signed in to our application with Google and want to add a passkey.

Step 1: The User Clicks "Create Passkey"

Our backend already knows which account is requesting the operation because the user has an authenticated session.

After the necessary authorization and reauthentication checks, the backend prepares a passkey registration request.

It generates a fresh, random challenge and includes the information needed for the new credential.

Step 2: The Browser Requests Credential Creation

The application passes the registration options to the browser using WebAuthn.

The browser coordinates with an appropriate authenticator.

That might be your phone's built-in authenticator, your computer, a hardware security key, or a passkey manager.

The authenticator creates a new cryptographic key pair.

The private key stays protected by the authenticator or credential manager.

The public key can be returned to the application.

Step 3: The Device May Ask You to Verify Yourself

You might see a familiar prompt asking for your fingerprint, face recognition, or device PIN.

We'll come back to why this matters.

For now, think of it as the device checking whether it should allow you to create or use the credential.

Step 4: The Website Verifies Registration

The browser returns credential information to our application.

The backend validates the WebAuthn registration response.

This includes checking the challenge, the expected website identity, and other required properties of the response.

Depending on the application's policy, it may also examine authenticator attestation information.

If the checks succeed, the backend stores the credential information, including the public key and credential identifier, under the correct user account.

The overall flow looks like this:

User clicks "Create Passkey"
              |
              v
Backend generates challenge
              |
              v
Browser invokes WebAuthn
              |
              v
Authenticator creates key pair
              |
              v
Private key remains protected
              |
              v
Browser returns registration data
              |
              v
Backend validates registration
              |
              v
Public key + credential ID
saved under the user account

Now our website has a registered public key associated with the user's account.

The private key wasn't sent to our backend.

Great.

But we've only registered the passkey.

How do we use it when the user comes back tomorrow?

16. Logging In with a Passkey: Proving You Have the Private Key

Imagine you open the website the next morning.

You click:

Sign In with Passkey

The website already has your registered public key.

Your authenticator has access to the corresponding private key.

So how does the website confirm that the private key is actually being used?

The simplest idea might be:

"Why doesn't the device just sign a fixed message?"

For example:

I am user_42

The server could verify the signature.

But there's a problem.

What If Someone Reuses an Old Signature?

Suppose the device signs the same message every time.

An attacker somehow captures a valid signed response.

Later, they send exactly the same response to the server.

If the server accepts the same response repeatedly, the attacker might authenticate without possessing the private key.

This is a replay attack.

The signature was valid when it was created.

The problem is that the attacker is reusing an old authentication response.

How do we stop that?

The Solution: A Fresh Challenge Every Time

Instead of asking the device to sign the same message repeatedly, our server generates a fresh random challenge for each authentication attempt.

Imagine the server sends:

Challenge: X7K92_RANDOM_VALUE

The authenticator signs the required authentication data, which is cryptographically bound to that challenge and the request context.

The browser returns the authentication response.

The backend retrieves the stored public key and verifies the response.

It also checks that the challenge matches the one issued for this attempt, along with the required website and authenticator information.

Conceptually:

         WEBSITE
            |
            | Fresh random challenge
            v
       YOUR DEVICE
            |
            | Verify user if required
            |
            | Sign authentication data
            | with private key
            v
     Signed Response
            |
            v
         WEBSITE
            |
            | Verify using public key
            |
            | Check challenge
            |
            | Check website identity
            v
     Authentication succeeds

The crucial point is that the private key never needs to be sent to the website.

The backend verifies the cryptographic proof using the public key it already registered.

And because each authentication attempt uses a fresh challenge, an old response cannot simply be replayed successfully.

Now we have a working passkey login flow.

But something interesting is still missing.

What if the website itself is fake?

17. Phishing Resistance: What If Someone Copies Google's Login Page?

Let's return to the password problem.

Imagine an attacker creates a website that looks almost identical to Google's login page.

Same colors.

Same layout.

Same buttons.

Even the same logo.

You might not immediately notice the difference.

But the actual domain is:

g00gle.com

Notice the zeros instead of the letter O.

With a traditional password, you might accidentally type your credentials into that fake website.

And the attacker could capture what you typed.

But what happens with a passkey?

Passkeys Are Bound to Website Identity

Passkeys aren't simply keys that work on any website requesting a signature.

WebAuthn credentials are associated with a website identity called a Relying Party ID, usually abbreviated as RP ID.

This is based on a domain name.

The browser and authenticator enforce rules about which websites can use credentials associated with that RP ID.

The real Google website and a lookalike domain such as g00gle.com are not the same relying party.

So a credential registered for the real Google service won't normally be offered for authentication to the fake domain.

The server also verifies the expected origin and RP ID information as part of WebAuthn authentication.

That's an important security property.

A fake website can copy the appearance of a login page. It cannot simply become the legitimate website's domain by copying its design.

With passwords, users often need to notice phishing attempts themselves.

With properly implemented passkeys, the browser and authenticator help enforce that credentials are used only for their intended website.

This is called phishing resistance.

Of course, passkeys don't make every possible phishing or account-recovery attack disappear.

But they provide strong protection against the common problem of entering reusable login credentials into an impersonating website.

Now consider another situation.

Even if the website is legitimate, what stops someone else from picking up your phone and using your passkey?

18. User Verification: How Does Your Device Know It's Really You?

Imagine your phone contains a passkey.

Someone picks up the phone and tries to authenticate using that passkey.

The website knows the passkey belongs to your account.

But remember our earlier session discussion?

Possessing a credential doesn't always mean you're the legitimate owner.

The device may need to verify the person requesting the operation.

The Solution: Local User Verification

This is why passkey flows can require user verification.

Your authenticator might ask for:

  • A fingerprint
  • Face recognition
  • A device PIN or password

The specific options depend on the authenticator and device.

When successful user verification is required, the authenticator indicates that verification was performed, and the backend checks the appropriate WebAuthn flags.

But here's something important.

Your fingerprint doesn't need to be sent to the website.

The website receives a cryptographic authentication response, not your biometric data.

The authenticator handles user verification locally.

That separates two jobs.

The device verifies that the person is authorized to use the authenticator.

The website verifies that the response came from the registered credential.

And these protections work together.

One more thing: user verification isn't the same as user presence.

Simply touching a security key, for example, can demonstrate user presence without necessarily providing stronger local identity verification.

Applications must configure and check the level of verification their security requirements call for.

So now the passkey login flow makes sense.

But we're assuming the user always has access to the same device.

What if they don't?

19. Passkey Syncing: What If You Use Multiple Devices?

Imagine you create a passkey on your phone.

Everything works.

The next day, you open the same website on your laptop.

Does that mean you need to register another passkey?

Not necessarily.

Some passkeys can be securely synchronized across devices through a credential manager.

For example, a supported passkey manager may make your passkeys available on multiple devices that belong to you.

The private-key material remains protected by the credential manager's security mechanisms, rather than being sent to the website.

This corrects an easy misconception:

A passkey doesn't always exist on only one physical device.

Some credentials are device-bound.

Others are synced through supported passkey managers.

Both can use WebAuthn.

But What If the Passkey Isn't on This Device?

Suppose your passkey is available on your phone, but you're using another computer.

Depending on the platform and supported WebAuthn flow, you may be able to authenticate using your nearby phone.

The computer and phone coordinate the authentication process using a supported cross-device mechanism.

Your phone performs the credential operation.

The private key doesn't need to be copied to the computer merely to complete that login.

So far, this sounds convenient.

But there's still one important problem.

What if you lose access to your phone and your passkey manager?

20. Account Recovery: What Happens When You Lose Your Passkey?

Imagine your phone is lost or damaged.

You buy another phone.

Now you try to sign in to your account.

If your passkeys were securely synced and you can recover access to your credential manager, you may be able to restore them.

But what if you can't?

Perhaps the passkey was stored only on your old device.

Or you no longer have access to your credential manager.

Now you're locked out.

This is why account recovery is an important part of designing a passkey authentication system.

How Can We Recover Access?

One approach is to let users register more than one passkey.

For example, they might have a passkey on their phone and a separate hardware security key.

Another approach is to provide carefully designed account recovery mechanisms.

Depending on the service, these might include recovery codes, another previously registered authentication method, or an established account recovery process.

But we have to be careful.

Imagine we create an extremely secure passkey authentication system.

Then we allow someone to bypass it by entering an email address and answering a weak security question.

We've just introduced an easier way to compromise the account.

An authentication system is only as resilient as the paths that allow someone to regain access to it.

Recovery should be designed with the same care as login.

And that brings us to another interesting question.

We've discussed Google Sign-In.

We've discussed passwords.

We've discussed passkeys.

But what happens when a user wants to use more than one method for the same account?

21. Account Linking: Google Today, Passkey Tomorrow

Imagine someone registers on our website using Google.

Our backend verifies their Google identity and creates a local account.

Let's call it:

User ID: user_42

A week later, the same person decides to add a passkey.

Should our application create a completely separate user account?

Of course not.

The user wants to access the same profile, settings, and saved content.

We need to associate the new passkey with their existing account.

How Does This Work?

The user first signs in using their existing Google authentication method.

Our application recognizes their local account through the established session.

After the necessary security checks, the user chooses Add Passkey.

The WebAuthn registration process begins.

Once the new credential has been successfully registered and verified, our backend associates its credential identifier and public key with the existing user account.

Conceptually, our database might contain:

USERS
--------------------
id: user_42


EXTERNAL IDENTITIES
--------------------
user_id: user_42
provider: google
provider_user_id: ...


PASSKEY CREDENTIALS
--------------------
user_id: user_42
credential_id: ...
public_key: ...

Now the user has two ways to authenticate.

They can choose Google Sign-In or their registered passkey.

Either method leads to the same local user account.

But there's one rule we must not forget.

Never link a new authentication method to an existing account merely because someone typed a matching email address.

Remember the question we discussed earlier?

What if an attacker enters someone else's email address and creates their own passkey?

A valid key pair doesn't prove ownership of that existing account.

That's why linking credentials requires appropriate proof that the person is authorized to control the account.

The same caution applies when linking accounts from different identity providers.

Email matching alone shouldn't be treated as universally sufficient proof of account ownership.

Now we've connected the major authentication methods.

But there's one final connection that makes the overall architecture much easier to understand.

22. What Happens After a Successful Passkey Login?

Let's return to where we started.

Earlier, Google authenticated the user, and our application created its own session.

What happens when someone authenticates with a passkey?

Does the application need a completely different session architecture?

Not necessarily.

The passkey answers the question:

Can this person prove possession of the registered credential, with the required user verification?

Once the backend successfully verifies the WebAuthn authentication response, it identifies the corresponding local user.

Then the application can create a normal session.

Just as it did after Google Sign-In.

Here's the flow:

        USER
          |
          v
   Sign In with Passkey
          |
          v
      WebAuthn
          |
          v
  Backend verifies proof
          |
          v
   Identify local user
          |
          v
   Create app session
          |
          v
  Set secure session cookie
          |
          v
       Logged in

Now compare that with Google Sign-In:

        USER
          |
          v
   Sign In with Google
          |
          v
     Google OIDC
          |
          v
  Backend verifies ID token
          |
          v
   Identify local user
          |
          v
   Create app session
          |
          v
  Set secure session cookie
          |
          v
       Logged in

Notice how similar the second half is.

That's the important architectural idea.

Google Sign-In and passkeys are different ways to authenticate the user. They don't necessarily require different systems for maintaining the user's logged-in session.

Once authentication succeeds, the backend can use the same local account, session infrastructure, and authorization system.

This separation makes the application easier to reason about.

We can support additional authentication methods later without rewriting how the rest of the application manages sessions.

23. Putting Everything Together

Let's imagine we've finished building our application.

A new user visits the website.

They choose Continue with Google.

Our application starts an OpenID Connect authorization flow.

It generates the required state value and PKCE information.

The browser redirects to Google.

Google authenticates the user and returns an authorization code through the callback.

Our backend validates the response, exchanges the code, and verifies the ID token.

We use the verified Google identity to find or create a local user account.

Then we create an application session.

The browser receives a secure session cookie.

From that point on, ordinary authenticated requests use the application's session.

The user doesn't need to sign in with Google every time they click something.

As they continue using the application, we manage the session's lifetime according to our expiration rules.

We protect session cookies with appropriate settings and defend sensitive actions against CSRF and other attacks.

We also check authorization before allowing access to protected resources.

Later, the user visits the security settings and chooses Add Passkey.

After the required authentication checks, WebAuthn creates a credential.

Our backend registers its public key under the existing account.

Now the user can sign in using either Google or a passkey.

When they choose the passkey, the backend creates a fresh challenge.

The authenticator signs the required authentication data using its private key.

The backend verifies the response using the stored public key.

Then it creates an application session, just as before.

If the user later tries to change important security settings, our application may require fresh authentication.

If a session is revoked, the server refuses to accept it.

If the user loses a device, they may be able to revoke that session and use another registered authentication method or an appropriately secured recovery process.

Each part of the system has a specific responsibility.

And more importantly, each part exists because of a problem we encountered along the way.

24. The Bigger Picture: Authentication Is More Than a Login Button

When we started, our problem was simple.

We wanted the website to know which user was accessing it.

But one question led to another.

How can someone prove their identity?

How can Google authenticate them without our application collecting their Google password?

How do we make the OAuth callback trustworthy?

Why do we need state and PKCE?

How do we verify Google's identity information?

How does the application remember the user after login?

Why do cookies need HttpOnly, Secure, and SameSite?

What happens when a session expires?

What if someone steals the session credential?

How do we protect sensitive operations without constantly interrupting the user?

And finally, can we get rid of the problems associated with passwords?

That's how we arrived at passkeys, public-key cryptography, WebAuthn, fresh challenges, phishing resistance, and account recovery.

The interesting thing is that we didn't need to start by memorizing all these technologies.

We started with a simple problem and discovered why each technology was needed.

And that's how I think authentication becomes easier to understand.

Instead of looking at OAuth, cookies, sessions, and passkeys as unrelated concepts, we can see them as parts of the same system.

Authentication establishes identity. Sessions maintain that authenticated state. Authorization determines what the user can do. And security controls protect the entire process.

Google Sign-In and passkeys are simply different ways of establishing that identity.

The goal isn't to make the login flow unnecessarily complicated.

It's to make authentication secure enough to protect users while keeping the experience simple enough that they barely have to think about it.

Because when you open YouTube, save a video, and return the next day to find your account exactly as you left it, you shouldn't have to worry about everything happening behind the scenes.

You should simply be able to use the application.

And that's the real goal of a well-designed authentication system.

Authentication From First Principles — End.

Author's note: The flows and examples in this article are simplified to make the concepts approachable. Production authentication systems require additional safeguards, careful configuration, and testing. For implementation details, the official OAuth 2.0 Security Best Current Practice (RFC 9700), OpenID Connect Core, and WebAuthn specification are useful references.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.