Isaac.

Secure Your API with Authentication and Authorization

APIs power modern applications, but without proper security, they can become easy targets. This guide explains authentication (who you are) and authorization (what you can do) with code in Node.js and ASP.NET Core, plus an enterprise-ready OAuth 2.0 / OpenID Connect setup using IdentityServer.

Introduction

Security is layered: authenticate callers, authorize their access, use TLS, validate inputs, and log everything that matters. We'll start with JWTs, then elevate to OAuth 2.0 / OpenID Connect using an external Identity Provider (IdP).

Authentication vs Authorization

Authentication: verifies identity (e.g., password, MFA, social login).

Authorization: enforces permissions (roles, policies, scopes).

JWT-Based Authentication

JSON Web Tokens carry signed claims (e.g., sub, role, scope). Clients send the token in Authorization: Bearer <token>.

🔹 Node.js (Express)

import express from "express";
import jwt from "jsonwebtoken";

const app = express();
app.use(express.json());

const SECRET_KEY = "replace-with-strong-secret";

app.post("/login", (req, res) => {
  const { username, password } = req.body;
  // Validate against DB...
  if (username === "admin" && password === "password") {
    const token = jwt.sign(
      { sub: "user-id-123", role: "Admin" },
      SECRET_KEY,
      { expiresIn: "1h", issuer: "your-api" }
    );
    return res.json({ access_token: token, token_type: "Bearer" });
  }
  return res.status(401).json({ error: "Invalid credentials" });
});

function requireAuth(req, res, next) {
  const auth = req.headers["authorization"];
  if (!auth) return res.sendStatus(401);
  const token = auth.split(" ")[1];
  jwt.verify(token, SECRET_KEY, (err, claims) => {
    if (err) return res.sendStatus(403);
    req.user = claims;
    next();
  });
}

app.get("/me", requireAuth, (req, res) => {
  res.json({ user: req.user });
});

🔹 ASP.NET Core (Minimal API)

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services
  .AddAuthentication("Bearer")
  .AddJwtBearer("Bearer", options =>
  {
      options.Authority = "https://your-issuer.example.com";
      options.TokenValidationParameters.ValidateAudience = false; // or set Audience
  });

builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/me", (HttpContext ctx) =>
{
    var userId = ctx.User.FindFirst("sub")?.Value;
    return Results.Ok(new { sub = userId });
}).RequireAuthorization();

app.Run();

Role & Policy Authorization

Prefer policies for fine-grained checks; use roles for broad buckets.

🔹 Node.js: Role Gate

function requireRole(role) {
  return (req, res, next) => {
    if (!req.user || req.user.role !== role) return res.sendStatus(403);
    next();
  };
}
app.get("/admin", requireAuth, requireRole("Admin"), (req, res) => {
  res.json({ message: "Welcome Admin" });
});

🔹 ASP.NET Core: Roles & Policies

// Program.cs (add policy)
builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("CanViewReports", p =>
        p.RequireClaim("scope", "reports.read"));
});

// Controller
using Microsoft.AspNetCore.Authorization;

[Authorize(Roles = "Admin")]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
    [HttpGet("reports")]
    [Authorize(Policy = "CanViewReports")]
    public IActionResult GetReports() => Ok(new { ok = true });
}

OAuth 2.0 / OpenID Connect with IdentityServer (ASP.NET Core)

Why: Centralize identity, support external login providers, issue standards-based tokens, and manage scopes/claims for multiple APIs and apps (web, SPA, mobile).

Core flows: Authorization Code + PKCE (for SPAs & mobile), Client Credentials (service-to-service), and Hybrid (web apps).

1) IdentityServer Host (IdP)

Create an ASP.NET Core project hosting IdentityServer. Define API scopes, clients, and identity resources. (For production, add secure key material, HTTPS, persistence, and consent.)

// IdentityServerConfig.cs (example using Duende IdentityServer)
using Duende.IdentityServer.Models;

