Authentifizierung und Autorisierung in .NET: JWT und Identity

14 Min. Lesezeit9. Februar 2026Aktualisiert: 9. März 2026
.NET authenticationJWT .NETASP.NET Core IdentityOAuth2 .NET.NET authorizationBearer token C#.NET securityClaims-based auth

# 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`:

csharp
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:

csharp
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.

csharp
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:

csharp
[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:

csharp
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`:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

  • **HTTPS uberall** -- erzwingen mit `app.UseHsts()` und `RequireHttpsMetadata = true` in der JWT-Konfiguration
  • **Token-Speicherung** -- Access Tokens nur im Speicher (niemals localStorage); Refresh Tokens in httpOnly, Secure, SameSite=Strict Cookies
  • **Schlusselverwaltung** -- Signaturschlussel in Azure Key Vault, AWS Secrets Manager oder HashiCorp Vault; niemals in appsettings.json
  • **Token-Lebensdauer** -- Access Tokens maximal 15 Minuten; Refresh Tokens 7 Tage mit Rotation bei jeder Verwendung
  • **CORS-Konfiguration** -- explizite erlaubte Origins, niemals `AllowAnyOrigin()` mit Credentials
  • **Rate Limiting auf Auth-Endpoints** -- Login, Registrierung und Token-Refresh-Endpoints mussen aggressive Ratenlimits haben
  • **Audit-Logging** -- jeden Login, fehlgeschlagenen Versuch, Token-Refresh und Berechtigungseskalation mit Korrelations-IDs protokollieren
  • **Passwortrichtlinie** -- mindestens 12 Zeichen, Prufung gegen die HaveIBeenPwned-API
  • **Kontosperrung** -- nach 5 fehlgeschlagenen Versuchen sperren, 15 Minuten Abkuhlzeit
  • **Sicherheits-Header** -- `X-Content-Type-Options`, `X-Frame-Options`, `Strict-Transport-Security`, `Content-Security-Policy`
  • csharp
    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.

    csharp
    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

    Haben Sie ein Flutter-Projekt?

    Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

    Kontakt aufnehmen