Successful credential verification can create the impression that the main security check is already complete.
The system knows who is calling. A key or another proof has been verified. An authenticated context exists.
But a second question remains: is this caller allowed to perform this particular operation?
I treat these as two separate boundaries.
Authentication establishes a verified participant. Authorization decides which actions that participant may perform in the current context.
I Separate Two Different Questions
Authentication Who is calling? | v authenticated context Authorization Is this action allowed? | v allow or denyThese questions are related, but they are not the same.
Authorization often depends on authentication because the system first needs to know whose access is being evaluated.
But the reverse conclusion is wrong: successful authentication does not mean that every operation is automatically allowed.
Authentication Gives Me Context, Not Unlimited Access
final readonly class AuthenticatedContext{ public function __construct( public string $actorId, public string $environment, ) {}}The result of authentication may be a small object containing data the system already trusts for the current request.
For example, it may carry an internal caller identifier and the environment in which that caller was authenticated.
I do not try to make this context proof of every possible permission.
Its responsibility is simpler: tell the next step whose permission should be evaluated for the requested operation.
The Dangerous Shortcut: Valid Means Allowed
$context = $authenticator->authenticate( $request,); if ($context !== null) { $service->performSensitiveOperation( $input, );}In this code, the existence of $context silently becomes permission to act.
If every authenticated participant currently has the same access, the problem may remain invisible for a long time.
It becomes visible later when different operations require different permissions.
At that point, every place where authentication has become implicit authorization must be found and corrected.
I Authorize the Operation Separately
$context = $authenticator->authenticate( $request,); $authorizer->ensureAllowed( $context, Operation::SensitiveAction,); $service->execute( $context, $input,);I prefer making this boundary visible in code.
First, the system establishes an authenticated context.
Then a separate step decides whether the requested operation is allowed.
Only after that does the application use case run.
This does not require a large generic permission framework. Even one sensitive operation may have its own explicit authorization precondition.
The Same Caller May Have Different Permissions for Different Actions
Authenticated caller | +--> read data ALLOW | +--> change settings DENY | +--> sensitive operation DENYThis makes the distinction especially clear.
The system does not stop trusting the caller identity merely because one operation is forbidden.
A participant may be successfully authenticated and allowed to read certain data while being forbidden from changing it or performing a sensitive action.
That is a normal authorization outcome, not an authentication failure.
401 and 403 Reflect the Same Separation
No acceptable authentication | v401 Unauthorized Authentication succeeded,but the operation is not allowed | v403 ForbiddenThe same distinction matters at the HTTP level.
401 is used when required authentication is missing or unsuccessful.
403 represents a different situation: the system already has an authenticated context, but the requested action is not permitted.
These statuses do not replace the internal security model, but they demonstrate that "who are you?" and "what may you do?" are different questions.
Permission May Depend on More Than Identity
Authorization often requires more information than just the caller identity.
The decision may depend on:
- the requested operation;
- the resource being acted upon;
- the current resource state;
- the environment;
- an explicitly granted permission;
- separate access rules for a product capability or feature.
This is why I do not try to encode every future access decision into the authentication result itself.
Possession of a Credential Should Have a Narrow Meaning
Another reason to separate these checks is to avoid giving credentials too many meanings.
A key, token, or other proof may have one narrow responsibility: establish a particular participant in an interaction.
If possession of that proof automatically grants every permission, the credential silently becomes identity, access policy, and approval for future operations at the same time.
That model becomes much harder to change or restrict later.
Authorization Should Be Established Before the Privileged Action
HTTP request | vauthentication | vauthenticated context | voperation authorization | +--> DENY | +--> ALLOW | vFor sensitive operations, I prefer the application use case to receive an already established authorization precondition or to be invoked only after an explicit authorization check.
This prevents one method from mixing credential verification, resource lookup, access policy, and the actual state change.
The important part is not the number of classes. It is that permission should not appear implicitly just because the previous step succeeded.
The Client Should Not Decide Its Own Authorization
An external request may provide data required for an operation, but the final access decision should remain server-side.
For example, I would not trust a field such as "isAdmin": true merely because it came from an authenticated client.
Authentication verifies the participant. It does not make every claim supplied by that participant trustworthy.
Authorization Failure Should Not Expose Unnecessary Detail
{ "type": "https://api.example.com/problems/operation-forbidden", "title": "Operation forbidden", "status": 403, "code": "operation_forbidden", "correlationId": "..."}Separate authorization also makes public error handling clearer.
The client only needs to know that the requested operation is not allowed.
The internal reason may be more specific, but it should not automatically become part of the public contract.
The names and codes in this example are intentionally generic.
What I Check in a Security-Sensitive API Flow
For operations with stronger access requirements, I ask several separate questions:
- What exactly does authentication prove?
- Which context can be trusted after authentication?
- Am I deriving permission merely from successful authentication?
- Where is permission for this specific operation decided?
- Which data influences that decision?
- Does the server retain authority over the decision?
- Does a denial avoid exposing unnecessary internal state?
Who You Are and What You May Do Are Separate Boundaries
In a simple API, authentication and access may initially look like one check.
As soon as different operation types, sensitive changes, or separate access rules appear, that model stops scaling well.
I keep authentication narrow: establish a trusted context.
Authorization separately decides whether a particular action is allowed for that context.
Other product restrictions can remain separate again when they answer different questions.
For me, a successful "I know who this is" never automatically means "this participant may do everything".
Authentication establishes the participant. Authorization establishes the boundaries of what that participant may do.



