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
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
@@ -0,0 +1,403 @@
# 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.
```http
GET {baseUrl}/config/jwt
```
**Response:**
```json
{
"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
```http
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
```csharp
// 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
```csharp
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)
```csharp
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
```csharp
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)
```csharp
// 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
```bash
# 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
```xml
<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 |