Authentifizierung und Autorisierung in .NET: JWT und Identity
# Authentifizierung und Autorisierung in .NET
Sichere APIs benotigen eine klare Trennung von Authentifizierung (Identitat) und Autorisierung (Berechtigung). ASP.NET Core bietet dafur eine ausgereifte Middleware und policy-basierte Mechanismen. In diesem Artikel gehe ich detailliert auf JWT-Konfiguration, Identity-Setup, OAuth2/OIDC-Integration und die Sicherheitsmuster ein, auf die ich in Produktionssystemen setze.
Authentifizierungsansatze
JWT Bearer Authentication
JWT (JSON Web Token) ist die bevorzugte Wahl fur stateless API-Authentifizierung. Der Server stellt ein signiertes Token mit den Claims des Benutzers aus, und jede nachfolgende Anfrage tragt dieses Token im `Authorization`-Header. Kein Session-State auf dem Server, keine Sticky Sessions erforderlich.
Eine typische JWT-Konfiguration in `Program.cs`:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration[class="code-string">"Jwt:Issuer"],
ValidAudience = builder.Configuration[class="code-string">"Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration[class="code-string">"Jwt:Key"]!)),
ClockSkew = TimeSpan.FromSeconds(class="code-number">30)
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception is SecurityTokenExpiredException)
{
context.Response.Headers.Append(class="code-string">"X-Token-Expired", class="code-string">"true");
}
return Task.CompletedTask;
}
};
});Den `ClockSkew` setze ich immer auf einen niedrigen Wert. Die standardmassigen 5 Minuten Toleranz sind fur die meisten Systeme viel zu grosszugig und schaffen ein Fenster, in dem abgelaufene Tokens noch akzeptiert werden.
Token-Generierung
Die Token-Generierung wirkt einfach, aber die Details sind entscheidend. Ein Service, den ich typischerweise verwende:
public class TokenService : ITokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config)
{
_config = config;
}
public string GenerateAccessToken(ApplicationUser user, IList<string> roles)
{
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new(class="code-string">"username", user.UserName!)
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config[class="code-string">"Jwt:Key"]!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config[class="code-string">"Jwt:Issuer"],
audience: _config[class="code-string">"Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(class="code-number">15),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public RefreshToken GenerateRefreshToken()
{
return new RefreshToken
{
Token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(class="code-number">64)),
ExpiresAt = DateTime.UtcNow.AddDays(class="code-number">7),
CreatedAt = DateTime.UtcNow
};
}
}In APIs, die ich fur die Produktion abgesichert habe, halte ich Access Tokens kurzlebig (15 Minuten oder weniger) und kombiniere sie mit serverseitig gespeicherten Refresh Tokens. Das begrenzt das Schadensfenster, falls ein Token kompromittiert wird.
ASP.NET Core Identity
Wenn Ihre Anwendung den gesamten Benutzerlebenszyklus kontrolliert -- Registrierung, Passwortverwaltung, E-Mail-Bestatigung, Zwei-Faktor-Authentifizierung -- ist ASP.NET Core Identity das richtige Fundament. Es ubernimmt die aufwandige Arbeit von Passwort-Hashing, Sperrrichtlinien und User-Store-Abstraktion.
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
class=class="code-string">"code-comment">// Passwortregeln
options.Password.RequireDigit = true;
options.Password.RequiredLength = class="code-number">12;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
class=class="code-string">"code-comment">// Kontosperrung
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(class="code-number">15);
options.Lockout.MaxFailedAccessAttempts = class="code-number">5;
options.Lockout.AllowedForNewUsers = true;
class=class="code-string">"code-comment">// Benutzer
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();Refresh-Token-Ablauf
Ein sauberer Refresh-Token-Ablauf ist unverzichtbar. Der Client sendet ein abgelaufenes Access Token zusammen mit einem gultigen Refresh Token und erhalt ein neues Paar:
[HttpPost(class="code-string">"refresh")]
[AllowAnonymous]
public async Task<IActionResult> Refresh([FromBody] TokenRefreshRequest request)
{
var principal = GetPrincipalFromExpiredToken(request.AccessToken);
if (principal is null)
return Unauthorized(class="code-string">"Ungultiges Access Token.");
var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
var user = await _userManager.FindByIdAsync(userId!);
if (user is null)
return Unauthorized();
var storedToken = await _tokenRepository.GetRefreshTokenAsync(user.Id);
if (storedToken is null ||
storedToken.Token != request.RefreshToken ||
storedToken.ExpiresAt <= DateTime.UtcNow ||
storedToken.IsRevoked)
{
class=class="code-string">"code-comment">// Moglicher Token-Wiederverwendungsangriff -- gesamte Familie widerrufen
if (storedToken?.IsRevoked == true)
{
await _tokenRepository.RevokeAllTokensForUserAsync(user.Id);
}
return Unauthorized(class="code-string">"Ungultiges oder abgelaufenes Refresh Token.");
}
class=class="code-string">"code-comment">// Rotation: altes widerrufen, neues ausstellen
storedToken.IsRevoked = true;
var newRefreshToken = _tokenService.GenerateRefreshToken();
newRefreshToken.UserId = user.Id;
newRefreshToken.ReplacedByToken = newRefreshToken.Token;
await _tokenRepository.SaveRefreshTokenAsync(newRefreshToken);
var roles = await _userManager.GetRolesAsync(user);
var newAccessToken = _tokenService.GenerateAccessToken(user, roles);
return Ok(new TokenResponse
{
AccessToken = newAccessToken,
RefreshToken = newRefreshToken.Token
});
}Das entscheidende Detail ist die Token-Familien-Verfolgung. Wird ein widerrufenes Refresh Token erneut verwendet, widerrufe ich die gesamte Kette. Damit werden Replay-Angriffe erkannt, bei denen ein Angreifer ein Token abfangt, das der legitime Client bereits rotiert hat.
Autorisierungsstrategie
Policy-basierte Autorisierung
Verstreute `[Authorize(Roles = "Admin")]`-Attribute werden schnell zum Wartungsalbtraum. Policy-basierte Autorisierung zentralisiert die Logik und macht sie testbar:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(class="code-string">"CanManageProducts", policy =>
policy.RequireRole(class="code-string">"Admin", class="code-string">"ProductManager"));
options.AddPolicy(class="code-string">"CanViewReports", policy =>
policy.RequireClaim(class="code-string">"permission", class="code-string">"reports:read"));
options.AddPolicy(class="code-string">"PremiumUser", policy =>
policy.Requirements.Add(new PremiumSubscriptionRequirement()));
class=class="code-string">"code-comment">// Deny-by-Default: alle Endpoints erfordern Authentifizierung
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});Fur komplexe Geschaftsregeln implementieren Sie `IAuthorizationHandler`:
public class PremiumSubscriptionHandler
: AuthorizationHandler<PremiumSubscriptionRequirement>
{
private readonly ISubscriptionService _subscriptionService;
public PremiumSubscriptionHandler(ISubscriptionService subscriptionService)
{
_subscriptionService = subscriptionService;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PremiumSubscriptionRequirement requirement)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is null) return;
var isActive = await _subscriptionService
.HasActiveSubscriptionAsync(userId);
if (isActive)
{
context.Succeed(requirement);
}
}
}Anschliessend sauber auf die Endpoints anwenden:
app.MapGet(class="code-string">"/api/reports", GetReports)
.RequireAuthorization(class="code-string">"CanViewReports");
app.MapDelete(class="code-string">"/api/products/{id}", DeleteProduct)
.RequireAuthorization(class="code-string">"CanManageProducts");Dieser Ansatz halt Controller schlank und macht die Autorisierungslogik isoliert testbar.
OAuth2/OIDC-Integration
Fur Enterprise-SSO oder Social Login ist die Integration mit einem externen Identity Provider uber OpenID Connect der Standardweg. Ein praktisches Setup mit einem Provider wie Azure AD oder Keycloak:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration[class="code-string">"Oidc:Authority"];
options.ClientId = builder.Configuration[class="code-string">"Oidc:ClientId"];
options.ClientSecret = builder.Configuration[class="code-string">"Oidc:ClientSecret"];
options.ResponseType = class="code-string">"code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add(class="code-string">"openid");
options.Scope.Add(class="code-string">"profile");
options.Scope.Add(class="code-string">"email");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = class="code-string">"name",
RoleClaimType = class="code-string">"role"
};
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async context =>
{
class=class="code-string">"code-comment">// Externen Benutzer mit lokaler Datenbank synchronisieren
var userService = context.HttpContext.RequestServices
.GetRequiredService<IUserSyncService>();
await userService.SyncExternalUserAsync(context.Principal!);
}
};
});Fur API-zu-API-Szenarien ohne Browser verwenden Sie den Client-Credentials-Flow:
public class MachineToMachineTokenService
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _config;
public async Task<string> GetAccessTokenAsync()
{
var disco = await _httpClient
.GetDiscoveryDocumentAsync(_config[class="code-string">"Oidc:Authority"]);
var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = _config[class="code-string">"Oidc:ServiceClientId"]!,
ClientSecret = _config[class="code-string">"Oidc:ServiceClientSecret"]!,
Scope = class="code-string">"api.read api.write"
});
if (tokenResponse.IsError)
throw new InvalidOperationException(
$class="code-string">"Token-Anfrage fehlgeschlagen: {tokenResponse.Error}");
return tokenResponse.AccessToken!;
}
}In APIs, die ich fur die Produktion abgesichert habe, synchronisiere ich beim ersten Login immer die externen Identity-Claims mit einem lokalen Benutzerdatensatz. Das gibt der Anwendung eine lokale Referenz fur Audit-Trails und vermeidet wiederholte Abfragen an den Identity Provider.
Sicherheits-Hartungs-Checkliste
Diese Prufpunkte gehe ich vor jedem Produktions-Deployment durch:
class=class="code-string">"code-comment">// Rate Limiting auf Auth-Endpoints
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter(class="code-string">"AuthEndpoints", limiter =>
{
limiter.PermitLimit = class="code-number">10;
limiter.Window = TimeSpan.FromMinutes(class="code-number">1);
limiter.QueueLimit = class="code-number">0;
});
});
class=class="code-string">"code-comment">// Auf Auth-Routen anwenden
app.MapPost(class="code-string">"/api/auth/login", Login)
.RequireRateLimiting(class="code-string">"AuthEndpoints");
app.MapPost(class="code-string">"/api/auth/refresh", Refresh)
.RequireRateLimiting(class="code-string">"AuthEndpoints");Haufige Auth-Fehler
Im Laufe der Jahre habe ich in Projekten immer wieder dieselben Fehler gesehen. Hier sind die, die echten Schaden verursachen:
Tokens im localStorage speichern
Das ist der haufigste Fehler uberhaupt. Jede XSS-Schwachstelle gibt einem Angreifer vollen Zugriff auf das Token. Access Tokens gehoren in den Arbeitsspeicher (eine JavaScript-Variable, nicht persistenter Speicher). Refresh Tokens gehoren in httpOnly Cookies, auf die JavaScript keinen Zugriff hat.
Keine Refresh-Token-Rotation
Wenn ein Refresh Token 30 Tage gultig ist und nie rotiert wird, gibt ein einziger Leak dem Angreifer eine monatelange Sitzung. Rotieren Sie bei jeder Verwendung und verfolgen Sie die Token-Familie, damit die Wiederverwendung eines alten Tokens die gesamte Kette widerruft.
Nur die Signatur validieren
Eine gultige Signatur bedeutet nicht ein gultiges Token. Validieren Sie immer die Claims `iss`, `aud`, `exp` und `nbf`. Ich habe Systeme gesehen, die die Signatur pruften, aber die Audience ignorierten -- Tokens, die fur einen Service ausgestellt wurden, funktionierten auf einem anderen.
Aufgeblahte Tokens mit sensiblen Daten
JWTs sind kodiert, nicht verschlusselt. Packen Sie keine E-Mail-Adressen, Telefonnummern oder interne IDs, die die Systemarchitektur offenlegen, in Tokens. Halten Sie Claims minimal -- eine Benutzer-ID und Rollenliste reichen normalerweise aus. Wenn Sie mehr Daten benotigen, schlagen Sie serverseitig nach.
Symmetrische Schlussel uber Services hinweg geteilt
Die Verwendung desselben HMAC-Schlussels uber mehrere Services hinweg bedeutet, dass jeder Service Tokens fur alle anderen falschen kann. In einer Microservices-Architektur verwenden Sie asymmetrische Schlussel (RSA oder ECDSA). Der Auth-Service halt den privaten Schlussel, konsumierende Services benotigen nur den offentlichen Schlussel.
class=class="code-string">"code-comment">// Asymmetrische Schlusselvalidierung fur konsumierende Services
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(rsaPublicKey),
ValidateIssuer = true,
ValidIssuer = class="code-string">"https:class="code-commentclass="code-string">">//auth.mycompany.com",
ValidateAudience = true,
ValidAudience = class="code-string">"https:class="code-commentclass="code-string">">//api.mycompany.com"
};Fehlende Token-Widerrufsstrategie
JWTs sind designbedingt zustandslos, was bedeutet, dass man sie ohne zusatzliche Infrastruktur nicht widerrufen kann. Fur Produktionssysteme pflege ich eine Widerrufsliste (kurzlebiger Cache in Redis), die die Middleware bei jeder Anfrage pruft. Der Overhead ist minimal im Vergleich zur Sicherheit, die er bietet.
Auth-Ereignisse nicht protokollieren
Wenn Sie nicht beantworten konnen "Wer hat sich von wo, wann eingeloggt und worauf zugegriffen?", sind Sie nicht produktionsreif. Jede Authentifizierungs- und Autorisierungsentscheidung sollte ein Audit-Event erzeugen.
Fazit
Eine stabile Sicherheitsarchitektur in .NET basiert auf standardbasierter Authentifizierung kombiniert mit expliziter, policy-gesteuerter Autorisierung und solider Betriebshygiene. Das Framework liefert solide Bausteine, aber der Unterschied zwischen einem sicheren und einem verwundbaren System liegt in den Details -- Token-Lebensdauern, Rotationsstrategien, Speicherentscheidungen und konsistentes Audit-Logging.
Gerne prufe ich Ihre Auth- und Autorisierungsstrategie fur die Produktion.
Verwandte Artikel
Was ist .NET? Ein moderner Backend-Entwicklungsleitfaden
Erfahren Sie, was .NET ist, wie es funktioniert und warum es für Enterprise-Backend gewählt wird.
RESTful APIs mit ASP.NET Core entwickeln
Lernen Sie die Grundlagen für produktionsreife REST-APIs mit ASP.NET Core. Controller, Routing und Best Practices.
Clean Architecture in .NET: Skalierbare Projektstruktur
Wenden Sie Clean Architecture in .NET-Projekten an. Schichten, Abhängigkeiten und testbarer Code.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen