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
403 lines
16 KiB
Markdown
403 lines
16 KiB
Markdown
# 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 | |