Files
Masterarbeit/QuellCode/CentronERP/docs/reference/security/anmelden-mit-microsoft-technische-anleitung.md
Christoph Schwörer f045b99a25 Codebasis als Dateien ins Arbeitsrepo statt als Gitlink
QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
2026-08-26 07:43:51 +02:00

16 KiB

Anmelden mit Microsoft — Technische Anleitung

Überblick

Die Funktion "Anmelden mit Microsoft" nutzt Microsoft Entra ID (Azure AD) über OpenID Connect mit der MSAL-Bibliothek. Der Client holt ein ID-Token von Microsoft, schickt es an die c-entron API, die es validiert, den User per Entra Object ID (oid-Claim) nachschlägt und ein c-entron Session-Ticket zurückgibt.


Kompletter Flow

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   Client App    │     │  c-entron API    │     │  Microsoft      │
│                 │     │  (Web Service)    │     │  Entra ID       │
└────────┬────────┘     └────────┬─────────┘     └────────┬────────┘
         │                       │                         │
    1. GET /config/jwt ─────────>│                         │
         │<── { Authority,       │                         │
         │     Audience,         │                         │
         │     Enabled }         │                         │
         │                       │                         │
    2. MSAL: Token von Microsoft holen ──────────────────> │
         │<──────────────────────────── AuthenticationResult│
         │   (enthält IdToken)   │                         │
         │                       │                         │
    3. POST /jwt/login ─────────>│                         │
         │  Authorization:       │                         │
         │    Bearer {IdToken}   │  4. JWT Middleware       │
         │  Body: { Application, │     validiert Token     │
         │    AppVersion,        │     (Signatur, Issuer,  │
         │    Device }           │      Audience, Lifetime)│
         │                       │                         │
         │                       │  5. oid-Claim extrahieren│
         │                       │     → User in DB suchen │
         │                       │     (OpenIdConnect-     │
         │                       │      SubjectIdentifier) │
         │                       │                         │
         │                       │  6. Ticket erstellen    │
         │<── "ticket-string"    │                         │
         │                       │                         │
    7. Ticket für alle           │                         │
       weiteren API-Calls nutzen │                         │

Schritt 1: JWT-Konfiguration abrufen

Anonymer Endpoint — kein Auth nötig.

GET {baseUrl}/config/jwt

Response:

{
    "Authority": "https://login.microsoftonline.com/{tenant-id}/v2.0",
    "Audience": "{azure-ad-client-id}",
    "Enabled": true
}
Feld Bedeutung
Authority OpenID Connect Authority URL (Entra ID Tenant)
Audience Azure AD Application (Client) ID
Enabled true wenn beide Werte konfiguriert sind

Wenn Enabled == false → OIDC ist nicht konfiguriert, Abbruch.


Schritt 2: ID-Token von Microsoft Entra ID holen (MSAL)

Mit den Werten aus Schritt 1 wird ein MSAL Public Client konfiguriert:

  • Client ID = Audience aus der JWT-Konfiguration
  • Authority = Authority aus der JWT-Konfiguration
  • Scopes = ["openid", "profile"]
  • Broker = Windows WAM (optional, für SSO mit Windows-Anmeldung)

Ergebnis: Ein AuthenticationResult mit einem IdToken (JWT).

Wichtig: Es wird das ID-Token verwendet, nicht das Access-Token. Die Scopes openid und profile reichen aus.


Schritt 3: ID-Token gegen c-entron Ticket tauschen

POST {baseUrl}/jwt/login
Authorization: Bearer {microsoft-id-token}
Content-Type: application/json

{
    "Application": "{verschlüsselte-lizenz-GUID}",
    "AppVersion": "2.1.2605.636",
    "Device": "MACHINE-NAME"
}
Feld Typ Bedeutung
Application string Verschlüsselte Lizenz-GUID der Anwendung
AppVersion string Version der Client-Anwendung
Device string Gerätename (Environment.MachineName)

Response (Erfolg): 200 OK

ticket-hash-string

Der Response-Body enthält direkt den Ticket-String (plain text, kein JSON).

Response (Fehler): 400 Bad Request oder 401 Unauthorized


Schritt 4: Ticket für weitere API-Calls verwenden

Das erhaltene Ticket wird für alle weiteren c-entron API-Aufrufe als Authentifizierung verwendet.


Was auf dem Server passiert

JWT-Validierung (Middleware)

Die ASP.NET Core JWT Bearer Middleware:

  1. Lädt das OpenID Connect Discovery Document von {Authority}/.well-known/openid-configuration
  2. Holt die Signing Keys vom JWKS-Endpoint
  3. Validiert: Signatur, Issuer, Audience, Lifetime
  4. Befüllt HttpContext.User mit den Claims

User-Lookup

// oid-Claim = Microsoft Entra Object ID
var oid = identity.Claims.FirstOrDefault(c => c.Type == "oid")?.Value;

// User in der Datenbank suchen
var user = dao.GetEntity(where => where.OpenIdConnectSubjectIdentifier == oid);

Die Spalte OpenIdConnectSubjectIdentifier in der Tabelle Sichbenu (AppUser) enthält die Microsoft Entra Object ID des verknüpften Benutzers.

Ticket-Erstellung

var salt = CryptoUtils.CreateSalt(32);
var ticketId = CryptoUtils.CreatePasswordHash(deviceId, salt);  // SHA-basierter Hash
var expireDate = DateTime.Now.AddMinutes(30);                   // 30 Min Gültigkeit

// INSERT INTO Ticket (TicketId, ExpiryDate, ApplicationID, LicenseGUID, UserI3D, DeviceId)

Voraussetzungen

Azure AD App Registration

Einstellung Wert
Application (Client) ID → wird als JwtAudience in c-entron gespeichert
Authority URL https://login.microsoftonline.com/{tenant-id}/v2.0 → JwtAuthority
Redirect URI MSAL Default für Public Client Apps
Token-Typ ID-Token (nicht Access-Token)

c-entron Konfiguration

Was Wo Setting ID
JwtAuthority ApplicationSettings 10351
JwtAudience ApplicationSettings 10352
SystemAuthenticationMethod ApplicationSettings 10360 (0=Any, 3=OpenIdConnect)
OpenIDConnect-Lizenz Lizenztabelle AB4181F6-EF3B-4763-B29B-F5D0603311F7

User-Verknüpfung

Jeder c-entron User braucht seine Microsoft Entra Object ID in der Spalte OpenIdConnectSubjectIdentifier (Tabelle Sichbenu). Verknüpfung über:

  • Self-Service: POST /jwt/connect_accounts
  • Admin-Zuweisung: WPF-UI unter "Persönliche Einstellungen"

API-Endpoints im Überblick

Endpoint Methode Auth Zweck
/config/jwt GET Keine JWT-Konfiguration abrufen
/config/jwt PATCH c-entron Ticket JWT-Konfiguration ändern
/jwt/login POST Bearer (ID-Token) ID-Token → c-entron Ticket
/jwt/connect_accounts POST Bearer (ID-Token) Microsoft-Konto mit c-entron verknüpfen

Implementierungsbeispiel: OAuth-Token gegen c-entron Ticket tauschen

Minimales Beispiel für eine externe Applikation, die bereits ein Microsoft ID-Token hat und dieses gegen ein c-entron Ticket tauschen möchte.

C# (.NET)

using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.Identity.Client;

public class CentronOAuthClient
{
    private readonly HttpClient _httpClient;
    private readonly string _centronBaseUrl;

    public CentronOAuthClient(string centronBaseUrl)
    {
        _centronBaseUrl = centronBaseUrl.TrimEnd('/');
        _httpClient = new HttpClient { BaseAddress = new Uri(_centronBaseUrl) };
    }

    // ──────────────────────────────────────────────────────
    // Schritt 1: JWT-Konfiguration vom c-entron Server holen
    // ──────────────────────────────────────────────────────
    public async Task<JwtConfiguration> GetJwtConfigurationAsync()
    {
        var response = await _httpClient.GetAsync("/config/jwt");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<JwtConfiguration>();
    }

    // ──────────────────────────────────────────────────────
    // Schritt 2: Microsoft ID-Token über MSAL holen
    // ──────────────────────────────────────────────────────
    public async Task<string> AcquireMicrosoftIdTokenAsync(JwtConfiguration config)
    {
        var app = PublicClientApplicationBuilder
            .Create(config.Audience)                 // Client ID aus c-entron Config
            .WithAuthority(config.Authority)          // Authority aus c-entron Config
            .WithDefaultRedirectUri()
            .Build();

        string[] scopes = ["openid", "profile"];

        AuthenticationResult result;
        var accounts = await app.GetAccountsAsync();
        var account = accounts.FirstOrDefault();

        try
        {
            // Silent: aus Cache oder SSO
            result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync();
        }
        catch (MsalUiRequiredException)
        {
            // Interaktiv: Microsoft Login-Dialog zeigen
            result = await app.AcquireTokenInteractive(scopes)
                .WithPrompt(Prompt.SelectAccount)
                .ExecuteAsync();
        }

        return result.IdToken;  // WICHTIG: IdToken, nicht AccessToken!
    }

    // ──────────────────────────────────────────────────────
    // Schritt 3: ID-Token gegen c-entron Ticket tauschen
    // ──────────────────────────────────────────────────────
    public async Task<string> ExchangeTokenForTicketAsync(
        string microsoftIdToken,
        string applicationGuid,
        string appVersion)
    {
        var request = new HttpRequestMessage(HttpMethod.Post, "/jwt/login")
        {
            Content = JsonContent.Create(new
            {
                Application = applicationGuid,
                AppVersion = appVersion,
                Device = Environment.MachineName
            })
        };
        request.Headers.Add("Authorization", $"Bearer {microsoftIdToken}");

        var response = await _httpClient.SendAsync(request);

        if (!response.IsSuccessStatusCode)
        {
            var error = await response.Content.ReadAsStringAsync();
            throw new Exception($"Login fehlgeschlagen: {response.StatusCode} — {error}");
        }

        // Response ist der Ticket-String (plain text)
        return await response.Content.ReadAsStringAsync();
    }

