General
Stop Returning “Access Denied”
Anusha Mukka DEV Community 周榜
3 views
Security teams spend a lot of time deciding whether a request should be allowed. The person making that request experiences the decision through a much smaller interface: usually a status code and a sentence.
This is Part 3 of Security Infrastructure in Practice, a series about what happens when security design meets production systems.
A user tries to export a report and receives:
403 Forbidden
The service is technically correct. It is also about to create a support ticket.
“Access denied” does not tell the user whether the request violated policy, an approval expired, or an identity dependency failed. Those cases may produce the same HTTP status. They do not have the same remedy.
Authorization errors are part of the product interface. Treating them as an afterthought makes secure systems harder to use and harder to operate.
Separate Effect From Reason
The enforcement effect is usually small: allow or deny. The reason needs more structure.
{
"effect": "deny",
"reason": {
"code": "APPROVAL_EXPIRED",
"message": "The approval for this export has expired.",
"next_step": "Request a new export approval."
},
"decision_id": "dec-9f31c2"
}
The code is stable for software. The message can change without breaking clients. The decision identifier connects the user-facing error to protected diagnostics.
Do not make clients parse prose. Someone will build a workflow around the exact punctuation.
Unknown Is Not the Same as No
Suppose a device posture service times out. The authorization layer cannot confirm that the device meets policy.
Failing closed may be correct for the operation. Returning DEVICE_NOT_COMPLIANT is still wrong because the system did not establish noncompliance.
Use a distinct reason:
DEVICE_STATUS_UNAVAILABLE
That tells the user to retry or contact the service owner. It tells operations to inspect the posture dependency. It avoids creating a false security finding against the device.
Give Different Readers Different Detail
The requester should not receive the entire policy trace. Detailed explanations can reveal group names, thresholds, or the existence of restricted resources.
I split the response by audience.
A requester sees a safe reason and a next step:
This export requires a current approval.
Request a new approval and try again.
The service owner sees policy identifiers and dependency status:
policy=restricted-export@4.3
reason=APPROVAL_EXPIRED
decision=dec-9f31c2
An authorized investigator can retrieve the attribute provenance and evaluation trace using the decision identifier.
The explanation endpoint needs authorization of its own. Otherwise it becomes a convenient policy-discovery tool for an attacker.
Make the Next Step Real
A message is not actionable because it contains a link. The linked workflow must apply to the reason.
“Contact your administrator” is rarely useful. Which administrator? What information should the request include? Can the person receiving it change the outcome?
For common denial reasons, define an owner and a tested remediation. If there is no remediation because the action is prohibited, say that plainly.
REASON_CATALOG = {
"APPROVAL_EXPIRED": {
"message": "The approval for this export has expired.",
"next_step": "request_export_approval"
},
"DEVICE_STATUS_UNAVAILABLE": {
"message": "Device status could not be verified.",
"next_step": "retry"
},
"EXPORT_NOT_PERMITTED": {
"message": "This report cannot be exported.",
"next_step": None
}
}
Test What the User Sees
Authorization tests often stop after asserting the effect. Add assertions for the reason and the absence of sensitive detail.
def test_expired_approval_is_actionable():
response = export_report(approval=expired_approval())
assert response.status_code == 403
assert response.body["reason"]["code"] == "APPROVAL_EXPIRED"
assert response.body["reason"]["next_step"] == "request_export_approval"
assert "required_clearance" not in response.body
Track whether users succeed after following the suggested next step. If the same person repeats the denied action five times, the explanation probably did not help.
A Denial Can Still Be a Good Experience
Security controls will block legitimate people sometimes. That does not mean the control is wrong. It means the path out of a safe denial deserves design work.
Return a stable reason code. Give the user one safe explanation. Preserve the detailed evidence somewhere protected.
403 Forbidden can remain the HTTP status. It should not be the entire conversation.
What is the most useful, or most frustrating, authorization error you have encountered?
Read original: https://dev.to/anusha_mukka/stop-returning-access-denied-197b
← Previous
I Moved Everything to Kubernetes, Then Moved It Back
Next →
How to find LM Studio plugins and MCP servers
Related
Your Cache Is Part of the Security Model
General
1
DEV Community 周榜
Why Most Medicine Reminders Fail (And How We Built One That Refuses to Be Ignored)
General
1
DEV Community 周榜
How to find LM Studio plugins and MCP servers
General
1
DEV Community 周榜
I Moved Everything to Kubernetes, Then Moved It Back
General
0
DEV Community 周榜
Comments0
No comments yet — be the first