Authentication & Authorization

Configure authentication strategies and entity-level authorization in your Spiderly application.

Authentication Strategies

Spiderly supports two authentication strategies. Both use JWT tokens under the hood — the difference is how tokens are transported between client and server.

Token-Based (Default)

Tokens are returned in the response body and sent via the Authorization header. The client is responsible for storing and managing tokens.

Angular methods:

MethodReturnsDescription
login(request)AuthResultLogin with email verification token
loginExternal(provider)AuthResultLogin with external provider (e.g., Google)
logout(browserId)Invalidate tokens
refreshTokenWithHeaders(request)AuthResultRefresh an expired access token

AuthResult includes accessToken, refreshToken, userId, and email.

Tokens are stored in HttpOnly cookies — the browser handles storage and sends them automatically. No token management needed on the client side.

Angular methods:

MethodReturnsDescription
loginWithCookies(request)AuthResultWithCookiesLogin with email verification token
loginExternalWithCookies(provider)AuthResultWithCookiesLogin with external provider (e.g., Google)
logoutWithCookies(browserId)Clear auth cookies
refreshTokenWithCookies(browserId)AuthResultWithCookiesRefresh via cookie

AuthResultWithCookies includes userId, email, and accessTokenExpiresAt — no tokens in the response body.

Cookie-based authentication requires CSRF protection. All state-changing requests (POST, PUT, DELETE, PATCH) must include the X-CSRF: "1" header.

Choosing a Strategy

Token-BasedCookie-Based
Best forMobile apps, cross-domain APIs, SPAs with explicit token handlingSame-domain/subdomain setups, simpler frontend code
Token storageClient manages (localStorage, memory)Browser manages (HttpOnly cookies)
CSRF protectionNot needed (tokens in headers)Required (X-CSRF header)
Cross-subdomainWorks out of the boxRequires CookieDomain and CookieSameSite configuration

Overview

