---
title: 'Authentication and Authorization'
source: 'https://academia.sh/en/courses/enterprise-application-framework/authentication-and-authorization'
course: 'The Enterprise Application Framework'
language: en
updated: '2026-09-13T05:08:56+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Runs an endpoint that carries an authorization annotation and shows that the decision comes from the annotation, compared against the caller's role"
    - "Shows that an endpoint carrying no authorization annotation gets its decision from the container's default, and that the same source produces two different results under two different defaults"
    - "Measures a hand-written check's decision by comparing it directly against the annotation-based decision"
---

# Authentication and Authorization

Whether an authorization decision comes from an annotation, from the container's default, or from a check written directly in the source is separated by modeling a filter chain; the lesson runs the case of an endpoint carrying no rule at all and shows that whether it is open or closed is decided by a single parameter given when the container is constructed, a difference that never appears in the source.

The last lesson of the previous topic measured how a transaction boundary opens in the source:
an annotation placed the boundary, and a call the component made from within itself never saw
that boundary at all — once the proxy dropped out, the annotation dropped out with it. This
topic asks the same question for security, testing, and distributed setup; the first stop is
authorization. Someone searching the source for who can reach an endpoint most often finds
nothing at all: no check statement, no role comparison. The decision comes either from an
annotation or from nowhere at all — from the IoC container's own default. The concepts of
authentication and authorization — who is who, how a permission is represented, the difference
between a role and a right — were built in the Authentication and Authorization and the
Identity, Access and Cryptography courses and are not repeated here; this lesson measures only
which mechanism supplies the decision. K06's question does not change for this topic either:
did a call give rise to this behavior in the source, an annotation, or neither? A filter chain
is modeled and three sources are separated: call, annotation, default. The third is the most
dangerous, because the container's default leaves no trace at all in the source; the same
source text produces two different security postures under two different defaults, and this
lesson shows that by running it.

The boundaries of this lesson are drawn from the start as well: the distinction between
authentication (the caller proving who it is) and authorization (deciding what a proven
identity may do) was built in the Authentication and Authorization course; this lesson takes
the caller's identity as an already-resolved role string and measures only the source of the
authorization decision — how the identity was proven is the subject of the next lesson. There
is no real socket, no real request body, no real session store; only the filter chain and the
container that reads the decision are modeled.

## The Decision the Filter Chain Applies

The modeled container is a small class that maps methods marked `@Endpoint` to a path name;
this is the same mechanism as the container in the Core Concepts topic — there, the annotations
that construct an object were measured, here it is the same container's decision about which
rule a request passes through that is measured. When `Gatekeeper` is constructed it scans every
method of the target object once, mapping the ones carrying `@Endpoint` to a path name; this
scan happens once at construction time, not on every call. When a call arrives, a single-link
filter chain takes over: if the method carries an `@Authorization` mark, it compares the
caller's role against the role in the mark; if there is no mark, it looks at the `defaultOpen`
field given when the container was constructed. There is no real network request, no real
session, no real server — only the chain itself and the reflection that reads the decision.

