Lesson 84 of 159
Written lesson · 14 min

Authentication vs Authorization

Who is calling versus what they may do, and which layer answers each question.

Preview

You are viewing a free preview lesson.

So far, we have deliberately postponed security implementation, yet our workflows already assume it. Create Order assumes an authenticated CustomerId that came from a trusted identity, and Product administration assumes that only an authorized administrator may perform the operation. Now we need to make those assumptions explicit.

Before introducing Spring Security configuration, filters, tokens or authorities, we need to separate two questions. Authentication asks "Who is making this request?" Authorization asks "What is that authenticated identity allowed to do?" The two are related, but they are not interchangeable. The goal of this lesson is:

Establish a clear security model where authentication determines trusted identity, authorization determines allowed operations, and resource ownership remains an application-level rule when it depends on business data.

Authentication and Authorization

Authentication turns an incoming HTTP request into an authenticated identity. The external identity capability may, for example, establish a stable subject such as user-abc-123. Our backend does not need to know how that user originally proved who they are (password, social login, magic link, MFA) unless it owns those responsibilities, and it does not. The Order Management Backend receives the result of authentication.

Authorization asks whether this authenticated identity may perform this operation. A Customer may create an Order and cancel their own Order; an Admin may modify a Product; a Customer may not modify Inventory. Authentication gives us identity; authorization evaluates permissions using that identity and the operation being attempted.

The order matters. We cannot meaningfully ask "Is this caller an Admin?" until we know who the caller is, so authentication establishes the trusted security context from which authorization decisions are made:

Drawing diagram…

Our Security Requirements

The requirements we have accumulated throughout the course fall into two groups. An authenticated Customer can:

  • create an Order
  • view their own Orders and their own Order details
  • cancel their own eligible Order
  • pay their own eligible Order

A Customer cannot operate on another Customer's Order. An authorized Admin can create, update and deactivate Products, manage Inventory and view administrative Order data. Our current requirements do not need a large permission system; these two categories are enough for now.

Identity Is Not a Role

CustomerId answers "Who owns this Order?"; the ADMIN authority answers "What may the current caller do?" As we decided when modeling Customer (4.05), there is no record CustomerId(String value, Role role), and Order holds its customerId but no Role or boolean admin: an Admin who may view all Orders is authorized to do so, which does not make the Admin their owner, and a person who later gains Admin access keeps their historical ownership.

Identity Comes From Outside, Authority Never From the Client

Our backend does not own registration, password storage or the identity lifecycle. It receives the result of authentication from an external identity capability, so it needs a stable CustomerId, not a customers table or an IdentityService called on every request. Which token claim carries the stable subject, and how it becomes a CustomerId, is the next lesson's subject; we do not invent claim names, an issuer or an OAuth flow here. Credentials are never identity: an access token is temporary and rotatable and must never become orders.customer_id.

The client controls intent, the server controls authority. A Create Order body with "customerId": "customer-999" would let a caller create an Order owned by someone else, so the request stays items only. A Create Product body with "isAdmin": true, or a command whose extra true means "caller is Admin", would be just as wrong. The client can say "I want to order Product X, quantity 2." It cannot authoritatively say "I am Customer Y", "I am an Admin" or "I own Order Z".

Keep Spring Security Out of the Domain and Use Cases

This would be wrong inside Order:

<!-- fragment -->
java
public void cancel() {
    Authentication authentication =
            SecurityContextHolder.getContext().getAuthentication();

    // determine current user...
}

Now the domain depends on Spring Security, thread-local security state and framework infrastructure. Our domain should stay testable with plain Java, without authentication infrastructure. The same applies one layer out: createOrderUseCase.execute(authentication, command) makes application code know Spring Security directly. Instead, the security/HTTP boundary extracts the application concept, and the use case receives only what it needs:

<!-- fragment -->
java
CustomerId customerId = currentUser.customerId();
Order order = createOrderUseCase.execute(
        new CreateOrderCommand(customerId, items));

