Back-end

- JWT : authentifier une API C# ASP.NET / Angular

Jwt Authentification Csharp Aspnet-Core Angular Sql-Server Entity-Framework Security Tokens Httpinterceptor Rbac Refresh-Token Dotnet Auth-Guard
JWT : authentifier une API C# ASP.NET / Angular

Implémentez l'authentification JWT complète avec C#, ASP.NET Core, SQL Server et Angular : access tokens, refresh tokens, RBAC et sécurité production.

1. Architecture JWT et flux d'authentification

Un JWT (JSON Web Token) se compose de trois parties encodées en Base64 séparées par des points : Header (algorithme), Payload (claims) et Signature. Sa nature stateless évite au serveur de stocker des sessions, idéal pour les architectures distribuées et les SPAs Angular.

Structure d'un JWT décodé
// Header
{ "alg": "HS256", "typ": "JWT" }

// Payload (claims)
{ "sub": "42", "email": "user@test.com", "role": "Admin", "exp": 1746460800 }

// Signature = HMAC-SHA256(header + "." + payload, secretKey)

Flux complet client/serveur

Angular                         ASP.NET Core              SQL Server
  |                                  |                          |
  |-- POST /api/auth/login --------> |                          |
  |   { email, password }            |-- SELECT user ---------> |
  |                                  | <-- user + hash ---------|
  |                                  |   BCrypt.Verify(pwd)     |
  | <-- { accessToken(15min),        |
  |       refreshToken(7j) } --------|
  |                                  |
  |-- GET /api/produits -----------> |
  |   Authorization: Bearer JWT      |   JwtBearer valide sig   |
  | <-- 200 OK { [...] } -----------|

Pourquoi JWT pour C#/Angular ?

  • Stateless : aucune session côté serveur, scalabilité horizontale facilitée
  • Claims intégrés : userId, email, roles transportés dans le token
  • CORS-friendly : le header Authorization traverse les origines sans cookie
  • Interopérable : un même token fonctionne pour Angular, mobile, CLI

2. Configuration ASP.NET Core

Packages NuGet requis

# Authentification et tokens JWT
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt

# Entity Framework Core + SQL Server
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

# Hash sécurisé des mots de passe
dotnet add package BCrypt.Net-Core

appsettings.json

{
  "JwtSettings": {
    "SecretKey": "votre-cle-secrete-tres-longue-minimum-32-car!",
    "Issuer": "https://angularforall.com",
    "Audience": "angular-client",
    "ExpirationMinutes": 15,
    "RefreshTokenExpirationDays": 7
  },
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=AngularAuthDb;Trusted_Connection=True;"
  }
}
⚠️ Production : Ne jamais mettre la SecretKey dans appsettings.json. Utilisez les variables d'environnement (DOTNET_JwtSettings__SecretKey) ou Azure Key Vault.

Program.cs — Enregistrement du middleware JWT

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secretKey   = Encoding.ASCII.GetBytes(jwtSettings["SecretKey"]!);

// ── JWT Bearer ──────────────────────────────────────────────────
builder.Services.AddAuthentication(options => {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme    = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey         = new SymmetricSecurityKey(secretKey),
        ValidateIssuer           = true,
        ValidIssuer              = jwtSettings["Issuer"],
        ValidateAudience         = true,
        ValidAudience            = jwtSettings["Audience"],
        ValidateLifetime         = true,
        ClockSkew                = TimeSpan.Zero   // Expiration stricte (pas de marge)
    };
});

// ── CORS pour Angular ───────────────────────────────────────────
builder.Services.AddCors(options => {
    options.AddPolicy("AngularApp", policy =>
        policy.WithOrigins("http://localhost:4200")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials());
});

builder.Services.AddControllers();
builder.Services.AddScoped<IAuthService, AuthService>();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseCors("AngularApp");
app.UseAuthentication(); // ← AVANT UseAuthorization() !
app.UseAuthorization();
app.MapControllers();
app.Run();

3. Design base de données SQL Server

La structure de base de données repose sur quatre tables : Users, Roles, UserRoles (jonction) et RefreshTokens pour la gestion des sessions longue durée.

Modèles Entity Framework Core

