Files
Sharepoint-Toolbox/.planning/milestones/v1.0-phases/01-foundation/01-04-SUMMARY.md
Dev 655bb79a99
All checks were successful
Release zip package / release (push) Successful in 10s
chore: complete v1.0 milestone
Archive 5 phases (36 plans) to milestones/v1.0-phases/.
Archive roadmap, requirements, and audit to milestones/.
Evolve PROJECT.md with shipped state and validated requirements.
Collapse ROADMAP.md to one-line milestone summary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:15:14 +02:00

8.2 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
01-foundation 04 auth
dotnet10
csharp
msal
msal-cache-helper
pnp-framework
sharepoint
csom
unit-tests
xunit
semaphoreslim
tdd
01-01 (solution scaffold, NuGet packages — Microsoft.Identity.Client, Microsoft.Identity.Client.Extensions.Msal, PnP.Framework)
01-02 (TenantProfile model with ClientId/TenantUrl fields)
01-03 (ProfileService/SettingsService — injection pattern)
MsalClientFactory
per-ClientId IPublicClientApplication with MsalCacheHelper persistent cache
MsalClientFactory.GetCacheHelper(clientId)
exposes MsalCacheHelper for PnP tokenCacheCallback wiring
SessionManager
singleton owning all live ClientContext instances with IsAuthenticated/GetOrCreateContextAsync/ClearSessionAsync
01-05 (TranslationSource/app setup — SessionManager ready for DI registration)
01-06 (FeatureViewModelBase — SessionManager is the auth gateway for all feature commands)
02-xx (all SharePoint feature services call SessionManager.GetOrCreateContextAsync)
added patterns
MsalClientFactory
per-clientId Dictionary<string, IPublicClientApplication> + SemaphoreSlim(1,1) for concurrent-safe lazy creation
MsalCacheHelper stored per-clientId alongside PCA — exposed via GetCacheHelper() for PnP tokenCacheCallback wiring
SessionManager
per-tenantUrl Dictionary<string, ClientContext> + SemaphoreSlim(1,1); NormalizeUrl (TrimEnd + ToLowerInvariant) for key consistency
PnP tokenCacheCallback pattern
cacheHelper.RegisterCache(tokenCache) wires persistent cache to PnP's internal MSAL token cache
ArgumentException.ThrowIfNullOrEmpty on all public method entry points requiring string arguments
created modified
SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs
SharepointToolbox/Services/SessionManager.cs
SharepointToolbox.Tests/Auth/MsalClientFactoryTests.cs
SharepointToolbox.Tests/Auth/SessionManagerTests.cs
MsalClientFactory stores both IPublicClientApplication and MsalCacheHelper per clientId — GetCacheHelper() exposes helper for PnP's tokenCacheCallback; PnP creates its own internal PCA so we cannot pass ours directly
SessionManager uses tokenCacheCallback to wire MsalCacheHelper to PnP's token cache — both PCA and PnP share the same persistent msal_{clientId}.cache file, preventing token duplication
CacheDirectory is a constructor parameter with a no-arg default — enables test isolation without real %AppData% writes
Interactive login test marked Skip in unit test suite — GetOrCreateContextAsync integration requires browser/WAM flow that cannot run in CI
Auth token cache wiring: Always call MsalClientFactory.GetOrCreateAsync first, then use GetCacheHelper() in PnP's tokenCacheCallback — ensures per-clientId cache isolation
SessionManager is the single source of truth for ClientContext: callers must not store returned contexts
FOUND-03
FOUND-04
4min 2026-04-02

Phase 1 Plan 04: Authentication Layer Summary

Per-tenant MSAL PCA with MsalCacheHelper persistent cache (one file per clientId in %AppData%) and SessionManager singleton owning all live PnP ClientContext instances — per-tenant isolation verified by 12 unit tests

Performance

  • Duration: 4 min
  • Started: 2026-04-02T10:20:49Z
  • Completed: 2026-04-02T10:25:05Z
  • Tasks: 2
  • Files modified: 4

Accomplishments

  • MsalClientFactory creates one IPublicClientApplication per unique clientId (never shared across tenants); SemaphoreSlim prevents duplicate creation under concurrent calls
  • MsalCacheHelper registered on each PCA's UserTokenCache; persistent cache files at %AppData%\SharepointToolbox\auth\msal_{clientId}.cache
  • SessionManager is the sole holder of ClientContext instances; IsAuthenticated/ClearSessionAsync/GetOrCreateContextAsync with full argument validation
  • ClearSessionAsync calls ctx.Dispose() and removes from internal dictionary; idempotent for unknown tenants
  • 12 unit tests pass (4 MsalClientFactory + 8 SessionManager), 1 integration test correctly skipped
  • PnP tokenCacheCallback pattern established: cacheHelper.RegisterCache(tokenCache) wires the factory-managed helper to PnP's internal MSAL token cache

