---
title: Evaluate a SecurityApiRequirement
summary: Evaluates a `SecurityApiRequirement` by calling the remote SecurityApi once per listed role, using OR semantics across roles.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.AuthService
reference_type: package
operation_id: OP-03
trigger: ASP.NET Core authorization middleware evaluates the built policy's `SecurityApiRequirement` against the current `ClaimsPrincipal`.
---

# Evaluate a SecurityApiRequirement

Evaluates a `SecurityApiRequirement` by calling the remote SecurityApi once per listed role, using OR semantics across roles.

Security enforcement points: [EP-04 (SecurityApiHandler)](../security-authorization.md#attribute-based-authorization).

| Field | Value |
| --- | --- |
| **Operation ID** | OP-03 |
| **Trigger** | ASP.NET Core authorization middleware evaluates the built policy's `SecurityApiRequirement` against the current `ClaimsPrincipal`. |
| **Inputs** | `AuthorizationHandlerContext` (current user, requirement), `SecurityApiRequirement.ProvidedRoles`. |
| **Outputs** | `context.Succeed(requirement)` when any role authorizes the user; otherwise the requirement remains unsatisfied. |
| **Primary components** | `SecurityApiHandler.HandleRequirementAsync` |
| **Entry point** | `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs` |

## Steps

1. If `context.User.Identity.Name` is null or whitespace, return without calling `context.Succeed` — this is an empty-name guard; `HandleRequirementAsync` never inspects `Identity.IsAuthenticated` ([BR-AUTH-006](../business-rules.md#br-auth-006-an-unnamed-identity-never-satisfies-a-securityapi-requirement))
2. Compute `userName` as the **second** `\`-delimited segment of `Identity.Name` (`Split("\\")[1]`) when the name contains a backslash, or the full name if there is no `\\` — not necessarily "everything after the last backslash" ([BR-AUTH-007](../business-rules.md#br-auth-007-the-handler-strips-the-windows-domain-before-calling-the-securityapi))
3. Split `requirement.ProvidedRoles` on `,`. For each role, `await ISecurityClient.IsAuthorized(userName, role)`; call `context.Succeed(requirement)` on any `true` result, without stopping the loop and without ever reversing an earlier `Succeed` call when a later role returns `false` ([BR-AUTH-008](../business-rules.md#br-auth-008-comma-separated-roles-use-or-semantics))
   - 3.1 Normalize the user ID: replace `\` with `/` in `userName`.
   - 3.2 Apply the resource-suffix rewrite to `role`: trim the role; if the trimmed role ends with `resource` (case-insensitive), replace the last occurrence of `resource` with the configured `SecurityClient:resourcesuffix` value; trim the result again.
   - 3.3 Issue `GET /v1/Users/IsAuthorized/{userId}/{suffixRole}`.
   - 3.4 On success, return the deserialized `bool`. On a non-success response with an inner exception, throw `SecurityClientException(message, innerException)`. On a non-success response without an inner exception, throw `SecurityClientException(message)` — there is no default-deny `false` for a failed call.

   > **Origin**: Steps 3.1–3.4 are implemented in [Check role authorization remotely](check-role-authorization-remotely.md) (`SAIF.NetCore.AuthService`) and executed on behalf of this operation, describing the library's registered `SecurityClient` implementation — not every possible `ISecurityClient` a consumer could substitute.

## Process flow

The following diagram visualizes this operation's control flow. `context.Succeed(requirement)` accumulates across loop iterations — once called for any role, a later role returning `false` does not undo it; the requirement is satisfied if `Succeed` was called at least once during the loop.

```mermaid
flowchart TD
    Start([Requirement evaluation]) --> NameCheck{Identity.Name null/whitespace?}
    NameCheck -->|Yes| Deny(["Requirement not satisfied<br/>no remote call made"])
    NameCheck -->|No| Strip["userName = second segment of Split('\\')<br/>or full name if no backslash"]
    Strip --> Loop[For each role in ProvidedRoles]
    Loop --> Call["await ISecurityClient.IsAuthorized user, role<br/>may throw SecurityClientException"]
    Call --> Result{Returned true?}
    Result -->|Yes| Succeed["context.Succeed requirement<br/>accumulated, not reversible"]
    Result -->|No| More{More roles remain?}
    Succeed --> More
    More -->|Yes| Loop
    More -->|No| Final{Succeed called<br/>at least once?}
    Final -->|Yes| Done([Requirement satisfied])
    Final -->|No| Deny2([Requirement not satisfied])
```

## Failure and edge-case behavior

- A non-success response from any role's remote call throws `SecurityClientException` from inside the handler (surfaced by the shared response handler described in [Check role authorization remotely](check-role-authorization-remotely.md)), surfacing as an unhandled exception rather than a 403*. This is distinct from an ordinary `false` result, which simply continues the loop.
- The handler never calls `context.Fail()`; an unsatisfied requirement denies access purely by omission.
- Once `context.Succeed(requirement)` has been called for any role, the requirement is satisfied even if a later role in the same loop returns `false`; the loop does not short-circuit on success but a later `false` never reverses the earlier success.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [Security and authorization](../security-authorization.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts. Statements marked with `**` could not be confirmed; treat
> as unknown until verified.

## Sources
- `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs`
