feat: add MSAL.NET authentication to MAUI clients (#45)#71
Merged
davidortinau merged 5 commits intomainfrom Mar 15, 2026
Merged
feat: add MSAL.NET authentication to MAUI clients (#45)#71davidortinau merged 5 commits intomainfrom
davidortinau merged 5 commits intomainfrom
Conversation
IAuthService + MsalAuthService for Entra ID public client auth. PKCE flow via system browser. AuthenticatedHttpMessageHandler attaches Bearer tokens. DevAuthService for local dev fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a first-pass MSAL.NET-based auth layer to the MAUI client shared library (SentenceStudio.AppLib) and wires it into all outgoing HTTP calls, plus registers the MacCatalyst URL scheme needed for browser-based redirects.
Changes:
- Introduce
IAuthServicewithMsalAuthService(Entra ID) andDevAuthService(local dev fallback). - Add
AuthenticatedHttpMessageHandlerand attach it to API + CoreSync HttpClients. - Register MacCatalyst URL scheme for the MSAL redirect URI and document the decision/history updates.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/SentenceStudio.MacCatalyst/Platforms/MacCatalyst/Info.plist | Adds CFBundleURLTypes entry for MSAL redirect scheme. |
| src/SentenceStudio.AppLib/Setup/SentenceStudioAppBuilder.cs | Registers auth services during MAUI app setup. |
| src/SentenceStudio.AppLib/Services/IAuthService.cs | Defines auth abstraction for sign-in/out and token acquisition. |
| src/SentenceStudio.AppLib/Services/MsalAuthService.cs | Implements MSAL public-client auth (silent-first + interactive fallback). |
| src/SentenceStudio.AppLib/Services/DevAuthService.cs | No-op dev auth implementation. |
| src/SentenceStudio.AppLib/Services/AuthenticatedHttpMessageHandler.cs | DelegatingHandler that attaches Bearer tokens when available. |
| src/SentenceStudio.AppLib/ServiceCollectionExtentions.cs | Adds AddAuthServices() and wires handler into HttpClient registrations. |
| src/SentenceStudio.AppLib/SentenceStudio.AppLib.csproj | Adds Microsoft.Identity.Client package reference. |
| .squad/decisions/inbox/kaylee-maui-msal.md | Records the MSAL MAUI decision and explicitly notes excluded items. |
| .squad/decisions.md | Documents squad-level decisions around auth/deployment planning. |
| .squad/agents/zoe/history.md | Adds work session notes. |
| .squad/agents/wash/history.md | Adds work session notes (includes a duplicated learning entry). |
| .squad/agents/kaylee/history.md | Adds learnings + work session notes for the MSAL work. |
| .squad/agents/jayne/history.md | Adds work session notes (includes duplicated learnings). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
Comment on lines
+1
to
+7
| using Microsoft.Identity.Client; | ||
|
|
||
| namespace SentenceStudio.Services; | ||
|
|
||
| public interface IAuthService | ||
| { | ||
| Task<AuthenticationResult?> SignInAsync(); |
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Identity.Client" Version="4.*" /> |
.squad/agents/jayne/history.md
Outdated
Comment on lines
+21
to
+23
| - "It compiles" is NOT sufficient — must verify in running app | ||
| - Must call `CacheService.InvalidateVocabSummary()` after recording attempts or dashboard is stale | ||
| - Playwright must use `pressSequentially` not `fill()` for Blazor server-side binding |
Comment on lines
+8
to
+16
| private const string TenantId = "49c0cd14-bc68-4c6d-b87b-9d65a56fa6df"; | ||
| private const string ClientId = "68d5abeb-9ca7-46cc-9572-42e33f15a0ba"; | ||
| private const string RedirectUri = "msal68d5abeb-9ca7-46cc-9572-42e33f15a0ba://auth"; | ||
|
|
||
| private static readonly string[] DefaultScopes = | ||
| [ | ||
| "api://8c051bcf-bd3a-4051-9cd3-0556ba5df2d8/user.read", | ||
| "api://8c051bcf-bd3a-4051-9cd3-0556ba5df2d8/sync.readwrite" | ||
| ]; |
Comment on lines
+20
to
+23
| private IAccount? _cachedAccount; | ||
|
|
||
| public bool IsSignedIn => _cachedAccount is not null; | ||
| public string? UserName => _cachedAccount?.Username; |
Comment on lines
+37
to
+52
| if (_authService.IsSignedIn) | ||
| { | ||
| try | ||
| { | ||
| var token = await _authService.GetAccessTokenAsync(DefaultScopes); | ||
| if (!string.IsNullOrEmpty(token)) | ||
| { | ||
| request.Headers.Authorization = | ||
| new AuthenticationHeaderValue("Bearer", token); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogWarning(ex, "Failed to attach Bearer token; proceeding without auth"); | ||
| } | ||
| } |
Comment on lines
+17
to
+21
| private static readonly string[] DefaultScopes = | ||
| [ | ||
| "api://8c051bcf-bd3a-4051-9cd3-0556ba5df2d8/user.read", | ||
| "api://8c051bcf-bd3a-4051-9cd3-0556ba5df2d8/sync.readwrite" | ||
| ]; |
Comment on lines
+35
to
+40
| ## What's NOT Included (deliberate) | ||
|
|
||
| - No sign-in UI yet (that's a separate issue) | ||
| - No SecureStorage token cache (in-memory only for now) | ||
| - No Android manifest changes (MacCatalyst is primary dev target) | ||
| - No `appsettings.json` changes — `Auth:UseEntraId` defaults to `false` when absent |
.squad/agents/wash/history.md
Outdated
| - DI registration in `SentenceStudioAppBuilder.cs` (AppLib) and `Program.cs` (WebApp) | ||
| - Aspire env var config: `builder.Configuration["AI:OpenAI:ApiKey"]` not `["AI__OpenAI__ApiKey"]` | ||
| - Server DB at: `/Users/davidortinau/Library/Application Support/sentencestudio/server/sentencestudio.db` | ||
| - Server DB at: `/Users/davidortinau/Library/Application Support/sentencestudio/server/sentencestudio.db` |
…ditional token acquisition - Read TenantId, ClientId, RedirectUri, Scopes from IConfiguration instead of hardcoded constants - Update _cachedAccount in AcquireTokenAsync on every successful token acquisition (silent and interactive), not just SignInAsync - Remove IsSignedIn gate in AuthenticatedHttpMessageHandler — attempt GetAccessTokenAsync unconditionally, proceed without token if null - Read scopes from config in handler instead of hardcoded GUIDs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #45