// Ukazka volani microsoft-365-service: vypis prvnich 10 uzivatelu z GET /users. // // Sluzba: https://services.csbot.cz/apps/microsoft-365-service // Dokumentace (Swagger): https://services.csbot.cz/apps/microsoft-365-service/docs // // Credentials se posilaji v hlavickach X-MS365-*. Kazda hodnota je zasifrovana // AES-256-GCM klicem odvozenym pres HKDF-SHA256 ze sdileneho secretu // (MS365_CREDENTIAL_ENCODING_SECRET na strane sluzby). Format hodnoty: // v1.. // // Spusteni: // set MS365_SERVICE_BASE_URL=https://services.csbot.cz/apps/microsoft-365-service // set MS365_TENANT_ID= // set MS365_CLIENT_ID= // set MS365_CLIENT_SECRET= // set MS365_CREDENTIAL_ENCODING_SECRET= // dotnet run using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Text.Json; var baseUrl = Env("MS365_SERVICE_BASE_URL", "https://services.csbot.cz/apps/microsoft-365-service"); var tenantId = Env("MS365_TENANT_ID"); var clientId = Env("MS365_CLIENT_ID"); var clientSecret = Env("MS365_CLIENT_SECRET"); var sharedSecret = Env("MS365_CREDENTIAL_ENCODING_SECRET"); const int top = 10; using var http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/") }; http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using var request = new HttpRequestMessage(HttpMethod.Get, $"users?top={top}"); request.Headers.Add("X-MS365-Tenant-Id", CredentialEncoder.Encode(tenantId, "X-MS365-Tenant-Id", sharedSecret)); request.Headers.Add("X-MS365-Client-Id", CredentialEncoder.Encode(clientId, "X-MS365-Client-Id", sharedSecret)); request.Headers.Add("X-MS365-Client-Secret", CredentialEncoder.Encode(clientSecret, "X-MS365-Client-Secret", sharedSecret)); request.Headers.Add("X-MS365-Credential-Version", "v1"); using var response = await http.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { // 400 = spatne/prosle hlavicky, 502 = chyba z Microsoft Graph (detail je v tele), // 403 text/plain = request neprosel pres reverzni proxy (IP allowlist pro GET). Console.Error.WriteLine($"HTTP {(int)response.StatusCode} {response.ReasonPhrase}"); Console.Error.WriteLine(body); return 1; } // Sluzba vraci odpoved Microsoft Graph beze zmeny: { "value": [ { ...user... } ], "@odata.nextLink": "..." } using var json = JsonDocument.Parse(body); var users = json.RootElement.GetProperty("value"); Console.WriteLine($"Nacteno uzivatelu: {users.GetArrayLength()}"); foreach (var user in users.EnumerateArray()) { var displayName = user.TryGetProperty("displayName", out var dn) ? dn.GetString() : null; var upn = user.TryGetProperty("userPrincipalName", out var up) ? up.GetString() : null; var mail = user.TryGetProperty("mail", out var m) && m.ValueKind == JsonValueKind.String ? m.GetString() : null; var id = user.GetProperty("id").GetString(); Console.WriteLine($"{displayName} | {upn} | {mail ?? "-"} | {id}"); } return 0; static string Env(string name, string? fallback = null) { var value = Environment.GetEnvironmentVariable(name); if (!string.IsNullOrWhiteSpace(value)) { return value; } if (fallback is not null) { return fallback; } throw new InvalidOperationException($"Chybi environment promenna {name}."); } /// /// Sifrovani hodnot pro hlavicky X-MS365-*. Musi presne odpovidat dekodovani na strane sluzby /// (app/credentials.py): HKDF-SHA256 se salt "microsoft-365-service.credentials.v1" /// a info = nazev hlavicky, AES-256-GCM s 12B nonce, 16B tagem a AAD = nazev hlavicky. /// static class CredentialEncoder { private static readonly byte[] HkdfSalt = Encoding.UTF8.GetBytes("microsoft-365-service.credentials.v1"); public static string Encode(string value, string headerName, string sharedSecret, TimeSpan? validity = null) { var now = DateTimeOffset.UtcNow; var payload = JsonSerializer.Serialize(new { value, issued_at = now.ToString("O"), expires_at = now.Add(validity ?? TimeSpan.FromHours(1)).ToString("O"), }); var headerBytes = Encoding.UTF8.GetBytes(headerName); var key = HKDF.DeriveKey( HashAlgorithmName.SHA256, Encoding.UTF8.GetBytes(sharedSecret), outputLength: 32, salt: HkdfSalt, info: headerBytes); var nonce = RandomNumberGenerator.GetBytes(12); var plaintext = Encoding.UTF8.GetBytes(payload); var ciphertext = new byte[plaintext.Length]; var tag = new byte[16]; using var aes = new AesGcm(key, tagSizeInBytes: 16); aes.Encrypt(nonce, plaintext, ciphertext, tag, associatedData: headerBytes); var ciphertextAndTag = new byte[ciphertext.Length + tag.Length]; ciphertext.CopyTo(ciphertextAndTag, 0); tag.CopyTo(ciphertextAndTag, ciphertext.Length); return $"v1.{Base64Url(nonce)}.{Base64Url(ciphertextAndTag)}"; } private static string Base64Url(byte[] data) => Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_'); }