Skip to content

Commit f0763c2

Browse files
authored
Blazor with Graph API (#20192)
1 parent 53277fe commit f0763c2

14 files changed

Lines changed: 898 additions & 305 deletions

aspnetcore/blazor/components/lifecycle.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ protected override bool ShouldRender()
189189

190190
Even if <xref:Microsoft.AspNetCore.Components.ComponentBase.ShouldRender%2A> is overridden, the component is always initially rendered.
191191

192-
For more information, see <xref:blazor/webassembly-performance-best-practices#avoid-unnecessary-component-renders>.
192+
For more information, see <xref:blazor/webassembly-performance-best-practices#avoid-unnecessary-rendering-of-component-subtrees>.
193193

194194
## State changes
195195

aspnetcore/blazor/forms-validation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ namespace BlazorSample.Server.Controllers
503503
}
504504
catch (Exception ex)
505505
{
506-
logger.LogError("Validation Error: {MESSAGE}", ex.Message);
506+
logger.LogError("Validation Error: {Message}", ex.Message);
507507
}
508508

509509
return BadRequest(ModelState);
@@ -690,7 +690,7 @@ In the client project, the *Starfleet Starship Database* form is updated to show
690690
}
691691
catch (Exception ex)
692692
{
693-
Logger.LogError("Form processing error: {MESSAGE}", ex.Message);
693+
Logger.LogError("Form processing error: {Message}", ex.Message);
694694
disabled = true;
695695
messageStyles = "color:red";
696696
message = "There was an error processing the form.";

aspnetcore/blazor/security/webassembly/additional-scenarios.md

Lines changed: 2 additions & 251 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Learn how to configure Blazor WebAssembly for additional security s
55
monikerRange: '>= aspnetcore-3.1'
66
ms.author: riande
77
ms.custom: mvc
8-
ms.date: 08/03/2020
8+
ms.date: 10/27/2020
99
no-loc: ["ASP.NET Core Identity", cookie, Cookie, Blazor, "Blazor Server", "Blazor WebAssembly", "Identity", "Let's Encrypt", Razor, SignalR]
1010
uid: blazor/security/webassembly/additional-scenarios
1111
---
@@ -159,128 +159,6 @@ For a Blazor app based on the Blazor WebAssembly Hosted project template, <xref:
159159
* The <xref:System.Net.Http.HttpClient.BaseAddress?displayProperty=nameWithType> (`new Uri(builder.HostEnvironment.BaseAddress)`).
160160
* A URL of the `authorizedUrls` array.
161161

162-
### Graph API example
163-
164-
In the following example, a named <xref:System.Net.Http.HttpClient> for Graph API is used to obtain a user's mobile phone number to process a call. After adding the Microsoft Graph API `User.Read` permission in the AAD area of the Azure portal, the scope is configured for the named client in the standalone app or *`Client`* app of a hosted Blazor solution.
165-
166-
> [!NOTE]
167-
> The example in this section obtains Graph API data for the user in *component code*. To create user claims from Graph API, see the following resources:
168-
>
169-
> * [Customize the user](#customize-the-user) section
170-
> * <xref:blazor/security/webassembly/aad-groups-roles>
171-
172-
`GraphAuthorizationMessageHandler.cs`:
173-
174-
```csharp
175-
public class GraphAPIAuthorizationMessageHandler : AuthorizationMessageHandler
176-
{
177-
public GraphAPIAuthorizationMessageHandler(IAccessTokenProvider provider,
178-
NavigationManager navigationManager)
179-
: base(provider, navigationManager)
180-
{
181-
ConfigureHandler(
182-
authorizedUrls: new[] { "https://graph.microsoft.com" },
183-
scopes: new[] { "https://graph.microsoft.com/User.Read" });
184-
}
185-
}
186-
```
187-
188-
In `Program.Main` (`Program.cs`):
189-
190-
```csharp
191-
builder.Services.AddScoped<GraphAPIAuthorizationMessageHandler>();
192-
193-
builder.Services.AddHttpClient("GraphAPI",
194-
client => client.BaseAddress = new Uri("https://graph.microsoft.com"))
195-
.AddHttpMessageHandler<GraphAPIAuthorizationMessageHandler>();
196-
```
197-
198-
In a Razor component (`Pages/CallUser.razor`):
199-
200-
```razor
201-
@page "/CallUser"
202-
@using System.ComponentModel.DataAnnotations
203-
@using System.Text.Json.Serialization
204-
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
205-
@using Microsoft.Extensions.Logging
206-
@inject IAccessTokenProvider TokenProvider
207-
@inject IHttpClientFactory ClientFactory
208-
@inject ILogger<CallUser> Logger
209-
@inject ICallProcessor CallProcessor
210-
211-
<h3>Call User</h3>
212-
213-
<EditForm Model="@callInfo" OnValidSubmit="@HandleValidSubmit">
214-
<DataAnnotationsValidator />
215-
<ValidationSummary />
216-
217-
<p>
218-
<label>
219-
Message:
220-
<InputTextArea @bind-Value="callInfo.Message" />
221-
</label>
222-
</p>
223-
224-
<button type="submit">Place call</button>
225-
226-
<p>
227-
@formStatus
228-
</p>
229-
</EditForm>
230-
231-
@code {
232-
private string formStatus;
233-
private CallInfo callInfo = new CallInfo();
234-
235-
private async Task HandleValidSubmit()
236-
{
237-
var tokenResult = await TokenProvider.RequestAccessToken(
238-
new AccessTokenRequestOptions
239-
{
240-
Scopes = new[] { "https://graph.microsoft.com/User.Read" }
241-
});
242-
243-
if (tokenResult.TryGetToken(out var token))
244-
{
245-
var client = ClientFactory.CreateClient("GraphAPI");
246-
247-
var userInfo = await client.GetFromJsonAsync<UserInfo>("v1.0/me");
248-
249-
if (userInfo != null)
250-
{
251-
CallProcessor.Send(userInfo.MobilePhone, callInfo.Message);
252-
253-
formStatus = "Form successfully processed.";
254-
Logger.LogInformation(
255-
$"Form successfully processed at {DateTime.UtcNow}. " +
256-
$"Mobile Phone: {userInfo.MobilePhone}");
257-
}
258-
}
259-
else
260-
{
261-
formStatus = "There was a problem processing the form.";
262-
Logger.LogError("Token failure");
263-
}
264-
}
265-
266-
private class CallInfo
267-
{
268-
[Required]
269-
[StringLength(1000, ErrorMessage = "Message too long (1,000 char limit)")]
270-
public string Message { get; set; }
271-
}
272-
273-
private class UserInfo
274-
{
275-
[JsonPropertyName("mobilePhone")]
276-
public string MobilePhone { get; set; }
277-
}
278-
}
279-
```
280-
281-
> [!NOTE]
282-
> In the preceding example, the developer implements the custom `ICallProcessor` (`CallProcessor`) to queue and then place automated calls.
283-
284162
## Typed `HttpClient`
285163

286164
A typed client can be defined that handles all of the HTTP and token acquisition concerns within a single class.
@@ -915,134 +793,6 @@ Register the `CustomAccountFactory` for the authentication provider in use. Any
915793
CustomUserAccount, CustomAccountFactory>();
916794
```
917795

918-
### Customize the user with Graph API claims
919-
920-
In the following example, the app creates a mobile phone number claim for the user from Graph API using the <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteUserAccount>. The app must have the `User.Read` Graph API permission (scope) configured in AAD.
921-
922-
`GraphAuthorizationMessageHandler.cs`:
923-
924-
```csharp
925-
using Microsoft.AspNetCore.Components;
926-
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
927-
928-
public class GraphAPIAuthorizationMessageHandler : AuthorizationMessageHandler
929-
{
930-
public GraphAPIAuthorizationMessageHandler(IAccessTokenProvider provider,
931-
NavigationManager navigationManager)
932-
: base(provider, navigationManager)
933-
{
934-
ConfigureHandler(
935-
authorizedUrls: new[] { "https://graph.microsoft.com" },
936-
scopes: new[] { "https://graph.microsoft.com/User.Read" });
937-
}
938-
}
939-
```
940-
941-
A named <xref:System.Net.Http.HttpClient> for Graph API is created in `Program.Main` (`Program.cs`) using the `GraphAPIAuthorizationMessageHandler`:
942-
943-
```csharp
944-
using System;
945-
946-
...
947-
948-
builder.Services.AddScoped<GraphAPIAuthorizationMessageHandler>();
949-
950-
builder.Services.AddHttpClient("GraphAPI",
951-
client => client.BaseAddress = new Uri("https://graph.microsoft.com"))
952-
.AddHttpMessageHandler<GraphAPIAuthorizationMessageHandler>();
953-
```
954-
955-
`Models/UserInfo.cs`:
956-
957-
```csharp
958-
using System.Text.Json.Serialization;
959-
960-
public class UserInfo
961-
{
962-
[JsonPropertyName("mobilePhone")]
963-
public string MobilePhone { get; set; }
964-
}
965-
```
966-
967-
In the following `CustomAccountFactory` (`CustomAccountFactory.cs`), the framework's <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteUserAccount> represents the user's account. If the app requires a custom user account class that extends <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteUserAccount>, swap the custom user account class for <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteUserAccount> in the following code:
968-
969-
```csharp
970-
using System.Net.Http;
971-
using System.Net.Http.Json;
972-
using System.Security.Claims;
973-
using System.Threading.Tasks;
974-
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
975-
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
976-
using Microsoft.Extensions.Logging;
977-
978-
public class CustomAccountFactory
979-
: AccountClaimsPrincipalFactory<RemoteUserAccount>
980-
{
981-
private readonly ILogger<CustomAccountFactory> logger;
982-
private readonly IHttpClientFactory clientFactory;
983-
984-
public CustomAccountFactory(IAccessTokenProviderAccessor accessor,
985-
IHttpClientFactory clientFactory,
986-
ILogger<CustomAccountFactory> logger)
987-
: base(accessor)
988-
{
989-
this.clientFactory = clientFactory;
990-
this.logger = logger;
991-
}
992-
993-
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
994-
RemoteUserAccount account,
995-
RemoteAuthenticationUserOptions options)
996-
{
997-
var initialUser = await base.CreateUserAsync(account, options);
998-
999-
if (initialUser.Identity.IsAuthenticated)
1000-
{
1001-
var userIdentity = (ClaimsIdentity)initialUser.Identity;
1002-
1003-
try
1004-
{
1005-
var client = clientFactory.CreateClient("GraphAPI");
1006-
1007-
var userInfo = await client.GetFromJsonAsync<UserInfo>("v1.0/me");
1008-
1009-
if (userInfo != null)
1010-
{
1011-
userIdentity.AddClaim(new Claim("mobilephone", userInfo.MobilePhone));
1012-
}
1013-
}
1014-
catch (AccessTokenNotAvailableException exception)
1015-
{
1016-
logger.LogError("Graph API access token failure: {MESSAGE}",
1017-
exception.Message);
1018-
}
1019-
}
1020-
1021-
return initialUser;
1022-
}
1023-
}
1024-
```
1025-
1026-
In `Program.Main` (`Program.cs`), configure the app to use the custom factory. If the app uses a custom user account class that extends <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteUserAccount>, swap the custom user account class for <xref:Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteUserAccount> in the following code:
1027-
1028-
```csharp
1029-
using Microsoft.Extensions.Configuration;
1030-
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
1031-
1032-
...
1033-
1034-
builder.Services.AddMsalAuthentication<RemoteAuthenticationState,
1035-
RemoteUserAccount>(options =>
1036-
{
1037-
builder.Configuration.Bind("AzureAd",
1038-
options.ProviderOptions.Authentication);
1039-
})
1040-
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount,
1041-
CustomAccountFactory>();
1042-
```
1043-
1044-
The preceding example is for an app that uses AAD authentication with MSAL. Similar patterns exist for OIDC and API authentication. For more information, see the examples at the end of the [Customize the user with a payload claim](#customize-the-user-with-a-payload-claim) section.
1045-
1046796
### AAD security groups and roles with a custom user account class
1047797

1048798
For an additional example that works with AAD security groups and AAD Administrator Roles and a custom user account class, see <xref:blazor/security/webassembly/aad-groups-roles>.
@@ -1283,4 +1033,5 @@ For more information, see <xref:grpc/browser>.
12831033

12841034
## Additional resources
12851035

1036+
* <xref:blazor/security/webassembly/graph-api>
12861037
* [`HttpClient` and `HttpRequestMessage` with Fetch API request options](xref:blazor/call-web-api#httpclient-and-httprequestmessage-with-fetch-api-request-options)

0 commit comments

Comments
 (0)