Quantcast
Channel: damienbod – Software Engineering
Viewing all articles
Browse latest Browse all 352

Use calendar, mailbox settings and Teams presence in ASP.NET Core hosted Blazor WASM with Microsoft Graph

$
0
0

This article shows how to use Microsoft Graph with delegated permissions in a Blazor WASM ASP.NET Core hosted application. The application uses Microsoft.Identity.Web and the BFF architecture to authenticate against Azure AD. All security logic is implemented in the trusted backend. Microsoft Graph is used to access mailbox settings, teams presence and a users calendar.

Code: https://github.com/damienbod/AspNetCoreBlazorMicrosoftGraph

Use Case and setup

A Blazor WASM UI hosted in ASP.NET Core is used to access any users mailbox settings, team presence or calendar data in the same tenant. The application uses Azure AD for authentication. Blazorise is used in the Blazor WASM UI client project. An authenticated user can enter the target email to view the required data. Delegated Microsoft Graph permissions are used to authorize the API calls.

Setup Azure App registration

The Azure App registration is setup to allow the Microsoft Graph delegated permissions to access the mailbox settings, the teams presence data and the calendar data. The mail permissions are also added if you would like to send emails using Microsoft Graph. The application is a trusted server rendered one and can keep a secret. Because of this, the app is authenticated using a secret or a certificate. You should always authenticate the application if possible.

Implement the Blazor WASM ASP.NET Core Hosted authentication

Only one application exists for the UI and the backend and so only one Azure app registration is used. All authentication is implemented in the trusted backend. The BFF security architecture is used. Microsoft.Identity.Web is used in the the trusted backend to authenticate the application and the identity. No authentication is implemented in the Blazor WASM. This is a view of the server rendered application. The security architecture is more simple and no sensitive data is stored in the client browser. This is especially important since Azure AD does not support revocation, introspection or any way to invalidate the tokens on a logout. SPAs cannot fully logout in Azure AD or Azure B2C because the tokens cannot be invalidated. You should not be sharing the tokens in the untrusted zone due to this as this is hard to secure and you need to evaluate the risk of losing the tokens for your system. Using cookies with same site protection and keeping the tokens in the trusted backend reduces these security risks. Here is a quick start for dotnet Blazor BFF using Azure AD: Blazor.BFF.AzureAD.Template.

The ConfigureServices method is used to setup the services. You can do this in the program file as well. The AddAntiforgery method is used because cookies are used to access the API. Same site is also used to protect the cookies which should only work on the same domain and no sub domains or any other domain. The AddMicrosoftIdentityWebAppAuthentication method is used with a downstream API used for Microsoft Graph. Razor pages are used as the Blazor WASM is hosted in a Razor page and dynamic server data can be used to protect the application or also be used to add meta tags.

public void ConfigureServices(IServiceCollection services)
{
	services.AddScoped<GraphApiClientService>();

	services.AddAntiforgery(options =>
	{
		options.HeaderName = "X-XSRF-TOKEN";
		options.Cookie.Name = "__Host-X-XSRF-TOKEN";
		options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
		options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
	});

	services.AddHttpClient();
	services.AddOptions();

	var scopes = Configuration.GetValue<string>("DownstreamApi:Scopes");
	string[] initialScopes = scopes?.Split(' ');

	services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
		.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
		.AddMicrosoftGraph("https://graph.microsoft.com/beta", scopes)
		.AddInMemoryTokenCaches();

	services.AddControllersWithViews(options =>
		options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));

	services.AddRazorPages().AddMvcOptions(options => { })
		.AddMicrosoftIdentityUI();
}

The Configure method adds the middleware as required. The UseSecurityHeaders method adds all the required security headers which are possible for Blazor. The Razor page _Host is used as the fallback, not a static page.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
		app.UseWebAssemblyDebugging();
	}
	else
	{
		app.UseExceptionHandler("/Error");
	}

	app.UseSecurityHeaders(
		SecurityHeadersDefinitions.GetHeaderPolicyCollection(env.IsDevelopment(),
			Configuration["AzureAd:Instance"]));

	app.UseHttpsRedirection();
	app.UseBlazorFrameworkFiles();
	app.UseStaticFiles();

	app.UseRouting();
	app.UseAuthentication();
	app.UseAuthorization();

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapRazorPages();
		endpoints.MapControllers();
		endpoints.MapFallbackToPage("/_Host");
	});
}