// Models/User.cs
public class User
{
    public int       Id           { get; set; }
    public string    Email        { get; set; } = null!;
    public string    FirstName    { get; set; } = null!;
    public string    LastName     { get; set; } = null!;
    public string    PasswordHash { get; set; } = null!; // BCrypt — jamais en clair
    public bool      IsActive     { get; set; } = true;
    public DateTime  CreatedAt    { get; set; } = DateTime.UtcNow;
    public DateTime? LastLoginAt  { get; set; }

    public ICollection<UserRole>     UserRoles     { get; set; } = new List<UserRole>();
    public ICollection<RefreshToken> RefreshTokens { get; set; } = new List<RefreshToken>();
}

// Models/Role.cs
public class Role
{
    public int    Id          { get; set; }
    public string Name        { get; set; } = null!; // "Admin", "Editor", "User"
    public string Description { get; set; } = "";
    public ICollection<UserRole> UserRoles { get; set; } = new List<UserRole>();
}

// Models/UserRole.cs — Table de jonction User ↔ Role
public class UserRole
{
    public int  Id     { get; set; }
    public int  UserId { get; set; }
    public User User   { get; set; } = null!;
    public int  RoleId { get; set; }
    public Role Role   { get; set; } = null!;
}

// Models/RefreshToken.cs
public class RefreshToken
{
    public int       Id        { get; set; }
    public int       UserId    { get; set; }
    public User      User      { get; set; } = null!;
    public string    Token     { get; set; } = null!; // UUID sans tirets
    public DateTime  ExpiresAt { get; set; }
    public DateTime  CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime? RevokedAt { get; set; }          // null = token actif
    public string?   IpAddress { get; set; }
}

AuthDbContext avec seed des rôles

// Data/AuthDbContext.cs
public class AuthDbContext : DbContext
{
    public AuthDbContext(DbContextOptions<AuthDbContext> options) : base(options) { }

    public DbSet<User>         Users         { get; set; } = null!;
    public DbSet<Role>         Roles         { get; set; } = null!;
    public DbSet<UserRole>     UserRoles     { get; set; } = null!;
    public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Seed des rôles par défaut au démarrage
        modelBuilder.Entity<Role>().HasData(
            new Role { Id = 1, Name = "Admin",  Description = "Accès complet" },
            new Role { Id = 2, Name = "Editor", Description = "Gestion du contenu" },
            new Role { Id = 3, Name = "User",   Description = "Accès standard" }
        );

        // Index uniques — performances des requêtes fréquentes
        modelBuilder.Entity<User>().HasIndex(u => u.Email).IsUnique();
        modelBuilder.Entity<RefreshToken>().HasIndex(rt => rt.Token).IsUnique();
    }
}

// Enregistrement dans Program.cs
builder.Services.AddDbContext<AuthDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

Migration et création des tables

# Générer la migration
dotnet ef migrations add InitialAuthSchema

# Appliquer à SQL Server (crée les tables)
dotnet ef database update

4. Service et Controller d'authentification

DTOs — Contrats de l'API

// DTOs (records C# 10+)
public record LoginRequest(
    [Required, EmailAddress] string Email,
    [Required, MinLength(8)] string Password
);

public record RegisterRequest(
    [Required] string FirstName,
    [Required] string LastName,
    [Required, EmailAddress] string Email,
    [Required, MinLength(8)] string Password
);

public record AuthResponse(
    int          UserId,
    string       Email,
    string       FirstName,
    string       AccessToken,
    string       RefreshToken,
    int          ExpiresIn,    // Secondes avant expiration
    List<string> Roles
);

AuthService — Génération et validation des tokens

public class AuthService : IAuthService
{
    private readonly AuthDbContext  _context;
    private readonly IConfiguration _config;

    public AuthService(AuthDbContext context, IConfiguration config)
    { _context = context; _config = config; }

    // ── Login ────────────────────────────────────────────────────
    public async Task<AuthResponse> LoginAsync(LoginRequest req)
    {
        var user = await _context.Users
            .Include(u => u.UserRoles).ThenInclude(ur => ur.Role)
            .FirstOrDefaultAsync(u => u.Email == req.Email && u.IsActive)
            ?? throw new UnauthorizedAccessException("Identifiants incorrects");

        if (!BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash))
            throw new UnauthorizedAccessException("Identifiants incorrects");

