Files
2026-05-29 11:03:58 +02:00

7.8 KiB

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é:

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:

X-MS365-Tenant-Id: <encoded MS365_TENANT_ID>
X-MS365-Client-Id: <encoded MS365_CLIENT_ID>
X-MS365-Client-Secret: <encoded 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:

v1.<base64url nonce>.<base64url ciphertext-and-auth-tag>

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:

{
  "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+:

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<byte>();
    var previous = Array.Empty<byte>();
    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<string, string>
{
    ["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:

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

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í

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