Task Commits

Each task was committed atomically:

  1. Task 1: MsalClientFactory — per-ClientId PCA with MsalCacheHelper - 0295519 (feat)
  2. Task 2: SessionManager — singleton ClientContext holder - 158aab9 (feat)

Plan metadata: (docs commit follows)

Files Created/Modified

  • SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs - Per-clientId PCA + MsalCacheHelper; CacheDirectory constructor param; GetCacheHelper() for PnP wiring
  • SharepointToolbox/Services/SessionManager.cs - Singleton; IsAuthenticated/GetOrCreateContextAsync/ClearSessionAsync; NormalizeUrl; tokenCacheCallback wiring
  • SharepointToolbox.Tests/Auth/MsalClientFactoryTests.cs - 4 unit tests: same-instance, different-instances, concurrent-safe, AppData path; IDisposable temp dir cleanup
  • SharepointToolbox.Tests/Auth/SessionManagerTests.cs - 8 unit tests + 1 skipped: IsAuthenticated before/after, ClearSessionAsync idempotency, ArgumentException on null/empty TenantUrl and ClientId

Decisions Made

  • MsalClientFactory stores MsalCacheHelper per clientId alongside the IPublicClientApplication. Added GetCacheHelper(clientId) to expose it. This is required because PnP.Framework's CreateWithInteractiveLogin creates its own internal PCA — we cannot pass our PCA to PnP directly. The tokenCacheCallback (Action<ITokenCache>) is the bridge: we call cacheHelper.RegisterCache(tokenCache) so PnP's internal cache uses the same persistent file.
  • CacheDirectory is a public constructor parameter with a no-arg default pointing to %AppData%\SharepointToolbox\auth. Tests inject a temp directory to avoid real AppData writes and ensure cleanup.
  • Interactive login test (GetOrCreateContextAsync_CreatesContext) is marked [Fact(Skip = "Requires interactive MSAL — integration test only")]. Browser/WAM flow cannot run in automated unit tests.

Deviations from Plan

Auto-fixed Issues

1. [Rule 2 - Missing Critical] Added GetCacheHelper() to MsalClientFactory

  • Found during: Task 2 (SessionManager implementation)
  • Issue: Plan's skeleton used a non-existent PnP overload that accepts IPublicClientApplication directly. PnP.Framework 1.18.0's CreateWithInteractiveLogin does not accept a PCA parameter — only tokenCacheCallback: Action<ITokenCache>. Without GetCacheHelper(), there was no way to wire the same MsalCacheHelper to PnP's internal token cache.
  • Fix: Added _helpers dictionary to MsalClientFactory, stored MsalCacheHelper alongside PCA, exposed via GetCacheHelper(clientId). SessionManager calls GetOrCreateAsync first, then GetCacheHelper, then uses it in tokenCacheCallback.
  • Files modified: SharepointToolbox/Infrastructure/Auth/MsalClientFactory.cs, SharepointToolbox/Services/SessionManager.cs
  • Verification: 12/12 unit tests pass, 0 build warnings
  • Committed in: 158aab9 (Task 2 commit)

Total deviations: 1 auto-fixed (Rule 2 — PnP API surface mismatch required bridge method) Impact on plan: The key invariant is preserved: MsalClientFactory is called first, the per-clientId MsalCacheHelper is wired to PnP before any token acquisition. One method added to factory, no scope creep.

Issues Encountered

None beyond the auto-fixed deviation above.

User Setup Required

None — MSAL cache files are created on demand in %AppData%. No external service configuration required.

Next Phase Readiness

  • SessionManager ready for DI registration in plan 01-05 or 01-06 (singleton lifetime)
  • MsalClientFactory ready for DI (singleton lifetime)
  • Auth layer complete: every SharePoint operation in Phases 2-4 can call SessionManager.GetOrCreateContextAsync(profile) to get a live ClientContext
  • Per-tenant isolation (one PCA + cache file per ClientId) confirmed by unit tests — token bleed between MSP client tenants is prevented

Phase: 01-foundation Completed: 2026-04-02