        user.LastLoginAt = DateTime.UtcNow;
        await _context.SaveChangesAsync();

        return BuildResponse(user,
            GenerateAccessToken(user),
            await GenerateRefreshTokenAsync(user));
    }

    // ── Register ─────────────────────────────────────────────────
    public async Task<AuthResponse> RegisterAsync(RegisterRequest req)
    {
        if (await _context.Users.AnyAsync(u => u.Email == req.Email))
            throw new InvalidOperationException("Email déjà utilisé");

        var user = new User {
            Email        = req.Email,
            FirstName    = req.FirstName,
            LastName     = req.LastName,
            PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password, workFactor: 12)
        };

        var defaultRole = await _context.Roles.FindAsync(3); // Rôle "User"
        user.UserRoles.Add(new UserRole { User = user, Role = defaultRole! });

        _context.Users.Add(user);
        await _context.SaveChangesAsync();

        return BuildResponse(user,
            GenerateAccessToken(user),
            await GenerateRefreshTokenAsync(user));
    }

    // ── Refresh ──────────────────────────────────────────────────
    public async Task<AuthResponse> RefreshTokenAsync(string token)
    {
        var stored = await _context.RefreshTokens
            .Include(rt => rt.User).ThenInclude(u => u.UserRoles).ThenInclude(ur => ur.Role)
            .FirstOrDefaultAsync(rt => rt.Token == token)
            ?? throw new UnauthorizedAccessException("Token introuvable");

        // Détection de réutilisation : token déjà révoqué → compromission
        if (stored.RevokedAt != null)
        {
            await RevokeAllUserTokensAsync(stored.UserId);
            throw new UnauthorizedAccessException("Compromission détectée. Reconnectez-vous.");
        }

        if (stored.ExpiresAt < DateTime.UtcNow)
            throw new UnauthorizedAccessException("Refresh token expiré");

        // Rotation : invalider l'ancien, créer un nouveau
        stored.RevokedAt = DateTime.UtcNow;
        await _context.SaveChangesAsync();

        var user = stored.User;
        return BuildResponse(user,
            GenerateAccessToken(user),
            await GenerateRefreshTokenAsync(user));
    }

    // ── Révocation ───────────────────────────────────────────────
    public async Task RevokeTokenAsync(int userId, string token)
    {
        var stored = await _context.RefreshTokens
            .FirstOrDefaultAsync(rt => rt.UserId == userId && rt.Token == token);
        if (stored != null) { stored.RevokedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); }
    }

    private async Task RevokeAllUserTokensAsync(int userId)
    {
        var tokens = await _context.RefreshTokens
            .Where(rt => rt.UserId == userId && rt.RevokedAt == null).ToListAsync();
        tokens.ForEach(t => t.RevokedAt = DateTime.UtcNow);
        await _context.SaveChangesAsync();
    }

    // ── Helpers privés ───────────────────────────────────────────
    private string GenerateAccessToken(User user)
    {
        var jwt = _config.GetSection("JwtSettings");
        var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwt["SecretKey"]!));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim> {
            new(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new(ClaimTypes.Email,          user.Email),
            new(ClaimTypes.GivenName,      user.FirstName),
        };
        foreach (var ur in user.UserRoles)
            claims.Add(new Claim(ClaimTypes.Role, ur.Role.Name));

        var token = new JwtSecurityToken(
            issuer: jwt["Issuer"], audience: jwt["Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(int.Parse(jwt["ExpirationMinutes"]!)),
            signingCredentials: creds);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    private async Task<string> GenerateRefreshTokenAsync(User user)
    {
        var rt = new RefreshToken {
            UserId    = user.Id,
            Token     = Guid.NewGuid().ToString("N"),
            ExpiresAt = DateTime.UtcNow.AddDays(int.Parse(_config["JwtSettings:RefreshTokenExpirationDays"]!))
        };
        _context.RefreshTokens.Add(rt);
        await _context.SaveChangesAsync();
        return rt.Token;
    }

    private AuthResponse BuildResponse(User user, string access, string refresh) =>
        new(user.Id, user.Email, user.FirstName, access, refresh,
            int.Parse(_config["JwtSettings:ExpirationMinutes"]!) * 60,
            user.UserRoles.Select(ur => ur.Role.Name).ToList());
}

AuthController

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IAuthService _auth;
    public AuthController(IAuthService auth) => _auth = auth;

    [HttpPost("login")]    [AllowAnonymous]
    public async Task<IActionResult> Login([FromBody] LoginRequest req)
    {
        try   { return Ok(await _auth.LoginAsync(req)); }
        catch (UnauthorizedAccessException ex) { return Unauthorized(new { message = ex.Message }); }
    }

    [HttpPost("register")] [AllowAnonymous]
    public async Task<IActionResult> Register([FromBody] RegisterRequest req)
    {
        try   { return Created("", await _auth.RegisterAsync(req)); }
        catch (InvalidOperationException ex) { return BadRequest(new { message = ex.Message }); }
    }

    [HttpPost("refresh")]  [AllowAnonymous]
    public async Task<IActionResult> Refresh([FromBody] RefreshRequest req)
    {
        try   { return Ok(await _auth.RefreshTokenAsync(req.RefreshToken)); }
        catch (UnauthorizedAccessException ex) { return Unauthorized(new { message = ex.Message }); }
    }

    [HttpPost("logout")]   [Authorize]
    public async Task<IActionResult> Logout([FromBody] LogoutRequest req)
    {
        var userId = int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
        await _auth.RevokeTokenAsync(userId, req.RefreshToken);
        return Ok(new { message = "Déconnecté avec succès" });
    }

    [HttpGet("me")]        [Authorize]
    public IActionResult Me() => Ok(new {
        email     = User.FindFirstValue(ClaimTypes.Email),
        firstName = User.FindFirstValue(ClaimTypes.GivenName),
        roles     = User.FindAll(ClaimTypes.Role).Select(c => c.Value)
    });
}

