JWT in Angular with web API dynamic role based authentication and authorization
Backend API (ASP.NET Core Example)
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
public class AuthController : ControllerBase
{
private readonly ITokenService _tokenService;
private readonly IUserService _userService;
public AuthController(ITokenService tokenService, IUserService userService)
{
_tokenService = tokenService;
_userService = userService;
}
[HttpPost("login")]
public ActionResult Login([FromBody] LoginModel model)
{
var user = _userService.Authenticate(model.Username, model.Password);
if (user == null) return Unauthorized("Invalid credentials");
var token = _tokenService.GenerateToken(user);
return Ok(new { Token = token });
}
}
Angular Authentication Service
// auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { JwtHelperService } from '@auth0/angular-jwt';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl = 'https://your-api.com/api';
private currentUserSubject: BehaviorSubject<any>;
public currentUser: Observable<any>;
constructor(private http: HttpClient, private jwtHelper: JwtHelperService) {
this.currentUserSubject = new BehaviorSubject<any>(JSON.parse(localStorage.getItem('currentUser')!));
this.currentUser = this.currentUserSubject.asObservable();
}
login(username: string, password: string) {
return this.http.post<any>(`${this.apiUrl}/login`, { username, password })
.subscribe(data => {
localStorage.setItem('currentUser', JSON.stringify(data));
this.currentUserSubject.next(data);
});
}
logout() {
localStorage.removeItem('currentUser');
this.currentUserSubject.next(null);
}
getToken(): string {
const user = JSON.parse(localStorage.getItem('currentUser')!);
return user ? user.token : '';
}
isAuthenticated(): boolean {
const token = this.getToken();
return token && !this.jwtHelper.isTokenExpired(token);
}
getUserRoles(): string[] {
const token = this.getToken();
return token ? this.jwtHelper.decodeToken(token).roles : [];
}
}
JWT Interceptor
// auth.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { AuthService } from './auth.service';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getToken();
if (token) {
const cloned = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(cloned);
}
return next.handle(req);
}
}
Role Guard
// role.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const requiredRoles = route.data['roles'] as Array<string>;
const userRoles = this.authService.getUserRoles();
if (this.authService.isAuthenticated() && requiredRoles.some(role => userRoles.includes(role))) {
return true;
}
this.router.navigate(['/login']);
return false;
}
}
Routing Module
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin/admin.component';
import { UserComponent } from './user/user.component';
import { RoleGuard } from './role.guard';
const routes: Routes = [
{ path: 'admin', component: AdminComponent, canActivate: [RoleGuard], data: { roles: ['admin'] } },
{ path: 'user', component: UserComponent, canActivate: [RoleGuard], data: { roles: ['user', 'admin'] } },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Role-Based UI Rendering in Angular
<!-- app.component.html -->
<div *ngIf="authService.isAuthenticated()">
<div *ngIf="authService.getUserRoles().includes('admin')">
<p>Welcome, Admin!</p>
</div>
<div *ngIf="authService.getUserRoles().includes('user')">
<p>Welcome, User!</p>
</div>
</div>
HTTP Interceptor Registration
// app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';
@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
],
})
export class AppModule { }
1. Backend (ASP.NET Core) - Refresh Token Implementation
public class RefreshTokenModel
{
public string RefreshToken { get; set; }
}
[HttpPost("refresh-token")]
public ActionResult RefreshToken([FromBody] RefreshTokenModel model)
{
var newToken = _tokenService.RefreshToken(model.RefreshToken);
if (newToken == null) return Unauthorized("Invalid or expired refresh token.");
return Ok(new { Token = newToken });
}
2. Backend Middleware to Validate JWT
public class JwtMiddleware
{
private readonly RequestDelegate _next;
public JwtMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, ITokenService tokenService)
{
var token = context.Request.Headers["Authorization"].ToString().Split(" ").Last();
if (!string.IsNullOrEmpty(token) && !tokenService.IsTokenExpired(token))
{
var claims = tokenService.ValidateToken(token);
if (claims != null)
{
context.User = new ClaimsPrincipal(new ClaimsIdentity(claims));
}
}
await _next(context);
}
}
3. Role-Based Authorization on Server Side
[Authorize(Roles = "admin")]
[HttpGet("admin-data")]
public IActionResult GetAdminData()
{
return Ok(new { data = "Admin Data" });
}
4. Angular - Token Expiry Handling with Refresh Token
// auth.service.ts
refreshToken() {
const refreshToken = this.getRefreshToken();
return this.http.post<any>(`${this.apiUrl}/refresh-token`, { refreshToken })
.pipe(map(response => {
localStorage.setItem('currentUser', JSON.stringify(response));
return response.token;
}));
}
5. Angular - HTTP Interceptor for Token Refresh
// auth.interceptor.ts
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = this.authService.getToken();
if (token && this.jwtHelper.isTokenExpired(token)) {
return this.authService.refreshToken().pipe(
switchMap((newToken) => {
const cloned = req.clone({
setHeaders: {
Authorization: `Bearer ${newToken}`
}
});
return next.handle(cloned);
})
);
}
const cloned = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next.handle(cloned);
}
6. Handling 401 Unauthorized Error
// auth.interceptor.ts
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((error) => {
if (error.status === 401) {
this.authService.logout();
this.router.navigate(['/login']);
}
return throwError(error);
})
);
}
7. Backend - Secure Cookie Setup for JWT
// Backend: Set JWT in HttpOnly cookie
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = true, // If using HTTPS
SameSite = SameSiteMode.Strict
};
Response.Cookies.Append("access_token", token, cookieOptions);
8. Frontend - Remove Token on Logout
logout() {
localStorage.removeItem('currentUser');
document.cookie = 'access_token=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
this.currentUserSubject.next(null);
}
9. CORS Configuration (Backend)
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("AllowAllOrigins", builder =>
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseCors("AllowAllOrigins");
app.UseAuthentication();
app.UseMvc();
}
10. Frontend - Role-Based UI Rendering
<!-- app.component.html -->
<div *ngIf="authService.isAuthenticated()">
<div *ngIf="authService.getUserRoles().includes('admin')">
<p>Welcome, Admin!</p>
</div>
<div *ngIf="authService.getUserRoles().includes('user')">
<p>Welcome, User!</p>
</div>
</div>
11. Frontend - Role Guard
// role.guard.ts
@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const requiredRoles = route.data['roles'] as Array<string>;
const userRoles = this.authService.getUserRoles();
if (this.authService.isAuthenticated() && requiredRoles.some(role => userRoles.includes(role))) {
return true;
}
this.router.navigate(['/login']);
return false;
}
}
12. Routing Module
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin/admin.component';
import { UserComponent } from './user/user.component';
import { RoleGuard } from './role.guard';
const routes: Routes = [
{ path: 'admin', component: AdminComponent, canActivate: [RoleGuard], data: { roles: ['admin'] } },
{ path: 'user', component: UserComponent, canActivate: [RoleGuard], data: { roles: ['user', 'admin'] } },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
overall explanations like Why choose JWT? Where do we store is that safe and secure? each functionalities what we do.. but no code?
Why Choose JWT (JSON Web Token)?
JWT is a compact, URL-safe means of representing claims between two parties. It's widely used in modern web applications for authentication and authorization. Here are the key reasons to choose JWT:
-
Stateless Authentication:
JWT enables stateless authentication, meaning the server doesn't need to store session information. Once the user is authenticated, a token is issued and stored client-side. The server can validate the token with every request, without needing to check a session in memory or a database.
-
Scalability:
Since JWTs are self-contained, they reduce the load on the server as there's no need to store session data or make repeated database calls for each request.
-
Cross-Domain Authentication:
JWT can be used to authenticate users across different domains or services. It’s ideal for single sign-on (SSO) scenarios, where a single token can grant access to multiple applications or APIs.
-
Interoperability:
JWT is a standard (RFC 7519) and can be used across different languages and platforms, making it ideal for distributed systems with different tech stacks.
-
Flexibility:
JWTs can carry not just authentication data, but also additional user or permission information, making it ideal for role-based access control.
Where to Store JWT: LocalStorage, SessionStorage, or Cookies?
1. LocalStorage:
- Pros: Easy to use, data is persistent even if the browser is closed.
- Cons: Vulnerable to XSS (Cross-Site Scripting) attacks. If an attacker manages to inject scripts into your page, they can access
localStorage
and steal the JWT.
2. SessionStorage:
- Pros: Stores data for the duration of the session. It's cleared when the browser window or tab is closed.
- Cons: Similar to
localStorage
, it's still susceptible to XSS attacks.
3. Cookies (Recommended Approach):
- Pros: Can be set as HttpOnly and Secure, making them inaccessible to JavaScript. This makes them much safer from XSS attacks. Cookies also support the
SameSite
attribute to mitigate CSRF (Cross-Site Request Forgery) attacks.
- Cons: Requires careful management, as cookies are sent automatically with every HTTP request, which can expose your JWT to cross-site attacks if not correctly configured.
Is Storing JWT in Cookies Safe and Secure?
While cookies are safer than localStorage
or sessionStorage
due to their HttpOnly and Secure flags, the security of the JWT depends on how the cookie is configured:
- HttpOnly Flag: When set, the cookie cannot be accessed via JavaScript. This prevents an attacker from stealing the token through XSS attacks.
- Secure Flag: When set, the cookie will only be sent over HTTPS connections, protecting the token during transmission and preventing it from being intercepted by attackers on unsecured networks.
- SameSite Flag: This prevents the browser from sending cookies with cross-site requests, reducing the risk of CSRF attacks.
JWT Structure
A JWT is composed of three parts:
- Header: Contains metadata about the token, such as the signing algorithm (e.g.,
HS256
).
- Payload: Contains the claims or data. This is where user-specific information, roles, and permissions are typically stored.
- Signature: Ensures the integrity of the token. It’s created by signing the header and payload with a secret key or private key.
What Does JWT Do? (Authentication & Authorization)
1. Authentication:
- Login: The user sends their credentials (e.g., username and password) to the backend API.
- Token Issuance: If the credentials are valid, the backend generates a JWT containing user information (like user ID and roles) and sends it back to the frontend.
- Storage: The frontend stores the JWT in either local storage, session storage, or cookies.
- Accessing Resources: With each subsequent request, the frontend includes the JWT in the
Authorization
header (usually Bearer <token>
), and the server verifies the token before granting access.
2. Authorization:
- Role-Based Access: The JWT payload can include claims related to the user’s roles or permissions. When a user requests a protected resource, the backend can check these claims to ensure they have the appropriate permissions to access the resource.
- Secure Endpoints: APIs can be configured with role-based access control (RBAC), using attributes like
[Authorize(Roles = "admin")]
to secure specific routes.
JWT Token Expiry and Refresh
Tokens typically have a short expiration time to reduce security risks if the token is compromised. However, constantly requiring the user to log in can degrade the user experience.
- Access Token: Has a short lifespan (e.g., 15 minutes).
- Refresh Token: Can have a longer lifespan (e.g., 7 days). It’s used to request a new access token once the old one expires.
The process works as follows:
- When the access token expires, the client sends the refresh token to the backend.
- The backend verifies the refresh token and, if valid, issues a new access token.
This allows for a seamless user experience without requiring the user to log in repeatedly.
Server-Side JWT Validation
Whenever a client makes a request to a protected API endpoint, the server must validate the JWT to ensure the request is from an authenticated user.
- Validation: The server checks the signature and expiration of the token. If the token is valid, the user can access the requested resource.
- Roles/Claims: The server can use the claims (such as roles) stored in the payload to control access to specific resources.
Handling Invalid or Expired Tokens
If a token is expired, the server can respond with a 401 Unauthorized
status. The client can then:
- Redirect to login page: If the token is expired or invalid, the client can prompt the user to log in again.
- Refresh Token: If using refresh tokens, the client can send the refresh token to obtain a new access token without requiring the user to log in.
CORS (Cross-Origin Resource Sharing) Configuration
When the frontend (Angular app) and backend API are on different domains, the backend must explicitly allow cross-origin requests. This is done by configuring CORS in the backend to allow the frontend domain to make requests.
Best Practices for Using JWT
- Use HTTPS: Always transmit JWTs over HTTPS to prevent them from being intercepted by attackers.
- Set Token Expiration: Always set an expiration time for JWTs and ensure it’s a reasonable duration.
- Store Tokens Securely: Use HttpOnly cookies or Secure Storage for token storage to prevent exposure to JavaScript.
- Use Refresh Tokens: Implement refresh tokens to keep the user authenticated without needing them to log in repeatedly.
- Check for Token Expiry: Both on the client and server sides, ensure the token is not expired before granting access.
Conclusion
JWT is a powerful and flexible mechanism for authentication and authorization in modern web applications. By using JWTs, you can implement stateless, scalable, and secure authentication systems. However, it's crucial to store tokens securely (preferably in HttpOnly cookies), set reasonable expiration times, and implement refresh tokens to ensure a seamless user experience while minimizing security risks.
Comments
Post a Comment