From 5aeb2f17d52cf9f0affecd7a407a10c75efd9c4d Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Tue, 17 Mar 2026 15:03:03 +0200 Subject: [PATCH 1/7] Add .NET 10 MauiBlazorWebIdentity sample (#647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add .NET 10 MauiBlazorWebIdentity sample Copied 9.0/MauiBlazorWebIdentity and upgraded to .NET 10 with: - All TFMs and packages updated to net10.0/10.0.2 - Identity Schema v3 with passkey support (PasskeySubmit, Passkeys, RenamePasskey) - NotFound page with UseStatusCodePagesWithReExecute - ReconnectModal component - All identity pages updated from official .NET 10 template - Cross-platform DB: SQL Server on Windows, SQLite on macOS/Linux - Bootstrap updated to lib/bootstrap/dist/ (full dist from template) - App.razor: ResourcePreloader, ImportMap, @Assets refs - Removed IdentityUserAccessor (replaced by RedirectToInvalidUser) - Removed Tizen platform target - Preserved all MAUI client auth behavior unchanged * Rename projects: MauiBlazorWeb → MauiBlazorWebIdentity - Solution: MauiBlazorWebIdentity.sln (was MauiBlazorWeb.sln) - Shared: MauiBlazorWebIdentity.Shared (was MauiBlazorWeb.Shared) - Web: MauiBlazorWebIdentity.Web (was MauiBlazorWeb.Web) - MAUI: MauiBlazorWebIdentity (was MauiBlazorWeb) - Removed unnecessary nested MauiBlazorWeb/ subfolder - All namespaces updated to match - Differentiates from the non-identity 9.0/MauiBlazorWeb sample * Fix MAUI build: restore AddMauiBlazorWebView API name, update package versions - AddMauiBlazorWebView() was incorrectly renamed during global namespace replace - MAUI packages updated to 10.0.10 (actual available version) - Microsoft.Extensions.Logging.Debug set to 10.0.0 * Fix nav and CSS to match .NET 10 template - Add missing 'nav' CSS class to + + +@code { + private string? currentUrl; + + protected override void OnInitialized() + { + currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); + NavigationManager.LocationChanged += OnLocationChanged; + } + + private void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + currentUrl = NavigationManager.ToBaseRelativePath(e.Location); + StateHasChanged(); + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/NavMenu.razor.css b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/NavMenu.razor.css new file mode 100644 index 000000000..0145d9dfd --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/NavMenu.razor.css @@ -0,0 +1,125 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + min-height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.bi-lock-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E"); +} + +.bi-person-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E"); +} + +.bi-person-badge-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E"); +} + +.bi-person-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E"); +} + +.bi-arrow-bar-left-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/ReconnectModal.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/ReconnectModal.razor new file mode 100644 index 000000000..e740b0c87 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/ReconnectModal.razor @@ -0,0 +1,31 @@ + + + +
+ +

+ Rejoining the server... +

+

+ Rejoin failed... trying again in seconds. +

+

+ Failed to rejoin.
Please retry or reload the page. +

+ +

+ The session has been paused by the server. +

+

+ Failed to resume the session.
Please retry or reload the page. +

+ +
+
diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/ReconnectModal.razor.css b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/ReconnectModal.razor.css new file mode 100644 index 000000000..3ad3773f3 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/ReconnectModal.razor.css @@ -0,0 +1,157 @@ +.components-reconnect-first-attempt-visible, +.components-reconnect-repeated-attempt-visible, +.components-reconnect-failed-visible, +.components-pause-visible, +.components-resume-failed-visible, +.components-rejoining-animation { + display: none; +} + +#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, +#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-paused .components-pause-visible, +#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible, +#components-reconnect-modal.components-reconnect-retrying, +#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, +#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-failed, +#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { + display: block; +} + + +#components-reconnect-modal { + background-color: white; + width: 20rem; + margin: 20vh auto; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; + animation: components-reconnect-modal-fadeOutOpacity 0.5s both; + &[open] + +{ + animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; + animation-fill-mode: both; +} + +} + +#components-reconnect-modal::backdrop { + background-color: rgba(0, 0, 0, 0.4); + animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; + opacity: 1; +} + +@keyframes components-reconnect-modal-slideUp { + 0% { + transform: translateY(30px) scale(0.95); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes components-reconnect-modal-fadeInOpacity { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes components-reconnect-modal-fadeOutOpacity { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.components-reconnect-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +#components-reconnect-modal p { + margin: 0; + text-align: center; +} + +#components-reconnect-modal button { + border: 0; + background-color: #6b9ed2; + color: white; + padding: 4px 24px; + border-radius: 4px; +} + + #components-reconnect-modal button:hover { + background-color: #3b6ea2; + } + + #components-reconnect-modal button:active { + background-color: #6b9ed2; + } + +.components-rejoining-animation { + position: relative; + width: 80px; + height: 80px; +} + + .components-rejoining-animation div { + position: absolute; + border: 3px solid #0087ff; + opacity: 1; + border-radius: 50%; + animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; + } + + .components-rejoining-animation div:nth-child(2) { + animation-delay: -0.5s; + } + +@keyframes components-rejoining-animation { + 0% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 4.9% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 5% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 1; + } + + 100% { + top: 0px; + left: 0px; + width: 80px; + height: 80px; + opacity: 0; + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/ReconnectModal.razor.js b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/ReconnectModal.razor.js new file mode 100644 index 000000000..a44de78d8 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Layout/ReconnectModal.razor.js @@ -0,0 +1,63 @@ +// Set up event handlers +const reconnectModal = document.getElementById("components-reconnect-modal"); +reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); + +const retryButton = document.getElementById("components-reconnect-button"); +retryButton.addEventListener("click", retry); + +const resumeButton = document.getElementById("components-resume-button"); +resumeButton.addEventListener("click", resume); + +function handleReconnectStateChanged(event) { + if (event.detail.state === "show") { + reconnectModal.showModal(); + } else if (event.detail.state === "hide") { + reconnectModal.close(); + } else if (event.detail.state === "failed") { + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } else if (event.detail.state === "rejected") { + location.reload(); + } +} + +async function retry() { + document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + const resumeSuccessful = await Blazor.resumeCircuit(); + if (!resumeSuccessful) { + location.reload(); + } else { + reconnectModal.close(); + } + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } +} + +async function resume() { + try { + const successful = await Blazor.resumeCircuit(); + if (!successful) { + location.reload(); + } + } catch { + reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed"); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === "visible") { + await retry(); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Pages/Error.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Pages/Error.razor new file mode 100644 index 000000000..576cc2d2f --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Pages/NotFound.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Pages/NotFound.razor new file mode 100644 index 000000000..5e17028fe --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Pages/NotFound.razor @@ -0,0 +1,5 @@ +@page "/not-found" +@layout MainLayout + +

Not Found

+

Sorry, the content you are looking for does not exist.

diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Routes.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Routes.razor new file mode 100644 index 000000000..42468e51b --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/Routes.razor @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/_Imports.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/_Imports.razor new file mode 100644 index 000000000..1e34ad32a --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Components/_Imports.razor @@ -0,0 +1,14 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using MauiBlazorWebEntraWorkforce.Shared +@using MauiBlazorWebEntraWorkforce.Web +@using MauiBlazorWebEntraWorkforce.Web.Components +@using MauiBlazorWebEntraWorkforce.Web.Components.Account +@using MauiBlazorWebEntraWorkforce.Web.Components.Layout diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/MauiBlazorWebEntraWorkforce.Web.csproj b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/MauiBlazorWebEntraWorkforce.Web.csproj new file mode 100644 index 000000000..2233cc993 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/MauiBlazorWebEntraWorkforce.Web.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Program.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Program.cs new file mode 100644 index 000000000..17647ce84 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Program.cs @@ -0,0 +1,107 @@ +using MauiBlazorWebEntraWorkforce.Shared.Services; +using MauiBlazorWebEntraWorkforce.Web.Components; +using MauiBlazorWebEntraWorkforce.Web.Services; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +// Add device-specific services used by the MauiBlazorWebEntraWorkforce.Shared project +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +builder.Services.AddCascadingAuthenticationState(); + +// Dual authentication: OIDC + Cookie for web browser, JWT Bearer for MAUI API calls. +// A policy scheme routes requests to the correct handler based on the Authorization header. +var authBuilder = builder.Services.AddAuthentication(options => + { + options.DefaultScheme = "BearerOrCookie"; + options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + .AddPolicyScheme("BearerOrCookie", "Bearer or Cookie", options => + { + options.ForwardDefaultSelector = context => + { + var authHeader = context.Request.Headers.Authorization.FirstOrDefault(); + if (authHeader?.StartsWith("Bearer ") == true) + return JwtBearerDefaults.AuthenticationScheme; + return CookieAuthenticationDefaults.AuthenticationScheme; + }; + }); + +// OpenID Connect + Cookie for web browser users +authBuilder.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + +// JWT Bearer validation for MAUI client API calls +authBuilder.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"), + jwtBearerScheme: JwtBearerDefaults.AuthenticationScheme); + +builder.Services.AddAuthorization(); + +// For more information on OpenAPI support in ASP.NET Core, +// see OpenAPI support in ASP.NET Core API apps at +// https://learn.microsoft.com/aspnet/core/fundamentals/openapi/overview +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddAdditionalAssemblies(typeof(MauiBlazorWebEntraWorkforce.Shared._Imports).Assembly); + +// Login endpoint: triggers the OIDC redirect to the workforce tenant +app.MapGet("/authentication/login", async (HttpContext context, string? returnUrl) => +{ + // Only allow local return URLs to prevent open-redirect attacks. + if (string.IsNullOrEmpty(returnUrl) || !Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)) + returnUrl = "/"; + + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties { RedirectUri = returnUrl }); +}); + +// Logout endpoint: clears cookie and signs out of Entra +app.MapPost("/authentication/logout", async (HttpContext context) => +{ + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties { RedirectUri = "/" }); +}); + +// Add the weather API endpoint and require authorization +app.MapGet("/api/weather", async (IWeatherService weatherService) => +{ + var forecasts = await weatherService.GetWeatherForecastsAsync(); + return Results.Ok(forecasts); +}).RequireAuthorization(); + +app.Run(); diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Properties/launchSettings.json b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Properties/launchSettings.json new file mode 100644 index 000000000..9376f521b --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:45108", + "sslPort": 44363 + } + }, + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7157;http://localhost:5101", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5101", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Services/FormFactor.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Services/FormFactor.cs new file mode 100644 index 000000000..f76bd98f4 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Services/FormFactor.cs @@ -0,0 +1,16 @@ +using MauiBlazorWebEntraWorkforce.Shared.Services; + +namespace MauiBlazorWebEntraWorkforce.Web.Services; + +public class FormFactor : IFormFactor +{ + public string GetFormFactor() + { + return "Web"; + } + + public string GetPlatform() + { + return Environment.OSVersion.ToString(); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Services/WeatherService.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Services/WeatherService.cs new file mode 100644 index 000000000..9a75a8582 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/Services/WeatherService.cs @@ -0,0 +1,26 @@ +using MauiBlazorWebEntraWorkforce.Shared.Services; + +namespace MauiBlazorWebEntraWorkforce.Web.Services; + +public class WeatherService : IWeatherService +{ + public async Task GetWeatherForecastsAsync() + { + // Simulate asynchronous loading to demonstrate coming from a slow data source + await Task.Delay(500); + + WeatherForecast[]? forecasts; + + var startDate = DateOnly.FromDateTime(DateTime.Now); + string[] summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"]; + + forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = startDate.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }).ToArray(); + + return forecasts; + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/appsettings.Development.json b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/appsettings.json b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/appsettings.json new file mode 100644 index 000000000..076b70dc7 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.Web/appsettings.json @@ -0,0 +1,18 @@ +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "YOUR_TENANT_DOMAIN", + "TenantId": "YOUR_TENANT_ID", + "ClientId": "YOUR_WEB_CLIENT_ID", + "ClientSecret": "YOUR_WEB_CLIENT_SECRET", + "CallbackPath": "/signin-oidc", + "SignedOutCallbackPath": "/signout-callback-oidc" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.sln b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.sln new file mode 100644 index 000000000..e24836253 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.34909.67 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MauiBlazorWebEntraWorkforce", "MauiBlazorWebEntraWorkforce\MauiBlazorWebEntraWorkforce.csproj", "{47022CF9-CC61-4F4F-808D-044285B07FF6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MauiBlazorWebEntraWorkforce.Shared", "MauiBlazorWebEntraWorkforce.Shared\MauiBlazorWebEntraWorkforce.Shared.csproj", "{2D0B68A7-4946-49D9-882F-550A2690B572}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MauiBlazorWebEntraWorkforce.Web", "MauiBlazorWebEntraWorkforce.Web\MauiBlazorWebEntraWorkforce.Web.csproj", "{602A0A61-85A3-4373-898C-FBE285A5D09A}" +EndProject +Global +GlobalSection(SolutionConfigurationPlatforms) = preSolution +Debug|Any CPU = Debug|Any CPU +Release|Any CPU = Release|Any CPU +EndGlobalSection +GlobalSection(ProjectConfigurationPlatforms) = postSolution +{47022CF9-CC61-4F4F-808D-044285B07FF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU +{47022CF9-CC61-4F4F-808D-044285B07FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU +{47022CF9-CC61-4F4F-808D-044285B07FF6}.Debug|Any CPU.Deploy.0 = Debug|Any CPU +{47022CF9-CC61-4F4F-808D-044285B07FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU +{47022CF9-CC61-4F4F-808D-044285B07FF6}.Release|Any CPU.Build.0 = Release|Any CPU +{47022CF9-CC61-4F4F-808D-044285B07FF6}.Release|Any CPU.Deploy.0 = Release|Any CPU +{2D0B68A7-4946-49D9-882F-550A2690B572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU +{2D0B68A7-4946-49D9-882F-550A2690B572}.Debug|Any CPU.Build.0 = Debug|Any CPU +{2D0B68A7-4946-49D9-882F-550A2690B572}.Release|Any CPU.ActiveCfg = Release|Any CPU +{2D0B68A7-4946-49D9-882F-550A2690B572}.Release|Any CPU.Build.0 = Release|Any CPU +{602A0A61-85A3-4373-898C-FBE285A5D09A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU +{602A0A61-85A3-4373-898C-FBE285A5D09A}.Debug|Any CPU.Build.0 = Debug|Any CPU +{602A0A61-85A3-4373-898C-FBE285A5D09A}.Release|Any CPU.ActiveCfg = Release|Any CPU +{602A0A61-85A3-4373-898C-FBE285A5D09A}.Release|Any CPU.Build.0 = Release|Any CPU +EndGlobalSection +GlobalSection(SolutionProperties) = preSolution +HideSolutionNode = FALSE +EndGlobalSection +GlobalSection(ExtensibilityGlobals) = postSolution +SolutionGuid = {883214E2-065B-4D34-89C1-21EAA3FEF0E7} +EndGlobalSection +EndGlobal diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/App.xaml b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/App.xaml new file mode 100644 index 000000000..6cb4d1f4d --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/App.xaml @@ -0,0 +1,26 @@ + + + + + + #512bdf + White + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/App.xaml.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/App.xaml.cs new file mode 100644 index 000000000..368a08910 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/App.xaml.cs @@ -0,0 +1,13 @@ +namespace MauiBlazorWebEntraWorkforce; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + } + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new MainPage()) { Title = "MauiBlazorWebEntraWorkforce" }; + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/MainLayout.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/MainLayout.razor new file mode 100644 index 000000000..78624f3dd --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/MainLayout.razor @@ -0,0 +1,23 @@ +@inherits LayoutComponentBase + +
+ + +
+
+ About +
+ +
+ @Body +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/MainLayout.razor.css b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/MainLayout.razor.css new file mode 100644 index 000000000..38d1f2598 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/MainLayout.razor.css @@ -0,0 +1,98 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/NavMenu.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/NavMenu.razor new file mode 100644 index 000000000..78f0bba7e --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/NavMenu.razor @@ -0,0 +1,55 @@ + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/NavMenu.razor.css b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/NavMenu.razor.css new file mode 100644 index 000000000..0145d9dfd --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Layout/NavMenu.razor.css @@ -0,0 +1,125 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + min-height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.bi-lock-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E"); +} + +.bi-person-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E"); +} + +.bi-person-badge-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E"); +} + +.bi-person-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E"); +} + +.bi-arrow-bar-left-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Pages/Login.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Pages/Login.razor new file mode 100644 index 000000000..5e49325aa --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Pages/Login.razor @@ -0,0 +1,63 @@ +@page "/login" +@inject NavigationManager Navigation +@inject MsalAuthenticationStateProvider AuthStateProvider + +Sign in + +

