Authentication & Authorization
Authentication asks "who are you?". Authorization asks "what are you allowed to do?". They are different questions, and mixing them up is where access-control bugs grow. Establish identity once, from a validated source. Then check permission on every action — server-side, default-deny, and scoped to the tenant.
Authentication must be strict (a fully validated token or session, strong credentials, MFA). Authorization must be everywhere (every protected operation re-checks that this identity may perform this action on this resource). The most common failures are not broken crypto. They are a route that forgot to check, a permission taken from client input, or an object reference that is not scoped to the caller.
In a multi-tenant AML platform, authorization also carries the tenant dimension: a valid user of tenant A must never reach tenant B's data, and a user must never raise their own privileges. The Finperiti findings — a fully unauthenticated controller and a shared HS256 secret — show both halves failing: missing authentication, and an identity mechanism that could not be trusted.
Authenticate rigorously
- DoRequire authentication by default on every route. Making something anonymous is an explicit, reviewed exception (see Secure Defaults).
- DoEstablish identity from a fully validated token or session — signature, issuer, audience, expiry — and work out who the caller is only from that.
- DoPrefer federated identity (Entra ID / SSO) and enforce MFA. Use asymmetric token signing so a verifier cannot forge tokens (see Session & Token Management).
- ConsiderCentralising authentication in middleware, so no single handler can skip it by accident.
- AvoidHand-rolling authentication — your own login, token format, or session scheme — when a managed identity provider (Entra ID / OIDC) does it correctly and keeps it maintained (see Identity Provider & SSO). Home-grown auth is where the worst, quietest mistakes live.
- NeverExpose an endpoint that touches PII, money, or regulated data without authentication, or ship an auth bypass (for example [AllowAnonymous]) on a protected route.
- NeverTrust identity, role, or tenant sent in the request body, query string, or a client-set header. Derive it server-side from the validated token.
Authorize every action
- AlwaysCheck authorization on every protected operation — not just at login or in the UI — and default to deny when no rule grants access.
- DoEnforce object-level (resource) authorization: confirm this identity owns or may access this specific record, scoped to their tenant.
- DoKeep authorization decisions server-side and centralised. The client may hide a button, but the server must enforce the rule.
- ConsiderExpressing role and permission checks declaratively and consistently, so coverage is auditable rather than scattered ad hoc.
- Do notRely on unguessable ids (IDOR) as access control. An opaque id is not a permission.
- NeverQuery, update, or delete tenant-owned data without enforcing the tenant predicate, or let a user grant themselves a role or raise their own privileges.
[Authorize]
public Customer Get(Guid id) =>
db.QuerySingle("SELECT * FROM Customers WHERE Id=@id", new { id });
The caller is logged in, but any logged-in user can read any customer of any tenant by guessing or listing ids. This is authentication without object-level, tenant-scoped authorisation.
[Authorize]
public Customer Get(Guid id) =>
db.QuerySingleOrDefault(
"SELECT * FROM Customers WHERE Id=@id AND TenantId=@t",
new { id, t = User.GetTenantId() }) ?? throw new NotFound();
The tenant comes from the validated token, the row can only belong to the caller's tenant, and a miss looks like 'not found' rather than revealing that the record exists.
Self-review checklist
- AskIs this route authenticated, and is identity derived only from a validated token?
- AskDoes every action re-check authorization server-side, not just rely on the UI or login?
- AskCan this identity reach another tenant's data, or a record it does not own?
- AskCould a caller grant themselves a role or raise privileges through this path?