The security headers are implemented using the NetEscapades.AspNetCore.SecurityHeaders Nuget package. This adds everything which is possible for a production deployment. If you want to use hot reload you need to disable some of these policies. You must ensure that the CSS, script code is not implemented in a bad way, otherwise you leave you application open for attacks. A good dev environment should be as close as possible to the production deployment. I don’t use hot reload due to this. Due to Blazorise, the style policy allows inline style in the CSP protection.

public static HeaderPolicyCollection GetHeaderPolicyCollection(bool isDev, string idpHost)
{
	var policy = new HeaderPolicyCollection()
		.AddFrameOptionsDeny()
		.AddXssProtectionBlock()
		.AddContentTypeOptionsNoSniff()
		.AddReferrerPolicyStrictOriginWhenCrossOrigin()
		.AddCrossOriginOpenerPolicy(builder =>
		{
			builder.SameOrigin();
		})
		.AddCrossOriginResourcePolicy(builder =>
		{
			builder.SameOrigin();
		})
		.AddCrossOriginEmbedderPolicy(builder => // remove for dev if using hot reload
		{
			builder.RequireCorp();
		})
		.AddContentSecurityPolicy(builder =>
		{
			builder.AddObjectSrc().None();
			builder.AddBlockAllMixedContent();
			builder.AddImgSrc().Self().From("data:");
			builder.AddFormAction().Self().From(idpHost);
			builder.AddFontSrc().Self();
			builder.AddStyleSrc().Self().UnsafeInline();
			builder.AddBaseUri().Self();
			builder.AddFrameAncestors().None();

			// due to Blazor
            builder.AddScriptSrc()
                  .Self()
                  .WithHash256("v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA=")
                  .UnsafeEval();

			// due to Blazor hot reload requires you to disable script and style CSP protection
			// if using hot reload, DO NOT deploy an with an insecure CSP
		})
		.RemoveServerHeader()
		.AddPermissionsPolicy(builder =>
		{
			builder.AddAccelerometer().None();
			builder.AddAutoplay().None();
			builder.AddCamera().None();
			builder.AddEncryptedMedia().None();
			builder.AddFullscreen().All();
			builder.AddGeolocation().None();
			builder.AddGyroscope().None();
			builder.AddMagnetometer().None();
			builder.AddMicrophone().None();
			builder.AddMidi().None();
			builder.AddPayment().None();
			builder.AddPictureInPicture().None();
			builder.AddSyncXHR().None();
			builder.AddUsb().None();
		});

	if (!isDev)
	{
		// maxage = one year in seconds
		policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(maxAgeInSeconds: 60 * 60 * 24 * 365);
	}

	return policy;
}

Microsoft Graph client service

The GraphServiceClient service can be used directly from the IoC because of how we setup the Microsoft.Identity.Web configuration in the Startup class to use Microsoft Graph and Azure AD. A persistent cache is required for this to work correctly. The GetUserIdAsync method is used to get the Id of the user behind the email address. An equals filter is used. This method is used in most of the services.

private readonly GraphServiceClient _graphServiceClient;

public GraphApiClientService(GraphServiceClient graphServiceClient)
{
	_graphServiceClient = graphServiceClient;
}

private async Task<string> GetUserIdAsync(string email)
{
	var filter = $"userPrincipalName eq '{email}'";
	//var filter = $"startswith(userPrincipalName,'{email}')";

	var users = await _graphServiceClient.Users
		.Request()
		.Filter(filter)
		.GetAsync();

	if(users.CurrentPage.Count == 0)
	{
		return string.Empty;
	}
	return users.CurrentPage[0].Id;
}

The GetGraphApiUser method uses an email to get the profile data of that user. This can be any email from your tenant. The users of the Microsoft Graph client is used.

public async Task<User> GetGraphApiUser(string email)
{
	var id= await GetUserIdAsync(email);
	if (string.IsNullOrEmpty(upn))
		return null;

	return await _graphServiceClient.Users[id]
		.Request()
		.GetAsync();
}

The GetUserMailboxSettings method is used to get the MailboxSettings for the given email. The Id for the user is requested, then the MailboxSettings settings are returned for the user. This only works, if the MailboxSettings.Read permission is granted to the Azure App registration.

