Implementing JWT Authentication in an Angular and .NET API Stack

Most full-stack applications built today demand a stateless way to verify users across distributed services. JSON Web Tokens have quietly become the workhorse for that pattern, particularly when an Angular front end consumes a .NET Web API hosted on a separate origin. Whether you are wiring up a booking platform for a Sydney clinic, a logistics dashboard for a Melbourne distributor, or a consumer portal for a Brisbane retailer, the same plumbing applies: issue a signed token after credentials are validated, attach it to every outgoing request, and protect endpoints that require it.

This walkthrough focuses on a pragmatic implementation rather than a textbook tour. It assumes you already have an Angular workspace and an ASP.NET Core project side by side, and it walks through issuing tokens, validating them, attaching them via an HTTP interceptor, and rotating them safely. The examples are deliberately small so you can adapt them to an internal app at a suburban accounting firm in Perth or a nationwide loyalty scheme running across state borders. Where relevant, notes about Australian deployment realities, such as the Australian Cyber Security Centre's Essential Eight guidance and the Australian Privacy Principles under the Privacy Act, are folded into the security discussion.

Why JSON Web Tokens Suit Modern Single-Page Applications

Token-based authentication replaces the server-side session with a self-contained credential. Each token carries its own claims, signature, and expiry, which means the API never has to look anything up in memory to know who the caller is. That single property makes JSON Web Tokens a natural fit for Single-Page Applications where the browser holds state but the server is stateless. When the Angular client sends a bearer token in the Authorization header, the API only needs to validate the signature against a secret or public key, then read the claims and move on.

The upside is performance and simplicity. There is no need for sticky sessions on a load balancer, no distributed cache to synchronise, and no awkward cross-domain cookie behaviour to wrestle with. The trade-off is that tokens are sensitive; if one leaks, the attacker has everything they need until the token expires. That risk is acceptable for many Australian SMEs running workloads in Sydney or Adelaide data centres, but it shifts the burden onto careful storage, transport, and rotation strategies. Treat the token like you would treat an API key for the ATO's systems: store it well, transmit it only over TLS, and revoke it quickly when something goes wrong.

A second consideration is claims design. A JWT is essentially a JSON document with a header, payload, and signature. The payload can hold anything you like, but it should stay small and never carry secrets like passwords or full Medicare numbers. Common claims include a subject identifier, a name, a role, and a token expiry. Because the payload is base64-encoded rather than encrypted by default, sensitive details belong elsewhere, ideally in a database behind the token, not inside the token itself.

Setting Up the ASP.NET Core API for Token Issuance

Begin with a clean ASP.NET Core 8 Web API project. Add the packages that handle token creation and the bearer authentication handler. The System.IdentityModel.Tokens.Jwt package from Microsoft covers the heavy lifting, while Microsoft.AspNetCore.Authentication.JwtBearer plugs into the authentication pipeline. Once these are referenced, register authentication services in the dependency injection container and configure the bearer options to read tokens from the Authorization header.

Inside the configuration block, point the validation parameters at your signing key. For most applications a symmetric key derived from a long random secret stored in user secrets or an environment variable is sufficient. Production deployments in Australia often pull such secrets from Azure Key Vault or AWS Secrets Manager to align with the ACSC's cloud security guidance. Set the issuer, audience, and clock skew to small values so that tokens presented with wildly skewed timestamps are rejected.

The next step is a login endpoint. Accept a username and password, validate them against your user store, and on success produce a JWT using a small TokenService class. That service builds a SecurityTokenDescriptor, signs it with SymmetricSecurityKey, and returns the encoded string. Pair the access token with a refresh token stored in a database, then return both as a JSON payload to the Angular client. A typical response includes the access token, its expiry in seconds, the refresh token, and basic profile fields used to render the home screen.

Crafting the Token Service and Middleware

A focused token service keeps the issuance logic tidy and testable. Inject IConfiguration to read the secret, define a method that accepts a user identifier and a list of roles, and return the encoded token plus its expiry. Add a separate method for refresh tokens, which are usually longer random strings rather than signed JWTs, and store their hashes server-side so that revocation is possible.

Middleware is where many implementations either shine or stumble. Add an authorisation policy attribute on controllers or actions that need protection, and configure fallback policies so that any endpoint without an explicit [AllowAnonymous] requires authentication. This is a strong default for any Australian business exposing personally identifiable information, because it enforces the principle of least access on every new endpoint by default. New developers cannot accidentally publish a route that leaks data; the policy refuses to serve the request until they think about it.