5. Intégration Angular — Service et Interceptor

Interface TypeScript

// models/auth.models.ts
export interface AuthResponse {
  userId:       number;
  email:        string;
  firstName:    string;
  accessToken:  string;
  refreshToken: string;
  expiresIn:    number;
  roles:        string[];
}

AuthService Angular

// services/auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly apiUrl = 'http://localhost:5000/api/auth';
  private currentUser$ = new BehaviorSubject<AuthResponse | null>(null);

  constructor(private http: HttpClient) {
    const stored = sessionStorage.getItem('currentUser');
    if (stored) this.currentUser$.next(JSON.parse(stored));
  }

  login(email: string, password: string): Observable<AuthResponse> {
    return this.http.post<AuthResponse>(`${this.apiUrl}/login`, { email, password })
      .pipe(tap(res => this.saveSession(res)));
  }

  register(data: { firstName: string; lastName: string; email: string; password: string }): Observable<AuthResponse> {
    return this.http.post<AuthResponse>(`${this.apiUrl}/register`, data)
      .pipe(tap(res => this.saveSession(res)));
  }

  refreshToken(): Observable<AuthResponse> {
    const token = localStorage.getItem('refreshToken');
    return this.http.post<AuthResponse>(`${this.apiUrl}/refresh`, { refreshToken: token })
      .pipe(tap(res => this.saveSession(res)));
  }

  logout(): Observable<any> {
    const token = localStorage.getItem('refreshToken');
    return this.http.post(`${this.apiUrl}/logout`, { refreshToken: token })
      .pipe(tap(() => this.clearSession()));
  }

  getAccessToken(): string | null { return sessionStorage.getItem('accessToken'); }
  isAuthenticated(): boolean      { return !!this.getAccessToken(); }
  hasRole(role: string): boolean  { return this.currentUser$.value?.roles.includes(role) ?? false; }
  user$(): Observable<AuthResponse | null> { return this.currentUser$.asObservable(); }

  private saveSession(res: AuthResponse): void {
    sessionStorage.setItem('accessToken', res.accessToken);
    sessionStorage.setItem('currentUser', JSON.stringify(res));
    localStorage.setItem('refreshToken', res.refreshToken);
    this.currentUser$.next(res);
  }

  private clearSession(): void {
    sessionStorage.clear();
    localStorage.removeItem('refreshToken');
    this.currentUser$.next(null);
  }
}