    // ──────────────────────────────────────────────────────
    // Kompletter Flow: Alles zusammen
    // ──────────────────────────────────────────────────────
    public async Task<string> LoginWithMicrosoftAsync(
        string applicationGuid,
        string appVersion)
    {
        // 1. JWT-Konfiguration abrufen
        var config = await GetJwtConfigurationAsync();
        if (!config.Enabled)
            throw new Exception("OpenID Connect ist auf diesem Server nicht aktiviert.");

        // 2. Microsoft ID-Token holen
        var idToken = await AcquireMicrosoftIdTokenAsync(config);

        // 3. Token gegen c-entron Ticket tauschen
        var ticket = await ExchangeTokenForTicketAsync(idToken, applicationGuid, appVersion);

        return ticket;
    }
}

// ──────────────────────────────────────────────────────
// DTOs
// ──────────────────────────────────────────────────────
public class JwtConfiguration
{
    public string Authority { get; set; }
    public string Audience { get; set; }
    public bool Enabled { get; set; }
}

Verwendung

var client = new CentronOAuthClient("https://mein-centron-server.example.com");

// Kompletter Flow
var ticket = await client.LoginWithMicrosoftAsync(
    applicationGuid: "{verschlüsselte-lizenz-guid}",
    appVersion: "1.0.0.0"
);

Console.WriteLine($"c-entron Ticket: {ticket}");
// → Ticket für alle weiteren API-Calls verwenden

Minimales Beispiel: Nur Token-Tausch (wenn ID-Token bereits vorhanden)

// Wenn du bereits ein Microsoft ID-Token hast (z.B. aus einer anderen Auth-Bibliothek):
var client = new CentronOAuthClient("https://mein-centron-server.example.com");

var ticket = await client.ExchangeTokenForTicketAsync(
    microsoftIdToken: "eyJ0eXAiOiJKV1QiLCJhbGciOi...",
    applicationGuid: "{verschlüsselte-lizenz-guid}",
    appVersion: "1.0.0.0"
);

cURL-Beispiel

# 1. JWT-Konfiguration abrufen
curl -s https://mein-centron-server.example.com/config/jwt

# 2. ID-Token gegen Ticket tauschen (ID-Token aus MSAL o.ä.)
curl -X POST https://mein-centron-server.example.com/jwt/login \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOi..." \
  -H "Content-Type: application/json" \
  -d '{
    "Application": "{verschlüsselte-lizenz-guid}",
    "AppVersion": "1.0.0.0",
    "Device": "MEIN-PC"
  }'
# Response: ticket-hash-string (plain text)

NuGet-Pakete

<PackageReference Include="Microsoft.Identity.Client" Version="4.*" />
<!-- Optional, für Token-Cache-Persistierung: -->
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.*" />
<!-- Optional, für Windows WAM Broker (SSO): -->
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.*" />

Relevante Quellcode-Dateien

Schicht Datei Rolle
WPF View src/centron/Centron.WPF.UI/Modules/Administration/Connections/LoginDialogView.xaml Button "Anmelden mit Microsoft"
WPF ViewModel src/centron/Centron.WPF.UI/Modules/Administration/Connections/LoginDialogViewModel.cs Command-Handler
Client MSAL src/centron/Centron.WPF.UI/Services/WebServices/CentronWebServiceConnection.cs MSAL-Konfiguration, Token-Akquise, Token-Tausch
HTTP Client src/webservice/Centron.WebServices.Core/HttpClients/JwtAuthClient.cs POST /jwt/login mit Bearer-Header
Server Middleware src/webservice/Centron.Host/CentronHost.cs JWT Bearer Validierung
Server Controller src/webservice/Centron.Controllers/Controllers/Unversioned/JwtAuthController.cs /jwt/login Endpoint
Auth Factory src/backend/Centron.BL/Administration/Logins/Auth/AuthenticatorFactory.cs Routing zum OIDC-Authenticator
OIDC Authenticator src/backend/Centron.BL/Administration/Logins/Auth/OpenIdConnectAuthenticator.cs User-Lookup per oid-Claim
Ticket-Erstellung src/backend/Centron.BL/Administration/Logins/Auth/Authenticator.cs + TicketBL.cs Ticket generieren & speichern
Account Linking src/backend/Centron.BL/Administration/Logins/Auth/OpenIdConnectAccountConnector.cs Microsoft ↔ c-entron verknüpfen
JWT Config Model src/webservice/Centron.WebServices.Core/RestRequests/JwtConfiguration.cs Authority + Audience DTO
Login Request Model src/webservice/Centron.WebServices.Core/RestRequests/JwtLoginRequest.cs Application + AppVersion + Device
Settings IDs src/backend/Centron.Interfaces/Administration/Settings/ApplicationSettingID.cs JwtAuthority=10351, JwtAudience=10352
Lizenz-GUIDs src/backend/Centron.Interfaces/Administration/Logins/LicenseGuids.cs OpenIDConnectAuthentication