credentials do hlavicky + crypto + docu

This commit is contained in:
JiriUhlir
2026-05-28 13:10:01 +02:00
parent f3553dec26
commit 41fb87cefa
5 changed files with 338 additions and 31 deletions
+176 -20
View File
@@ -1,29 +1,185 @@
# Microsoft 365 Service
FastAPI service for server-to-server communication with Microsoft 365 through Microsoft Graph.
FastAPI služba pro server-to-server komunikaci s Microsoft 365 přes Microsoft Graph.
## Configuration
## Konfigurace
Create an app registration in Microsoft Entra ID, grant the required Microsoft Graph application permissions, and expose these environment variables:
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
```
The service uses the OAuth2 client credentials flow, so Microsoft Graph permissions must be application permissions approved by an administrator.
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.
## Implemented Services
## Credentials v request hlavičkách
- Users: list and read users.
- Mail: list messages and send email, including file attachments.
- Calendar: list and create events, including Teams online meetings.
- OneDrive: list root files and upload small files.
- Groups and Teams: list groups and list team channels.
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: <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:
```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"
}
```
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();
```
## 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
@@ -43,24 +199,24 @@ GET /groups?top=25
GET /teams/{team_id}/channels
```
`user_id` can be a Microsoft Graph user id or user principal name, for example `jane@example.com`.
`user_id` může být Microsoft Graph user id nebo user principal name, například `jane@example.com`.
## Required Graph Permissions
## Požadovaná Graph oprávnění
Grant only the permissions your deployment actually uses:
Přidělte pouze oprávnění, která dané nasazení skutečně používá:
- Users: `User.Read.All`
- Mail read: `Mail.Read`
- Mail send: `Mail.Send`
- Calendar read/write: `Calendars.ReadWrite`
- OneDrive read/write: `Files.ReadWrite.All`
- Groups and Teams channel listing: `Group.Read.All`, `Team.ReadBasic.All`, `Channel.ReadBasic.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`
## Run Locally
## Lokální spuštění
```bash
pip install -r requirements.txt
uvicorn app.main:app --reload
```
Open `http://localhost:8000/docs` for the generated OpenAPI UI.
Vygenerované OpenAPI UI otevřete na `http://localhost:8000/docs`.