public async Task<MailboxSettings> GetUserMailboxSettings(string email)
{
	var id= await GetUserIdAsync(email);
	if (string.IsNullOrEmpty(upn))
		return null;

	var user = await _graphServiceClient.Users[id]
		.Request()
		.Select("MailboxSettings")
		.GetAsync();

	return user.MailboxSettings;
}

The GetCalanderForUser returns the calendar for the given email. This returns a flat list of FilteredEvent items. Microsoft Graph returns a IUserCalendarViewCollectionPage which is a bit complicated for using if only requesting small amounts of data. This works well for large results which needs to be paged, streamed or whatever. The CalendarView is used with a ‘to’ and a ‘from’ datetime filter to request the calendar events.

public async Task<List<FilteredEvent>> GetCalanderForUser(string email, string from, string to)
{

	var userCalendarViewCollectionPages = await GetCalanderForUserUsingGraph(email, from, to);

	var allEvents = new List<FilteredEvent>();

	while (userCalendarViewCollectionPages != null && userCalendarViewCollectionPages.Count > 0)
	{
		foreach (var calenderEvent in userCalendarViewCollectionPages)
		{
			var filteredEvent = new FilteredEvent
			{
				ShowAs = calenderEvent.ShowAs,
				Sensitivity = calenderEvent.Sensitivity,
				Start = calenderEvent.Start,
				End = calenderEvent.End,
				Subject = calenderEvent.Subject,
				IsAllDay = calenderEvent.IsAllDay,
				Location = calenderEvent.Location
			};
			allEvents.Add(filteredEvent);
		}

		if (userCalendarViewCollectionPages.NextPageRequest == null)
			break;
	}

	return allEvents;
}

private async Task<IUserCalendarViewCollectionPage> GetCalanderForUserUsingGraph(string email, string from, string to)
{
	var id= await GetUserIdAsync(email);
	if (string.IsNullOrEmpty(id))
		return null;

	var queryOptions = new List<QueryOption>()
	{
		new QueryOption("startDateTime", from),
		new QueryOption("endDateTime", to)
	};

	var calendarView = await _graphServiceClient.Users[id].CalendarView
		.Request(queryOptions)
		.Select("start,end,subject,location,sensitivity, showAs, isAllDay")
		.GetAsync();

	return calendarView;
}

The GetPresenceforEmail returns a teams presence list for the given email. This only works if the Presence.Read.All delegated permission is granted to the Azure App registration. Again Microsoft Graph returns a paged result which is not required in our use case. We only want this for a single email.

public async Task<List<Presence>> GetPresenceforEmail(string email)
{
	var cloudCommunicationPages = await GetPresenceAsync(email);

	var allPresenceItems = new List<Presence>();

	while (cloudCommunicationPages != null && cloudCommunicationPages.Count > 0)
	{
		foreach (var presence in cloudCommunicationPages)
		{
			allPresenceItems.Add(presence);
		}

		if (cloudCommunicationPages.NextPageRequest == null)
			break;
	}

	return allPresenceItems;
}

private async Task<ICloudCommunicationsGetPresencesByUserIdCollectionPage> GetPresenceAsync(string email)
{
	var id = await GetUserIdAsync(email);

	var ids = new List<string>()
	{
		id
	};

	return await _graphServiceClient.Communications
		.GetPresencesByUserId(ids)
		.Request()
		.PostAsync();
}

Blazor Server API

The Blazor server host application implements an API which requires cookies and a correct anti-forgery token to access the protected resource. This can only be accessed from the same domain. The used cookie has also same site protection and should only work for the exact same domain. All the Blazor WASM API calls use this API in for the Microsoft Graph data displays. The WASM application does not authenticate directly or use the Microsoft Graph service directly. The Microsoft Graph client is not exposed to the untrusted client browser.

[ValidateAntiForgeryToken]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[AuthorizeForScopes(Scopes = new string[] { "User.ReadBasic.All user.read" })]
[ApiController]
[Route("api/[controller]")]
public class GraphApiCallsController : ControllerBase
{
	private GraphApiClientService _graphApiClientService;

	public GraphApiCallsController(GraphApiClientService graphApiClientService)
	{
		_graphApiClientService = graphApiClientService;
	}