Logging deserves a careful touch as well. Write structured logs for successful logins, failed attempts, and token refreshes, and make sure that the token itself never appears in log output. If you are running across multiple regions, consider stamping log entries with Australia Eastern Standard Time so that your operations team in Brisbane and your on-call engineer in Perth can compare timestamps without doing mental arithmetic. Tools like Seq or Application Insights can apply a converter on the client side, but storing the canonical timestamp in UTC remains the safest choice.

Building the Angular Authentication Module

On the Angular side, install nothing exotic; the framework's built-in HTTP client and reactive forms handle most of the work. Create an AuthService with methods for login, logout, refresh, and a currentUser$ observable that components can subscribe to. Store the access token in memory and only persist the refresh token in an HTTP-only cookie set by the API. This pattern is widely recommended because JavaScript cannot read HTTP-only cookies, so even a cross-site scripting flaw cannot lift the refresh credential.

The next layer is an HTTP interceptor. Implement HttpInterceptor and attach it through withInterceptors in provideHttpClient. Inside the interceptor, read the access token from the AuthService, and if it exists, clone the request and add an Authorization: Bearer ... header. Refresh handling sits inside the same interceptor using a BehaviorSubject to coalesce concurrent requests into a single refresh call. When a 401 comes back, the interceptor pauses outgoing calls, runs refresh(), and replays the original request with the new token. If the refresh fails, the user is redirected to the login page and any cached data is cleared.

Routing guards protect individual pages. Add a functional authGuard that checks whether the user is authenticated and an optional roleGuard that examines claims such as role. These guards integrate with the standalone component APIs now common in Angular 17 and later, so there is no need to register them in a module. Keeping route protection in guards, rather than hiding UI behind *ngIf, ensures that direct URL access still respects the policy.

Handling Refresh Tokens and Silent Renewal

Refresh tokens exist because short-lived access tokens are a defence-in-depth measure. A fifteen-minute access window means a stolen token is useless soon after, while a refresh token with a thirty-day lifetime keeps the user productive. Implement sliding expiration: every successful refresh extends the refresh token's expiry, but only if it has been used recently. This mirrors how the myGov platform behaves for Australian citizens logging in repeatedly through the day, balancing convenience with the need to revoke dormant sessions.

Silent renewal happens behind the scenes using the interceptor pattern described earlier. Avoid proactive refresh timers; they tend to drift across tabs and produce races. Instead, refresh on demand when a 401 arrives, because that is the moment you actually know the token has expired or been revoked. Pair this with a heartbeat ping on long-lived pages, such as a claims dashboard that a user may leave open all afternoon while grabbing a flat white in a Melbourne laneway café.

Revocation matters too. When a user logs out, deletes their account, or changes a password, invalidate the refresh token in the database so that it can no longer exchange for a new access token. Maintain an "issued before" timestamp on the user record; any refresh token issued before the user's last credential change is rejected. This pattern catches attackers who have stolen an old refresh token alongside a new password, and it lines up nicely with the Australian Privacy Principles' requirement to take reasonable steps to destroy or de-identify personal information when it is no longer needed.

Securing Tokens in Production and Hardening the API

The final stretch is hardening. Always run the API behind HTTPS, and set the cookie that carries the refresh token to Secure, HttpOnly, and SameSite=Strict. Disable sliding expiration from the ASP.NET Core data-protection keys by using a persistent key ring stored in a database or Azure Blob Storage, so that restarts do not invalidate every outstanding token. Apply rate limiting to the login and refresh endpoints to slow credential stuffing and brute-force attempts; the ACSC's Essential Eight recommends application control and patching at the application layer, and brute-force throttling is a close cousin.

Think about audience and issuer validation carefully. The API should refuse any token whose iss or aud does not match the configured values. This rejects tokens minted by a different service in the same environment, a common mistake during multi-tenant development when a developer's local token slips into a staging build. If you operate in a regulated sector, such as healthcare or finance, document where tokens are stored, how they are revoked, and which audit logs are produced; this evidence will be useful during any review by the Office of the Australian Information Commissioner.

Practical Recommendations for a Solid Auth Pipeline

With the plumbing in place, building the next feature becomes easier. The Angular HTTP client now reliably carries identity, the .NET API can rely on validated claims, and your Australian customers, whether they are checking their super balance from Bondi or arranging a freight booking from Townsville, experience the kind of seamless session that builds trust in a brand. Take the next step in your own project: sketch the token service, wire the interceptor, and watch how much simpler the rest of the application becomes once authentication is no longer an afterthought.