Hlavicky X-MS365-* jako obycejne hodnoty, GET /users/{user_id}/calendar

- credentials: sifrovani AES-GCM/HKDF odstraneno, tri hlavicky se berou
  tak, jak jsou; bez hlavicek se pouziji hodnoty z prostredi
- zrusena promenna MS365_CREDENTIAL_ENCODING_SECRET a zavislost cryptography
- novy endpoint GET /users/{user_id}/calendar (objekt kalendare schranky,
  id a name) pro read-only overeni pristupu pres e-mail schranky
- C# ukazka posila hodnoty primo, CredentialEncoder smazan
- README: sekce o hlavickach vcetne PowerShell ukazky, zaznam zmen
- .gitignore: bin/ a obj/, zaverzovane obj/ soubory ukazky odstraneny z gitu

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-09-22 12:49:25 +02:00
co-authored by Claude Fable 5.1
parent fe3c3a736a
commit d80193da8b
20 changed files with 61 additions and 560 deletions
+25 -149
View File
@@ -10,7 +10,6 @@ V Microsoft Entra ID vytvořte app registration, přidělte požadovaná aplika
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
@@ -20,157 +19,28 @@ Služba používá OAuth2 client credentials flow, takže oprávnění pro Micro
## 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:
Credentials se posílají v každém requestu v hlavičkách jako obyčejné hodnoty:
```http
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
X-MS365-Tenant-Id: <tenant id klienta>
X-MS365-Client-Id: <client id aplikace CSBOT>
X-MS365-Client-Secret: <client secret VALUE aplikace CSBOT, ne secret id>
```
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á.
Všechny tři hlavičky musí být pohromadě, jinak služba vrátí 400. Když se nepošle
žádná, použijí se `MS365_TENANT_ID`, `MS365_CLIENT_ID` a `MS365_CLIENT_SECRET`
z prostředí služby.
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.
Příklad v PowerShellu:
Doporučený formát zakódované hodnoty:
```text
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:
```json
{
"value": "tenant-id-client-id-or-client-secret",
"issued_at": "2026-05-28T00:00:00Z",
"expires_at": "2026-05-28T01:00:00Z"
```powershell
$Headers = @{
"X-MS365-Tenant-Id" = $TenantId
"X-MS365-Client-Id" = $ClientId
"X-MS365-Client-Secret" = $ClientSecret
}
```
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<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:
```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();
Invoke-RestMethod -Method Get -Headers $Headers `
-Uri "https://services.csbot.cz/apps/microsoft-365-service/users/$UserMailbox/calendar"
```
## Multitenant napojení klientů (CSBOT MS365 Connector)
@@ -192,7 +62,8 @@ Microsoft 365 tenanty jednotlivých klientů. Podrobné zadání je v Notion str
zákazníkovi. Tenant isolation (customer id -> tenant id, mailbox, plan id, bucket id)
musí držet volající backend, hodnoty nesmí přicházet přímo od LLM.
Postup testu po consentu klienta: `GET /status`, `GET /users/{mailbox}/calendar/view`,
Postup testu po consentu klienta: `GET /status`, `GET /users/{mailbox}/calendar`
(vrátí id a name kalendáře, nejlevnější ověření přístupu), `GET /users/{mailbox}/calendar/view`,
`POST /users/{mailbox}/calendar/events` + `DELETE`, `GET /planner/plans/{plan_id}/tasks`,
`POST /planner/tasks` + `DELETE`.
@@ -200,7 +71,7 @@ Postup testu po consentu klienta: `GET /status`, `GET /users/{mailbox}/calendar/
- 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 událostí, calendarView v zadaném rozsahu, volné termíny (getSchedule),
- Calendar: načtení kalendáře schránky (id, name), výpis událostí, calendarView v zadaném rozsahu, volné termíny (getSchedule),
načtení, vytvoření, úprava a smazání události včetně Teams online meetingů.
- Planner: výpis plánů skupiny, bucketů a tasků plánu, načtení, vytvoření, úprava
a smazání tasku. Úprava a smazání používají ETag (`If-Match`). Když hlavička chybí,
@@ -220,6 +91,7 @@ GET /users/{user_id}
GET /users/{user_id}/mail/messages?folder=Inbox&top=25
POST /users/{user_id}/mail/send
GET /admin-consent/url?redirect_uri=...&state=...
GET /users/{user_id}/calendar
GET /users/{user_id}/calendar/events?top=25
POST /users/{user_id}/calendar/events
GET /users/{user_id}/calendar/view?start=...&end=...&top=25&time_zone=Europe/Prague
@@ -267,8 +139,7 @@ omezení na konkrétní mailbox se řeší na straně klienta v Exchange Online
## Ukázky pro klienty
- `examples/csharp/ListUsersTop10`: konzolová aplikace .NET 8, volá `GET /users?top=10`
a obsahuje třídu `CredentialEncoder` pro šifrování hlaviček `X-MS365-*`.
Popis v `examples/csharp/README.md`.
s hlavičkami `X-MS365-*`. Popis v `examples/csharp/README.md`.
## Lokální spuštění
@@ -284,6 +155,11 @@ Vygenerované OpenAPI UI otevřete na `http://localhost:8000/docs`.
## Záznam změn
- 2026-09-22: šifrování hlaviček `X-MS365-*` odstraněno, hodnoty se posílají tak, jak jsou.
Zrušena proměnná `MS365_CREDENTIAL_ENCODING_SECRET` a závislost `cryptography`.
- 2026-09-22: `GET /users/{user_id}/calendar` (objekt kalendáře schránky, id a name) pro
read-only ověření přístupu ke kalendáři klienta přes e-mail schránky.
- 2026-09-09: verze 1.1.0. Podle Notion zadání "MS365 práva konektoru" doplněn Calendar
(calendarView, getSchedule, get/patch/delete události), Planner (plány, buckety, tasky,
create/patch/delete s ETag) a `GET /admin-consent/url`. Kód ověřen jen importem aplikace