	[HttpGet("UserProfile")]
	public async Task<IEnumerable<string>> UserProfile()
	{
		var userData = await _graphApiClientService.GetGraphApiUser(User.Identity.Name);
		return new List<string> { $"DisplayName: {userData.DisplayName}",
			$"GivenName: {userData.GivenName}", $"Preferred Language: {userData.PreferredLanguage}" };
	}

	[HttpPost("MailboxSettings")]
	public async Task<IActionResult> MailboxSettings([FromBody] string email)
	{
		if (string.IsNullOrEmpty(email))
			return BadRequest("No email");
		try
		{
			var mailbox = await _graphApiClientService.GetUserMailboxSettings(email);

			if(mailbox == null)
			{
				return NotFound($"mailbox settings for {email} not found");
			}
			var result = new List<MailboxSettingsData> {
			new MailboxSettingsData { Name = "User Email", Data = email },
			new MailboxSettingsData { Name = "AutomaticRepliesSetting", Data = mailbox.AutomaticRepliesSetting.Status.ToString() },
			new MailboxSettingsData { Name = "TimeZone", Data = mailbox.TimeZone },
			new MailboxSettingsData { Name = "Language", Data = mailbox.Language.DisplayName }
		};

			return Ok(result);
		}
		catch (Exception ex)
		{
			return BadRequest(ex.Message);
		}
	}

	[HttpPost("TeamsPresence")]
	public async Task<IActionResult> PresencePost([FromBody] string email)
	{
		if (string.IsNullOrEmpty(email))
			return BadRequest("No email");
		try
		{
			var userPresence = await _graphApiClientService.GetPresenceforEmail(email);

			if (userPresence.Count == 0)
			{
				return NotFound(email);
			}

			var result = new List<PresenceData> {
			new PresenceData { Name = "User Email", Data = email },
			new PresenceData { Name = "Availability", Data = userPresence[0].Availability }
		};

			return Ok(result);
		}
		catch (Exception ex)
		{
			return BadRequest(ex.Message);
		}
	}

	[HttpPost("UserCalendar")]
	public async Task<IEnumerable<FilteredEventDto>> UserCalendar(UserCalendarDataModel userCalendarDataModel)
	{
		var userCalendar = await _graphApiClientService.GetCalanderForUser(
			userCalendarDataModel.Email, 
			userCalendarDataModel.From.Value.ToString("yyyy-MM-ddTHH:mm:ss.sssZ"),
			userCalendarDataModel.To.Value.ToString("yyyy-MM-ddTHH:mm:ss.sssZ"));

		return userCalendar.Select(l => new FilteredEventDto
		{
			IsAllDay = l.IsAllDay.GetValueOrDefault(),
			Sensitivity = l.Sensitivity.ToString(),
			Start = l.Start?.DateTime,
			End = l.End?.DateTime,
			ShowAs = l.ShowAs.Value.ToString(),
			Subject=l.Subject
		});
	}

Blazor Calendar client

The Blazor WASM client does not implement any security and does not require a Microsoft.Identity.Web client. I like Blazorize and the Nuget packages are added to the UI so that these components can be used. These are nice components but use inline css.

<ItemGroup>
 <PackageReference Include="blazored.sessionstorage" Version="2.2.0" />
 <PackageReference Include="Blazorise" Version="0.9.5.2" />
 <PackageReference Include="Blazorise.Components" Version="0.9.5.2" />
 <PackageReference Include="Blazorise.DataGrid" Version="0.9.5.2" />
 <PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.5.2" />
 <PackageReference Include="Blazorise.Icons.Material" Version="0.9.5.2" />
 <PackageReference Include="Blazorise.Material" Version="0.9.5.2" />
 <PackageReference Include="Blazorise.Sidebar" Version="0.9.5.2" />
 <PackageReference Include="Blazorise.Snackbar" Version="0.9.5.2" />
 <PackageReference Include="Blazorise.SpinKit" Version="0.9.5.2" />
 <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.1" />
 <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.1" PrivateAssets="all" />
 <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
 <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="6.0.1" />
</ItemGroup>

The Blazor WASM client is hosted in a Razor page. This makes it possible to add dynamic data. The anti-forgery token is added here as well as the security headers and the dynamic meta data if required.

@page "/"
@namespace AspNetCoreMicrosoftGraph.Pages
@using AspNetCoreMicrosoftGraph.Client
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
    <link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
    <link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
    <link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
    <link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
    <link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
    <link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
    <link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
    <link rel="icon" type="image/png" sizes="192x192"  href="/android-icon-192x192.png">
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
    <link rel="manifest" href="/manifest.json">
    <meta name="msapplication-TileColor" content="#ffffff">
    <meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
    <meta name="theme-color" content="#ffffff">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Blazor Graph</title>
    <base href="~/" />
    
