Authentication vs Authorization: Two Problems People Keep Conflating

“Authentication” and “authorization” are often used interchangeably. They’re not the same problem, they’re not the same solution, and conflating them produces security architectures that fail in predictable ways.

The Definitions That Matter

Authentication: verifying who someone is.

A request is authenticated when the system has verified that the claimed identity (user, service) is legitimate. After authentication, you know who is making the request.

Authorization: determining what someone is allowed to do.

A request is authorized when the system has verified that the authenticated identity has permission to perform the requested action. After authorization, you know whether the identified party can do this.

The sequence is always: authenticate first, then authorize. You can’t check what someone is allowed to do before you know who they are.

Authentication Mechanisms

Passwords and Sessions

The traditional web authentication model: the user provides credentials, the server verifies them, and issues a session token (cookie). Subsequent requests include the cookie; the server looks up the session to verify the user’s identity.

This works. The security properties to maintain:

  • Passwords stored as hashes (bcrypt, Argon2), never plaintext
  • Sessions invalidated on logout, on password change, and with a reasonable expiry
  • Session IDs regenerated after authentication (session fixation prevention)
  • HTTPS for all requests carrying sessions

JWT (JSON Web Tokens)

JWTs are tokens that contain claims (who the user is, what roles they have) signed by the issuer. The verifying party checks the signature without contacting the issuer.

Header: {"alg": "RS256", "typ": "JWT"}
Payload: {
  "sub": "user-123",         ← Subject (user ID)
  "email": "user@example.com",
  "roles": ["admin", "user"],
  "iat": 1711000000,         ← Issued at
  "exp": 1711003600          ← Expires at
}
Signature: RS256(base64(header) + "." + base64(payload), private_key)

JWT advantages: stateless (no server-side session store), portable (works across services), self-contained (carries claims).

JWT disadvantages: hard to revoke before expiry. Once issued, a JWT is valid until it expires. Logging out a user doesn’t invalidate their JWT unless you maintain a revocation list — which reintroduces statefulness.

The practical implication: JWT expiry times should be short (15 minutes to 1 hour for access tokens). Use refresh tokens for longer-lived sessions.

Common JWT mistakes:

// NEVER accept "alg: none" — disables signature verification
// NEVER use HS256 in multi-service architectures (shared secret is a liability)
// NEVER put sensitive data in JWT payload (it's base64-encoded, not encrypted)
// ALWAYS verify the signature before trusting any claims
// ALWAYS verify exp, iss, and aud claims

OAuth 2.0 and OpenID Connect

OAuth 2.0 is an authorization delegation framework, not an authentication protocol. It allows a user to grant a third party limited access to their resources without sharing credentials.

OpenID Connect (OIDC) extends OAuth 2.0 with authentication: the authorization server issues an ID token (a JWT) that contains the user’s identity claims.

The common confusion: OAuth 2.0 access tokens say “the user authorized this application to do X” — they’re about authorization. OIDC ID tokens say “the user is who they claim to be” — they’re about authentication.

In modern web applications, OIDC is the standard approach for delegating authentication to an identity provider (Google, Azure AD, Okta, Auth0, Keycloak):

1. User clicks "Sign in with Google"
2. App redirects to Google with authorization request
3. User authenticates with Google, grants consent
4. Google redirects back with authorization code
5. App exchanges code for tokens (access token + ID token + refresh token)
6. App verifies ID token signature, extracts user claims
7. User is authenticated

The access token is used to call the identity provider’s APIs (e.g., GET /userinfo). It’s not the authentication credential for your own API — your own access tokens for your API are separate.

Authorization Models

Role-Based Access Control (RBAC)

Users have roles; roles have permissions; decisions are based on whether the user’s roles include the required permission.

// RBAC check
boolean canEditOrder(User user, Order order) {
    return user.hasRole("ORDER_MANAGER") || user.hasRole("ADMIN");
}

Simple to implement and understand. Limitations: role explosion (you end up with many specific roles), doesn’t express resource-level access (“user can edit their own orders but not others”).

Attribute-Based Access Control (ABAC)

Access decisions based on attributes of the user, the resource, and the environment.

// ABAC check
boolean canEditOrder(User user, Order order, Environment env) {
    return (user.id().equals(order.customerId())  // Own order
            || user.hasRole("ORDER_MANAGER"))       // Or manager role
           && !order.isLocked()                     // Order not locked
           && !env.isMaintenanceMode();             // Not in maintenance
}

More expressive. More complex. The tradeoff is worth it for systems with complex access control requirements.

Policy Evaluation With OPA

For complex authorization logic, Open Policy Agent (OPA) separates authorization policy from application code:

# OPA Rego policy
package orders.authz

default allow := false

allow if {
    input.method == "PUT"
    input.path[0] == "orders"
    order := data.orders[input.path[1]]
    order.customer_id == input.user.id
}

allow if {
    input.user.roles[_] == "order_manager"
}

Application code calls the OPA API to evaluate access decisions. Policy changes don’t require application code changes.

Service-to-Service Authentication

When services call each other, they need mutual authentication. Don’t use shared secrets (“the inventory service uses password X to call the payment service”) — they’re difficult to rotate and don’t scale.

Mutual TLS (mTLS): both parties present certificates. The service mesh (Istio, Linkerd) handles this transparently.

JWT-based service tokens: services authenticate with an identity provider to get a service JWT, then include it in requests to other services. The receiving service verifies the JWT signature.

In Kubernetes environments, service account tokens provide this pattern natively:

// Spring Boot: pass service token from security context
WebClient.builder()
    .baseUrl("http://inventory-service")
    .defaultHeader("Authorization", "Bearer " + serviceTokenProvider.getToken())
    .build();

Practical Authorization in Spring Boot

Spring Security’s method-level security for authorization:

@Service
public class OrderService {

    // Only users with ORDER_MANAGER role can view all orders
    @PreAuthorize("hasRole('ORDER_MANAGER')")
    public List<Order> findAllOrders() { ... }

    // Users can view their own orders; ORDER_MANAGER can view any
    @PreAuthorize("hasRole('ORDER_MANAGER') or #userId == authentication.name")
    public List<Order> findOrdersByCustomer(String userId) { ... }

    // Custom SpEL expression for complex authorization
    @PreAuthorize("@orderAuthService.canModify(authentication, #orderId)")
    public void updateOrder(String orderId, UpdateOrderCommand command) { ... }
}

For resource-level authorization (can this user modify this specific order?), pre-authorize with the ID is insufficient — you need to load the resource and check its ownership:

@Component
public class OrderAuthService {
    
    private final OrderRepository orders;

    public boolean canModify(Authentication auth, String orderId) {
        Order order = orders.findById(orderId).orElseThrow();
        return order.customerId().equals(auth.getName())
               || auth.getAuthorities().stream()
                      .anyMatch(a -> a.getAuthority().equals("ROLE_ORDER_MANAGER"));
    }
}

The Mental Model

Authentication: “I know who you are.” Authorization: “I know what you’re allowed to do.”

Build authentication first. It’s usually simpler and more standardized — use an existing identity provider rather than building your own.

Build authorization second. It’s usually more complex and domain-specific. Don’t hard-code authorization rules in controller methods — centralize them in a service that knows about your domain’s access rules.

The failure mode to avoid: treating authentication success as implicit authorization. “The user is authenticated” does not mean “the user is authorized to do everything an authenticated user might want to do.”