```java
// Gatekeeper.java — small IoC container that passes a request through a filter chain and gives the authorization decision from either the source annotation or the container's default (helper source, no main)
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface Endpoint { String path(); }

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface Authorization { String role(); }

class Gatekeeper {
    final Object target;
    final boolean defaultOpen;
    final Map<String, Method> endpoints = new LinkedHashMap<>();

    Gatekeeper(Object target, boolean defaultOpen) {
        this.target = target;
        this.defaultOpen = defaultOpen;
        for (Method m : target.getClass().getDeclaredMethods()) {
            Endpoint e = m.getAnnotation(Endpoint.class);
            if (e != null) endpoints.put(e.path(), m);
        }
    }

    // the filter chain's single link: reads the annotation if present, otherwise falls back to the container's default.
    boolean allow(String path, String callerRole) {
        Method m = endpoints.get(path);
        if (m == null) return false;
        Authorization a = m.getAnnotation(Authorization.class);
        if (a == null) return defaultOpen;
        if (a.role().equals("everyone")) return true;
        return a.role().equals(callerRole);
    }

    String call(String path, String callerRole) throws Exception {
        if (!allow(path, callerRole)) return "blocked";
        Method m = endpoints.get(path);
        m.setAccessible(true);
        return (String) m.invoke(target);
    }

    static void observe(String behavior, String source, Object result) {
        System.out.printf("%-40s %-11s: %s%n", behavior, source, result);
    }
}

class Endpoints {
    @Endpoint(path = "/panel") @Authorization(role = "manager")
    String panel() { return "panel-data"; }

    @Endpoint(path = "/status")
    String status() { return "running"; }

    @Endpoint(path = "/version") @Authorization(role = "everyone")
    String version() { return "version-3"; }
}
```

The source of the `Endpoints` class has no line that calls `allow`, nor any that compares
roles. The `@Authorization(role = "manager")` sitting above the `panel` method is only a mark;
the one who reads and applies it is `Gatekeeper`. The annotation types are marked
`@Retention(RetentionPolicy.RUNTIME)`, because the decision is given by reflection at run time,
not by the compiler — an annotation stripped from the class path at compile time leaves
nothing for `Gatekeeper` to read; the target is `ElementType.METHOD`, so the mark can only be
placed on methods, not carried by the class itself or by fields. **ST1.** The filter chain's
single link reads the authorization annotation and compares it against the caller's role; if
there is no annotation, it falls back to the container's own default. **ST2.** At an endpoint
carrying an authorization annotation, no call expression in the source gives rise to the
decision at all — the decision comes from the annotation.

```java
// FilterDecision.java — at an endpoint with an authorization annotation, the decision comes from the filter chain, which compares the caller's role against the annotation
public class FilterDecision {
    public static void main(String[] args) throws Exception {
        Gatekeeper gatekeeper = new Gatekeeper(new Endpoints(), true);
        Gatekeeper.observe("panel, caller manager", "annotation", gatekeeper.call("/panel", "manager"));
        Gatekeeper.observe("panel, caller employee", "annotation", gatekeeper.call("/panel", "employee"));
    }
}
```

```
panel, caller manager                    annotation : panel-data
panel, caller employee                   annotation : blocked
```

The same `gatekeeper` object, the same `call` invocation, the same `/panel` path — the only
thing that changes is the role given as an argument. The call arriving with the manager role
reaches the method and returns its body; the call arriving with the employee role never runs
the method and gets the result `blocked`. The decision sits not in the body of the `panel`
method but in the mark above it, and the party that reads the mark is `Gatekeeper` itself; the
`panel` method itself knows neither the word manager nor asks who is calling. Without rewriting
the method, the same behavior can be opened to a different role by changing only the mark
above it — for example to `role = "employee"` — and this change never touches the body at all.

The path mapping is extracted once when `Gatekeeper` is constructed; `call` does not repeat the
reflection scan on every invocation, it looks at the already-extracted `endpoints` map. When an
unrecognized path is requested, `allow` returns `false` directly — and this too is not a call
but the container's own rule: every path with no entry in the map is rejected without any
annotation being read at all. So there is a third default as well, but its direction is fixed
this time: an unknown path is closed under every condition, independent of the `defaultOpen`
field. This asymmetry between a known and an unknown path is also invisible in the source; it
can only be understood by looking at the contents of the `endpoints` map itself.

## When No Rule Is Written: The Direction of the Default

The `status` method in the `Endpoints` class carries no authorization mark at all. This is the
case where someone reading the source finds nothing — and this is exactly why the most
restrictive measurement lies here. When `allow` finds no mark, it looks at the `defaultOpen`
field; this field is a constructor parameter given when `Gatekeeper` is constructed, a value
that appears nowhere in the source. The difference between the two setups is only this single
`boolean` value: no class is rewritten, no annotation is added.