A useful mental model is that security integration is an adapter: it translates Spring Security's Authentication into an application-friendly identity. In this module that adapter becomes a CurrentUser abstraction, built in the next lesson, from which the handler obtains a CustomerId without exposing Spring Security throughout the application.

Authorization Has Two Levels

For our application, authorization happens at two levels that solve different problems.

Endpoint-Level Authorization

Some operations have simple role requirements. These are Admin only:

http
POST /api/v1/products
PATCH /api/v1/products/{productId}
POST /api/v1/products/{productId}/deactivate
PUT /api/v1/inventory/{productId}

The decision does not require loading a Product or Inventory row to discover who owns it: the caller either has the ADMIN authority and reaches the handler, or is rejected. That is a good fit for security-layer authorization.

Resource-Level Authorization

Now consider POST /api/v1/orders/{orderId}/cancel. Knowing that the caller is a CUSTOMER is not enough; we also need to know whether this Order belongs to this Customer. That depends on application data, so the decision needs the authenticated CustomerId plus the Order's ownership. This is resource-level authorization.

It also shows that authentication does not prove ownership. Customer A may be fully authenticated and request an Order belonging to Customer B: authentication succeeds, but authorization must still fail. Authenticated does not mean authorized for every resource, and this distinction is fundamental.

Our Order operations already scope such queries by owner, findOwnedById(orderId, customerId) and the locking findOwnedForUpdate(orderId, customerId), so a missing Order and another Customer's Order both produce 404 ORDER_NOT_FOUND rather than a 403 that would reveal the Order exists (5.05). Lesson 8.03 enforces this across every Customer endpoint. For a Customer calling an Admin Inventory endpoint, an explicit 403 ACCESS_DENIED is perfectly appropriate.

401, 403 and 404

Our error model (7.13) already established three meanings, and the security implementation must preserve them instead of introducing ad-hoc responses:

SituationCodeStatus
No valid authenticationAUTHENTICATION_REQUIRED401 Unauthorized
Authenticated, but lacks permission for the operationACCESS_DENIED403 Forbidden
Customer cannot see the requested OrderORDER_NOT_FOUND404 Not Found

A 401 never reaches business processing and is never thrown by the domain. A 403 means the caller's identity is known but lacks authority, as when a regular Customer sends PUT /api/v1/inventory/{productId}.

