Authentication and Authorization
Authentication vs Authorization
So far, we have deliberately postponed security implementation.
Our application workflows already assume concepts such as:
authenticated CustomerId
and:
ADMIN-only operation
For example, Create Order assumes:
CustomerId
→ came from authenticated identity
Product administration assumes:
only an authorized administrator
may perform this operation
Now we need to make those assumptions explicit.
Before introducing Spring Security configuration, filters, tokens, or authorities, we need to understand two different questions:
Authentication
→ Who is making this request?
Authorization
→ What is that authenticated identity allowed to do?
These concepts are related.
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
Authentication answers:
Who is making the request?
Conceptually:
Incoming HTTP Request
↓
authentication mechanism
↓
authenticated identity
For example, the external identity capability may establish a stable subject such as:
user-abc-123
Our backend does not need to know how that user originally proved their identity through:
password
social login
magic link
MFA
unless our backend actually owns those responsibilities.
It does not.
Our Order Management Backend receives the result of authentication.
Authorization
Authorization answers:
Is this authenticated identity allowed to perform this operation?
Examples:
Customer
→ create Order
Customer
→ cancel own Order
Admin
→ modify Product
Customer
→ modify Inventory
→ not allowed
Authentication gives us identity.
Authorization evaluates permissions using that identity and the operation being attempted.
Authentication Must Happen Before Authorization
We cannot meaningfully ask:
Is this caller an Admin?
until we know:
Who is this caller?
Conceptually:
Request
↓
authenticate
↓
AuthenticatedUser
↓
authorize
↓
Handler / UseCase
Authentication therefore establishes the trusted security context from which authorization decisions can be made.
Our Security Requirements
The requirements we have accumulated throughout the course can now be grouped.
Customer
An authenticated Customer can:
create an Order
view their own Orders
view their own Order details
cancel their own eligible Order
pay their own eligible Order
A Customer cannot operate on another Customer's Order.
Admin
An authorized Admin can:
create Products
update Products
deactivate Products
manage Inventory
view administrative Order data
Our current requirements do not need a large permission system.
These two categories are enough for now.
Do Not Confuse Identity With Role
These are different facts:
CustomerId
→ who owns this Order?
and:
ADMIN authority
→ what may the current caller do?
Suppose the same person later receives Admin access.
Their historical Order ownership does not change.
Therefore we do not model:
public record CustomerId(
String value,
Role role
) {
}
Identity and authorization have different responsibilities and potentially different lifecycles.
Order Stores Identity, Not Role
Our Order contains:
private final CustomerId customerId;
That records durable ownership.
It does not contain:
private Role role;
or:
private boolean admin;
because runtime authorization state is not part of historical Order ownership.
An Admin viewing an Order does not become its owner.
Authentication Comes From an External Identity Capability
Our backend does not currently own:
user registration
password storage
password reset
identity lifecycle
Those responsibilities belong outside this application.
Our security architecture is therefore conceptually:
External Identity System
↓
authentication result
↓
Spring Security
↓
application security context
↓
CustomerId / authorities
We do not create a local Customer subsystem merely because authenticated Customers exist.
No Local Customer Table Is Required
Authentication does not imply:
customers table
Our application currently needs:
stable CustomerId
to associate Orders with authenticated Customers.
That does not require:
CustomerEntity
CustomerRepository
CustomerService
CustomerController
The identity provider owns account lifecycle.
Our application owns Order state.
Authentication Result vs Application Identity
A security framework may eventually expose something like:
AuthenticatedUser
containing:
subject
authorities
The application may then derive:
CustomerId
from the trusted subject.
Conceptually:
AuthenticatedUser
↓
stable subject
↓
CustomerId
The exact external token claim or provider-specific field is not defined in this lesson.
We do not invent:
JWT claim names
issuer
OAuth flow
token URL
before integrating the real authentication contract.
CustomerId Remains an Application Concept
We already use:
public record CustomerId(
String value
) {
public CustomerId {
if (
value == null ||
value.isBlank()
) {
throw new IllegalArgumentException(
"Customer ID is required"
);
}
}
}
This represents:
stable business identity reference
It does not represent:
access token
session ID
email
display name
unless the external identity contract explicitly defines one of those values as the stable subject.
Credentials Are Not Identity
An access token may prove authentication.
But the token itself is usually:
temporary
rotatable
expiring
It should not become:
orders.customer_id
Likewise:
session ID
is not a durable Customer identity.
The backend stores:
stable identity reference
not authentication credentials.
Never Trust customerId From the Request Body
Consider:
{
"customerId": "customer-999",
"items": [
{
"productId": "9ed4c9a9-8761-4e4f-9f50-aef83f14c13d",
"quantity": 1
}
]
}
If Order ownership is determined by authentication, this API is unsafe.
A caller could attempt:
create an Order
owned by someone else
Our correct Create Order request remains:
{
"items": [
{
"productId": "9ed4c9a9-8761-4e4f-9f50-aef83f14c13d",
"quantity": 1
}
]
}
Ownership comes from:
authenticated identity
↓
CustomerId
not client input.
Client Controls Intent, Server Controls Authority
This principle now becomes especially important.
The client can say:
I want to order Product X
quantity 2
The client cannot authoritatively say:
I am Customer Y
I am an Admin
I own Order Z
Those facts come from trusted server-side context and persisted application state.
Do Not Accept isAdmin
This would be obviously wrong:
{
"name": "Mechanical Keyboard",
"priceCents": 8990,
"isAdmin": true
}
or:
new CreateProductCommand(
name,
priceCents,
true
);
where:
true
→ caller is Admin
A client cannot grant itself authority.
Admin access must come from authenticated security context.
Security Context Should Not Leak Into Domain
This would be wrong inside Order:
public void cancel() {
Authentication authentication =
SecurityContextHolder
.getContext()
.getAuthentication();
// determine current user...
}
Now the Domain depends on:
Spring Security
thread-local security state
framework infrastructure
Our Domain should still be testable using:
plain Java
without authentication infrastructure.
UseCases Should Not Depend on Spring Authentication
Avoid:
createOrderUseCase.execute(
authentication,
command
);
Now application code knows Spring Security directly.
Instead, the security/HTTP boundary extracts the application concept:
CustomerId customerId =
currentUser.customerId();
Then:
createOrderUseCase.execute(
new CreateOrderCommand(
customerId,
items
)
);
The UseCase receives only what it needs.
Security Integration Is an Adapter
A useful mental model is:
Spring Security Authentication
↓
security adapter
↓
application-friendly identity
The adapter understands framework-specific security representation.
The application does not.
Conceptually, Module 8 may introduce something like:
AuthenticatedUser
CurrentUser
or equivalent.
The exact implementation comes later.
Authorization Has Two Levels
For our application, authorization happens broadly at two levels:
Endpoint-level authorization
and:
Resource-level authorization
They solve different problems.
Endpoint-Level Authorization
Some operations have simple role requirements.
For example:
POST /api/v1/products
→ Admin only
PATCH /api/v1/products/{productId}
→ Admin only
POST /api/v1/products/{productId}/deactivate
→ Admin only
PUT /api/v1/inventory/{productId}
→ Admin only
The authorization decision does not require loading a Product or Inventory row to discover who owns it.
This is a good fit for security-layer authorization.
Conceptually:
authenticated caller
↓
has ADMIN authority?
├── no → reject
└── yes → Handler
Resource-Level Authorization
Now consider:
POST /api/v1/orders/{orderId}/cancel
Knowing that the caller is:
CUSTOMER
is not enough.
We also need to know:
Does this Order belong
to this Customer?
That depends on application data.
So authorization requires:
authenticated CustomerId
+
Order ownership
This is resource-level authorization.
Authentication Does Not Prove Ownership
Suppose:
Customer A
is fully authenticated.
They request:
Order belonging to Customer B
Authentication succeeds.
But authorization must still fail.
Therefore:
authenticated
does not mean:
authorized for every resource
This distinction is fundamental.
Ownership-Aware Repository Queries
Our canonical Order operations already support ownership-aware persistence.
For example:
Optional<Order> findOwnedForUpdate(
OrderId orderId,
CustomerId customerId
);
For ordinary detail reads we can similarly use an ownership-scoped query.
Conceptually:
WHERE id = :orderId
AND customer_id = :customerId
Then:
missing Order
and:
Order owned by someone else
both produce:
ORDER_NOT_FOUND
for Customer-facing operations.
Why Return 404 Instead of 403 for Another Customer's Order?
Suppose Customer A requests Customer B's Order.
Returning:
403 ACCESS_DENIED
can reveal:
that Order exists
Our accepted policy is to hide that distinction for Customer-owned Orders.
So:
does not exist
and:
not owned by caller
both become:
404 ORDER_NOT_FOUND
This is an application resource-access policy.
It does not mean:
403 is never useful
For a Customer calling an Admin Inventory endpoint, explicit:
ACCESS_DENIED
→ 403
is perfectly appropriate.
Authentication Error
If a protected endpoint receives no valid authenticated identity:
AUTHENTICATION_REQUIRED
maps to:
401 Unauthorized
The request never reaches business processing as an authenticated Customer or Admin.
The Domain must not throw this error.
It belongs to the security boundary.
Authorization Error
If a valid authenticated Customer tries to perform an Admin-only operation:
ACCESS_DENIED
maps to:
403 Forbidden
For example:
PUT /api/v1/inventory/{productId}
from a regular Customer.
The caller's identity is known.
They simply lack authority for the operation.
401 vs 403 vs 404
These meanings should remain clear:
401
→ caller has not been successfully authenticated
403
→ caller is authenticated
but lacks permission for this operation
404
→ requested resource is not visible/found
under this application's access policy
For Customer-owned Orders, our application may deliberately use:
404
instead of:
403
for ownership failures.
Customer and Admin Are Authorization Concepts
Our requirements currently use:
Customer
Admin
as useful application language.
We should not assume that the external identity provider literally sends:
ROLE_CUSTOMER
ROLE_ADMIN
claims.
The future security adapter may need to map provider-specific representation into application authorities.
Conceptually:
external security representation
↓
security integration
↓
application authorities
This lesson defines the application rule.
It does not invent the provider contract.
Admin Does Not Become Order Owner
Suppose an Admin can:
view all Orders
That is authorization.
It does not change:
Order.customerId
The Order remains owned by the Customer who created it.
Authorization to inspect or administer a resource is not ownership.
Create Order Security Flow
Conceptually:
POST /api/v1/orders
↓
authentication
↓
AuthenticatedUser
↓
CustomerId
↓
OrderHandler
↓
CreateOrderUseCase
↓
Order.customerId =
authenticated CustomerId
The request body contains:
items
not:
customerId
This creates trustworthy Order ownership.
Order History Security Flow
Requirement:
Customer can view only their own Orders.
Flow:
authenticated CustomerId
↓
GET /api/v1/orders
↓
GetOrderHistoryUseCase
↓
OrderRepository
↓
WHERE customer_id =
authenticated CustomerId
The Customer does not send:
GET /api/v1/orders?customerId=someone-else
Customer identity is server-controlled context.
Order Detail Security Flow
For:
GET /api/v1/orders/{orderId}
the application should use:
authenticated CustomerId
+
OrderId
to find the visible resource.
Conceptually:
findOwned(
orderId,
customerId
)
This makes ownership harder to accidentally forget.
Cancellation Security Flow
We already designed:
authenticated CustomerId
↓
CancelOrderUseCase
↓
findOwnedForUpdate(
orderId,
customerId
)
↓
order.cancel()
↓
restore Inventory
There are two separate decisions:
Does this Customer own the Order?
→ authorization/application
Can this Order be cancelled?
→ Domain lifecycle
They should remain separate.
Payment Security Flow
The same principle applies later:
authenticated CustomerId
↓
PayOrderUseCase
↓
load owned Order
↓
verify payment eligibility
↓
PaymentService
The external Payment Provider does not decide:
who owns this Order
Our application does.
Product Administration Flow
Conceptually:
PATCH /api/v1/products/{productId}
↓
authentication
↓
authorization
↓
ADMIN authority required
↓
ProductHandler
↓
UpdateProductUseCase
The Product Domain does not ask:
is caller Admin?
That is an operation-access rule, not a Product invariant.
Inventory Administration Flow
Likewise:
PUT /api/v1/inventory/{productId}
↓
authenticated caller
↓
ADMIN authority?
├── no
│ → ACCESS_DENIED
│
└── yes
↓
SetInventoryUseCase
Inventory should not contain:
if (!currentUser.isAdmin()) {
...
}
Security remains outside the Domain.
Public Product Browsing
Our existing requirements define Customer Product browsing behavior:
active Product
AND
positive Inventory
But they have not yet required us to decide whether:
GET /api/v1/products
must be:
public
or:
authenticated-only
We do not invent that access policy in this lesson.
When configuring endpoint security, we will make the decision from the actual product requirement.
Authentication Is Not Business Validation
Suppose an authenticated Customer creates an Order.
Security establishes:
this request belongs to Customer A
The application must still validate:
Product exists
Product active
Inventory available
Authentication does not replace business validation.
Authorization Is Not Domain Lifecycle Validation
Likewise:
Customer owns Order
does not mean:
Order can be cancelled
The complete cancellation decision is:
authorization
→ Customer owns Order
then:
Domain
→ status is UNPAID
Both conditions are required.
Domain Rules Should Not Depend on Roles
Consider:
PAID Order cannot be cancelled in v1
That Domain rule should remain true independently of:
Customer
Admin
If a future Admin refund workflow is introduced, that would be a different business operation.
We should not weaken:
order.cancel();
into:
order.cancel(
boolean isAdmin
);
to anticipate requirements that do not exist.
Do Not Put Roles Into Commands Without Need
Avoid:
new CancelOrderCommand(
customerId,
orderId,
Role.CUSTOMER
);
if the UseCase only needs:
trusted CustomerId
for ownership.
Likewise, Admin Product UseCases should not receive:
isAdmin = true
after the security boundary has already authorized the operation.
Pass business/application inputs.
Do not repeatedly propagate security implementation details.
Current User Abstraction
A useful future abstraction may look conceptually like:
CurrentUser
which can answer application-relevant questions such as:
authenticated identity
authorities
The Handler/security adapter can obtain:
CustomerId
or enforce:
ADMIN authority
without exposing Spring Security throughout the application.
We will implement the concrete shape in the next lesson rather than guessing prematurely.
No Direct Identity Provider Call Is Required Yet
If Spring Security can validate authentication and expose a stable subject, our application may already have everything needed for:
Order ownership
Admin authorization
There is no reason to call an external identity API for every request.
Do not automatically introduce:
IdentityService
simply because authentication is external.
Introduce an external Service only if a workflow genuinely needs to call that system.
Security Architecture
The intended architecture is:
Incoming Request
↓
Spring Security
↓
Authenticated Identity
↓
Security Integration
↓
Handler
↓
application-friendly context
↓
UseCase
↓
Domain / Repository
Spring Security stays near the outer boundary.
Domain logic stays framework-light.
Responsibility Map
External Identity Capability
Owns:
authentication
credential management
identity lifecycle
Security Integration
Owns:
interpret authenticated principal
map trusted identity
to application context
map external authorities
to application authorization
Handler / API Boundary
Owns:
receiving authenticated context
mapping HTTP input
passing trusted application identity
into UseCases
UseCase
Owns:
resource-level ownership
operation-specific authorization
when application data is required
Domain
Owns:
business invariants
lifecycle rules
and does not authenticate users.
Repository
Owns:
ownership-scoped queries
when useful for safe access
but does not read Spring Security context.
Why Controller Annotations Alone Are Not Enough
An annotation can express something like:
ADMIN authority required
or:
authenticated user required
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.
We need both.
Do Not Put Everything in Spring Security
It can be tempting to encode every rule into complex security expressions.
For example:
Can this Customer cancel
this specific Order
in its current state?
But that question combines:
identity
ownership
Order state
business lifecycle
Our existing architecture already has natural homes for those concerns.
Keep:
authentication + coarse authority
→ security boundary
ownership
→ application workflow / Repository
cancellability
→ Order Domain
This separation remains easier to reason about and test.
Testing Strategy
Security will require several levels of tests.
Domain Tests
Should not need Spring Security.
For example:
UNPAID Order can be cancelled
PAID Order cannot be cancelled
No authenticated user is required to test lifecycle behavior.
UseCase Tests
Can use explicit application identity:
CustomerId customerId =
new CustomerId(
"customer-1"
);
Then verify:
Customer can access own Order
Customer cannot access another Customer's Order
No token parsing is required.
Security Integration Tests
Later, when Spring Security is wired, verify:
protected endpoint rejects unauthenticated request
Customer cannot access Admin endpoint
Admin can access Admin endpoint
authenticated identity becomes correct CustomerId
This keeps each test focused on the layer it actually verifies.
Error Semantics
Our previous error model already established:
no valid authentication
→ AUTHENTICATION_REQUIRED
→ 401
authenticated but forbidden
→ ACCESS_DENIED
→ 403
Customer cannot see requested Order
→ ORDER_NOT_FOUND
→ 404
Security implementation must preserve these semantics.
It should not introduce inconsistent ad-hoc responses.
Common Mistakes
Avoid:
customerId from request body
for authenticated Customer workflows.
Avoid:
isAdmin from request
or from a command supplied by the client.
Avoid storing:
access token
as Order ownership identity.
Avoid using:
email
name
username
as stable identity without an explicit identity-provider contract.
Avoid:
SecurityContextHolder
inside Domain objects.
Avoid passing:
Authentication
through every UseCase.
Avoid storing:
role
on Order.
Avoid building:
CustomerEntity
CustomerRepository
customers table
without local Customer state.
Avoid assuming:
authenticated
→ authorized for every resource
Avoid using only endpoint-level role checks for resource ownership.
Avoid exposing another Customer's resource existence unnecessarily.
Security Review Checklist
For every protected operation, ask:
Does this endpoint require authentication?
Where does trusted identity come from?
Is identity coming from the request body?
Does the operation require a role/authority?
Does authorization depend on application data?
Is resource ownership involved?
Should non-owned resources appear as 404?
Does the Domain accidentally know Spring Security?
Are credentials being confused with durable identity?
Are roles being confused with ownership?
Can this rule be tested without the authentication framework?
Our Current Security Model
The model is now:
Authentication
→ establish trusted caller identity
Authorization
→ determine permitted operations
Ownership
→ determine whether a specific resource
belongs to the authenticated Customer
Domain lifecycle
→ determine whether the requested
business transition itself is valid
These are separate layers of reasoning.
Example — Cancel Order
A cancellation request must pass all of these questions:
1. Is there an authenticated caller?
2. Is the caller acting as a Customer
for this Customer endpoint?
3. Does this Order belong
to that Customer?
4. Is the Order currently UNPAID?
Then and only then:
cancel Order
restore Inventory
Notice how each question belongs to a different responsibility.
Example — Update Product
Product update is simpler:
1. Is the caller authenticated?
2. Does the caller have Admin authority?
3. Does the Product exist?
4. Is the requested Product change valid?
There is no Customer ownership check.
Different operations need different authorization models.
What We Deliberately Do Not Add
We do not yet introduce:
JWT-specific claims
OAuth flows
identity-provider URLs
token issuer details
token refresh logic
password management
Customer table
custom permission database
ACL framework
resource-policy engine
Spring Security annotations everywhere
Those details should follow actual requirements and the concrete authentication integration.
This lesson defines the security model first.
Engineering Principle
The core principle is:
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.
Another:
Trusted identity comes from the security boundary, never from client-controlled business input. The client expresses intent; the backend determines identity, authority, ownership, and business validity.
And:
Keep security infrastructure near the application boundary. UseCases should receive application-friendly identity such as
CustomerId, while Domain objects remain unaware of Spring Security, tokens, roles, and request context.
Summary
In this lesson, we established that:
- Authentication asks who is making the request.
- Authorization asks what the authenticated identity may do.
- Authentication happens before authorization.
- Customer and Admin capabilities have different authorization requirements.
- Authentication is provided by an external identity capability rather than a local Customer subsystem.
- The backend does not need a Customer table merely to authenticate Customers.
- Stable Customer ownership is represented using
CustomerId. - Credentials such as tokens and sessions are not durable Customer identity.
- CustomerId must not come from authenticated Customer request bodies.
- Admin authority must never come from client-controlled
isAdmininput. - Identity and role are different concepts.
- Order stores Customer ownership but does not store runtime roles.
- Spring Security details must not leak into Domain objects.
- UseCases should not depend directly on Spring Security
Authentication. - Security integration maps framework/external identity into application-friendly context.
- Endpoint-level authorization works well for coarse Admin/Customer restrictions.
- Resource-level authorization is required for Customer-owned Orders.
- Authentication alone does not prove resource ownership.
- Ownership-aware Repository queries help enforce Customer access safely.
- Missing and non-owned Customer Orders may both return
ORDER_NOT_FOUND. 401represents missing/failed authentication.403represents an authenticated caller lacking operation-level permission.404may intentionally hide another Customer's resource.- Admin access does not change Order ownership.
- Create Order derives ownership from authenticated CustomerId.
- Customer Order history is automatically scoped to authenticated CustomerId.
- Cancellation separates authentication, ownership, and Domain lifecycle checks.
- Payment will use the same ownership principle.
- Product and Inventory administration use Admin authorization.
- We do not yet invent whether Product browsing must be public or authenticated.
- Security does not replace Product, Inventory, or Order business validation.
- Domain lifecycle rules do not depend on role.
- A
CurrentUserorAuthenticatedUserabstraction may isolate framework security details. - We do not need a direct IdentityService call if authenticated context already provides everything required.
- Domain tests remain independent from Spring Security.
- UseCase ownership tests can use explicit CustomerId values.
- Security integration tests will verify real endpoint authentication and authorization behavior.
Next lesson:
Integrating Authenticated User Identity
There we will connect the conceptual model to the application boundary:
Bearer authentication
↓
Spring Security
↓
AuthenticatedUser / CurrentUser
↓
stable subject
↓
CustomerId
while keeping Spring Security types out of UseCases and Domain objects, preserving the external identity system as the authority, and avoiding a fake local Customer subsystem.
