The per-client app registered its redirect URI under the `web` platform,
so Entra treated it as a confidential client and the connect token
exchange (PKCE, no secret) failed with AADSTS7000218 (client_secret
required). Register the redirect under `publicClient` instead — matching
the desktop reference (PublicClient.RedirectUris) — so the secretless
PKCE code redemption is accepted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AADSTS700016 came from the register flow sending the configured
Oidc:ClientId (still a placeholder) as the auth client. The desktop
reference app never needs config: it bootstraps with the first-party
"Microsoft Graph Command Line Tools" public client (14d82eec-...) via
MSAL interactive, which exists in every tenant.
Replicate that for the web app. A server can't do MSAL loopback and the
bootstrap client's redirect URIs don't include /connect/callback, so use
the OAuth 2.0 device authorization grant instead — the web-equivalent of
the desktop interactive flow:
- Add EntraDeviceCodeFlow: POST /devicecode then poll /token with the
bootstrap client. No backing app, no client id/secret, no redirect URI.
- Profiles "Register in Entra" now shows the verification URL + user code
and polls until the admin signs in, then calls AppRegistrationService
to create the per-client app and adopts its appId.
- Remove the dead /connect/register-initiate endpoint and the
IsRegistration branch from the callback (connect flow only now).
The client-tenant register/connect flows are now fully secretless. The
Oidc:* config is used only by the toolbox's own sign-in (unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
data/profiles.json and data/users.json hold live tenant config and
user records that the app rewrites at runtime (lastLogin etc.), so they
dirtied git status on every run. Untrack them and gitignore data/*.json
(plus logs/exports/templates/audit), keeping committed *.example.json
seeds. Repositories already create empty files on first save, so a
fresh clone starts clean and provisions the first user as Admin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The register flow exchanged the auth code as a confidential client
(Oidc:ClientId + Oidc:ClientSecret), requiring a pre-provisioned
backing app with a secret. Drop client_secret from the exchange so it
uses PKCE only — the backing app is now a public client and no secret
touches the client-tenant register/connect flows.
The toolbox's own OIDC sign-in still uses Oidc:ClientSecret (unchanged).
Also enable user-secrets (UserSecretsId) so Oidc config stays out of
the committed appsettings.json.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add missing modal CSS (.modal-overlay/.modal-dialog/.modal-header):
the "Connect to Microsoft" auth modal was rendering unstyled inline
at the bottom of the page. Now a centered dialog with backdrop.
- Surface OAuth connect errors in the modal instead of silently
reopening it with no explanation.
- MainLayout: implement IDisposable so event handlers are actually
unsubscribed (Dispose existed but was never invoked).
- Wire up the Settings theme selector (was a dead control): drop the
unsupported Dark option, call sptb.setTheme on save and on load,
resolve System via prefers-color-scheme.
- Add branded 404 page via UseStatusCodePagesWithReExecute + Routes
<NotFound> (blank white page before).
- Add .progress-fill.indeterminate animation and .progress-panel.
- Home: replace inline JS hover handlers with a .feature-card CSS class.
- Define missing --surface-2 variable referenced by MainLayout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>