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
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 =
Audienceaus der JWT-Konfiguration - Authority =
Authorityaus 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
openidundprofilereichen 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:
- Lädt das OpenID Connect Discovery Document von
{Authority}/.well-known/openid-configuration - Holt die Signing Keys vom JWKS-Endpoint
- Validiert: Signatur, Issuer, Audience, Lifetime
- Befüllt
HttpContext.Usermit 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 |