Building an Authentication Service in Angular with Signals, JWT & Refresh Tokens
Introduction
📊 Key Facts At A Glance
- → For example: API Request ↓ Interceptor ↓ Attach Access Token ↓ API ↓ 401
- → For example, a decoded JWT might contain: { "nameid": "123", "unique_name": "John", "email": "john@example
What Happened
Historically, web applications relied on server‑side sessions stored in memory or a database, with a session identifier passed via a cookie. While simple, that model does not work well for single‑page applications (SPAs) that need to make frequent API calls from the client, often across domains, and that must remain stateless for scalability. The industry therefore shifted toward token‑based authentication, and JSON Web Tokens (JWT) quickly became the de‑facto standard because they are compact, URL‑safe, and self‑contained. A JWT carries a set of claims—such as the user’s identifier, email, and role—signed with a secret key, allowing the server to verify its integrity without a database lookup on every request.
However, JWTs are typically short‑lived (e.g., 15 minutes) to limit exposure if a token is compromised. When the token expires, the client must obtain a new one without forcing the user to re‑enter credentials. This is where refresh tokens come in: a long‑lived, securely stored token that the client can exchange for a fresh JWT. The combination of an access token (the JWT) and a refresh token provides both security (short‑lived access) and usability (transparent renewal).
Angular’s recent introduction of Signals—a reactive primitive that replaces many use‑cases of RxJS Subjects—offers a more intuitive way to manage authentication state across the application. Signals automatically propagate changes, making it easier to keep UI components in sync with the current user’s status, token expiration, and role information.
Key Details
The authentication flow built with Angular Signals, JWT, and refresh tokens follows a clear sequence. First, the user submits credentials to a login endpoint on the ASP.NET Core API. The server validates the credentials, then issues two tokens: an access token (JWT) with a short expiration and a refresh token stored in a secure HttpOnly cookie or encrypted local storage. The Angular service receives the JWT, decodes its payload to extract claims (such as user ID and role), and writes those values into a Signal that represents the current authentication state.
Because Signals are observable by any component, a navigation bar can subscribe to the authentication Signal and automatically show “Login” or “Logout” links, display the user’s name, or hide admin‑only menus based on the role claim. When the page reloads, the Angular app checks for a stored JWT. If it exists and is still valid, the service restores the Signal’s state, preserving the user’s session without a round‑trip to the server. If the JWT is missing or expired, the service automatically attempts to use the refresh token to request a new JWT. This refresh request is made to a dedicated endpoint that validates the refresh token, issues a fresh JWT, and optionally rotates the refresh token for added security.
Key implementation points include: storing the JWT in memory or a short‑lived storage (never in plain localStorage unless encrypted), protecting the refresh token with HttpOnly cookies to mitigate XSS, using Angular’s HttpInterceptor to attach the JWT to every outgoing API request, handling 401 responses by triggering a token refresh, and finally, clearing both tokens on logout to fully terminate the session.
Background
Authentication has always been a fundamental requirement, but the rise of SPAs, mobile clients, and microservice architectures has reshaped how developers approach it. Stateless token‑based authentication aligns with REST principles, allowing each service to verify a token independently without shared session state. JWTs encode claims in a JSON payload, signed with HS256 or RS256 algorithms, making them verifiable by any service that holds the secret or public key. Refresh tokens, on the other hand, are opaque strings that the authorization server treats as a credential to issue new access tokens, and they can be revoked or rotated to reduce risk.
Signals, introduced in Angular 16, replace many patterns that previously required manual subscription management with RxJS. A Signal holds a value and notifies dependents whenever that value changes. For authentication, a single Signal can hold an object like { isAuthenticated: true, user: { id, email, role }, tokenExpiresAt }. Any component that reads this Signal automatically re‑renders when the user logs in, logs out, or when the token is refreshed, eliminating boilerplate and reducing the chance of stale UI state.
Why It Matters
Security is non‑negotiable. By using short‑lived JWTs, the window for token replay attacks is minimized. Refresh tokens stored in HttpOnly cookies are inaccessible to JavaScript, protecting them from cross‑site scripting (XSS) attacks. Moreover, rotating refresh tokens on each use adds another layer of defense, as a stolen token becomes useless after the next successful refresh.
From a user experience perspective, the seamless token refresh process means users rarely see “session expired” prompts. The application can silently obtain a new JWT in the background, keeping the UI responsive and the workflow uninterrupted. This reliability is especially important for enterprise applications where downtime directly impacts productivity.
Scalability also benefits from this architecture. Because the API validates tokens without consulting a central session store, horizontal scaling of the backend is straightforward. Each instance can independently verify the JWT signature, and the refresh endpoint can be protected with rate limiting and revocation lists to handle abuse without affecting the main request path.
What Happens Next
The next phase involves turning the concepts into code. In Angular, you will create an AuthService that encapsulates the Signal representing authentication state, provides methods like login(), register(), logout(), and refreshToken(), and sets up an HttpInterceptor to attach the JWT to outgoing requests. The interceptor will also listen for 401 responses and trigger a token refresh before retrying the failed request.
On the ASP.NET Core side, you will configure JWT authentication middleware, define endpoints for login, registration, and token refresh, and implement secure storage for refresh tokens (e.g., a database table with hashed tokens). You will also enforce role‑based authorization using the [Authorize(Roles = "Admin")] attribute, which reads the role claim from the JWT. Finally, you will write unit and integration tests to verify that token issuance, expiration handling, and refresh logic work as expected under various scenarios.
By following these steps, you will have a full‑stack authentication solution that leverages Angular Signals for reactive UI updates, JWTs for stateless, verifiable access control, and refresh tokens for a smooth, secure user experience.
Conclusion
đź“– See Also
📚 Sources & Attribution
- âś“ Dev.to