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>
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 |
|
|
|
|
|
|
|
|
|
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:
- Task 1: MsalClientFactory — per-ClientId PCA with MsalCacheHelper -
0295519(feat) - 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 wiringSharepointToolbox/Services/SessionManager.cs- Singleton; IsAuthenticated/GetOrCreateContextAsync/ClearSessionAsync; NormalizeUrl; tokenCacheCallback wiringSharepointToolbox.Tests/Auth/MsalClientFactoryTests.cs- 4 unit tests: same-instance, different-instances, concurrent-safe, AppData path; IDisposable temp dir cleanupSharepointToolbox.Tests/Auth/SessionManagerTests.cs- 8 unit tests + 1 skipped: IsAuthenticated before/after, ClearSessionAsync idempotency, ArgumentException on null/empty TenantUrl and ClientId
Decisions Made
MsalClientFactorystoresMsalCacheHelperper clientId alongside theIPublicClientApplication. AddedGetCacheHelper(clientId)to expose it. This is required because PnP.Framework'sCreateWithInteractiveLogincreates its own internal PCA — we cannot pass our PCA to PnP directly. ThetokenCacheCallback(Action<ITokenCache>) is the bridge: we callcacheHelper.RegisterCache(tokenCache)so PnP's internal cache uses the same persistent file.CacheDirectoryis 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
IPublicClientApplicationdirectly. PnP.Framework 1.18.0'sCreateWithInteractiveLogindoes not accept a PCA parameter — onlytokenCacheCallback: Action<ITokenCache>. WithoutGetCacheHelper(), there was no way to wire the same MsalCacheHelper to PnP's internal token cache. - Fix: Added
_helpersdictionary toMsalClientFactory, storedMsalCacheHelperalongside PCA, exposed viaGetCacheHelper(clientId).SessionManagercallsGetOrCreateAsyncfirst, thenGetCacheHelper, then uses it intokenCacheCallback. - 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
SessionManagerready for DI registration in plan 01-05 or 01-06 (singleton lifetime)MsalClientFactoryready for DI (singleton lifetime)- Auth layer complete: every SharePoint operation in Phases 2-4 can call
SessionManager.GetOrCreateContextAsync(profile)to get a liveClientContext - 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