Authentication, Authorization, and API Trust

LESSON

Backend and API Architecture

003 30 min intermediate

Authentication, Authorization, and API Trust

The core idea: API trust is a chain of identity proof, permission decisions, and trust transport, trading convenient access for explicit control over who may do what to which resource.

Core Insight

Imagine the learning platform after the REST and GraphQL decisions are already in place. A student opens the mobile app and asks for their course progress. An instructor opens the dashboard and edits a course they own. An admin opens a console and changes a system-wide setting. All three requests may arrive with valid credentials, but they should not receive the same authority inside the API.

This is where the word "auth" becomes too blurry. A token can prove that the caller is u-17. It does not prove that u-17 may edit course 42, read another learner's progress, or see billing fields in a GraphQL response. The first question is authentication: who is this request acting as? The second question is authorization: is that principal allowed to perform this action on this resource? The third question is trust transport: how did that proof or permission information cross the boundary into this service?

The non-obvious insight is that most serious API security bugs are not caused by forgetting to install a login library. They happen when a valid identity is mistaken for a valid permission. The API receives a believable caller and then fails to ask the more specific question that the business actually depends on.

Once those questions are separated, sessions, JWTs, OAuth, and OpenID Connect become easier to reason about. They are not magic security architectures. They are ways to establish, carry, or delegate trust so that the backend can make explicit resource-level decisions.

The Trust Chain

A useful API trust chain looks like this:

incoming request
  -> verify credential, session, token, or client identity
  -> establish a principal
  -> evaluate policy for action + resource + context
  -> allow, deny, audit, or require stronger proof

The principal is the actor the system believes the request represents. It might be a user such as student:u-17, a staff account such as instructor:u-42, a machine identity such as service:grading-worker, or an external integration. Authentication is the step that attaches that principal to the request.

Authorization begins only after that. It asks what the principal may do. A good authorization decision usually includes at least four pieces of information:

That last part matters because permissions are rarely global. "Instructor" is not enough information if instructors may edit only their own courses. "Student" is not enough if students may read only their own progress. "Admin" may still require extra audit or step-up verification for destructive operations.

The trade-off is simplicity versus precision. Coarse role checks are easy to build and easy to explain, but they often fail to capture the real boundary. Resource-aware rules are more work, but they match the shape of the product more closely and prevent whole classes of accidental access.

Worked Example: Course Progress and Course Editing

Start with three requests that all authenticate successfully:

GET   /courses/42/progress        principal=student:u-17
PATCH /courses/42                 principal=instructor:u-42
POST  /admin/reindex-search       principal=admin:u-1

The API should not ask only "is the request logged in?" It should ask a policy question for each operation:

Request Policy question Likely decision
Student reads progress Is this progress record owned by the student? allow own progress, deny others
Instructor edits course Does this instructor own or manage this course? allow assigned courses only
Admin reindexes search Does this admin have system operations permission? allow with audit, maybe stronger proof

One small policy function makes the shape visible:

def can_edit_course(principal, course):
    if principal["role"] == "admin":
        return principal["permissions"].get("course:write") is True

    if principal["role"] == "instructor":
        return course["owner_id"] == principal["user_id"]

    return False

This is not a complete security framework. It is a useful discipline: name the principal, name the action, name the target resource, and then make the decision in backend code that the client cannot bypass.

The same idea applies to GraphQL from the previous lesson. A caller might be allowed to query a course title but not moderation notes, payment state, or another learner's progressForViewer. GraphQL makes the access boundary more granular because fields can have different sensitivity. REST makes the boundary more visible through resources and endpoints. Both styles need the same trust chain underneath.

The common failure is putting the permission check in the wrong place. Hiding an "Edit Course" button in the UI is helpful for experience, but it is not authorization. The endpoint or resolver still has to enforce the rule because an API client can call it directly.

Tokens, Sessions, OAuth, and OIDC

Security discussions often collapse into "Should we use sessions or JWTs?" That is a useful implementation question, but it is not the full model. These mechanisms answer different parts of the trust transport problem.

All of them can be used well or badly. A JWT with a long lifetime and broad claims can be harder to revoke than a server-managed session. A session cookie without CSRF protection or careful same-site behavior can create browser-specific risk. An OAuth integration with confused scopes can grant more delegation than the user intended. An ID token can be incorrectly treated as an access token.

The practical questions are:

That framing keeps the implementation in its place. Token validation can establish a principal and some trusted claims. It should not become a substitute for checking ownership, tenant boundaries, scopes, or resource-level permissions.

The trade-off is distribution versus control. Self-contained tokens can reduce central lookups and fit distributed systems nicely, but server-side sessions or introspection can make revocation and policy updates easier to centralize. Delegation improves integration, but every delegation flow expands the trust boundary that must be understood and audited.

Failure Modes and Design Limits

The first failure mode is treating a valid token as complete permission. This usually happens because token validation feels like the visible security step. The fix is to treat validation as the identity stage and put authorization checks at sensitive endpoints, commands, and fields.

The second failure mode is relying on broad roles when the domain needs relationships. "Instructor" should not usually mean "can edit every course." "Customer support" should not automatically mean "can see every private field." Roles are often useful, but they need to combine with ownership, tenancy, action, and resource sensitivity.

The third failure mode is allowing clients to shape data without field-level authorization. In GraphQL, one query can traverse several objects and fields. In REST, one endpoint can return a large representation. Either way, the backend must decide which parts of the response this principal may see.

The fourth failure mode is forgetting service-to-service trust. Internal workers, cron jobs, and backend services are also principals. They need identities, least-privilege permissions, audit trails, and clear boundaries. "Internal" is not a permission model.

The limit to keep in mind is that no single mechanism eliminates judgment. You can buy an identity provider, install middleware, or adopt a policy engine, but the team still has to model the product's real boundaries. Security becomes clearer when those boundaries are explicit enough to review.

Connections

The REST lesson emphasized stable resources and HTTP semantics. Authorization is where those resources become security boundaries: GET /courses/42/progress is not only a route, it is a decision about who may read a particular progress record.

The GraphQL lesson emphasized client-shaped data and resolver execution. API trust becomes more granular there because each object and field may need its own permission rule.

The next lesson on API versioning adds another pressure: once clients depend on an API contract, changing authentication flows, token claims, error shapes, or permission semantics becomes a contract evolution problem as well as a security problem.

Resources

Key Takeaways

PREVIOUS GraphQL and Client-Shaped Data NEXT API Versioning and Contract Evolution