> ## Content Index
> Fetch the complete content index at: https://blog.jesusguerra.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Authentication Is Easy Until It Isn't
- URL: https://blog.jesusguerra.io/authentication-is-easy-until-it-isnt/
- Published: 2026-08-26T21:08:39.000Z
- Updated: 2026-08-26T21:08:39.000Z
- Description: Authentication starts as a login screen and a token. Then sessions expire, requests fail, devices change, and sensitive operations require more trust. Eventually, authentication stops being a feature and becomes part of the architecture.
- Author: Jesus Guerra
- Tags: Software Engineering, Mobile Architecture, Security, iOS

Authentication is one of those parts of software that looks almost solved when you first encounter it. You show a login screen, send some credentials to an API, receive a token, store it somewhere secure, and attach it to future requests. Add logout and password recovery, and the authentication layer can feel mostly finished.

Earlier in my career, that was roughly how I thought about it. Authentication was the thing that happened before the user entered the application. After working on products where accounts and sensitive operations required stronger protection, I started seeing it differently. Logging a user in is usually the easy part. The harder problem is deciding what the system should trust after that, and for how long.

## Logging In Is Only the Beginning

A successful login tells you something very specific: at a particular moment, the user provided enough evidence for the system to trust their identity. But applications don't operate at a single moment. Sessions can last hours, days, or months. Users change passwords, tokens expire, devices disappear, accounts change state, and security settings are modified somewhere else. A backend can decide that a session should no longer be trusted even though the mobile application still has perfectly valid-looking credentials stored locally.

Then there are operations where simply being logged in isn't enough. I've worked on products where certain actions required additional verification. A user could have a completely valid session and still need an OTP, biometric confirmation, or another verification step before performing something sensitive.

That changed how I thought about authentication. There wasn't really a single boolean called `isAuthenticated`. There were different levels of trust. A user might be authenticated but not authorized for a particular operation. They might be authorized but still require recent verification. Their local session might look valid while the backend has already decided otherwise. Once those states exist, authentication stops being just a login flow.

## Sessions Have More States Than We Expect

One pattern I've seen repeatedly is that authentication complexity tends to appear gradually. The first implementation might only understand two states: logged in and logged out. Then access tokens expire, so token refresh gets introduced. Refresh tokens eventually expire too, and before long you need to handle revoked sessions, account restrictions, additional verification, device changes, password resets, or security policies that invalidate existing credentials.

Without explicitly designing for those states, they tend to end up scattered throughout the application. A networking layer retries requests, a screen detects a particular error and redirects somewhere, another feature checks whether verification is required, and secure storage contains credentials that another part of the application assumes are still valid.

Each decision can make sense individually, but together they create an authentication system without anyone really designing one.

I've learned to think of session management much closer to a state machine than a boolean flag. The exact states depend on the product, but recognizing the transitions between those states as part of the architecture makes a big difference. Expired credentials, revoked sessions, additional verification, and logout aren't isolated errors or UI events. They're transitions the rest of the application needs to respond to consistently.

## The 401 Problem

A good example is something as ordinary as receiving a `401 Unauthorized` response. The obvious implementation is to assume the session has expired and send the user back to login. Sometimes that's correct. Other times, the access token simply expired and can be refreshed without the user noticing anything.

That sounds straightforward until several requests fail at roughly the same time. If ten API requests receive a 401 simultaneously, you probably don't want ten independent token refresh attempts. One part of the system needs to recognize that a refresh is already happening while the other requests wait for the result.

From there, the behavior depends on why the refresh succeeds or fails. A successful refresh may mean retrying the waiting requests. An expired refresh token may mean ending the session. A temporary network failure probably shouldn't log the user out at all, while a session deliberately revoked by the backend isn't going to be fixed by repeatedly trying to refresh it.

What originally looked like simple error handling has suddenly become synchronization, retry behavior, session state, navigation, and user experience.

This is where I've seen authentication bugs become particularly frustrating. The happy path usually works perfectly. The difficult bugs live in transitions: the token expires while requests are running, connectivity disappears during a refresh, the application returns from the background after several hours, or account state changes somewhere else. Those are also exactly the situations that are harder to reproduce during development.

## Biometrics Don't Make the Problem Simpler

Biometric authentication adds another interesting layer because the user experience can make it appear that Face ID or Touch ID is the authentication system. Usually it isn't. In many mobile architectures, biometrics protect access to credentials or allow the application to reuse an existing trusted session. Face ID might prove that the person holding the device is allowed to access locally protected information, but the backend still has its own understanding of whether that session is valid.

A device can therefore successfully complete biometric authentication while the server-side session has expired or been revoked. The local device and the backend can temporarily disagree about what "authenticated" means, and the application needs to know how to reconcile those two states.

I've worked on security-sensitive features where these distinctions mattered much more than the login screen itself. Once you introduce OTPs, biometrics, account verification, secure credential storage, and sensitive operations, authentication becomes less about proving identity once and more about continuously managing trust.

## Authentication Eventually Becomes Architecture

What surprised me over time was how far authentication reaches into an application. The networking layer needs credentials and refresh behavior. Navigation needs to react when session state changes. Secure storage needs to manage credentials correctly. Features need to understand authorization and verification requirements. Account recovery can invalidate existing assumptions, while push notifications, analytics, and other user-specific services may also need to react when the active identity changes.

Authentication becomes cross-cutting infrastructure, but that doesn't mean every application needs an elaborate authentication architecture from day one. A small application with simple requirements shouldn't be designed as if it were a financial platform. The complexity should follow the requirements rather than anticipating every security scenario that might exist someday.

What I do think is worth asking much earlier is a deceptively simple question:

**What does a valid session actually mean in this product?**

That tends to expose the real requirements surprisingly quickly. How long should the system trust a login? What can invalidate that trust? Which actions require stronger verification? What happens when the client and server disagree? Which component owns those decisions?

The answers tell you much more about the authentication system you're actually building than the login screen does.

## Simple for the User, Complicated Underneath

I still think authentication should feel simple. Users shouldn't have to understand token expiration, refresh strategies, session synchronization, or why the backend decided their credentials needed to be renewed. Ideally, most of that complexity remains invisible.

But I've stopped expecting the system underneath that experience to be simple. Once an application has long-lived sessions, multiple devices, expiring credentials, biometrics, account recovery, sensitive operations, authorization rules, and different levels of verification, authentication isn't really a login feature anymore. It's part of the architecture.

The login screen might only take a small amount of code. The interesting engineering starts after the user taps Continue.