public static class IdentityServerConfig
{
    public static IEnumerable<IdentityResource> IdentityResources =>
        new IdentityResource[]
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile()
        };

    public static IEnumerable<ApiScope> ApiScopes =>
        new ApiScope[]
        {
            new ApiScope("api.read", "Read access to API"),
            new ApiScope("api.write", "Write access to API")
        };

    public static IEnumerable<Client> Clients =>
        new Client[]
        {
            // SPA using Auth Code + PKCE
            new Client
            {
                ClientId = "spa-client",
                AllowedGrantTypes = GrantTypes.Code,
                RequirePkce = true,
                RequireClientSecret = false,
                RedirectUris = { "https://localhost:5173/callback" },
                PostLogoutRedirectUris = { "https://localhost:5173/" },
                AllowedCorsOrigins = { "https://localhost:5173" },
                AllowedScopes = { "openid", "profile", "api.read" },
                AccessTokenLifetime = 3600
            },
            // Machine-to-machine
            new Client
            {
                ClientId = "svc-client",
                ClientSecrets = { new Secret("super-secret".Sha256()) },
                AllowedGrantTypes = GrantTypes.ClientCredentials,
                AllowedScopes = { "api.read", "api.write" }
            }
        };
}

2) Wire Up IdentityServer

// Program.cs (IdentityServer host)
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddIdentityServer()
    .AddInMemoryIdentityResources(IdentityServerConfig.IdentityResources)
    .AddInMemoryApiScopes(IdentityServerConfig.ApiScopes)
    .AddInMemoryClients(IdentityServerConfig.Clients)
    .AddDeveloperSigningCredential(); // dev only

var app = builder.Build();

app.UseHttpsRedirection();
app.UseIdentityServer(); // exposes /.well-known/openid-configuration
app.MapGet("/", () => "IdentityServer up");

app.Run();

Discovery Document: https://your-idp/.well-known/openid-configurationprovides endpoints (authorize, token, jwks) and metadata your API and clients will use.

3) Protect Your ASP.NET Core API with JwtBearer

// Program.cs (Protected API)
var builder = WebApplication.CreateBuilder(args);

builder.Services
  .AddAuthentication("Bearer")
  .AddJwtBearer("Bearer", options =>
  {
      options.Authority = "https://localhost:5001"; // your IdentityServer
      options.Audience = "api"; // optional if you validate scope instead
      options.TokenValidationParameters.ValidateAudience = false;
  });

builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("ApiReader", p => p.RequireClaim("scope", "api.read"));
    o.AddPolicy("ApiWriter", p => p.RequireClaim("scope", "api.write"));
});

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/data", () => new { ok = true })
   .RequireAuthorization("ApiReader");

app.MapPost("/data", (object body) => Results.Ok(body))
   .RequireAuthorization("ApiWriter");

app.Run();

4) Calling the API (Two Common Flows)

Client Credentials (service ➜ API)

POST https://localhost:5001/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=svc-client
&client_secret=super-secret
&scope=api.read api.write

# Response: { "access_token": "...", "token_type": "Bearer" }

Authorization Code + PKCE (SPA/mobile ➜ IdP ➜ API)

  1. App generates code_verifier & code_challenge.
  2. User signs in at IdP; app receives code.
  3. App swaps code + code_verifier for tokens.
  4. App calls API with Bearer access token.
GET https://localhost:5001/connect/authorize?
  client_id=spa-client&
  response_type=code&
  scope=openid%20profile%20api.read&
  redirect_uri=https%3A%2F%2Flocalhost%3A5173%2Fcallback&
  code_challenge=...&
  code_challenge_method=S256

⚠️ Notes

  • Use HTTPS everywhere; never expose client secrets in SPAs.
  • Duende IdentityServer (commercial for most production use). Alternatives: Azure AD (Entra ID), Auth0, Okta, Keycloak.
  • Persist IdentityServer config and keys; don't use developer signing creds in production.
  • Validate scopes or audiences in your API; prefer scope policies for multi-API scenarios.

Best Practices

  • Short-lived access tokens; rotate refresh tokens; revoke on suspicion.
  • Pin token issuer (iss) and audience (aud) strictly.
  • Use policy-based authorization for scopes/claims; avoid hardcoding roles everywhere.
  • Store secrets in a vault; never in source control.
  • Enable structured logging and trace IDs for auth flows.
  • Threat-model your endpoints; limit data exposure; add rate limits.

Conclusion

Start with JWTs for simple cases, then move to OAuth 2.0 / OpenID Connect with a dedicated IdP as your system grows. Use scopes, claims, and policies to express clear, testable permissions.

With IdentityServer (or a managed IdP) + ASP.NET Core JwtBearer, you get standards, interoperability, and a clean separation of concerns.