# SharePoint Toolbox A web admin toolbox for Microsoft 365 / SharePoint Online, built with Blazor Server (.NET 10) and Microsoft Graph. ## Features - **Site management** — bulk site creation, folder-structure provisioning, templates - **Members & permissions** — bulk member add, permission inspection - **Content tools** — search, duplicate finder, file transfer, storage usage, version cleanup - **Reporting** — on-demand reports, scheduled reports (unattended via app-only cert auth) - **Auditing** — tenant-wide user-access audit (SP + M365/AAD group expansion) - **Directory** — user directory browsing - Multi-tenant via connection profiles. EN / FR localization. ## Requirements - An Entra ID (Azure AD) app registration — see [Configuration](#configuration) - Docker, **or** the .NET 10 SDK for bare-metal ## Configuration Signing into the toolbox itself uses Microsoft OIDC (or a local account). Per-profile SharePoint/Graph access uses **certificate (app-only) auth** as the primary path — the same app identity drives both technician work and scheduled reports, so technicians never sign in per profile. Profiles without a certificate fall back to an interactive per-profile sign-in. Set these as environment variables (or in `appsettings.json` under the `Oidc` section). .NET maps `Section__Key` to `Section:Key`. | Variable | Description | |----------|-------------| | `Oidc__TenantId` | Entra tenant GUID | | `Oidc__ClientId` | App registration client ID | | `Oidc__ClientSecret` | App registration client secret | | `App__Domain` | Public domain the app is reached at, e.g. `sptb.example.com` or `https://sptb.example.com` (scheme defaults to `https`). Pins the OIDC sign-in redirect (`/signin-oidc`) and derives the SharePoint-connect redirect URI. | | `DataFolder` | Persistent data path (default `/data`) | | `ASPNETCORE_ENVIRONMENT` | Must be `Production` to enable OIDC | > In `Development`, OIDC is disabled — the app uses a cookie-only auto-login (hardcoded Admin) for local work. ### Two distinct OAuth flows — two redirect URIs These are separate and registered on **different** Entra apps. Don't conflate them. 1. **App sign-in (OIDC).** Logging into the toolbox itself via "Sign in with Microsoft". Uses the `Oidc__*` app above. Callback path is the framework default `/signin-oidc` (not configurable here). When `App__Domain` is set, the redirect is pinned to `/signin-oidc`; otherwise it's derived from the request host (`X-Forwarded-Host`/`Host`). → On **this** app registration, add redirect URI `https://your-host/signin-oidc` under the **Web** platform. This app also needs the Graph permissions the audit/reporting features require: `GroupMember.Read.All`, `Group.Read.All`, `User.Read.All`. 2. **SharePoint connect (per-profile).** Getting a SharePoint/Graph token for a client tenant. The primary path is **certificate (app-only) auth** (see below), which needs no redirect URI. Profiles without a certificate fall back to an interactive PKCE public-client flow that uses **each connection profile's own `ClientId`/`TenantId`** — not the `Oidc__*` app. That fallback's callback is derived from `App__Domain` as `/connect/callback`; set `ClientConnect__RedirectUri` to override the full URL directly. → Only when using the interactive fallback: on **each client-tenant profile's** app registration, add that callback value (e.g. `https://your-host/connect/callback`) under the **Mobile and desktop / public client** platform. See [Per-profile app registration permissions](#per-profile-app-registration-permissions) below for the API permissions it needs. ### Per-profile app registration permissions The **Register App** action in a profile provisions this registration for you (single tenant, public client) and grants org-wide admin consent automatically. The list below documents what it creates, for manual registration or auditing. Per-profile access uses **certificate (app-only) auth** as the primary path: the app identity drives both interactive technician work and unattended features (scheduled reports, tenant-wide audits), so technicians never sign in per profile. The cert is provisioned alongside the registration and attached as a sign-in credential. The registration grants **application permissions** for that app-only auth; it also keeps **delegated scopes** for the interactive connect flow that profiles *without* a certificate fall back to. **Application permissions** (app-only certificate auth — primary): | API | Permission | Used for | |-----|------------|----------| | Microsoft Graph | `User.Read.All` | Look up users (unattended) | | Microsoft Graph | `Group.ReadWrite.All` | Read group members; add members/owners (unattended) | | Microsoft Graph | `Directory.Read.All` | Expand M365/AAD group membership in the user-access audit | | Microsoft Graph | `Sites.FullControl.All` | Site operations (unattended) | | Microsoft Graph | `Mail.Send` | Send emailed scheduled reports | | SharePoint | `Sites.FullControl.All` | App-only CSOM | **Delegated permissions** (interactive connect flow — only for profiles without a certificate): | API | Permission | Used for | |-----|------------|----------| | Microsoft Graph | `User.Read` | Signed-in user's basic profile | | Microsoft Graph | `User.Read.All` | Look up users by email/UPN | | Microsoft Graph | `Group.ReadWrite.All` | Read group members; add members/owners | | Microsoft Graph | `Sites.Read.All` | Resolve a site's `groupId` from its `siteId` | | SharePoint | `AllSites.FullControl` | CSOM — site permissions, content, admin operations | All permissions above require **admin consent**. > **HTTPS note.** The sign-in app is a confidential (Web) client, so Entra requires its `/signin-oidc` redirect URI to be **HTTPS** — plain HTTP is allowed only for `http://localhost`, not a LAN host/IP. To run OIDC on a plain-HTTP LAN deployment, put the app behind an HTTPS-terminating reverse proxy: register `https://your-host/signin-oidc`, and the app honours `X-Forwarded-Proto` (see `UseForwardedHeaders`) to build the correct `https` redirect. Without a proxy, OIDC sign-in won't work over a non-localhost HTTP host — use the local email/password login instead. > **Reverse-proxy host.** Set `App__Domain` so the app builds every redirect (cookie login, OIDC) against the public domain regardless of what host the proxy forwards. Without it, a proxy that doesn't forward the `Host` header makes the app 302 to the internal `IP:port` it actually received. Persistent state (profiles, settings, templates, logs, exports, certs) lives in `DataFolder`. ## Installation — Docker (prebuilt image) Pulls the published image from the Gitea registry — no local build needed. ```bash cp .env.example .env # then edit .env with your OIDC values docker compose -f docker-compose.prebuilt.yml pull docker compose -f docker-compose.prebuilt.yml up -d ``` The compose file reads config from `.env` (see [`.env.example`](.env.example)). Pin a version with `SPTB_TAG`, e.g. `SPTB_TAG=v1.2.0` in `.env`. Don't quote values — the list form embeds literal quotes and breaks OIDC discovery. ## Installation — Docker (build locally) ```bash docker compose up -d --build ``` App listens on **http://localhost:8080**. Data persists in the `sptb-data` volume. Set your OIDC values in `docker-compose.yml` under `environment:`, or pass an env file: ```yaml environment: - ASPNETCORE_ENVIRONMENT=Production - DataFolder=/data - Oidc__TenantId=... - Oidc__ClientId=... - Oidc__ClientSecret=... - ClientConnect__RedirectUri=https://your-host/connect/callback ``` Plain Docker (no compose): ```bash docker build -t sptb-web . docker run -d -p 8080:8080 \ -v sptb-data:/data \ -e ASPNETCORE_ENVIRONMENT=Production \ -e Oidc__TenantId=... \ -e Oidc__ClientId=... \ -e Oidc__ClientSecret=... \ -e ClientConnect__RedirectUri=https://your-host/connect/callback \ sptb-web ``` ## Installation — Bare metal Requires the [.NET 10 SDK](https://dotnet.microsoft.com/download). ```bash # Restore + build dotnet restore dotnet publish -c Release -o ./publish # Configure (PowerShell example) $env:ASPNETCORE_ENVIRONMENT = "Production" $env:DataFolder = "C:\sptb-data" $env:Oidc__TenantId = "..." $env:Oidc__ClientId = "..." $env:Oidc__ClientSecret = "..." $env:ClientConnect__RedirectUri = "https://your-host/connect/callback" # Run dotnet ./publish/SharepointToolbox.Web.dll ``` By default it listens on the Kestrel port (`http://localhost:5000`). Override with `ASPNETCORE_URLS`, e.g. `http://+:8080`. ### Local development ```bash dotnet run ``` Runs in `Development` mode — OIDC off, auto-login as Admin. No Entra config needed. ## Tech stack .NET 10 · Blazor Server · Microsoft Graph SDK · PnP.Framework · Serilog · CsvHelper ## Dependencies NuGet packages and their pinned versions (see [`SharepointToolbox.Web.csproj`](SharepointToolbox.Web.csproj)). Last reviewed 2026-06-26. | Package | Version | Purpose | |---------|---------|---------| | `Azure.Identity` | 1.21.0 | Entra ID token credentials — app-only certificate and client-secret auth | | `CsvHelper` | 33.1.0 | CSV parsing for bulk import/export (sites, members, folder structures) | | `Microsoft.AspNetCore.Authentication.OpenIdConnect` | 10.0.9 | OIDC "Sign in with Microsoft" app sign-in | | `Microsoft.Graph` | 6.2.0 | Microsoft Graph SDK — users, groups, sites, mail | | `Microsoft.Kiota.Abstractions` | 2.0.0 | Graph SDK request runtime (abstractions) | | `Microsoft.Kiota.Authentication.Azure` | 2.0.0 | Graph SDK Azure auth provider | | `Microsoft.Kiota.Http.HttpClientLibrary` | 2.0.0 | Graph SDK HTTP transport | | `Microsoft.Kiota.Serialization.Form` | 2.0.0 | Graph SDK form serialization | | `Microsoft.Kiota.Serialization.Json` | 2.0.0 | Graph SDK JSON serialization | | `Microsoft.Kiota.Serialization.Multipart` | 2.0.0 | Graph SDK multipart serialization | | `Microsoft.Kiota.Serialization.Text` | 2.0.0 | Graph SDK text serialization | | `PnP.Framework` | 1.19.0 | SharePoint CSOM — site permissions, content, admin operations | | `Serilog.AspNetCore` | 10.0.0 | Structured logging integration | | `Serilog.Sinks.File` | 7.0.0 | Rolling-file log sink | To check for newer releases: `dotnet list package --outdated`.