By default, all entities require authorization for Create, Read, Update, and Delete operations. If your code encounters an authorization issue (e.g., an UnauthorizedException - You don't have the necessary rights to perform the operation.), it means the current user lacks the necessary permissions.

If your entity requires access control, follow the steps in this tutorial to configure proper authorization.

Registration

Authentication and authorization are turned on by a single call inside the AddSpiderly builder in your Startup.cs. The spiderly init template scaffolds it for you — you rarely write it by hand:

services.AddSpiderly<ApplicationDbContext>(Configuration, spiderly =>
{
    spiderly.UsePostgreSQL();
    spiderly.AddSecurity<User, UserExternalLogin, AuthorizationService>();
});

AddSecurity<TUser, TUserExternalLogin, TAuthorizationService> registers the whole auth core as one unit: the email-login and JWT services, the User principal kind, your AuthorizationService, and — crucially — the permission handler that forwards to it. These pieces are co-required: an app with some but not all of them is silently broken, so AddSecurity bundles them all-or-nothing. A single-principal app needs nothing more.

Optional add-ons opt in through a sub-builder. API keys, for example, register a second principal kind and its auth scheme:

spiderly.AddSecurity<User, UserExternalLogin, AuthorizationService>(s => s.AddApiKeys<ApiKey>());

If you replace the scaffolded AddSecurity call with hand-rolled registration and forget the permission handler, the app fails at startup: "Spiderly authentication is enabled, so [AuthGuard("Code")] materializes a PermissionRequirement policy, but no IAuthorizationHandler that satisfies PermissionRequirement is registered." Fix it by calling services.AddSpiderlyAuthorization<AuthorizationServiceGenerated>() — or simply keep using AddSecurity, which registers the handler for you. It's a fail-loud boot guard, never a silent 403.

Authorization Attributes

[AuthGuard]

The single attribute that protects an endpoint. Bare, it requires an authenticated principal. With a permission code, it additionally requires the principal — a logged-in user or an API key — to hold that permission, or the request is rejected with 403 Forbidden.

[SpiderlyController]
[ApiController]
[Route("/api/[controller]/[action]")]
public class ReportController : ReportBaseController
{
    [HttpPost]
    [AuthGuard] // Any authenticated principal
    public async Task<IActionResult> RunExport() { /* ... */ }

    [HttpPost]
    [AuthGuard("RunSalesReport")] // ...and must hold RunSalesReport
    public async Task<IActionResult> RunSalesReport() { /* ... */ }
}

The permission code must be seeded and assigned to the caller's role.

Use it on endpoints you write yourself; entity CRUD already enforces the default Read{Entity} / Insert{Entity} / Update{Entity} / Delete{Entity} permissions with no attribute needed.

[AuthGuard] derives from ASP.NET Core's [Authorize], so it composes the way you would expect: put it on the controller class to apply one guard to every action, add it to an action to require both, and use [AllowAnonymous] to opt a single action out.

Earlier versions split this across two attributes — [AuthGuard] for authentication and [HasPermission("Code")] for authorization. They were required together but easy to write apart, and the failure was silent: an endpoint with only [AuthGuard] accepts any logged-in account, including an ordinary customer on a public storefront. Replace [HasPermission("Code")] with [AuthGuard("Code")] and delete the now-redundant [AuthGuard] line above it.

One permission per attribute. Stack two to require both. "Either A or B" needs a real composite policy and is deliberately not modelled here.

[DoNotAuthorize]

Disables authorization checks for all CRUD operations on the decorated entity. By default, every entity requires the user to hold the relevant Read{Entity} / Insert{Entity} / Update{Entity} / Delete{Entity} permission.

[DoNotAuthorize] bypasses security checks entirely and is intended primarily for testing or for entities that are truly public. Avoid it on entities that contain user data or any value worth protecting in production.

[SpiderlyEntity]
[DoNotAuthorize] // Public catalog — no permissions required
public class PublicAnnouncement : BusinessObject<long>
{
    [DisplayName]
    public string Title { get; set; }
}

CSRF Protection

Cookie authentication is ambient: the browser attaches the cookie to any request a page makes, including one triggered from another site. Spiderly rejects such a request unless it carries an X-CSRF header — the value doesn't matter, only its presence, because a custom header cannot be set cross-origin without passing a CORS preflight first.

Register the middleware after UseRouting():

app.UseRouting();

app.UseSpiderlyCsrf();

app.UseAuthentication();
app.UseAuthorization();

Protection is global and opt-out, matching ASP.NET Core's own antiforgery guidance: with per-endpoint opt-in, an endpoint eventually gets left unprotected by mistake. Exempt an endpoint that a browser never reaches with the user's cookies — a payment-gateway callback, a partner webhook — with [IgnoreCsrf].

Not challenged, by design: requests authenticated via an Authorization: Bearer header (a token is sent deliberately, not ambiently), and GET / HEAD / OPTIONS (they must not change state to begin with).

This defense rests on your CORS origin allow-list: it works because a cross-origin caller cannot add a custom header without a preflight your policy must approve. An app that allows arbitrary origins with credentials (SetIsOriginAllowed(_ => true)) makes it a no-op. ASP.NET already blocks the common form of this mistake — AllowAnyOrigin() and AllowCredentials() are mutually exclusive — so keep the explicit escape hatch out of a cookie-authenticated app.

This check used to live inside [AuthGuard], which made it opt-in per endpoint: anything reachable with cookies but annotated differently — or not at all — had no CSRF protection, silently. If the middleware is missing while authentication is enabled, the app now fails at startup rather than serving unprotected writes. Register it on the main pipeline: a registration inside an app.Map(...) branch protects only that branch, and the startup guard treats it as missing.

Per-action permissions

For finer-grained control — e.g. requiring an extra permission on top of the default Insert{Entity} / Update{Entity} permission — see [UIAdditionalPermissionCodeForInsert] and [UIAdditionalPermissionCodeForUpdate] in UI Customization — Per-Action Permissions.

Add Permissions to Your Application

Navigate to your ApplicationDbContext.cs file:

Backend\{your-app-name}.Infrastructure\{your-app-name}ApplicationDbContext.cs

In the SeedData method, add your entity permissions to the permissions array. Replace YourEntityName with your actual entity name:

private static void SeedData(ModelBuilder modelBuilder)
{
    Permission[] permissions =
    [
        // ... existing permissions ...

        // Add your new entity permissions
        new Permission { Id = 13, Name = "View YourEntityName", Code = "ReadYourEntityName" },
        new Permission { Id = 14, Name = "Edit existing YourEntityName", Code = "UpdateYourEntityName" },
        new Permission { Id = 15, Name = "Add new YourEntityName", Code = "InsertYourEntityName" },
        new Permission { Id = 16, Name = "Delete YourEntityName", Code = "DeleteYourEntityName" },
    ];

    modelBuilder.Entity<Permission>().HasData(permissions);

    // ... rest of seed data ...
}

Important: Make sure to use sequential IDs that don't conflict with existing permissions.

After adding the permissions, create and apply a migration:

spiderly add-migration AddYourEntityNamePermissions
spiderly update-database

Assign Permissions to a Role

In the application UI:

  1. Navigate to Administration → Roles.
  2. Select the role you want to modify.
  3. In the Permissions control, add the newly created permissions.

This ensures users assigned to this role will have access to the specified entity operations.

See Also