# 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 GetJwtConfigurationAsync() { var response = await _httpClient.GetAsync("/config/jwt"); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } // ────────────────────────────────────────────────────── // Schritt 2: Microsoft ID-Token über MSAL holen // ────────────────────────────────────────────────────── public async Task 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 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 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 ``` --- ## 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 |