From 582cc54189ec8561c94c930f9ee5dd9cc6157788 Mon Sep 17 00:00:00 2001 From: Kawa Date: Wed, 10 Jun 2026 15:42:05 +0200 Subject: [PATCH] Add App__Domain config to derive connect redirect URI Let deployments set a single App__Domain (e.g. sptb.example.com) instead of spelling out the full ClientConnect__RedirectUri. The SharePoint-connect callback is derived as /connect/callback; an explicit RedirectUri still wins for back-compat. Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 5 +++++ Core/Config/AppDomainOptions.cs | 29 +++++++++++++++++++++++++++++ Program.cs | 15 +++++++++++++++ README.md | 5 +++-- appsettings.json | 3 +++ docker-compose.prebuilt.yml | 3 +++ docker-compose.yml | 4 ++++ 7 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 Core/Config/AppDomainOptions.cs diff --git a/.env.example b/.env.example index be804f7..c489f3e 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,11 @@ # Image tag to run (default: latest) SPTB_TAG=latest +# Public domain the app is reached at (e.g. sptb.example.com or https://sptb.example.com). +# Scheme defaults to https when omitted. The SharePoint-connect redirect URI is derived +# from this as /connect/callback — register that on each client profile's app. +# App__Domain=sptb.example.com + # OIDC app sign-in (required in Production). Authority is derived from TenantId. Oidc__TenantId=00000000-0000-0000-0000-000000000000 Oidc__ClientId=00000000-0000-0000-0000-000000000000 diff --git a/Core/Config/AppDomainOptions.cs b/Core/Config/AppDomainOptions.cs new file mode 100644 index 0000000..57d1e95 --- /dev/null +++ b/Core/Config/AppDomainOptions.cs @@ -0,0 +1,29 @@ +namespace SharepointToolbox.Web.Core.Config; + +/// +/// The app's public domain (e.g. sptb.example.com or https://sptb.example.com), +/// configured via App__Domain. Used to derive the SharePoint-connect redirect URI when +/// isn't set explicitly. +/// +public class AppDomainOptions +{ + public string Domain { get; set; } = string.Empty; + + /// + /// Builds an absolute URL for rooted at the configured domain, or + /// null when no domain is set. Defaults to https when the domain has no scheme, + /// and tolerates accidental surrounding quotes / trailing slashes (docker-compose's list-form + /// env values can embed literal quotes). + /// + public string? BuildUrl(string path) + { + var domain = Domain?.Trim().Trim('"', '\'').TrimEnd('/'); + if (string.IsNullOrEmpty(domain)) + return null; + + if (!domain.Contains("://", StringComparison.Ordinal)) + domain = "https://" + domain; + + return domain + "/" + path.TrimStart('/'); + } +} diff --git a/Program.cs b/Program.cs index b6ac86c..3aabd9d 100644 --- a/Program.cs +++ b/Program.cs @@ -172,6 +172,21 @@ builder.Services.AddHttpClient("oauth"); // ── ClientConnect options ───────────────────────────────────────────────────── builder.Services.Configure(builder.Configuration.GetSection("ClientConnect")); +// Derive the SharePoint-connect redirect URI from the app's public domain (App__Domain) +// when ClientConnect__RedirectUri isn't set explicitly. Lets a deployment configure a +// single domain (e.g. sptb.example.com) instead of spelling out the full callback URL. +// An explicit RedirectUri still wins, so existing configs are unaffected. +var appDomain = new AppDomainOptions(); +builder.Configuration.GetSection("App").Bind(appDomain); +builder.Services.PostConfigure(opts => +{ + if (string.IsNullOrWhiteSpace(opts.RedirectUri) && + appDomain.BuildUrl("/connect/callback") is { } callback) + { + opts.RedirectUri = callback; + } +}); + // ── App config ──────────────────────────────────────────────────────────────── var certsFolder = Path.Combine(dataFolder, "appcerts"); builder.Services.Configure(opt => diff --git a/README.md b/README.md index b741006..5ad7d60 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Set these as environment variables (or in `appsettings.json` under the `Oidc` se | `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`). The SharePoint-connect redirect URI is derived from it. | | `DataFolder` | Persistent data path (default `/data`) | | `ASPNETCORE_ENVIRONMENT` | Must be `Production` to enable OIDC | @@ -40,8 +41,8 @@ These are separate and registered on **different** Entra apps. Don't conflate th 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). → 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 delegated SharePoint/Graph token for a client tenant. A PKCE public-client flow that uses **each connection profile's own `ClientId`/`TenantId`** — not the `Oidc__*` app. `ClientConnect__RedirectUri` is the callback for this flow. - → On **each client-tenant profile's** app registration, add the `ClientConnect__RedirectUri` value (e.g. `https://your-host/connect/callback`) under the **Mobile and desktop / public client** platform. +2. **SharePoint connect (per-profile).** Getting a delegated SharePoint/Graph token for a client tenant. A PKCE public-client flow that uses **each connection profile's own `ClientId`/`TenantId`** — not the `Oidc__*` app. The callback for this flow is derived from `App__Domain` as `/connect/callback`; set `ClientConnect__RedirectUri` to override the full URL directly. + → 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. > **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. diff --git a/appsettings.json b/appsettings.json index 5655436..3a780e5 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,5 +1,8 @@ { "DataFolder": "/data", + "App": { + "Domain": "" + }, "Oidc": { "TenantId": "YOUR_ENTRA_TENANT_ID", "ClientId": "YOUR_SPTB_APP_CLIENT_ID", diff --git a/docker-compose.prebuilt.yml b/docker-compose.prebuilt.yml index 4c9cd5a..9a07811 100644 --- a/docker-compose.prebuilt.yml +++ b/docker-compose.prebuilt.yml @@ -15,6 +15,9 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Production - DataFolder=/data + # Public domain the app is reached at (e.g. sptb.example.com). The SharePoint-connect + # redirect URI is derived from it as /connect/callback. + - App__Domain=${App__Domain:-} # OIDC config — overrides the placeholder values baked into appsettings.json. # Authority is derived from TenantId in code; do NOT set an Authority key. # Put real values in a .env file beside this compose file (NO quotes around diff --git a/docker-compose.yml b/docker-compose.yml index 6b69790..18dba56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,10 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Production - DataFolder=/data + # Public domain the app is reached at (e.g. sptb.example.com). The SharePoint-connect + # redirect URI (/connect/callback) is derived from it. Set your OIDC values + # here too, or pass an env file. + - App__Domain=${App__Domain:-} restart: unless-stopped healthcheck: # /account/login is anonymous and returns 200 (the app root now 302-redirects