HttpInterceptor — Bearer token automatique + refresh

// interceptors/jwt.interceptor.ts
@Injectable()
export class JwtInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshSubject = new BehaviorSubject<string | null>(null);

  constructor(private auth: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = this.auth.getAccessToken();
    if (token) req = this.addBearer(req, token);

    return next.handle(req).pipe(
      catchError((err: HttpErrorResponse) => {
        if (err.status === 401 && !this.isRefreshing) return this.handle401(req, next);
        return throwError(() => err);
      })
    );
  }

  private handle401(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.isRefreshing = true;
    return this.auth.refreshToken().pipe(
      switchMap(res => {
        this.isRefreshing = false;
        this.refreshSubject.next(res.accessToken);
        return next.handle(this.addBearer(req, res.accessToken));
      }),
      catchError(err => {
        this.isRefreshing = false;
        this.auth.logout().subscribe();
        return throwError(() => err);
      })
    );
  }

  private addBearer(req: HttpRequest<any>, token: string) {
    return req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
  }
}

// Dans app.config.ts (Angular 17+ standalone) ou app.module.ts
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }

6. Refresh tokens et rotation sécurisée

La rotation des refresh tokens est fondamentale : chaque utilisation génère un nouveau refresh token et révoque l'ancien. Toute tentative de réutilisation d'un token déjà révoqué déclenche la révocation de tous les tokens de l'utilisateur — signe d'une compromission.

Comparaison des stratégies de stockage
StockageProtection XSSProtection CSRFUsage recommandé
httpOnly Cookie✅ Protégé⚠️ AttentionRefresh token (prod)
sessionStorage⚠️ Exposé✅ OKAccess token
localStorage❌ Vulnérable✅ OKÀ éviter
Memory JS✅ Protégé✅ OKAccess token (max sécu)

Nettoyage automatique des tokens expirés

// Job à planifier (Hangfire, Quartz, ou hosted service)
public async Task CleanExpiredTokensAsync()
{
    var expired = await _context.RefreshTokens
        .Where(rt => rt.ExpiresAt < DateTime.UtcNow || rt.RevokedAt != null)
        .ToListAsync();

    _context.RefreshTokens.RemoveRange(expired);
    await _context.SaveChangesAsync();
}

7. Rôles et permissions (RBAC)

Autorisation backend par rôle

// Réservé aux Admins uniquement
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
public IActionResult Delete(int id) { ... }

// Accessible aux Admins ET Editors
[HttpPut("{id}")]
[Authorize(Roles = "Admin,Editor")]
public IActionResult Update(int id) { ... }

// Politique personnalisée (Program.cs)
builder.Services.AddAuthorization(options => {
    options.AddPolicy("CanPublish", p => p.RequireRole("Admin", "Editor"));
});

[Authorize(Policy = "CanPublish")]
public IActionResult Publish(int id) { ... }

Auth Guard Angular

// guards/auth.guard.ts
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private auth: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot): boolean {
    if (!this.auth.isAuthenticated()) {
      this.router.navigate(['/login']); return false;
    }
    const roles = route.data['roles'] as string[] | undefined;
    if (roles?.length && !roles.some(r => this.auth.hasRole(r))) {
      this.router.navigate(['/403']); return false;
    }
    return true;
  }
}

// Routes protégées
const routes: Routes = [
  { path: 'admin',     component: AdminComponent,  canActivate: [AuthGuard], data: { roles: ['Admin'] } },
  { path: 'editor',    component: EditorComponent, canActivate: [AuthGuard], data: { roles: ['Admin','Editor'] } },
  { path: 'dashboard', component: DashComponent,   canActivate: [AuthGuard] },
];

8. Sécurité en production

Rate limiting sur les endpoints d'auth