```java
// DefaultDirection.java — an endpoint whose source never changes produces two different results from a single parameter given when the container is constructed
public class DefaultDirection {
    public static void main(String[] args) throws Exception {
        Gatekeeper openGatekeeper = new Gatekeeper(new Endpoints(), true);
        Gatekeeper closedGatekeeper = new Gatekeeper(new Endpoints(), false);

        Gatekeeper.observe("status, open-setup", "default", openGatekeeper.call("/status", "guest"));
        Gatekeeper.observe("status, closed-setup", "default", closedGatekeeper.call("/status", "guest"));
        System.out.println("source unchanged: Endpoints.status() carries no authorization mark");
    }
}
```

```
status, open-setup                       default    : running
status, closed-setup                     default    : blocked
source unchanged: Endpoints.status() carries no authorization mark
```

`Endpoints.java` did not change by a single character between the two runs; `openGatekeeper`
and `closedGatekeeper` are constructed from the same class. Even so, the same call with the
guest role returns data under one setup and is rejected under the other. **ST3.** At an
endpoint carrying no authorization annotation, the decision comes from the container's own
default; the source text shows neither of these two directions. **ST4.** The same source,
without being recompiled, can produce two different security postures from only a constructor
parameter given when the container is constructed — this is the lesson's limiting measurement.
An unmarked endpoint is neither secure nor insecure; which one it is can be understood not by
reading the source but by knowing which default the container was constructed with. This is the
lesson's most important observation: calling the difference between the two endpoints by a
fixed adjective such as "open" or "closed" is misleading, because the adjective belongs not to
the source but to the setup decision.

This decision does not belong to the party that wrote the `Endpoints` class either; whoever
constructs `Gatekeeper` holds the default in their hands. In a class with many endpoints, a
single method for which the authorization annotation was forgotten stays silently reachable
under a default-open setup — there is no error, no warning in the source, because a forgotten
mark leaves no trace at all by definition. Under a default-closed setup, the same oversight
works in the opposite direction: every unmarked method becomes unreachable, and the omission is
noticed only by the caller's observation of being "unexpectedly blocked." Both directions fill
the same gap, but the result they fill it with is exactly opposite; which direction was chosen
is written once, on the line that constructs the container, and it covers the entire unmarked
surface at once.

If the direction of the default does not appear in the source, can an annotation override it?
The `@Authorization(role = "everyone")` above the `version` method attempts exactly that: not a
specific role name, but an open-door mark.

```java
// ExplicitlyDeclared.java — an endpoint carrying an open-to-everyone mark passes through the annotation even when the container's default is closed
public class ExplicitlyDeclared {
    public static void main(String[] args) throws Exception {
        Gatekeeper closedGatekeeper = new Gatekeeper(new Endpoints(), false);
        Gatekeeper.observe("version, closed-setup", "annotation", closedGatekeeper.call("/version", "guest"));
        Gatekeeper.observe("status, closed-setup", "default", closedGatekeeper.call("/status", "guest"));
    }
}
```

```
version, closed-setup                    annotation : version-3
status, closed-setup                     default    : blocked
```

Two calls on the same `closedGatekeeper` object: `version` was declared open to everyone by an
annotation, so it overrides the default and passes; `status` carries no mark at all, so it
falls to the same container's closed default. When an annotation exists, the default never
comes into play at all — the order reaches the default only if there is no annotation. This
shows the precedence between the two sources: the annotation is read first, and only if it is
absent does the flow fall back to the default; a rule written in the source wins independent of
the direction of the default. This order of precedence, too, sits not in the source but in the
body of the `allow` method itself — whoever wrote `Gatekeeper` decided in advance which source
gets asked first.

## Manually Written Authorization Checks

