Pin OIDC redirect to App__Domain when set

Override the OIDC redirect_uri (and post-logout redirect) to <domain>/signin-oidc
instead of deriving it from the request host. Set in both the authorize request
and the code->token redemption so Entra sees a matching redirect_uri. Falls back
to request-host derivation when App__Domain is unset. Domain binding hoisted so
OIDC and ClientConnect share one AppDomainOptions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 15:47:04 +02:00
parent 582cc54189
commit 5f51e9d16d
2 changed files with 36 additions and 4 deletions
+34 -2
View File
@@ -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) &&
+2 -2
View File
@@ -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.