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 <domain>/connect/callback; an explicit RedirectUri
still wins for back-compat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 15:42:05 +02:00
parent 0ded1af6bc
commit 582cc54189
7 changed files with 62 additions and 2 deletions
+5
View File
@@ -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 <domain>/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
+29
View File
@@ -0,0 +1,29 @@
namespace SharepointToolbox.Web.Core.Config;
/// <summary>
/// The app's public domain (e.g. <c>sptb.example.com</c> or <c>https://sptb.example.com</c>),
/// configured via <c>App__Domain</c>. Used to derive the SharePoint-connect redirect URI when
/// <see cref="ClientConnectOptions.RedirectUri"/> isn't set explicitly.
/// </summary>
public class AppDomainOptions
{
public string Domain { get; set; } = string.Empty;
/// <summary>
/// Builds an absolute URL for <paramref name="path"/> rooted at the configured domain, or
/// <c>null</c> when no domain is set. Defaults to <c>https</c> when the domain has no scheme,
/// and tolerates accidental surrounding quotes / trailing slashes (docker-compose's list-form
/// env values can embed literal quotes).
/// </summary>
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('/');
}
}
+15
View File
@@ -172,6 +172,21 @@ builder.Services.AddHttpClient("oauth");
// ── ClientConnect options ─────────────────────────────────────────────────────
builder.Services.Configure<ClientConnectOptions>(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<ClientConnectOptions>(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<AppConfiguration>(opt =>
+3 -2
View File
@@ -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 `<domain>/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.
+3
View File
@@ -1,5 +1,8 @@
{
"DataFolder": "/data",
"App": {
"Domain": ""
},
"Oidc": {
"TenantId": "YOUR_ENTRA_TENANT_ID",
"ClientId": "YOUR_SPTB_APP_CLIENT_ID",
+3
View File
@@ -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 <domain>/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
+4
View File
@@ -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 (<domain>/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