A third source sits in a class separate from the previous two: `ManualAuthorization`, an
ordinary method that never enters the filter chain, carrying neither `@Endpoint` nor
`@Authorization`. Here the decision is given not by the container but by a check expressed
directly in the method's own body.

```java
// ManualAuthorization.java — the authorization decision comes not through the IoC container or an annotation, but directly from a check written in the source (helper source, no main)
class ManualAuthorization {
    String sensitiveOperation(String callerRole) {
        if (!"manager".equals(callerRole)) throw new SecurityException("unauthorized access");
        return "sensitive-data";
    }

    String attempt(String callerRole) {
        try { return sensitiveOperation(callerRole); }
        catch (SecurityException e) { return e.getClass().getSimpleName(); }
    }
}
```

```java
// ManualCheck.java — called with the same two roles, showing that the decision comes from a condition in the source
public class ManualCheck {
    public static void main(String[] args) {
        ManualAuthorization manual = new ManualAuthorization();
        Gatekeeper.observe("sensitiveOperation, caller manager", "call", manual.attempt("manager"));
        Gatekeeper.observe("sensitiveOperation, caller employee", "call", manual.attempt("employee"));
    }
}
```

```
sensitiveOperation, caller manager       call       : sensitive-data
sensitiveOperation, caller employee      call       : SecurityException
```

The result looks the same as in the `FilterDecision` lesson — a manager passes, an employee is
rejected — but its source differs. **ST5.** A manually written check gives the decision
directly from the source; neither an annotation nor the container comes in between, the
decision comes from the comparison sitting in the first line of the `sensitiveOperation`
method. This third path lives in a class `Gatekeeper` never manages;
`ManualAuthorization` never registered with it, so neither the annotation nor the default
direction applies to it — the same boundary observed in the previous topic's closing lesson
holds here as well: an object left outside the container receives none of its mechanisms.

The three separate sources shown by the three lessons sit side by side here:

| Source | Where the decision is made | Its example in this lesson |
|---|---|---|
| annotation | the `@Authorization` mark, read by `Gatekeeper` | `panel`, `version` |
| default | `defaultOpen`, given when `Gatekeeper` is constructed | `status` |
| call | the check expressed in the method's own body | `sensitiveOperation` |

The same question of "manager or not" is answered in three different ways from three different
places, and someone who only reads the source — even seeing the annotations — sets out without
knowing which answer produces which behavior. For `status` specifically this lack of
information is even sharper: there is no mark to read there at all, the decision belongs
entirely to the moment the container is constructed. None of these three sources is "correct"
or "incorrect"; each is held in a different place, and the person reading the source needs to
know in advance which places to look — the measurement axis built through this course exists
precisely to make this need to know countable.

The separation also concerns scale: an annotation and a default cover an entire class or
container with a single decision, while a manual check covers only the method it is written
in. Ten methods like `sensitiveOperation` would repeat the same comparison ten times; the
mark-based approach removes this repetition but moves the decision from the source to the
container. This lesson took the caller's identity as an already-resolved role string —
`"manager"`, `"employee"`, `"guest"` — and never asked where that string came from.

## Summary

- At an endpoint carrying an authorization annotation, the filter chain reads the decision from
  the mark and compares it against the caller's role; the source of this behavior is the
  annotation.
- At an endpoint carrying no authorization annotation, the decision comes from the container's
  own default; the source text never shows this behavior at all.
- The same source, without being recompiled, can produce two different security postures from
  only a parameter given when the container is constructed — this is the limiting measurement.
- When an annotation is present, the default never comes into play at all; the order of
  precedence is first the annotation, then the default.
- A manually written check gives the decision directly from the source; because this method
  never registers with the container, neither the annotation nor the default direction applies
  to it.

## Next Step

This lesson took the caller's role as a ready-made string — who it was was already a known
input. A real caller has to carry this information from somewhere, and this course does not
keep a session on the server side. The next lesson models how this role and identity come not
from state stored on the server, but from a token the caller brings along with every request.
