Who You Are and What You May Do Are Different Questions

I spent three nights last year staring at a production log that looked like a crime scene, all because a junior dev thought they understood the distinction between authentication versus authorisation. They had built a beautiful, seamless login flow that felt like magic, but they’d completely neglected the permission gates behind it. The result? A standard user somehow found themselves with a direct line to the transaction settlement API. Most tutorials treat these as two neat, academic boxes to check, but in a high-stakes payment environment, that gap is exactly where the real disasters live.

I’m not here to give you a textbook definition or a diagram of a JWT. I’ve spent twenty years maintaining legacy codebases where these concepts weren’t just “concepts”—they were the difference between a stable system and a catastrophic data breach. I’m going to show you how to build the walls that actually hold, focusing on the messy edge cases where your logic will inevitably fail. We aren’t just talking about who a user is; we’re talking about preventing them from accidentally burning the whole house down.

Table of Contents

The Login Lie Why Identity Management Principles Break

The Login Lie Why Identity Management Principles Break

The problem starts when you treat a successful login as a green light for the entire session. Most tutorials stop once the JWT is issued or the session cookie is set, implying that once a user is “in,” the hard work is done. But in a production environment—especially in the messy, legacy payment systems I spend my days untangling—the difference between login and permissions is where the actual danger lives. You can have a perfectly valid identity, verified via multi-factor authentication, still walking straight into a database vulnerability because your session object lacks granular context.

I’ve seen entire microservices buckle because the developers conflated identity with capability. They build a robust authentication gate, but once the user is through, they rely on a flat structure that doesn’t account for shifting roles. When you move from simple RBAC vs ABAC models, you realize that knowing who someone is doesn’t tell you what they should be allowed to do under specific conditions, like a sudden change in transaction limits or a suspicious IP jump. If your code assumes that a valid ID equals total trust, you aren’t building a system; you’re building a house with a heavy front door and no internal walls.

Multi Factor Authentication vs Authorization the Broken Gate

Multi Factor Authentication vs Authorization the Broken Gate

People treat MFA like it’s the final boss of security, but it’s actually just a more annoying version of the front door. You can have the most robust multi-factor authentication vs authorization setup in the world—biometrics, hardware keys, the works—and still end up with a massive leak. The mistake I see constantly is thinking that because a user proved they are “John Doe” via a fingerprint, they should automatically be trusted to do anything. That’s not how it works. MFA only verifies the identity; it says nothing about what that identity is allowed to touch once they’re inside the perimeter.

The real disaster happens in the gap between the login and the actual resource request. I’ve spent too many late nights debugging systems where a user bypassed every security protocol for user access simply because the session token was over-privileged. You might have implemented a strict RBAC vs ABAC model on paper, but if your backend doesn’t re-validate permissions at the granular level for every single API call, you’ve just built a very secure entrance to a house with no internal locks. The gate is heavy, but the windows are wide open.

Five ways your permission logic will actually fail you

  • Stop treating the user object as a source of truth. By the time your code reaches the authorization check, that JWT might be stale, or worse, the user’s roles were updated in the database three seconds ago while your session is still clinging to the old permissions.
  • Don’t let your API endpoints assume that because a request is authenticated, it’s safe. I’ve seen countless systems where a user logs in perfectly, but because the developer forgot to check if `user_id` actually owns `resource_id`, they can suddenly edit anyone’s profile.
  • Avoid the “God Mode” trap in your middleware. It’s tempting to write a single check that says `if (user.isAdmin) allow()`, but that’s how you end up with massive, unmaintainable blocks of code that make it impossible to audit exactly who can touch the payment ledger.
  • Test the “No” as hard as the “Yes.” Most unit tests check if an admin can delete a record; almost none of them check if a regular user can trigger a 500 error by sending a malformed request to an endpoint they shouldn’t even know exists.
  • Log the failure, not just the success. When an authorization check fails, don’t just return a 403. Log the context—who was trying to do what, and what specific permission was missing. If you don’t, you’ll be staring at a silent security breach for months wondering why the logs look so clean.

The Reality Check

Authentication tells you who they are; authorization tells you what they can actually touch. If you conflate the two, you aren’t building a security layer, you’re just building a front door that leads to a room full of unlocked safes.

Don’t trust the “happy path” of your identity provider. A successful login token is not a blanket permission slip; you still need to verify if that specific user has the right to execute that specific database write.

Most production outages in my experience don’t happen because the login failed, but because the permission check was too broad or too brittle, causing the system to either leak data or lock everyone out when a single role changed.

## The Gap Between Knowing and Doing

Authentication is just confirming the person is who they say they are; it’s the easy part. The real nightmare begins with authorization, where you realize you’ve built a house with a heavy front door but no internal locks, leaving every room wide open to anyone who managed to step inside.

Pernille Askeland

The Residual Debt of a Broken Gate

The Residual Debt of a Broken Gate.

At the end of the day, your system doesn’t care if your identity provider is top-tier or if your MFA is unhackable if the underlying permissions are a mess. You can spend weeks hardening the login process, but if the authorization logic is just a series of leaky `if` statements scattered across a legacy codebase, you haven’t built a fortress; you’ve just built a fancy front door on a house with no walls. Authentication tells you who is standing in the hallway, but authorization is what keeps them from ripping the floorboards up. Don’t let a successful handshake mask a fundamental failure in how you manage access.

When I’m staring at a production log at 3:00 AM, I’m rarely looking at a failed login attempt. I’m looking at a user who proved exactly who they were, only to find they had the keys to the entire database. Stop chasing the perfect authentication flow and start auditing your permission boundaries. Real security isn’t found in the “happy path” of a tutorial; it’s found in the edge cases where everything goes wrong. Build for the moment the gate fails, because in twenty years of maintaining payment systems, I’ve learned that the error is the only truth you can actually rely on.

Frequently Asked Questions

If I've already verified the user via a JWT, why am I still seeing permission errors on every single database query?

Because your JWT proves who they are, not what they can touch. You’ve likely validated the signature and checked the expiration, so the token is technically “valid.” But once that payload hits the service layer, you’re hitting a wall because the token lacks the specific scopes or roles required by your row-level security. You verified the driver’s license, but you forgot to check if they’re actually allowed behind the wheel.

How do I handle a scenario where a user's roles change mid-session without forcing them to log out and back in?

The stateless dream of JWTs is exactly where this bites you. If you bake roles into a signed token, that user is a god until the token expires, even if you stripped their permissions five minutes ago. To fix it without killing the session, you have to stop trusting the token blindly. Implement a lightweight “permission check” against your cache or database on high-stakes actions. It adds latency, but it beats the alternative: a security hole that stays open until the next refresh.

At what point in the request lifecycle should the authorization check actually happen to prevent a race condition?

If you check permissions at the edge—like a gateway or a middleware layer—and then assume that state remains true when the request hits the database, you’re asking for a race condition. I’ve seen it happen: a user’s access is revoked, but the inflight request carries the old “authorized” flag straight to the write operation. You have to re-verify the authorization at the point of execution, inside the transaction, right before the data changes.

About Pernille Askeland

I write the part the tutorial skipped: what it looks like when it breaks.

Scroll to Top