    <link rel="stylesheet" href="/lib/font-awesome/css/all.min.css">

    <!-- Material CSS -->
    <link href="css/material.min.css" rel="stylesheet">

    <!-- Add Material font (Roboto) and Material icon as needed -->
    <link href="css/googlefontsroboto.css" rel="stylesheet">
    <link href="css/fonts.google.icons.css" rel="stylesheet">

    <link href="_content/Blazorise/blazorise.css" rel="stylesheet" />
    <link href="_content/Blazorise.Material/blazorise.material.css" rel="stylesheet" />
    <link href="_content/Blazorise.Icons.Material/blazorise.icons.material.css" rel="stylesheet" />

    <link href="_content/Blazorise.SpinKit/blazorise.spinkit.css" rel="stylesheet" />
    <link href="_content/Blazorise.Snackbar/blazorise.snackbar.css" rel="stylesheet" />

    <link href="css/app.css" rel="stylesheet" />
    <link href="AspNetCoreMicrosoftGraph.Client.styles.css" rel="stylesheet" />
    <link href="manifest.json" rel="manifest" />
    <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
</head>
<body>

    <div id="app">
        <!-- Spinner -->
        <div class="spinner d-flex align-items-center justify-content-center spinner">
            <div class="spinner-border text-success" role="status">
                <span class="sr-only">Loading...</span>
            </div>
        </div>
    </div>

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="lib/jquery/jquery.slim.min.js"></script>
    <script src="lib/popper.js/umd/popper.min.js"></script>
    <script src="js/material.min.js"></script>

    @*<script src="_content/Blazorise/blazorise.js"></script>*@
    <script src="_content/Blazorise.Material/blazorise.material.js"></script>

    <script src="lib/flatpickr/l10n/default.js"></script>
    <script src="lib/flatpickr/l10n/de.js"></script>
    <script src="lib/flatpickr/l10n/fr.js"></script>
    <script src="lib/flatpickr/l10n/it.js"></script>
    <script language="javascript">
        flatpickr.localize(flatpickr.l10ns.default); /*set global*/
    </script>

    <script src="_framework/blazor.webassembly.js" ></script>
    <script src="antiForgeryToken.js" ></script>
    @Html.AntiForgeryToken()
</body>
</html>

The user calendar WASM view uses an input text field to enter any tenant email. This posts a request to the server API add returns the data which is displayed in the Blazorise Data Grid.

@page "/usercalendar"
@inject IHttpClientFactory HttpClientFactory
@inject IJSRuntime JSRuntime

<h4>Calendar Events</h4>

<Validations StatusChanged="@OnStatusChanged">
    <Validation Validator="@ValidateEmail"  >
        <TextEdit Placeholder="Enter email" @bind-Text="userCalendarDataModel.Email" >
            <Feedback>
                <ValidationNone>Please enter the email.</ValidationNone>
                <ValidationSuccess>Email is good.</ValidationSuccess>
                <ValidationError>Enter valid email!</ValidationError>
            </Feedback>
        </TextEdit>
    </Validation>

    <Field Horizontal="true">
        <FieldLabel ColumnSize="ColumnSize.IsFull.OnTablet.Is2.OnDesktop">From</FieldLabel>
        <FieldBody ColumnSize="ColumnSize.IsFull.OnTablet.Is10.OnDesktop">
            <DateEdit TValue="DateTime?" InputMode="DateInputMode.DateTime" @bind-Date="userCalendarDataModel.From" />
        </FieldBody>
    </Field>

    <Field Horizontal="true">
        <FieldLabel ColumnSize="ColumnSize.IsFull.OnTablet.Is2.OnDesktop">To</FieldLabel>
        <FieldBody ColumnSize="ColumnSize.IsFull.OnTablet.Is10.OnDesktop">
            <DateEdit TValue="DateTime?" InputMode="DateInputMode.DateTime" @bind-Date="userCalendarDataModel.To" />
        </FieldBody>
    </Field>

