Protect a Web API
Validate Signward access tokens in your ASP.NET Core or FastAPI backend in under 10 lines.
Any REST backend can delegate authentication to Signward by validating the JWT access token on every request. No cookies, no redirects — just a bearer header.
How it works
- Your frontend (SPA, mobile, server-rendered app) obtains an access token from Signward via the OIDC flow.
- It calls your backend with
Authorization: Bearer <jwt>. - Your backend validates the token against Signward's JWKS (public keys at
/.well-known/jwks.json) — signature, issuer, audience, expiration. - If valid, the token's claims (
sub,tenant_id,roles,email) are available on the request context.
JWKS is cached on the backend; Signward rotates keys without breaking verification — the JWT's kid header points to the active key.
ASP.NET Core
The official SDK wraps the setup. See the .NET SDK page for install instructions.
using IdServer.Client.Middleware;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddIdServerAuth(o =>
{
o.Authority = "https://api.signward.com";
o.Audience = "your-api-audience";
o.UseCookieAuth = false;
});
var app = builder.Build();
app.UseIdServerAuth();
app.MapGet("/api/me", (ClaimsPrincipal user) => new
{
sub = user.GetUserId(),
email = user.GetEmail(),
tenantId = user.GetTenantId(),
roles = user.FindAll("roles").Select(c => c.Value)
}).RequireAuthorization();
app.Run();
Role-based policies
[HttpDelete("/api/users/{id}")]
[IdServerAuthorize("admin", "owner")]
public IActionResult Delete(Guid id) { /* ... */ }
IdServerAuthorize accepts Signward's built-in roles (admin, owner, user) and any custom roles defined per-tenant in the Portal.
Heads-up: roles only reach the token if your client requests the
rolesscope during the OIDC flow (e.g.openid profile email roles). Without it therolesclaim is empty and every role-gated endpoint returns 403 — even for an owner. The custom roles are then carried consistently in the ID token, the access token, and the userinfo response.
Permission-style roles (resource:action)
Custom roles are opaque strings, so the recommended way to model fine-grained permissions is to name them resource:action — invoices:read, invoices:write, reports:read. The action lives in the role name (the same approach as Auth0's write:invoices, Entra's Invoices.ReadWrite, and Okta's invoices.write), so no extra claim or backend wiring is needed — gate each endpoint on the exact string:
[HttpGet("/api/invoices")]
[IdServerAuthorize("invoices:read")]
public IActionResult List() { /* ... */ }
[HttpPost("/api/invoices")]
[IdServerAuthorize("invoices:write")]
public IActionResult Create(InvoiceDto dto) { /* ... */ }
There is no implicit hierarchy: invoices:write does not grant invoices:read. Either assign both roles to a writer, or accept either string on read endpoints. Define the roles in the Portal — see Roles → Permission-style roles.
Without the SDK
If you need to integrate with an existing auth stack, bypass the SDK and wire up AddJwtBearer directly:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
o.Authority = "https://api.signward.com";
o.Audience = "your-api-audience";
o.RequireHttpsMetadata = true;
o.TokenValidationParameters.NameClaimType = "sub";
o.TokenValidationParameters.RoleClaimType = "roles";
});
The handler discovers the JWKS URL from /.well-known/openid-configuration automatically.
FastAPI
from fastapi import FastAPI, Depends
from idserver import IdServerClient, IdServerOptions
from idserver.fastapi import IdServerAuth, CurrentUser
client = IdServerClient(options=IdServerOptions(
authority="https://api.signward.com",
client_id="your-api",
audience="your-api-audience",
))
auth = IdServerAuth(client)
app = FastAPI()
@app.get("/api/me")
async def me(user: CurrentUser = Depends(auth.current_user)):
return {
"id": str(user.user_id),
"email": user.email,
"tenant_id": str(user.tenant_id),
"roles": user.roles,
}
@app.delete("/api/users/{user_id}")
async def delete(user_id: str, user: CurrentUser = Depends(auth.require_role("admin", "owner"))):
return {"ok": True}
See the Python SDK page for install + async details.
Audience claim
Every Signward access token carries an aud claim matching the Jwt:Audience configured on the server. Set the same value on your backend — this is the third defense line after signature + issuer.
If you run multiple APIs, register a distinct OIDC client per API and request the relevant audience during the authorization flow. Signward's token endpoint will refuse tokens that aren't scoped to the requested audience.
Tenant isolation
Tokens are scoped to one tenant. The tenant_id claim is populated from the signed-in user's tenant. Your backend should enforce tenant isolation on every DB query — never trust a tenantId coming from the request body over the claim.
var tenantId = user.GetTenantId();
var reports = await db.Reports.Where(r => r.TenantId == tenantId).ToListAsync();
What to test
- Wrong audience → 401. Set a different
Audiencein your test and make sure validation fails. - Expired token → 401 with
WWW-Authenticate: Bearer error="invalid_token". - Missing
rolesclaim → your policy-gated endpoints return 403. - Cross-tenant query → return 403 / 404, never leak data.
Next steps
- Webhooks — react to user lifecycle server-to-server
- API reference — full endpoint list