Spring Security draws the first two lines for us. With a bearer-token resource server, an Admin-only rule on PUT /api/v1/inventory/** and every other endpoint requiring authentication, a quick MockMvc run against Spring Boot 4.1 gives:

text
GET /api/v1/orders, no token            -> 401
  WWW-Authenticate: Bearer resource_metadata="http://localhost/.well-known/oauth-protected-resource"
GET /api/v1/orders, invalid token       -> 401
  WWW-Authenticate: Bearer error="invalid_token", ...
PUT /api/v1/inventory/p1, no ADMIN role -> 403
  WWW-Authenticate: Bearer error="insufficient_scope", ...
PUT /api/v1/inventory/p1, ADMIN role    -> 200

The statuses and WWW-Authenticate challenges (RFC 6750) are right by default, but the bodies are empty. Later in this module we keep these statuses and add our ApiProblem bodies, so security errors look like every other error in the API.

Customer and Admin Are Authorization Concepts

Our requirements use Customer and Admin as application language. We should not assume that the external identity provider literally sends ROLE_CUSTOMER or ROLE_ADMIN claims. The security integration may need to map the provider-specific representation into application authorities. This lesson defines the application rule; BACKEND-118 settles the provider contract (a roles claim mapped to ROLE_ADMIN, lesson 8.04).

Security in Our Workflows

Each workflow takes its trusted inputs from this model. Create Order receives the authenticated CustomerId, so Order.customerId is trustworthy. Order history binds WHERE customer_id = :customerId to the authenticated Customer, so nobody sends ?customerId=someone-else, and Order detail, cancellation and later payment load the owned Order before anything else; the payment provider does not decide who owns an Order. Product and Inventory administration pass the ADMIN check before the handler runs, so neither Product nor Inventory ever contains if (!currentUser.isAdmin()). Whether GET /api/v1/products is public is not yet decided; we take it from the product requirement when configuring endpoint security.

Security Does Not Replace Business Rules

Security establishes that a request belongs to Customer A; the application must still validate that the Product exists, is active and has Inventory. And "the Customer owns the Order" does not mean "the Order can be cancelled": both checks are required. Domain rules should not depend on roles either. A PAID Order cannot be cancelled in v1 stays true whether the caller is a Customer or an Admin. If a future Admin refund workflow is introduced, it will be a different business operation; we do not weaken order.cancel() into order.cancel(boolean isAdmin) to anticipate requirements that do not exist.

For the same reason, roles do not go into commands without need. Avoid new CancelOrderCommand(customerId, orderId, Role.CUSTOMER) when the use case only needs a trusted CustomerId for ownership, and do not hand Admin Product use cases an isAdmin = true after the security boundary has already authorized the operation. Pass business and application inputs; do not keep propagating security implementation details.

Framework Authorization and Application Rules Complement Each Other

An annotation or a security rule can express "ADMIN authority required" or "authenticated user required", and that works well for coarse endpoint authorization. But it cannot necessarily answer "Does Customer A own Order X?" without loading application data. So:

Framework authorization and application ownership rules complement each other.

Nor do we go the other way and encode "Can this Customer cancel this specific Order in its current state?" into a complex security expression: authentication and coarse authority belong to the security boundary, ownership to the application workflow and repository, and cancellability to the Order domain.

Testing Strategy

Because of this separation, domain tests need no Spring Security, and use case tests pass an explicit new CustomerId("customer-1") to prove that a Customer can access their own Order and not another's. Only security integration tests need the wired framework: an unauthenticated request is rejected, a Customer cannot reach an Admin endpoint, an Admin can, and the authenticated identity becomes the correct CustomerId.

Our Current Security Model

The model now has four separate layers of reasoning:

LayerQuestion it answers
AuthenticationWho is the trusted caller?
AuthorizationWhich operations is the caller permitted to perform?
OwnershipDoes this specific resource belong to the authenticated Customer?
Domain lifecycleIs the requested business transition itself valid?

A cancellation request must pass all four questions in turn: is there an authenticated caller; is the caller acting as a Customer for this Customer endpoint; does this Order belong to that Customer; is the Order currently UNPAID? Then and only then do we cancel the Order and restore Inventory, and each question belongs to a different responsibility.

Product update is simpler: is the caller authenticated, does the caller have Admin authority, does the Product exist, and is the requested Product change valid? There is no Customer ownership check. Different operations need different authorization models.

Practice Exercise 1: Impersonate a Customer Locally

Solutions: lesson 8.06.

Start the application from the module 7 checkpoint (7.14) with the local profile, where LocalCurrentUser reads the Customer from the X-Customer-Id header. As customer-ana, create an Order for one unit of a stocked Product. Then act as Ben, who wants to cancel Ana's Order:

  1. With X-Customer-Id: customer-ben, request GET /api/v1/orders and POST /api/v1/orders/{orderId}/cancel for Ana's Order.
  2. Change only the header to customer-ana and repeat both requests.
  3. With no header at all, send PUT /api/v1/inventory/{productId} with {"availableQuantity": 500}.

Predict each status and code before you send it. Then, for each step, name which of the four questions of this lesson (authentication, authorization, ownership, domain lifecycle) the application answered correctly and which one it could not answer.

Practice Exercise 2: Which Layer Says No?

Assume the model of this lesson is fully built. Ana and Ben are Customers; Order A-1 is Ana's and PAID, Order B-1 is Ben's and UNPAID. For each request, name the layer that rejects it (or say that it succeeds), with status and code:

  1. POST /api/v1/orders/{A-1}/cancel with no token
  2. Ana: POST /api/v1/orders/{A-1}/cancel
  3. Ana: POST /api/v1/orders/{B-1}/cancel
  4. Ana: PUT /api/v1/inventory/{productId} with {"availableQuantity": 50}
  5. An Admin: POST /api/v1/products with {"name": "Desk Mat", "priceCents": -100}
  6. Ana: POST /api/v1/orders with a valid item and "customerId": "customer-ben" in the body

Practice Exercise 3: Review a Cancellation Shortcut

A teammate wants "Admins can cancel paid Orders" and proposes:

<!-- fragment -->
java
public record CancelOrderCommand(String customerId, OrderId orderId,
        boolean isAdmin) {
}

@PostMapping("/{orderId}/cancel")
public OrderResponse cancel(@PathVariable String orderId,
        @RequestParam String customerId,
        @RequestHeader(name = "X-Admin", defaultValue = "false") boolean admin) {
    return OrderResponse.from(cancelOrder.execute(
            new CancelOrderCommand(customerId, toOrderId(orderId), admin)));
}

// in Order
public void cancel(boolean isAdmin) {
    if (status != OrderStatus.UNPAID && !isAdmin) {
        throw new OrderNotCancellable();
    }
    status = OrderStatus.CANCELLED;
}

Find at least four problems, using this lesson's rules.

Knowledge Check

Question 1

A request carries a valid token for Customer A and asks for Customer B's Order. Which question has been answered, and which has not?

Question 2

Why is the Admin check for PUT /api/v1/inventory/{productId} a good fit for the security boundary, while Order ownership is not?

Question 3

Why does Order store a CustomerId but no role?

Question 4

Why does a Customer requesting another Customer's Order get 404, while a Customer calling an Admin endpoint gets 403?

Question 5

Why should CreateOrderUseCase receive a CustomerId instead of Spring Security's Authentication?

Knowledge Check Answers

Answer 1

Authentication is answered: we know the caller is Customer A. Authorization for this resource is not: being authenticated does not make Customer A the owner of Order B.

Answer 2

"Has ADMIN authority" depends only on the caller, so it can be decided before any data is loaded. Ownership depends on which Customer the Order belongs to, which only the application's data can answer.

Answer 3

Ownership is durable business state that must stay true for years. A role is runtime authorization state that can change at any time and says nothing about who owns the Order.

Answer 4

An Order ID is private to its owner, and 404 avoids confirming that another Customer's Order exists. Admin endpoints are part of the public API; there is nothing to hide, so 403 honestly says "known caller, not allowed".

Answer 5

The use case only needs the trusted identity. With Authentication it would depend on Spring Security, and its tests would need a security context instead of a plain new CustomerId("customer-1").

Engineering Principle

Authentication determines who the caller is. Authorization determines what that caller is allowed to do. Resource ownership is a form of authorization that may require application data and therefore cannot always be solved by endpoint-level role checks alone.

Trusted identity comes from the security boundary, never from client-controlled business input, and security infrastructure stays there: use cases receive a CustomerId, and domain objects never see Spring Security.

Summary

  • Authentication asks who is making the request; authorization asks what that identity may do, and it comes second.
  • Authentication is provided by an external identity capability; we need a stable CustomerId, not a local Customer table, and credentials such as tokens and sessions are never durable identity.
  • Identity and role are different: Order stores Customer ownership but no role, and Admin access does not change ownership.
  • The client expresses intent; customerId and Admin authority never come from the request.
  • Spring Security stays at the boundary: an adapter maps it to CustomerId and authorities, and neither use cases nor domain objects see Authentication.
  • Endpoint-level authorization handles coarse Admin restrictions; resource-level authorization, through ownership-aware queries, handles Customer-owned Orders, and missing and non-owned Orders both return 404 ORDER_NOT_FOUND.
  • 401 means no valid authentication, 403 an authenticated caller without permission; Spring Security produces both by default, and security never replaces business validation or domain lifecycle rules.
  • Domain tests need no Spring Security, use case tests use explicit CustomerId values, and security integration tests verify real endpoint behavior.

In the next lesson, Integrating Authenticated User Identity, we connect this model to the application boundary: bearer authentication through Spring Security to an AuthenticatedUser and CurrentUser, and from the stable subject to a CustomerId, while keeping Spring Security types out of use cases and domain objects and leaving the external identity system as the authority.

Think first

Coming soon

Checkpoint questions that ask you to commit to an answer before the lesson shows it.