dotnet add package AspNetCoreRateLimit
// appsettings.json
"IpRateLimiting": {
  "EnableEndpointRateLimiting": true,
  "GeneralRules": [
    { "Endpoint": "POST:/api/auth/login",    "Period": "1m", "Limit": 5  },
    { "Endpoint": "POST:/api/auth/register", "Period": "1h", "Limit": 10 },
    { "Endpoint": "POST:/api/auth/refresh",  "Period": "1m", "Limit": 20 }
  ]
}
  • SecretKey > 32 caractères, en variable d'environnement uniquement
  • HTTPS obligatoire — UseHttpsRedirection() activé
  • CORS restreint aux origines autorisées (pas de wildcard *)
  • BCrypt work factor ≥ 12 pour les mots de passe
  • Rate limiting sur /login, /register, /refresh
  • Rotation des refresh tokens + détection de réutilisation
  • Job de nettoyage quotidien des tokens expirés
  • Logs des connexions échouées et révocations de tokens
  • Headers de sécurité : X-Content-Type-Options, X-Frame-Options
  • Tester les failles OWASP Top 10 avant déploiement

9. Tests et debugging

Tests unitaires C# avec xUnit

public class AuthServiceTests
{
    [Fact]
    public async Task Login_AvecBonsIdentifiants_RetourneTokens()
    {
        // Arrange : utilisateur avec hash BCrypt
        var hash = BCrypt.Net.BCrypt.HashPassword("Password123!");
        // ... setup mock DbContext avec cet utilisateur ...

        // Act
        var result = await _authService.LoginAsync(
            new LoginRequest("test@test.com", "Password123!"));

        // Assert
        Assert.NotNull(result.AccessToken);
        Assert.NotEmpty(result.RefreshToken);
        Assert.Contains("User", result.Roles);
    }

    [Fact]
    public async Task Login_AvecMauvaisMotDePasse_LanceException()
    {
        await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
            _authService.LoginAsync(new LoginRequest("test@test.com", "Mauvais!")));
    }
}

Tests Angular avec HttpClientTestingModule

describe('AuthService', () => {
  let service: AuthService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [AuthService]
    });
    service  = TestBed.inject(AuthService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => httpMock.verify());

  it('stocke les tokens après login', () => {
    const mockRes = {
      accessToken: 'jwt-abc', refreshToken: 'ref-xyz', userId: 1,
      email: 'a@b.com', firstName: 'Test', expiresIn: 900, roles: ['User']
    };

    service.login('a@b.com', 'Password123!').subscribe(() => {
      expect(sessionStorage.getItem('accessToken')).toBe('jwt-abc');
      expect(localStorage.getItem('refreshToken')).toBe('ref-xyz');
    });

    httpMock.expectOne(`${service['apiUrl']}/login`).flush(mockRes);
  });
});

Debugging avec curl

# 1. Login — récupérer les tokens
curl -X POST http://localhost:5000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@test.com","password":"Password123!"}'

# 2. Appel protégé avec le token
curl http://localhost:5000/api/auth/me \
  -H "Authorization: Bearer [votre-access-token]"

# 3. Refresh du token
curl -X POST http://localhost:5000/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken":"[votre-refresh-token]"}'

Conclusion et checklist de production

Vous disposez maintenant d'une implémentation JWT complète et sécurisée pour votre stack C#/ASP.NET Core + SQL Server + Angular. L'architecture couvre tous les maillons : génération de tokens avec HMAC-SHA256, stockage en SQL Server via EF Core, rotation des refresh tokens avec détection de compromission, contrôle d'accès par rôle et intégration transparente dans Angular via l'interceptor HTTP.

  • Backend : [Authorize] sur toutes les routes sensibles
  • Backend : BCrypt work factor ≥ 12, mots de passe jamais en clair
  • Backend : SecretKey en variable d'environnement
  • Backend : Rotation + détection de réutilisation des refresh tokens
  • Backend : Rate limiting sur /login, /register, /refresh
  • Frontend : HttpInterceptor ajoute le Bearer automatiquement
  • Frontend : AuthGuard protège toutes les routes privées
  • Frontend : Refresh automatique en cas de 401
  • BDD : Index sur Users.Email et RefreshTokens.Token
  • BDD : Job de nettoyage des tokens expirés (quotidien)
  • Infra : HTTPS obligatoire, CORS restreint, logs d'auth activés

Partager