Sign in

+
+
+
+

Sign in with your Microsoft Entra workforce account.

+ +
+ +
+
+
+
+ +@code { + private bool isSigningIn; + private bool showError; + + protected override async Task OnInitializedAsync() + { + // Try silent sign-in first (cached account) + if (await AuthStateProvider.TrySignInSilentAsync()) + { + Navigation.NavigateTo(""); + } + } + + private async Task SignInAsync() + { + isSigningIn = true; + showError = false; + + await AuthStateProvider.SignInInteractiveAsync(); + + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + if (authState.User.Identity?.IsAuthenticated == true) + { + Navigation.NavigateTo(""); + } + else + { + showError = true; + } + + isSigningIn = false; + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Pages/Logout.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Pages/Logout.razor new file mode 100644 index 000000000..59fd257dd --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Pages/Logout.razor @@ -0,0 +1,12 @@ +@page "/logout" +@inject NavigationManager Navigation +@inject MsalAuthenticationStateProvider AuthStateProvider + +
Signing out...
+@code{ + protected override async Task OnInitializedAsync() + { + await AuthStateProvider.SignOutAsync(); + Navigation.NavigateTo("/login"); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Pages/NotFound.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Pages/NotFound.razor new file mode 100644 index 000000000..5e17028fe --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Pages/NotFound.razor @@ -0,0 +1,5 @@ +@page "/not-found" +@layout MainLayout + +

Not Found

+

Sorry, the content you are looking for does not exist.

diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Routes.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Routes.razor new file mode 100644 index 000000000..49b6a4f22 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/Routes.razor @@ -0,0 +1,17 @@ +@using Microsoft.AspNetCore.Components.Authorization + + + + + + Authorizing... + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/_Imports.razor b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/_Imports.razor new file mode 100644 index 000000000..5d8469993 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Components/_Imports.razor @@ -0,0 +1,14 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.JSInterop +@using MauiBlazorWebEntraWorkforce +@using MauiBlazorWebEntraWorkforce.Components +@using MauiBlazorWebEntraWorkforce.Components.Pages +@using MauiBlazorWebEntraWorkforce.Components.Layout +@using MauiBlazorWebEntraWorkforce.Shared +@using MauiBlazorWebEntraWorkforce.Services diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MainPage.xaml b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MainPage.xaml new file mode 100644 index 000000000..319ea65e9 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MainPage.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MainPage.xaml.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MainPage.xaml.cs new file mode 100644 index 000000000..913458b27 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MainPage.xaml.cs @@ -0,0 +1,9 @@ +namespace MauiBlazorWebEntraWorkforce; + +public partial class MainPage : ContentPage +{ + public MainPage() + { + InitializeComponent(); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.csproj b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.csproj new file mode 100644 index 000000000..30b85adc9 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce.csproj @@ -0,0 +1,76 @@ + + + + net10.0-android;net10.0-ios;net10.0-maccatalyst + $(TargetFrameworks);net10.0-windows10.0.19041.0 + + + + + Exe + MauiBlazorWebEntraWorkforce + true + true + enable + false + enable + + + MauiBlazorWebEntraWorkforce + + + com.companyname.MauiBlazorWebEntraWorkforce + + + 1.0 + 1 + + + None + + 15.0 + 15.0 + 24.0 + 10.0.17763.0 + 10.0.17763.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MauiProgram.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MauiProgram.cs new file mode 100644 index 000000000..b0f0690c8 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MauiProgram.cs @@ -0,0 +1,41 @@ +using MauiBlazorWebEntraWorkforce.Services; +using MauiBlazorWebEntraWorkforce.Shared.Services; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Logging; + +namespace MauiBlazorWebEntraWorkforce; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + }); + + builder.Services.AddMauiBlazorWebView(); + +#if DEBUG + builder.Services.AddBlazorWebViewDeveloperTools(); + builder.Logging.AddDebug(); +#endif + + // Add MSAL services + builder.Services.AddMsalClient(); + + // Add Blazor authentication and authorization services + builder.Services.AddAuthorizationCore(); + builder.Services.AddScoped(); + builder.Services.AddScoped(sp => sp.GetRequiredService()); + + // Add device-specific services used by the MauiBlazorWebEntraWorkforce.Shared project + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + + return builder.Build(); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MsalConfig.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MsalConfig.cs new file mode 100644 index 000000000..9ee3a5af1 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/MsalConfig.cs @@ -0,0 +1,37 @@ +namespace MauiBlazorWebEntraWorkforce.Services; + +/// +/// Configuration for Microsoft Entra workforce tenant authentication. +/// Run Setup-Azure.ps1 to populate these values automatically, +/// or replace them manually from the Azure portal. +/// +public static class MsalConfig +{ + // The Entra workforce tenant domain (for example, "contoso.onmicrosoft.com"). + public const string TenantDomain = "YOUR_TENANT_DOMAIN"; + + // The Entra workforce tenant ID (a GUID). + public const string TenantId = "YOUR_TENANT_ID"; + + // The MAUI app (public client) registration client ID. + public const string ClientId = "YOUR_MAUI_CLIENT_ID"; + + // Authority for the workforce tenant. + public static string Authority => $"https://login.microsoftonline.com/{TenantId}"; + + // The API scope exposed by the web server app registration. + // Format: api://{web-client-id}/access_as_user + public const string ApiScope = "api://YOUR_WEB_CLIENT_ID/access_as_user"; + + // Scopes to request when acquiring tokens. + public static string[] Scopes => [ApiScope]; + + // MSAL redirect URI for native apps. +#if WINDOWS + // Windows uses http://localhost with the desktop auth experience handled in-process. + public static string RedirectUri => "http://localhost"; +#else + // iOS/Android/Mac Catalyst use a custom scheme redirect. + public static string RedirectUri => $"msal{ClientId}://auth"; +#endif +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/AndroidManifest.xml b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/AndroidManifest.xml new file mode 100644 index 000000000..41e2f038d --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/MainActivity.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/MainActivity.cs new file mode 100644 index 000000000..751b37fe3 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/MainActivity.cs @@ -0,0 +1,17 @@ +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.OS; +using Microsoft.Identity.Client; + +namespace MauiBlazorWebEntraWorkforce; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity +{ + protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data) + { + base.OnActivityResult(requestCode, resultCode, data); + AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(requestCode, resultCode, data); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/MainApplication.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/MainApplication.cs new file mode 100644 index 000000000..3f0db1c0d --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace MauiBlazorWebEntraWorkforce; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/MsalActivity.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/MsalActivity.cs new file mode 100644 index 000000000..650ecf4b6 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/MsalActivity.cs @@ -0,0 +1,20 @@ +using Android.App; +using Android.Content; +using Microsoft.Identity.Client; + +namespace MauiBlazorWebEntraWorkforce.Platforms.Android +{ + /// + /// Activity that handles the MSAL redirect URI callback from the system browser + /// after workforce authentication completes. + /// + [Activity(Exported = true)] + [IntentFilter( + [Intent.ActionView], + Categories = [Intent.CategoryBrowsable, Intent.CategoryDefault], + DataScheme = "msalYOUR_MAUI_CLIENT_ID", + DataHost = "auth")] + public class MsalActivity : BrowserTabActivity + { + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/Resources/values/colors.xml b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 000000000..fbaa64a5a --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/AppDelegate.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 000000000..73950c892 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace MauiBlazorWebEntraWorkforce; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/Entitlements.plist b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/Entitlements.plist new file mode 100644 index 000000000..b144e6236 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/Entitlements.plist @@ -0,0 +1,17 @@ + + + + + + + com.apple.security.app-sandbox + + + com.apple.security.network.client + + keychain-access-groups + + $(AppIdentifierPrefix)com.microsoft.adalcache + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/Info.plist b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/Info.plist new file mode 100644 index 000000000..973e230a3 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + UIDeviceFamily + + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + CFBundleURLTypes + + + CFBundleURLName + com.companyname.mauiblazorwebentraworkforce + CFBundleURLSchemes + + msalYOUR_MAUI_CLIENT_ID + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/MacCatalystWebUi.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/MacCatalystWebUi.cs new file mode 100644 index 000000000..b2016bf3b --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/MacCatalystWebUi.cs @@ -0,0 +1,99 @@ +using AuthenticationServices; +using Foundation; +using Microsoft.Identity.Client.Extensibility; +using UIKit; + +namespace Microsoft.Identity.Client; + +/// +/// Extension method to configure Mac Catalyst authentication using +/// ASWebAuthenticationSession. MSAL doesn't ship a maccatalyst TFM yet, +/// so the built-in system browser flow throws PlatformNotSupportedException. +/// This provides an equivalent experience using ICustomWebUi. +/// +/// Remove this file once MSAL ships Mac Catalyst support: +/// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3527 +/// +internal static class MacCatalystWebViewExtensions +{ + /// + /// Configures the interactive token request to use ASWebAuthenticationSession + /// on Mac Catalyst, working around the missing maccatalyst TFM in MSAL. + /// Replace with .WithSystemWebViewOptions(new SystemWebViewOptions()) + /// once MSAL ships Mac Catalyst support. + /// + public static AcquireTokenInteractiveParameterBuilder WithMacCatalystWebView( + this AcquireTokenInteractiveParameterBuilder builder) => + builder.WithCustomWebUi(new MacCatalystWebUi()); + + private class MacCatalystWebUi : ICustomWebUi + { + public async Task AcquireAuthorizationCodeAsync( + Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + + using var registration = cancellationToken.Register(() => tcs.TrySetCanceled()); + + var callbackScheme = redirectUri.Scheme; + + MainThread.BeginInvokeOnMainThread(() => + { + // The callback-based constructor is deprecated on macCat 17.4+ in favor + // of ASWebAuthenticationSessionCallback, but remains the simplest approach + // for broad compatibility. Suppress the warning until MSAL ships native + // Mac Catalyst support and this file can be deleted entirely. +#pragma warning disable CA1422 + var session = new ASWebAuthenticationSession( + new NSUrl(authorizationUri.AbsoluteUri), + callbackScheme, + (callbackUrl, error) => + { + if (error is not null) + { + if (error.Code == (long)ASWebAuthenticationSessionErrorCode.CanceledLogin) + tcs.TrySetException(new MsalClientException( + "authentication_canceled", "User canceled authentication.")); + else + tcs.TrySetException(new Exception( + $"ASWebAuthenticationSession error: {error.LocalizedDescription}")); + } + else if (callbackUrl is not null) + { + tcs.TrySetResult(new Uri(callbackUrl.ToString())); + } + else + { + tcs.TrySetException(new Exception( + "No callback URL received from ASWebAuthenticationSession.")); + } + }); +#pragma warning restore CA1422 + + session.PresentationContextProvider = new PresentationContextProvider(); + session.PrefersEphemeralWebBrowserSession = false; + + if (!session.Start()) + { + tcs.TrySetException(new Exception("Failed to start ASWebAuthenticationSession.")); + } + }); + + return await tcs.Task; + } + + private class PresentationContextProvider : NSObject, IASWebAuthenticationPresentationContextProviding + { + public UIWindow GetPresentationAnchor(ASWebAuthenticationSession session) + { + var scene = UIApplication.SharedApplication.ConnectedScenes + .OfType() + .FirstOrDefault(); + + return scene?.KeyWindow + ?? scene?.Windows.FirstOrDefault() + ?? throw new InvalidOperationException("No window found for authentication presentation."); + } + } + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/Program.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/Program.cs new file mode 100644 index 000000000..22bc6452c --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace MauiBlazorWebEntraWorkforce; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/App.xaml b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/App.xaml new file mode 100644 index 000000000..51219e370 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/App.xaml.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/App.xaml.cs new file mode 100644 index 000000000..a64b25401 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/App.xaml.cs @@ -0,0 +1,23 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace MauiBlazorWebEntraWorkforce.WinUI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/Package.appxmanifest b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/Package.appxmanifest new file mode 100644 index 000000000..724edb316 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,46 @@ + + + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/app.manifest b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/app.manifest new file mode 100644 index 000000000..f5956b568 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/AppDelegate.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/AppDelegate.cs new file mode 100644 index 000000000..c5d15d6f8 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,17 @@ +using Foundation; +using Microsoft.Identity.Client; +using UIKit; + +namespace MauiBlazorWebEntraWorkforce; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + + public override bool OpenUrl(UIApplication application, NSUrl url, NSDictionary options) + { + AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(url); + return base.OpenUrl(application, url, options); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/Entitlements.plist b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/Entitlements.plist new file mode 100644 index 000000000..79e1684e4 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/Entitlements.plist @@ -0,0 +1,10 @@ + + + + + keychain-access-groups + + $(AppIdentifierPrefix)com.microsoft.adalcache + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/Info.plist b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/Info.plist new file mode 100644 index 000000000..421e03e39 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/Info.plist @@ -0,0 +1,45 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + CFBundleURLTypes + + + CFBundleURLName + com.companyname.mauiblazorwebentraworkforce + CFBundleURLSchemes + + msalYOUR_MAUI_CLIENT_ID + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/Program.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/Program.cs new file mode 100644 index 000000000..22bc6452c --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Platforms/iOS/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace MauiBlazorWebEntraWorkforce; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Properties/launchSettings.json b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Properties/launchSettings.json new file mode 100644 index 000000000..de9182acd --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "Project", + "nativeDebugging": false + } + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/AppIcon/appicon.svg b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/AppIcon/appicon.svg new file mode 100644 index 000000000..456d12024 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/AppIcon/appiconfg.svg b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/AppIcon/appiconfg.svg new file mode 100644 index 000000000..14f493237 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Fonts/OpenSans-Regular.ttf b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..29bfd35a2bfdd92b6e8b4ec2970f4d1eebf49357 GIT binary patch literal 96932 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

_T_kl)Xoan=RX{3lY#VQTZ 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 literal 0 HcmV?d00001 diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Images/dotnet_bot.svg b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Images/dotnet_bot.svg new file mode 100644 index 000000000..abfaff26a --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Raw/AboutAssets.txt b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Raw/AboutAssets.txt new file mode 100644 index 000000000..6de1c1527 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with your package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Splash/splash.svg b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Splash/splash.svg new file mode 100644 index 000000000..14f493237 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/FormFactor.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/FormFactor.cs new file mode 100644 index 000000000..b47346bde --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/FormFactor.cs @@ -0,0 +1,16 @@ +using MauiBlazorWebEntraWorkforce.Shared.Services; + +namespace MauiBlazorWebEntraWorkforce.Services; + +public class FormFactor : IFormFactor +{ + public string GetFormFactor() + { + return DeviceInfo.Idiom.ToString(); + } + + public string GetPlatform() + { + return DeviceInfo.Platform.ToString() + " - " + DeviceInfo.VersionString; + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/HttpClientHelper.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/HttpClientHelper.cs new file mode 100644 index 000000000..79487b518 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/HttpClientHelper.cs @@ -0,0 +1,66 @@ +namespace MauiBlazorWebEntraWorkforce.Services; + +///

+/// Helper class to manage HttpClient configuration and API endpoint URLs. +/// +internal class HttpClientHelper +{ + private static string _baseUrl = "https://localhost:7157/"; + public static string BaseUrl + { + get + { +#if DEBUG + //See: https://learn.microsoft.com/dotnet/maui/data-cloud/local-web-services + //Android Emulator uses 10.0.2.2 to refer to localhost + if (DeviceInfo.Platform == DevicePlatform.Android) + { + _baseUrl = _baseUrl.Replace("localhost", "10.0.2.2"); + } +#endif + return _baseUrl; + } + } + public static string WeatherUrl => $"{BaseUrl}api/weather"; + + public static HttpClient GetHttpClient() + { +#if WINDOWS || MACCATALYST + return new HttpClient(); +#else + return new HttpClient(new HttpsClientHandlerService().GetPlatformMessageHandler()); +#endif + } +} + +internal class HttpsClientHandlerService +{ + public HttpMessageHandler GetPlatformMessageHandler() + { +#if ANDROID + var handler = new Xamarin.Android.Net.AndroidMessageHandler(); + handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => + { + if (cert != null && cert.Issuer.Equals("CN=localhost")) + return true; + return errors == System.Net.Security.SslPolicyErrors.None; + }; + return handler; +#elif IOS + var handler = new NSUrlSessionHandler + { + TrustOverrideForUrl = IsHttpsLocalhost + }; + return handler; +#else + throw new PlatformNotSupportedException("Only Android and iOS supported."); +#endif + } + +#if IOS + public bool IsHttpsLocalhost(NSUrlSessionHandler sender, string url, Security.SecTrust trust) + { + return url.StartsWith("https://localhost"); + } +#endif +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/MsalAuthenticationStateProvider.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/MsalAuthenticationStateProvider.cs new file mode 100644 index 000000000..955d4a61f --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/MsalAuthenticationStateProvider.cs @@ -0,0 +1,157 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Identity.Client; +using System.Diagnostics; +using System.Security.Claims; + +namespace MauiBlazorWebEntraWorkforce.Services; + +/// +/// Authentication state provider that uses MSAL.NET to authenticate against +/// Microsoft Entra workforce identity. Handles interactive sign-in, silent token +/// acquisition, and sign-out. +/// +public class MsalAuthenticationStateProvider(IPublicClientApplication msalClient) : AuthenticationStateProvider +{ + private static readonly ClaimsPrincipal _anonymousUser = new(new ClaimsIdentity()); + private ClaimsPrincipal _currentUser = _anonymousUser; + private bool _initialized; + + public override async Task GetAuthenticationStateAsync() + { + if (!_initialized) + { + _initialized = true; + await TrySignInSilentAsync(); + } + + return new AuthenticationState(_currentUser); + } + + /// + /// Attempts silent authentication using cached accounts, then falls back + /// to interactive sign-in if no cached token is available. + /// + public async Task TrySignInSilentAsync() + { + try + { + var accounts = await msalClient.GetAccountsAsync(); + var account = accounts.FirstOrDefault(); + + if (account is not null) + { + var result = await msalClient.AcquireTokenSilent(MsalConfig.Scopes, account) + .ExecuteAsync(); + + SetUserFromResult(result); + return true; + } + } + catch (MsalUiRequiredException) + { + // Silent auth failed — user must sign in interactively + } + catch (Exception ex) + { + Debug.WriteLine($"Silent sign-in failed: {ex.Message}"); + } + + return false; + } + + /// + /// Starts an interactive sign-in flow. The platform-specific UI (embedded + /// WebView2 on Windows and the system browser on iOS/Android) is configured + /// via the platform-specific MSAL helpers. + /// + public async Task SignInInteractiveAsync() + { + try + { + var request = msalClient.AcquireTokenInteractive(MsalConfig.Scopes) + .WithPlatformOptions(); + var result = await request.ExecuteAsync(); + + SetUserFromResult(result); + } + catch (MsalClientException ex) when (ex.ErrorCode == "authentication_canceled") + { + Console.WriteLine($"MSAL canceled: {ex.ErrorCode}"); + } + catch (Exception ex) + { + Console.WriteLine($"MSAL error: {ex.GetType().Name}: {ex.Message}"); + if (ex.InnerException != null) Console.WriteLine($"MSAL inner: {ex.InnerException.Message}"); + } + } + + /// + /// Signs out the current user and clears the MSAL token cache. + /// + public async Task SignOutAsync() + { + var accounts = await msalClient.GetAccountsAsync(); + foreach (var account in accounts) + { + await msalClient.RemoveAsync(account); + } + + _currentUser = _anonymousUser; + NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + } + + /// + /// Gets a valid access token for API calls. Returns null if not authenticated. + /// + public async Task GetAccessTokenAsync() + { + try + { + var accounts = await msalClient.GetAccountsAsync(); + var account = accounts.FirstOrDefault(); + + if (account is null) + return null; + + var result = await msalClient.AcquireTokenSilent(MsalConfig.Scopes, account) + .ExecuteAsync(); + + return result.AccessToken; + } + catch (MsalUiRequiredException) + { + // Token expired and refresh failed — user needs to sign in again + await SignOutAsync(); + return null; + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to get access token: {ex.Message}"); + return null; + } + } + + private void SetUserFromResult(AuthenticationResult result) + { + List claims = + [ + new(ClaimTypes.Name, result.Account.Username ?? "User"), + ]; + + // Add ID token claims if available + if (result.ClaimsPrincipal?.Claims is not null) + { + foreach (var claim in result.ClaimsPrincipal.Claims) + { + if (!claims.Any(c => c.Type == claim.Type)) + { + claims.Add(claim); + } + } + } + + var identity = new ClaimsIdentity(claims, "Entra Workforce"); + _currentUser = new ClaimsPrincipal(identity); + NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/MsalServiceExtensions.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/MsalServiceExtensions.cs new file mode 100644 index 000000000..0ba3c9fa5 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/MsalServiceExtensions.cs @@ -0,0 +1,109 @@ +using Microsoft.Identity.Client; +#if WINDOWS +using Microsoft.Identity.Client.Desktop; +#endif + +namespace MauiBlazorWebEntraWorkforce.Services; + +internal static class MsalServiceExtensions +{ + /// + /// Registers the MSAL public client application and authentication services. + /// + public static IServiceCollection AddMsalClient(this IServiceCollection services) + { + var msalBuilder = PublicClientApplicationBuilder + .Create(MsalConfig.ClientId) + .WithAuthority(MsalConfig.Authority) + .WithRedirectUri(MsalConfig.RedirectUri); + +#if WINDOWS + // Windows: enable WAM integration for single sign-on with workforce accounts. + // MSAL handles the best available experience for the current device. + msalBuilder.WithWindowsDesktopFeatures(new BrokerOptions(BrokerOptions.OperatingSystems.Windows)); +#endif + + var msalClient = msalBuilder.Build(); + +#if WINDOWS || MACCATALYST + // MSAL persists tokens natively on iOS (Keychain) and Android (SharedPreferences). + // Windows and Mac Catalyst need manual persistence — Windows because it uses a + // generic .NET assembly, Mac Catalyst because MSAL doesn't ship a maccatalyst TFM yet. + // Remove MACCATALYST from this condition once MSAL ships Mac Catalyst support: + // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3527 + msalClient.EnableSecureStorageTokenCachePersistence(); +#endif + + services.AddSingleton(msalClient); + + return services; + } + + /// + /// Persists the MSAL token cache to SecureStorage on platforms that don't + /// automatically persist (Windows). iOS uses Keychain and Android uses + /// SharedPreferences, both handled natively by MSAL. + /// + private static void EnableSecureStorageTokenCachePersistence(this IPublicClientApplication msalClient) + { + const string cacheKey = "msal_token_cache"; + msalClient.UserTokenCache.SetBeforeAccessAsync(async args => + { + var cached = await SecureStorage.GetAsync(cacheKey); + if (!string.IsNullOrEmpty(cached)) + { + args.TokenCache.DeserializeMsalV3(Convert.FromBase64String(cached)); + } + }); + msalClient.UserTokenCache.SetAfterAccessAsync(async args => + { + if (args.HasStateChanged) + { + var data = args.TokenCache.SerializeMsalV3(); + await SecureStorage.SetAsync(cacheKey, Convert.ToBase64String(data)); + } + }); + } + + /// + /// Applies platform-specific options to the MSAL interactive token request. + /// On Android, sets the parent activity. On iOS/Mac Catalyst, configures + /// system web view. On Windows, sets the WinUI3 parent window for the + /// embedded WebView2 auth dialog. + /// + public static AcquireTokenInteractiveParameterBuilder WithPlatformOptions(this AcquireTokenInteractiveParameterBuilder builder) + { +#if ANDROID + + if (Platform.CurrentActivity is not Android.App.Activity activity) + throw new InvalidOperationException("No running activity found."); + + return builder.WithParentActivityOrWindow(activity); + +#elif IOS + + return builder.WithSystemWebViewOptions(new SystemWebViewOptions()); + +#elif MACCATALYST + + // MSAL doesn't ship a maccatalyst TFM yet, so WithSystemWebViewOptions + // throws PlatformNotSupportedException. Use WithMacCatalystWebView() which + // drives ASWebAuthenticationSession via ICustomWebUi. + // Remove this #elif once MSAL ships Mac Catalyst support: + // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3527 + return builder.WithMacCatalystWebView(); + +#elif WINDOWS + + if (IPlatformApplication.Current?.Application is not IApplication app) + throw new InvalidOperationException("No running application found."); + if (app.Windows.FirstOrDefault() is not IWindow win || win.Handler?.PlatformView is not Microsoft.UI.Xaml.Window platformWindow) + throw new InvalidOperationException("No running window found."); + + return builder.WithParentActivityOrWindow(platformWindow); + +#else + return builder; +#endif + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/WeatherService.cs b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/WeatherService.cs new file mode 100644 index 000000000..1e9f00e5c --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/Services/WeatherService.cs @@ -0,0 +1,41 @@ +using MauiBlazorWebEntraWorkforce.Shared.Services; +using System.Diagnostics; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +namespace MauiBlazorWebEntraWorkforce.Services; + +public class WeatherService(MsalAuthenticationStateProvider authStateProvider) : IWeatherService +{ + + public async Task GetWeatherForecastsAsync() + { + WeatherForecast[] forecasts = []; + try + { + var httpClient = HttpClientHelper.GetHttpClient(); + var weatherUrl = HttpClientHelper.WeatherUrl; + + var accessToken = await authStateProvider.GetAccessTokenAsync(); + + if (accessToken is null) + { + Debug.WriteLine("No access token available for weather API call."); + return forecasts; + } + + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + forecasts = (await httpClient.GetFromJsonAsync(weatherUrl)) ?? []; + } + catch (HttpRequestException httpEx) + { + Debug.WriteLine($"HTTP Request error: {httpEx.Message}"); + } + catch (Exception ex) + { + Debug.WriteLine($"An error occurred: {ex.Message}"); + } + + return forecasts; + } +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/wwwroot/app.css b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/wwwroot/app.css new file mode 100644 index 000000000..1339e430b --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/wwwroot/app.css @@ -0,0 +1,13 @@ +.status-bar-safe-area { + display: flex; + position: sticky; + top: 0; + height: env(safe-area-inset-top); + background-color: rgb(3, 23, 62); + width: 100%; + z-index: 1; +} + +.flex-column, .navbar-brand { + padding-left: env(safe-area-inset-left, 0px); +} diff --git a/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/wwwroot/index.html b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/wwwroot/index.html new file mode 100644 index 000000000..9b497b7ea --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/MauiBlazorWebEntraWorkforce/wwwroot/index.html @@ -0,0 +1,25 @@ + + + + + + MauiBlazorWebEntraWorkforce + + + + + + + + + + +
+ +
Loading...
+ + + + + + diff --git a/10.0/MauiBlazorWebEntraWorkforce/README.md b/10.0/MauiBlazorWebEntraWorkforce/README.md new file mode 100644 index 000000000..f78ff97f2 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/README.md @@ -0,0 +1,105 @@ +# MauiBlazorWebEntraWorkforce + +A .NET MAUI Blazor Hybrid + ASP.NET Core Web App sample that uses **Microsoft Entra workforce identity** for employee and organizational sign-in instead of ASP.NET Core Identity. + +## Architecture + +This solution contains three projects: + +| Project | Description | +|---|---| +| **MauiBlazorWebEntraWorkforce** | .NET MAUI Blazor Hybrid app (Android, iOS, Mac Catalyst, Windows) | +| **MauiBlazorWebEntraWorkforce.Shared** | Razor Class Library with shared UI components (Home, Counter, Weather) | +| **MauiBlazorWebEntraWorkforce.Web** | ASP.NET Core Blazor Server web app + API | + +### Authentication flow + +**Web browser users:** OpenID Connect -> Microsoft Entra sign-in -> Cookie-based session + +**MAUI native app:** MSAL.NET -> Microsoft Entra sign-in -> Access token -> Bearer header to API + +Both flows authenticate against the same workforce tenant. There is no local user database, and users must already exist in the tenant or be invited as guests. + +## Prerequisites + +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) +- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) (`az`) +- [PowerShell Core 7+](https://learn.microsoft.com/powershell/scripting/install/installing-powershell) (`pwsh`) +- A Microsoft Entra workforce tenant where you can create app registrations + +## Quick start + +### 1. Set up Azure resources + +Run the interactive setup script from the sample root: + +```bash +pwsh ./scripts/Setup-Azure.ps1 +``` + +The script will: + +1. Sign you into your workforce tenant +2. Create or reuse the web app registration +3. Create or reuse the MAUI public client registration +4. Grant the MAUI app permission to call the web API +5. Patch the local config files with the generated values + +### 2. Run the web server + +```bash +cd MauiBlazorWebEntraWorkforce.Web +dotnet run --launch-profile https +``` + +### 3. Run the MAUI app + +```bash +# Android +cd MauiBlazorWebEntraWorkforce +dotnet build -f net10.0-android + +# iOS Simulator +dotnet build -f net10.0-ios -r iossimulator-arm64 +``` + +## Cleanup + +To remove the Azure app registrations created by the script: + +```bash +pwsh ./scripts/Teardown-Azure.ps1 +``` + +## Known issues + +### Windows: MSAL token cache is not persisted by default + +On iOS and Android, MSAL persists tokens automatically using native platform features (Keychain and SharedPreferences respectively). On Windows, the generic .NET assembly does not include built-in token persistence, so tokens would be lost when the app restarts. + +This sample calls `EnableSecureStorageTokenCachePersistence()` to persist the MSAL token cache using .NET MAUI [`SecureStorage`](https://github.com/dotnet/maui/blob/main/src/Essentials/src/SecureStorage/SecureStorage.windows.cs), which encrypts data using [`DataProtectionProvider("LOCAL=user")`](https://learn.microsoft.com/uwp/api/windows.security.cryptography.dataprotection.dataprotectionprovider) scoped to the current Windows user. + +### Mac Catalyst: MSAL does not ship a `maccatalyst` TFM + +MSAL.NET (as of v4.x) does not include a `net*-maccatalyst` target framework. When running on Mac Catalyst, the app loads the generic .NET assembly instead of a platform-specific one. This causes two problems: + +1. **System browser auth throws `PlatformNotSupportedException`** - MSAL's browser-based interactive login is not implemented in the generic assembly. +2. **Token cache is not persisted** - the generic assembly has no native token persistence, so users must sign in again every time the app restarts. + +#### Workarounds in this sample + +**Authentication:** A custom [`ICustomWebUi`](MauiBlazorWebEntraWorkforce/Platforms/MacCatalyst/MacCatalystWebUi.cs) implementation uses Apple's `ASWebAuthenticationSession` to handle the interactive login flow. It is wired up via a `WithMacCatalystWebView()` extension method in [`MsalServiceExtensions.cs`](MauiBlazorWebEntraWorkforce/Services/MsalServiceExtensions.cs). + +**Token persistence:** The `SecureStorage`-based token cache (normally used only on Windows) is also enabled for Mac Catalyst so that tokens survive app restarts. + +## Key differences from MauiBlazorWebIdentity + +| Aspect | MauiBlazorWebIdentity | MauiBlazorWebEntraWorkforce | +|---|---|---| +| **Auth provider** | ASP.NET Core Identity (local DB) | Microsoft Entra workforce tenant | +| **Web auth** | Cookie + Identity pages | OIDC redirect to Entra | +| **API auth** | JWT from `/identity/login` | JWT from MSAL.NET | +| **User store** | SQLite (EF Core) | Entra directory (no local DB) | +| **User onboarding** | Self-service registration pages | Admin-managed or invited tenant users | +| **NuGet (Web)** | EF Core + Identity | Microsoft.Identity.Web | +| **NuGet (MAUI)** | - | Microsoft.Identity.Client (MSAL) | diff --git a/10.0/MauiBlazorWebEntraWorkforce/scripts/Setup-Azure.ps1 b/10.0/MauiBlazorWebEntraWorkforce/scripts/Setup-Azure.ps1 new file mode 100644 index 000000000..32596b4e1 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/scripts/Setup-Azure.ps1 @@ -0,0 +1,384 @@ +<# +.SYNOPSIS + Configures Microsoft Entra workforce authentication for the + MauiBlazorWebEntraWorkforce sample. + +.DESCRIPTION + This interactive script: + 1. Signs in to an existing workforce tenant + 2. Creates or reuses the web app registration + 3. Creates or reuses the MAUI public client registration + 4. Grants the MAUI app permission to call the web API + 5. Patches local config files with the generated values + +.NOTES + Prerequisites: + - Azure CLI (az) installed + - PowerShell Core 7+ + - Permission to create app registrations in the target tenant +#> + +$ErrorActionPreference = 'Stop' + +function Write-Step { + param([int]$Number, [string]$Title, [string]$Type = "AUTO") + Write-Host "" + Write-Host "=== Step $Number - $Title [$Type] ===" -ForegroundColor Blue +} + +function Read-RequiredInput { + param([string]$Prompt) + + while ($true) { + Write-Host $Prompt -ForegroundColor Magenta -NoNewline + $value = Read-Host + if (-not [string]::IsNullOrWhiteSpace($value)) { + return $value.Trim() + } + + Write-Host "This value is required." -ForegroundColor Red + } +} + +function Ensure-ServicePrincipal { + param([string]$AppId) + + az ad sp create --id $AppId 2>&1 | Out-Null +} + +function New-AppSecret { + param( + [string]$ApplicationObjectId, + [string]$DisplayName + ) + + $body = @{ + passwordCredential = @{ + displayName = $DisplayName + } + } | ConvertTo-Json -Depth 5 -Compress + + $json = az rest --method POST ` + --uri "https://graph.microsoft.com/v1.0/applications/$ApplicationObjectId/addPassword" ` + --headers "Content-Type=application/json" ` + --body $body 2>$null + + if ($LASTEXITCODE -ne 0) { + throw "Failed to create client secret." + } + + return ($json | ConvertFrom-Json).secretText +} + +Write-Host "" +Write-Host "=== MauiBlazorWebEntraWorkforce - Azure Setup ===" -ForegroundColor Blue +Write-Host "" + +if (-not (Get-Command az -ErrorAction SilentlyContinue)) { + Write-Host "Azure CLI (az) is not installed." -ForegroundColor Red + Write-Host "Install it from: https://learn.microsoft.com/cli/azure/install-azure-cli" -ForegroundColor Blue + exit 1 +} + +Write-Host "Azure CLI found." -ForegroundColor Green + +$sampleRoot = Join-Path $PSScriptRoot ".." +$webAppName = "MauiBlazorWebEntraWorkforce-Web" +$mauiAppName = "MauiBlazorWebEntraWorkforce-MAUI" + +Write-Step 1 "Sign in to workforce tenant" "AUTO" + +Write-Host "" +Write-Host "Use an existing Microsoft Entra workforce tenant." -ForegroundColor Cyan +$tenantInput = Read-RequiredInput "Tenant domain or tenant ID (for example, contoso.onmicrosoft.com): " + +Write-Host "" +Write-Host "A browser window may open for sign-in..." -ForegroundColor Cyan +az login --tenant $tenantInput --allow-no-subscriptions 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to sign in to tenant '$tenantInput'." + exit 1 +} + +$account = az account show 2>$null | ConvertFrom-Json +$tenantId = $account.tenantId + +$org = az rest ` + --method GET ` + --uri "https://graph.microsoft.com/v1.0/organization?`$select=displayName,verifiedDomains" 2>$null | ConvertFrom-Json + +$tenantDomain = $null +if ($org.value -and $org.value.Count -gt 0) { + $tenantDomain = $org.value[0].verifiedDomains | + Where-Object { $_.isDefault -eq $true } | + Select-Object -ExpandProperty name -First 1 +} +if (-not $tenantDomain) { + $tenantDomain = $tenantInput +} + +Write-Host " Tenant ID: $tenantId" -ForegroundColor Green +Write-Host " Tenant domain: $tenantDomain" -ForegroundColor Green + +Write-Step 2 "Create web app registration" "AUTO" + +$existingWebApp = az ad app list --display-name $webAppName --query "[?displayName=='$webAppName'] | [0]" 2>$null | ConvertFrom-Json +if ($existingWebApp) { + $webClientId = $existingWebApp.appId + $webObjectId = $existingWebApp.id + Write-Host " Reusing web app: $webClientId" -ForegroundColor Green +} else { + $webAppJson = az ad app create ` + --display-name $webAppName ` + --sign-in-audience "AzureADMyOrg" ` + --web-redirect-uris "https://localhost:7157/signin-oidc" "https://localhost:7157/signout-callback-oidc" ` + --enable-id-token-issuance true 2>$null + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to create web app registration." + exit 1 + } + + $webApp = $webAppJson | ConvertFrom-Json + $webClientId = $webApp.appId + $webObjectId = $webApp.id + Write-Host " Created web app: $webClientId" -ForegroundColor Green +} + +Ensure-ServicePrincipal -AppId $webClientId + +# Ensure redirect URIs are correct (idempotent — handles reuse with stale URIs) +$webRedirectBody = @{ + web = @{ + redirectUris = @( + "https://localhost:7157/signin-oidc", + "https://localhost:7157/signout-callback-oidc" + ) + implicitGrantSettings = @{ + enableIdTokenIssuance = $true + } + } +} | ConvertTo-Json -Depth 5 -Compress + +az rest --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$webObjectId" ` + --headers "Content-Type=application/json" ` + --body $webRedirectBody 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to update web redirect URIs. Verify manually in the Azure portal." +} + +$clientSecret = $null +$appSettingsPath = Join-Path $sampleRoot "MauiBlazorWebEntraWorkforce.Web" "appsettings.json" +if (Test-Path $appSettingsPath) { + $existingSettings = Get-Content $appSettingsPath -Raw | ConvertFrom-Json + $existingConfigClientId = $existingSettings.AzureAd.ClientId + $existingConfigSecret = $existingSettings.AzureAd.ClientSecret + + if ($existingConfigClientId -eq $webClientId -and + $existingConfigSecret -and + -not $existingConfigSecret.StartsWith("YOUR_")) { + Write-Host "Keep the existing client secret from appsettings.json? (Y/n): " -ForegroundColor Blue -NoNewline + $keepSecret = Read-Host + if ($keepSecret -eq "" -or $keepSecret -match "^[Yy]") { + $clientSecret = $existingConfigSecret + Write-Host " Reusing saved client secret." -ForegroundColor Green + } + } +} + +if (-not $clientSecret) { + Write-Host "Create a new client secret for the web app? (Y/n): " -ForegroundColor Blue -NoNewline + $createSecret = Read-Host + if ($createSecret -eq "" -or $createSecret -match "^[Yy]") { + $clientSecret = New-AppSecret -ApplicationObjectId $webObjectId -DisplayName "MauiBlazorWebEntraWorkforce Setup" + Write-Host " New client secret created." -ForegroundColor Green + } else { + $clientSecret = Read-RequiredInput "Paste the existing client secret: " + } +} + +$webAppDetails = az ad app show --id $webClientId 2>$null | ConvertFrom-Json +$existingScope = $webAppDetails.api.oauth2PermissionScopes | + Where-Object { $_.value -eq "access_as_user" } | + Select-Object -First 1 + +if ($existingScope) { + $scopeId = $existingScope.id + Write-Host " Reusing API scope: api://$webClientId/access_as_user" -ForegroundColor Green +} else { + $scopeId = [guid]::NewGuid().ToString() + $apiBody = @{ + identifierUris = @("api://$webClientId") + api = @{ + oauth2PermissionScopes = @( + @{ + id = $scopeId + adminConsentDescription = "Allow the MAUI app to access the web API on behalf of the signed-in user." + adminConsentDisplayName = "Access web API as user" + isEnabled = $true + type = "User" + userConsentDescription = "Allow this app to access the web API on your behalf." + userConsentDisplayName = "Access web API" + value = "access_as_user" + } + ) + } + } | ConvertTo-Json -Depth 10 -Compress + + az rest --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$webObjectId" ` + --headers "Content-Type=application/json" ` + --body $apiBody 2>&1 | Out-Null + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to expose the access_as_user scope on the web app." + exit 1 + } + + Write-Host " API scope created: api://$webClientId/access_as_user" -ForegroundColor Green +} + +Write-Step 3 "Create MAUI app registration" "AUTO" + +$existingMauiApp = az ad app list --display-name $mauiAppName --query "[?displayName=='$mauiAppName'] | [0]" 2>$null | ConvertFrom-Json +if ($existingMauiApp) { + $mauiClientId = $existingMauiApp.appId + $mauiObjectId = $existingMauiApp.id + Write-Host " Reusing MAUI app: $mauiClientId" -ForegroundColor Green +} else { + $mauiAppJson = az ad app create ` + --display-name $mauiAppName ` + --sign-in-audience "AzureADMyOrg" ` + --is-fallback-public-client true 2>$null + + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to create MAUI app registration." + exit 1 + } + + $mauiApp = $mauiAppJson | ConvertFrom-Json + $mauiClientId = $mauiApp.appId + $mauiObjectId = $mauiApp.id + Write-Host " Created MAUI app: $mauiClientId" -ForegroundColor Green +} + +Ensure-ServicePrincipal -AppId $mauiClientId + +$redirectBody = @{ + publicClient = @{ + redirectUris = @( + "msal$mauiClientId`://auth", + "http://localhost" + ) + } +} | ConvertTo-Json -Depth 5 -Compress + +az rest --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$mauiObjectId" ` + --headers "Content-Type=application/json" ` + --body $redirectBody 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to configure redirect URIs for the MAUI app." + exit 1 +} + +Write-Host " Redirect URIs configured." -ForegroundColor Green + +Write-Step 4 "Grant API permission" "AUTO" + +$permissionBody = @{ + requiredResourceAccess = @( + @{ + resourceAppId = $webClientId + resourceAccess = @( + @{ + id = $scopeId + type = "Scope" + } + ) + } + ) +} | ConvertTo-Json -Depth 10 -Compress + +az rest --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$mauiObjectId" ` + --headers "Content-Type=application/json" ` + --body $permissionBody 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to grant the MAUI app access to the web API scope." + exit 1 +} + +az ad app permission admin-consent --id $mauiClientId 2>&1 | Out-Null +if ($LASTEXITCODE -eq 0) { + Write-Host " Admin consent granted." -ForegroundColor Green +} else { + Write-Warning "Admin consent may require a tenant administrator." +} + +Write-Step 5 "Patch local configuration" "AUTO" + +if (Test-Path $appSettingsPath) { + $appSettings = Get-Content $appSettingsPath -Raw | ConvertFrom-Json + $appSettings.AzureAd.Instance = "https://login.microsoftonline.com/" + $appSettings.AzureAd.Domain = $tenantDomain + $appSettings.AzureAd.TenantId = $tenantId + $appSettings.AzureAd.ClientId = $webClientId + $appSettings.AzureAd.ClientSecret = $clientSecret + $appSettings | ConvertTo-Json -Depth 10 | Set-Content $appSettingsPath -Encoding UTF8 + Write-Host " Updated appsettings.json" -ForegroundColor Green +} + +$msalConfigPath = Join-Path $sampleRoot "MauiBlazorWebEntraWorkforce" "MsalConfig.cs" +if (Test-Path $msalConfigPath) { + $content = Get-Content $msalConfigPath -Raw + $content = $content -replace 'YOUR_TENANT_DOMAIN', $tenantDomain + $content = $content -replace 'YOUR_TENANT_ID', $tenantId + $content = $content -replace 'YOUR_MAUI_CLIENT_ID', $mauiClientId + $content = $content -replace 'YOUR_WEB_CLIENT_ID', $webClientId + Set-Content $msalConfigPath $content -Encoding UTF8 + Write-Host " Updated MsalConfig.cs" -ForegroundColor Green +} + +$msalActivityPath = Join-Path $sampleRoot "MauiBlazorWebEntraWorkforce" "Platforms" "Android" "MsalActivity.cs" +if (Test-Path $msalActivityPath) { + $content = Get-Content $msalActivityPath -Raw + $content = $content -replace 'msalYOUR_MAUI_CLIENT_ID', "msal$mauiClientId" + Set-Content $msalActivityPath $content -Encoding UTF8 +} + +foreach ($plistPath in @( + (Join-Path $sampleRoot "MauiBlazorWebEntraWorkforce" "Platforms" "iOS" "Info.plist"), + (Join-Path $sampleRoot "MauiBlazorWebEntraWorkforce" "Platforms" "MacCatalyst" "Info.plist") +)) { + if (Test-Path $plistPath) { + $content = Get-Content $plistPath -Raw + $content = $content -replace 'msalYOUR_MAUI_CLIENT_ID', "msal$mauiClientId" + Set-Content $plistPath $content -Encoding UTF8 + } +} + +$teardownDataPath = Join-Path $sampleRoot ".azure-setup.json" +@{ + webClientId = $webClientId + mauiClientId = $mauiClientId + tenantId = $tenantId + tenantDomain = $tenantDomain +} | ConvertTo-Json | Set-Content $teardownDataPath -Encoding UTF8 + +Write-Host "" +Write-Host "=== Setup Complete ===" -ForegroundColor Green +Write-Host " Tenant: $tenantDomain ($tenantId)" +Write-Host " Web App: $webAppName -> $webClientId" +Write-Host " MAUI App: $mauiAppName -> $mauiClientId" +Write-Host " API Scope: api://$webClientId/access_as_user" +Write-Host "" +Write-Host "Next steps:" -ForegroundColor Blue +Write-Host " cd MauiBlazorWebEntraWorkforce.Web && dotnet run --launch-profile https" +Write-Host " # Then build and deploy the MAUI app" +Write-Host "" diff --git a/10.0/MauiBlazorWebEntraWorkforce/scripts/Teardown-Azure.ps1 b/10.0/MauiBlazorWebEntraWorkforce/scripts/Teardown-Azure.ps1 new file mode 100644 index 000000000..3e33b19f9 --- /dev/null +++ b/10.0/MauiBlazorWebEntraWorkforce/scripts/Teardown-Azure.ps1 @@ -0,0 +1,75 @@ +<# +.SYNOPSIS + Deletes the app registrations created for the + MauiBlazorWebEntraWorkforce sample. +#> + +$ErrorActionPreference = 'Stop' + +Write-Host "" +Write-Host "=== MauiBlazorWebEntraWorkforce - Azure Teardown ===" -ForegroundColor Blue +Write-Host "" + +$sampleRoot = Join-Path $PSScriptRoot ".." +$dataPath = Join-Path $sampleRoot ".azure-setup.json" + +if (-not (Test-Path $dataPath)) { + Write-Error "Setup data file not found: $dataPath" + exit 1 +} + +$data = Get-Content $dataPath -Raw | ConvertFrom-Json +$tenantTarget = if ($data.tenantDomain) { $data.tenantDomain } else { $data.tenantId } + +Write-Host "Signing in to tenant: $tenantTarget" -ForegroundColor Cyan +az login --tenant $tenantTarget --allow-no-subscriptions 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to sign in to tenant '$tenantTarget'." + exit 1 +} + +Write-Host "This will delete these app registrations:" -ForegroundColor Yellow +Write-Host " Web App: $($data.webClientId)" +Write-Host " MAUI App: $($data.mauiClientId)" +Write-Host "" +Write-Host "Continue? (y/N): " -ForegroundColor Magenta -NoNewline +$confirm = Read-Host + +if ($confirm -notmatch "^[Yy]$") { + Write-Host "Cancelled." + exit 0 +} + +$failed = $false + +az ad app delete --id $data.mauiClientId 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to delete MAUI app registration: $($data.mauiClientId)" + $failed = $true +} else { + Write-Host " Deleted MAUI app: $($data.mauiClientId)" -ForegroundColor Green +} + +az ad app delete --id $data.webClientId 2>&1 | Out-Null +if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to delete web app registration: $($data.webClientId)" + $failed = $true +} else { + Write-Host " Deleted web app: $($data.webClientId)" -ForegroundColor Green +} + +if ($failed) { + Write-Error "One or more app registrations could not be deleted. The setup data file has been kept." + exit 1 +} + +Remove-Item $dataPath -Force + +Write-Host "" +Write-Host "Teardown complete." -ForegroundColor Green +Write-Host "" +Write-Host "NOTE: The configuration values in appsettings.json and MsalConfig.cs" -ForegroundColor Magenta +Write-Host "were NOT reverted to placeholders. You can re-run Setup-Azure.ps1 to" -ForegroundColor Magenta +Write-Host "configure new resources, or manually restore the placeholder values." -ForegroundColor Magenta +Write-Host ""