    <br />
    <Button Color="Color.Primary" Disabled="@saveDisabled" PreventDefaultOnSubmit="true" Clicked="@Submit">Get calendar events for user</Button>
</Validations>

 <br /><br />

@if (filteredEvents == null)
{
    <p><em>@noDataResult</em></p>
}
else
{
    <DataGrid TItem="FilteredEventDto"
              Data="@filteredEvents" Bordered="true"
              @bind-SelectedRow="@selectedFilteredEvent" PageSize=15
              Responsive>
        <DataGridCommandColumn TItem="FilteredEventDto" />
        <DataGridColumn TItem="FilteredEventDto" Field="@nameof(FilteredEventDto.Subject)" Caption="Subject" Sortable="true" />
        <DataGridColumn TItem="FilteredEventDto" Field="@nameof(FilteredEventDto.Start)" Caption="Start" Editable="false" />
        <DataGridColumn TItem="FilteredEventDto" Field="@nameof(FilteredEventDto.End)" Caption="End" Editable="false" />
        <DataGridColumn TItem="FilteredEventDto" Field="@nameof(FilteredEventDto.Sensitivity)" Caption="Sensitivity" Editable="false"/>
        <DataGridColumn TItem="FilteredEventDto" Field="@nameof(FilteredEventDto.IsAllDay)" Caption="IsAllDay" Editable="false"/>
        <DataGridColumn TItem="FilteredEventDto" Field="@nameof(FilteredEventDto.ShowAs)" Caption="ShowAs" Editable="false"/>
    </DataGrid>
}

@code {
    private List<FilteredEventDto> filteredEvents;
    private UserCalendarDataModel userCalendarDataModel { get; set; } = new UserCalendarDataModel()
    {
        From = DateTime.UtcNow.AddDays(-7.0),
        To = DateTime.UtcNow.AddDays(7.0)
    };

    private FilteredEventDto selectedFilteredEvent;
    private string noDataResult { get; set; } = "no data";
    bool saveDisabled = true;

    Task OnStatusChanged( ValidationsStatusChangedEventArgs eventArgs )
    {
        saveDisabled = eventArgs.Status != ValidationStatus.Success;

        return Task.CompletedTask;
    }

    void ValidateEmail( ValidatorEventArgs e )
    {
        var email = Convert.ToString( e.Value );

        e.Status = string.IsNullOrEmpty( email ) ? ValidationStatus.None :
             email.Contains( "@" ) ? ValidationStatus.Success : ValidationStatus.Error;
    }

    async Task Submit()
    {
        await PostData(userCalendarDataModel);
    }

    private async Task PostData(UserCalendarDataModel userCalendarDataModel)
    {
        var token = await JSRuntime.InvokeAsync<string>("getAntiForgeryToken");
        var client = HttpClientFactory.CreateClient("default");
        client.DefaultRequestHeaders.Add("X-XSRF-TOKEN", token);
        var response = await client.PostAsJsonAsync<UserCalendarDataModel>("api/GraphApiCalls/UserCalendar", userCalendarDataModel);
        if(response.IsSuccessStatusCode)
        {
            filteredEvents = await response.Content.ReadFromJsonAsync<List<FilteredEventDto>>();
        }
        else
        {
            var error = await response.Content.ReadAsStringAsync();
            filteredEvents = null;
            noDataResult = error;
        }
    }
}

Running the application, a user can sign-in and request calendar data, mailbox settings or teams presence of any user in the tenant.

Using Graph together with Microsoft.Identity.Web works really well and can be implemented with little effort. By using the BFF and hosting the WASM in an ASP.NET Core application, less sensitive data needs to be exposed and it is possible to sign out without tokens possibly still existing in the untrusted zone after the logout. Blazor and Blazorise could be improved to support a better CSP and better security headers.

Links

https://blazorise.com/

https://github.com/AzureAD/microsoft-identity-web

https://docs.microsoft.com/en-us/graph/api/user-get-mailboxsettings

https://docs.microsoft.com/en-us/graph/api/presence-get

https://docs.microsoft.com/en-us/aspnet/core/blazor/security/content-security-policy

https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders


Viewing all articles
Browse latest Browse all 352

Trending Articles