Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e190e40b07 | |||
| 5f51e9d16d |
@@ -26,4 +26,11 @@ public class AppDomainOptions
|
||||
|
||||
return domain + "/" + path.TrimStart('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The configured domain as an absolute base <see cref="Uri"/> (scheme + host [+ port]), or
|
||||
/// <c>null</c> when no domain is set or it can't be parsed.
|
||||
/// </summary>
|
||||
public Uri? GetBaseUri() =>
|
||||
BuildUrl("/") is { } url && Uri.TryCreate(url, UriKind.Absolute, out var uri) ? uri : null;
|
||||
}
|
||||
|
||||
+51
-2
@@ -69,6 +69,12 @@ builder.Services.AddDataProtection()
|
||||
// Localization string source — Scoped: one per circuit, with its own explicit culture.
|
||||
builder.Services.AddScoped<SharepointToolbox.Web.Localization.TranslationSource>();
|
||||
|
||||
// ── Public domain ─────────────────────────────────────────────────────────────
|
||||
// App__Domain (e.g. sptb.example.com) drives both OIDC sign-in (below) and the
|
||||
// SharePoint-connect redirect URI. Bound once here so both consumers share it.
|
||||
var appDomain = new AppDomainOptions();
|
||||
builder.Configuration.GetSection("App").Bind(appDomain);
|
||||
|
||||
// ── Authentication ────────────────────────────────────────────────────────────
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -133,6 +139,34 @@ else
|
||||
options.MapInboundClaims = false;
|
||||
options.TokenValidationParameters.NameClaimType = "preferred_username";
|
||||
|
||||
// When App__Domain is set, pin the OIDC redirect_uri (and post-logout redirect) to that
|
||||
// public host instead of deriving it from the request scheme/host. Keeps sign-in working
|
||||
// when the app can't see its real external host (no/incorrect forwarded Host header, or
|
||||
// several hostnames reach the same instance). The value must match the /signin-oidc URI
|
||||
// registered on the Oidc app. The authorize request and the code→token redemption MUST
|
||||
// send the identical redirect_uri, so override it in both events.
|
||||
var oidcRedirectUri = appDomain.BuildUrl(options.CallbackPath.Value ?? "/signin-oidc");
|
||||
var postLogoutUri = appDomain.BuildUrl(options.SignedOutCallbackPath.Value ?? "/signout-callback-oidc");
|
||||
if (oidcRedirectUri is not null)
|
||||
{
|
||||
options.Events.OnRedirectToIdentityProvider = ctx =>
|
||||
{
|
||||
ctx.ProtocolMessage.RedirectUri = oidcRedirectUri;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
options.Events.OnAuthorizationCodeReceived = ctx =>
|
||||
{
|
||||
if (ctx.TokenEndpointRequest is not null)
|
||||
ctx.TokenEndpointRequest.RedirectUri = oidcRedirectUri;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
options.Events.OnRedirectToIdentityProviderForSignOut = ctx =>
|
||||
{
|
||||
ctx.ProtocolMessage.PostLogoutRedirectUri = postLogoutUri;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
|
||||
options.Events.OnTokenValidated = async ctx =>
|
||||
{
|
||||
var userService = ctx.HttpContext.RequestServices.GetRequiredService<IUserService>();
|
||||
@@ -176,8 +210,6 @@ builder.Services.Configure<ClientConnectOptions>(builder.Configuration.GetSectio
|
||||
// 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) &&
|
||||
@@ -277,6 +309,23 @@ var app = builder.Build();
|
||||
// Must run before anything that inspects the request scheme/IP (auth, OIDC, cookies).
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
// When App__Domain is set, rewrite every request's scheme + host to the public domain. The
|
||||
// framework builds absolute URLs (the cookie login redirect, the OIDC redirect_uri, …) from
|
||||
// Request.Scheme/Host; behind a proxy that doesn't forward the Host header these are the
|
||||
// internal host (server IP:port), so loading https://<domain>/ would 302 to http://<ip>:8080.
|
||||
// Forcing the host here keeps every generated URL on the public domain. Must run before auth.
|
||||
var publicBaseUri = appDomain.GetBaseUri();
|
||||
if (publicBaseUri is not null)
|
||||
{
|
||||
var publicHost = HostString.FromUriComponent(publicBaseUri);
|
||||
app.Use((context, next) =>
|
||||
{
|
||||
context.Request.Scheme = publicBaseUri.Scheme;
|
||||
context.Request.Host = publicHost;
|
||||
return next(context);
|
||||
});
|
||||
}
|
||||
|
||||
// ── First-run bootstrap ───────────────────────────────────────────────────────
|
||||
// Seed a local admin when no users exist yet, so a plain-HTTP / LAN deployment that
|
||||
// can't use Microsoft OIDC (which requires HTTPS + a matching Entra redirect URI) can
|
||||
|
||||
@@ -28,7 +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. |
|
||||
| `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 |
|
||||
|
||||
@@ -38,7 +38,7 @@ Set these as environment variables (or in `appsettings.json` under the `Oidc` se
|
||||
|
||||
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).
|
||||
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 `<domain>/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 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.
|
||||
@@ -46,6 +46,8 @@ These are separate and registered on **different** Entra apps. Don't conflate th
|
||||
|
||||
> **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)
|
||||
|
||||
Reference in New Issue
Block a user