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:
co-authored by
Claude Fable 5.1
parent
fe3c3a736a
commit
d80193da8b
@@ -3,29 +3,22 @@
|
||||
// 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.<base64url nonce>.<base64url ciphertext+tag>
|
||||
// Credentials se posilaji v hlavickach X-MS365-* jako obycejne hodnoty.
|
||||
//
|
||||
// Spusteni:
|
||||
// set MS365_SERVICE_BASE_URL=https://services.csbot.cz/apps/microsoft-365-service
|
||||
// set MS365_TENANT_ID=<tenant id>
|
||||
// set MS365_CLIENT_ID=<client id>
|
||||
// set MS365_CLIENT_SECRET=<client secret>
|
||||
// set MS365_CREDENTIAL_ENCODING_SECRET=<sdileny secret>
|
||||
// set MS365_TENANT_ID=<tenant id klienta>
|
||||
// set MS365_CLIENT_ID=<client id aplikace CSBOT>
|
||||
// set MS365_CLIENT_SECRET=<client secret VALUE, ne secret id>
|
||||
// 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;
|
||||
|
||||
@@ -33,17 +26,16 @@ 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");
|
||||
request.Headers.Add("X-MS365-Tenant-Id", tenantId);
|
||||
request.Headers.Add("X-MS365-Client-Id", clientId);
|
||||
request.Headers.Add("X-MS365-Client-Secret", clientSecret);
|
||||
|
||||
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),
|
||||
// 400 = neuplne 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);
|
||||
@@ -80,49 +72,3 @@ static string Env(string name, string? fallback = null)
|
||||
}
|
||||
throw new InvalidOperationException($"Chybi environment promenna {name}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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('/', '_');
|
||||
}
|
||||
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")]
|
||||
@@ -1,22 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("ListUsersTop10")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+275bd6c46764ec8db008fa2fa8c30673bb96f3d8")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("ListUsersTop10")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("ListUsersTop10")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
// Generated by the MSBuild WriteCodeFragment class.
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
502171e0ece4b0be631032bff07cea1c6b79151ad77b399268a548f2c009e1d0
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net8.0
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkVersion = v8.0
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property.EntryPointFilePath =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = ListUsersTop10
|
||||
build_property.ProjectDir = D:\GitHubRepository\Hracicky\x\ms365\microsoft-365-service\examples\csharp\ListUsersTop10\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.EffectiveAnalysisLevelStyle = 8.0
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
@@ -1,8 +0,0 @@
|
||||
// <auto-generated/>
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.IO;
|
||||
global using System.Linq;
|
||||
global using System.Net.Http;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
Binary file not shown.
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj",
|
||||
"projectName": "ListUsersTop10",
|
||||
"projectPath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj",
|
||||
"packagesPath": "C:\\Users\\GamingPC\\.nuget\\packages\\",
|
||||
"outputPath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
|
||||
],
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\GamingPC\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net8.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"C:\\Program Files\\dotnet\\library-packs": {},
|
||||
"D:\\CustomNuGetPackages": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"framework": "net8.0",
|
||||
"targetAlias": "net8.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.400"
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"framework": "net8.0",
|
||||
"targetAlias": "net8.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.400/PortableRuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\GamingPC\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\GamingPC\.nuget\packages\" />
|
||||
<SourceRoot Include="C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
@@ -1,83 +0,0 @@
|
||||
{
|
||||
"version": 4,
|
||||
"targets": {
|
||||
"net8.0": {}
|
||||
},
|
||||
"libraries": {},
|
||||
"projectFileDependencyGroups": {
|
||||
"net8.0": []
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\GamingPC\\.nuget\\packages\\": {},
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj",
|
||||
"projectName": "ListUsersTop10",
|
||||
"projectPath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj",
|
||||
"packagesPath": "C:\\Users\\GamingPC\\.nuget\\packages\\",
|
||||
"outputPath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
|
||||
],
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\GamingPC\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net8.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"C:\\Program Files\\dotnet\\library-packs": {},
|
||||
"D:\\CustomNuGetPackages": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"framework": "net8.0",
|
||||
"targetAlias": "net8.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.400"
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"framework": "net8.0",
|
||||
"targetAlias": "net8.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.400/PortableRuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "4FfrW7UlSBU=",
|
||||
"success": true,
|
||||
"projectFilePath": "D:\\GitHubRepository\\Hracicky\\x\\ms365\\microsoft-365-service\\examples\\csharp\\ListUsersTop10\\ListUsersTop10.csproj",
|
||||
"expectedPackageFiles": [],
|
||||
"logs": []
|
||||
}
|
||||
@@ -3,31 +3,26 @@
|
||||
## ListUsersTop10
|
||||
|
||||
Konzolova aplikace (.NET 8), ktera zavola `GET /users?top=10` a vypise
|
||||
displayName, userPrincipalName, mail a id kazdeho uzivatele.
|
||||
|
||||
Obsahuje tridu `CredentialEncoder`, ktera sifruje hodnoty pro hlavicky
|
||||
`X-MS365-Tenant-Id`, `X-MS365-Client-Id` a `X-MS365-Client-Secret` presne tak,
|
||||
jak je sluzba dekoduje v `app/credentials.py`. Tridu lze zkopirovat do
|
||||
libovolneho projektu, nema zadne externi zavislosti.
|
||||
displayName, userPrincipalName, mail a id kazdeho uzivatele. Credentials
|
||||
posila v hlavickach `X-MS365-Tenant-Id`, `X-MS365-Client-Id`
|
||||
a `X-MS365-Client-Secret` jako obycejne hodnoty. Nema zadne externi zavislosti.
|
||||
|
||||
### Spusteni
|
||||
|
||||
```powershell
|
||||
cd examples/csharp/ListUsersTop10
|
||||
$env:MS365_SERVICE_BASE_URL = "https://services.csbot.cz/apps/microsoft-365-service"
|
||||
$env:MS365_TENANT_ID = "<tenant id>"
|
||||
$env:MS365_CLIENT_ID = "<client id>"
|
||||
$env:MS365_CLIENT_SECRET = "<client secret>"
|
||||
$env:MS365_CREDENTIAL_ENCODING_SECRET = "<sdileny secret, stejny jako na serveru>"
|
||||
$env:MS365_TENANT_ID = "<tenant id klienta>"
|
||||
$env:MS365_CLIENT_ID = "<client id aplikace CSBOT>"
|
||||
$env:MS365_CLIENT_SECRET = "<client secret VALUE, ne secret id>"
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### Co ocekavat
|
||||
|
||||
- `200` a JSON ve tvaru Microsoft Graph: `{ "value": [ ... ], "@odata.nextLink": "..." }`.
|
||||
- `400` kdyz hlavicky chybi, jsou neuplne, prosle nebo je spatny sdileny secret.
|
||||
- `400` kdyz hlavicky chybi nebo jsou neuplne.
|
||||
- `502` kdyz Microsoft Graph nebo prihlaseni k Entra ID selze, detail je v tele odpovedi.
|
||||
- `503` kdyz sluzba nema nastaveny `MS365_CREDENTIAL_ENCODING_SECRET`.
|
||||
- `403 text/plain` kdyz GET neprojde pres reverzni proxy (IP allowlist), tj. problem
|
||||
neni ve sluzbe, ale v sitovem pristupu klienta.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user