# Microsoft 365 Service FastAPI služba pro server-to-server komunikaci s Microsoft 365 přes Microsoft Graph. ## Konfigurace V Microsoft Entra ID vytvořte app registration, přidělte požadovaná aplikační oprávnění pro Microsoft Graph a nastavte tyto environment proměnné: ```env MS365_TENANT_ID=00000000-0000-0000-0000-000000000000 MS365_CLIENT_ID=00000000-0000-0000-0000-000000000000 MS365_CLIENT_SECRET=secret-value MS365_CREDENTIAL_ENCODING_SECRET=shared-secret-for-header-credentials MS365_GRAPH_BASE_URL=https://graph.microsoft.com/v1.0 MS365_GRAPH_SCOPE=https://graph.microsoft.com/.default MS365_REQUEST_TIMEOUT_SECONDS=30 ``` Služba používá OAuth2 client credentials flow, takže oprávnění pro Microsoft Graph musí být aplikační oprávnění schválená administrátorem. ## Credentials v request hlavičkách Pokud se credentials předávají pro každý request v hlavičkách místo environment proměnných, použijte vlastní HTTP hlavičky s prefixem `X-`. Názvy hlaviček musí odpovídat vzoru `X-MS365-(NAME)` a hodnoty hlaviček musí obsahovat zakódované credentials, ne plaintext secrety. Doporučené hlavičky: ```http X-MS365-Tenant-Id: X-MS365-Client-Id: X-MS365-Client-Secret: X-MS365-Credential-Version: v1 ``` Tyto hlavičky nesou stejné Microsoft Entra aplikační credentials, které by jinak byly nastavené přes `MS365_TENANT_ID`, `MS365_CLIENT_ID` a `MS365_CLIENT_SECRET`. Rozdíl je pouze ve způsobu přenosu: každá hodnota je před vložením do HTTP hlavičky zašifrovaná. Zakódovaná hodnota musí být službou replikovatelně zpracovatelná, ale nesmí být čitelná bez sdíleného secretu, který zná volající i služba. Nepoužívejte samotné Base64, URL encoding, ROT encoding ani jinou reverzibilní obfuskaci bez tajného klíče. Doporučený formát zakódované hodnoty: ```text v1.. ``` Pravidla zpracování: - Dekódovat pouze hlavičky s prefixem `X-MS365-`. - Před dekódováním credentials ověřit verzi. - Každou zakódovanou hodnotu dešifrovat a autentizovat pomocí AES-256-GCM. - Šifrovací klíč odvodit z `MS365_CREDENTIAL_ENCODING_SECRET` pomocí HKDF-SHA256. - Použít associated data navázaná na název hlavičky, například `X-MS365-Client-Secret`, aby zakódovanou hodnotu nešlo přesunout mezi hlavičkami. - Odmítnout chybějící, poškozené, expirované hodnoty nebo hodnoty, které neprojdou autentizací. - Nikdy nelogovat dekódované credentials ani celé zakódované hodnoty hlaviček. Maximálně logovat název hlavičky, verzi credentials a krátký fingerprint. Příklad payloadu před šifrováním: ```json { "value": "tenant-id-client-id-or-client-secret", "issued_at": "2026-05-28T00:00:00Z", "expires_at": "2026-05-28T01:00:00Z" } ``` Tím zůstanou hodnoty v hlavičkách bezpečné i při průchodu systémy, které vidí HTTP metadata, a zároveň je může deterministicky zpracovat každá instance služby, která zná sdílený secret. ### Příprava header credentials v .NET Volající musí použít stejný sdílený secret, jaký má služba v `MS365_CREDENTIAL_ENCODING_SECRET`. Každá credential hodnota se šifruje samostatně a váže se na cílový název hlavičky. Příklad pro .NET 8+: ```csharp using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; static string Base64Url(byte[] value) { return Convert.ToBase64String(value) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } static byte[] HkdfSha256(byte[] inputKeyMaterial, byte[] salt, byte[] info, int length) { using var hmacExtract = new HMACSHA256(salt); var pseudoRandomKey = hmacExtract.ComputeHash(inputKeyMaterial); var output = new List(); var previous = Array.Empty(); var counter = 1; while (output.Count < length) { using var hmacExpand = new HMACSHA256(pseudoRandomKey); var blockInput = previous .Concat(info) .Concat(new[] { (byte)counter }) .ToArray(); previous = hmacExpand.ComputeHash(blockInput); output.AddRange(previous); counter++; } return output.Take(length).ToArray(); } static string EncodeCredential(string value, string headerName, string sharedSecret) { var payload = JsonSerializer.Serialize(new { value, issued_at = DateTimeOffset.UtcNow.ToString("O"), expires_at = DateTimeOffset.UtcNow.AddHours(1).ToString("O") }); var salt = Encoding.UTF8.GetBytes("microsoft-365-service.credentials.v1"); var key = HkdfSha256( Encoding.UTF8.GetBytes(sharedSecret), salt, Encoding.UTF8.GetBytes(headerName), 32); var nonce = RandomNumberGenerator.GetBytes(12); var plaintext = Encoding.UTF8.GetBytes(payload); var ciphertext = new byte[plaintext.Length]; var tag = new byte[16]; var aad = Encoding.UTF8.GetBytes(headerName); using var aes = new AesGcm(key, tagSizeInBytes: 16); aes.Encrypt(nonce, plaintext, ciphertext, tag, aad); var ciphertextAndTag = ciphertext.Concat(tag).ToArray(); return $"v1.{Base64Url(nonce)}.{Base64Url(ciphertextAndTag)}"; } var sharedSecret = "same-value-as-MS365_CREDENTIAL_ENCODING_SECRET"; var headers = new Dictionary { ["X-MS365-Tenant-Id"] = EncodeCredential( "00000000-0000-0000-0000-000000000000", "X-MS365-Tenant-Id", sharedSecret), ["X-MS365-Client-Id"] = EncodeCredential( "00000000-0000-0000-0000-000000000000", "X-MS365-Client-Id", sharedSecret), ["X-MS365-Client-Secret"] = EncodeCredential( "client-secret-value", "X-MS365-Client-Secret", sharedSecret), ["X-MS365-Credential-Version"] = "v1" }; ``` Příklad requestu: ```csharp using var http = new HttpClient(); using var request = new HttpRequestMessage(HttpMethod.Get, "https://service.example.com/users?top=25"); foreach (var header in headers) { request.Headers.Add(header.Key, header.Value); } using var response = await http.SendAsync(request); response.EnsureSuccessStatusCode(); ``` ## Implementované služby - Users: výpis a načtení uživatelů. - Mail: výpis zpráv a odesílání e-mailů včetně příloh. - Calendar: výpis a vytváření událostí včetně Teams online meetingů. - OneDrive: výpis souborů v rootu a upload malých souborů. - Groups a Teams: výpis skupin a výpis kanálů týmu. ## API ```http GET /health GET /version GET /status GET /users?top=25&search=jane GET /users/{user_id} GET /users/{user_id}/mail/messages?folder=Inbox&top=25 POST /users/{user_id}/mail/send GET /users/{user_id}/calendar/events?top=25 POST /users/{user_id}/calendar/events GET /users/{user_id}/drive/root/children?top=25 PUT /users/{user_id}/drive/root/{path} GET /groups?top=25 GET /teams/{team_id}/channels ``` `user_id` může být Microsoft Graph user id nebo user principal name, například `jane@example.com`. ## Požadovaná Graph oprávnění Přidělte pouze oprávnění, která dané nasazení skutečně používá: - Users: `User.Read.All` - Čtení mailů: `Mail.Read` - Odesílání mailů: `Mail.Send` - Čtení a zápis do kalendáře: `Calendars.ReadWrite` - Čtení a zápis do OneDrive: `Files.ReadWrite.All` - Výpis skupin a Teams kanálů: `Group.Read.All`, `Team.ReadBasic.All`, `Channel.ReadBasic.All` ## Lokální spuštění ```bash pip install -r requirements.txt uvicorn app.main:app --reload ``` Vygenerované OpenAPI UI otevřete na `http://localhost:8000/docs`. # job queue test Thu May 28 02:12:07 PM CEST 2026 # job queue test Thu May 28 02:12:28 PM CEST 2026 # websocket logs test Fri May 29 11:03:58 AM CEST 2026