Skip to content

feat: add MSAL.NET authentication to MAUI clients (#45)#71

Merged
davidortinau merged 5 commits intomainfrom
feature/45-maui-msal
Mar 15, 2026
Merged

feat: add MSAL.NET authentication to MAUI clients (#45)#71
davidortinau merged 5 commits intomainfrom
feature/45-maui-msal

Conversation

@davidortinau
Copy link
Copy Markdown
Owner

Closes #45

  • IAuthService + MsalAuthService for Entra ID public client auth
  • PKCE flow via system browser
  • AuthenticatedHttpMessageHandler attaches Bearer tokens
  • DevAuthService for local dev fallback
  • MacCatalyst URL scheme registered

davidortinau and others added 2 commits March 13, 2026 20:49
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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IAuthService with MsalAuthService (Entra ID) and DevAuthService (local dev fallback).
  • Add AuthenticatedHttpMessageHandler and 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.*" />
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
- 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`
davidortinau and others added 2 commits March 14, 2026 16:45
…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>
@davidortinau davidortinau marked this pull request as ready for review March 15, 2026 00:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidortinau davidortinau merged commit 4ed8059 into main Mar 15, 2026
1 of 4 checks passed
@davidortinau davidortinau deleted the feature/45-maui-msal branch March 15, 2026 00:01
davidortinau added a commit that referenced this pull request Mar 15, 2026
…thResult)

The merge of PRs #71 and #72 left a mismatch — MsalAuthService returned
AuthenticationResult instead of the abstracted AuthResult record.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1.4 — Add MSAL.NET authentication to MAUI clients

2 participants