diff --git a/eng/Versions.props b/eng/Versions.props
index 924244aaba..5aa781e731 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -247,8 +247,8 @@
3.0.0
3.0.0
2.1.90
- 0.2.0-preview
- 0.2.0-preview
+ 0.2.1-preview
+ 0.2.1-preview
3.8.0
$(MessagePackPackageVersion)
4.10.0
diff --git a/src/ProjectTemplates/BlazorTemplates.Tests/BlazorServerTemplateTest.cs b/src/ProjectTemplates/BlazorTemplates.Tests/BlazorServerTemplateTest.cs
index e8893c1338..7a47f9b84a 100644
--- a/src/ProjectTemplates/BlazorTemplates.Tests/BlazorServerTemplateTest.cs
+++ b/src/ProjectTemplates/BlazorTemplates.Tests/BlazorServerTemplateTest.cs
@@ -188,5 +188,30 @@ namespace Templates.Test
Browser.Exists(By.CssSelector("table>tbody>tr"));
Browser.Equal(5, () => Browser.FindElements(By.CssSelector("p+table>tbody>tr")).Count);
}
+
+ [Theory]
+ [QuarantinedTest]
+ [InlineData("IndividualB2C", null)]
+ [InlineData("IndividualB2C", new string[] { "--called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })]
+ [InlineData("SingleOrg", null)]
+ [InlineData("SingleOrg", new string[] { "--called-api-url \"https://graph.microsoft.com\"", "--called-api-scopes user.readwrite" })]
+ [InlineData("SingleOrg", new string[] { "--calls-graph" })]
+ public async Task BlazorServerTemplat_IdentityWeb_BuildAndPublish(string auth, string[] args)
+ {
+ Project = await ProjectFactory.GetOrCreateProject("blazorserveridweb" + Guid.NewGuid().ToString().Substring(0, 10).ToLower(), Output);
+
+ var createResult = await Project.RunDotNetNewAsync("blazorserver", auth: auth, args: args);
+ Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
+
+ var publishResult = await Project.RunDotNetPublishAsync();
+ Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
+
+ // Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release
+ // The output from publish will go into bin/Release/netcoreappX.Y/publish and won't be affected by calling build
+ // later, while the opposite is not true.
+
+ var buildResult = await Project.RunDotNetBuildAsync();
+ Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", Project, buildResult));
+ }
}
}
diff --git a/src/ProjectTemplates/BlazorTemplates.Tests/BlazorWasmTemplateTest.cs b/src/ProjectTemplates/BlazorTemplates.Tests/BlazorWasmTemplateTest.cs
index f6f5cea506..1ef58465c4 100644
--- a/src/ProjectTemplates/BlazorTemplates.Tests/BlazorWasmTemplateTest.cs
+++ b/src/ProjectTemplates/BlazorTemplates.Tests/BlazorWasmTemplateTest.cs
@@ -418,6 +418,27 @@ namespace Templates.Test
"--default-scope", "full",
"--app-id-uri", "ApiUri",
"--api-client-id", "1234123413241324"),
+ new TemplateInstance(
+ "blazorwasmhostedaadgraph", "-ho",
+ "-au", "SingleOrg",
+ "--calls-graph",
+ "--domain", "my-domain",
+ "--tenant-id", "tenantId",
+ "--client-id", "clientId",
+ "--default-scope", "full",
+ "--app-id-uri", "ApiUri",
+ "--api-client-id", "1234123413241324"),
+ new TemplateInstance(
+ "blazorwasmhostedaadapi", "-ho",
+ "-au", "SingleOrg",
+ "--called-api-url", "\"https://graph.microsoft.com\"",
+ "--called-api-scopes", "user.readwrite",
+ "--domain", "my-domain",
+ "--tenant-id", "tenantId",
+ "--client-id", "clientId",
+ "--default-scope", "full",
+ "--app-id-uri", "ApiUri",
+ "--api-client-id", "1234123413241324"),
new TemplateInstance(
"blazorwasmstandaloneaadb2c",
"-au", "IndividualB2C",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/BlazorServerWeb-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/BlazorServerWeb-CSharp.csproj.in
index 868f60d4af..c47e9753cd 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/BlazorServerWeb-CSharp.csproj.in
+++ b/src/ProjectTemplates/Web.ProjectTemplates/BlazorServerWeb-CSharp.csproj.in
@@ -17,14 +17,15 @@
-
-
+
+
+
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/ComponentsWebAssembly-CSharp.Server.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/ComponentsWebAssembly-CSharp.Server.csproj.in
index 1c350c70aa..20efa3e9e2 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/ComponentsWebAssembly-CSharp.Server.csproj.in
+++ b/src/ProjectTemplates/Web.ProjectTemplates/ComponentsWebAssembly-CSharp.Server.csproj.in
@@ -38,8 +38,9 @@
-
-
+
+
+
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/dotnetcli.host.json
index 2473827568..3a5e6c90a8 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/dotnetcli.host.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/dotnetcli.host.json
@@ -67,6 +67,18 @@
"NoHttps": {
"longName": "no-https",
"shortName": ""
+ },
+ "CalledApiUrl": {
+ "longName": "called-api-url",
+ "shortName": ""
+ },
+ "CalledApiScopes": {
+ "longName": "called-api-scopes",
+ "shortName": ""
+ },
+ "CallsMicrosoftGraph": {
+ "longName": "calls-graph",
+ "shortName": ""
}
},
"usageExamples": [
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json
index 484e81863c..463e0eb7ba 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/.template.config/template.json
@@ -127,6 +127,52 @@
"Shared/LoginDisplay.IndividualB2CAuth.razor",
"Shared/LoginDisplay.OrganizationalAuth.razor"
]
+ },
+ {
+ "condition": "(!GenerateApi)",
+ "exclude": [
+ "Services/DownstreamWebApi.cs",
+ "Pages/CallWebApi.razor"
+ ]
+ },
+ {
+ "condition": "(!GenerateGraph)",
+ "exclude": [
+ "Services/MicrosoftGraphServiceExtensions.cs",
+ "Services/TokenAcquisitionCredentialProvider.cs",
+ "Shared/NavMenu.CallsMicrosoftGraph.razor",
+ "Pages/ShowProfile.razor"
+ ]
+ },
+ {
+ "condition": "(!GenerateApiOrGraph)",
+ "rename": {
+ "Shared/NavMenu.NoGraphOrApi.razor": "Shared/NavMenu.razor"
+ },
+ "exclude": [
+ "Shared/NavMenu.CallsMicrosoftGraph.razor",
+ "Shared/NavMenu.CallsWebApi.razor"
+ ]
+ },
+ {
+ "condition": "(GenerateGraph)",
+ "rename": {
+ "Shared/NavMenu.CallsMicrosoftGraph.razor": "Shared/NavMenu.razor"
+ },
+ "exclude": [
+ "Shared/NavMenu.NoGraphOrApi.razor",
+ "Shared/NavMenu.CallsWebApi.razor"
+ ]
+ },
+ {
+ "condition": "(GenerateApi)",
+ "rename": {
+ "Shared/NavMenu.CallsWebApi.razor": "Shared/NavMenu.razor"
+ },
+ "exclude": [
+ "Shared/NavMenu.NoGraphOrApi.razor",
+ "Shared/NavMenu.CallsMicrosoftGraph.razor"
+ ]
}
]
}
@@ -174,21 +220,28 @@
"SignUpSignInPolicyId": {
"type": "parameter",
"datatype": "string",
- "defaultValue": "",
+ "defaultValue": "b2c_1_susi",
"replaces": "MySignUpSignInPolicyId",
"description": "The sign-in and sign-up policy ID for this project (use with IndividualB2C auth)."
},
+ "SignedOutCallbackPath": {
+ "type": "parameter",
+ "datatype": "string",
+ "defaultValue": "/signout/B2C_1_susi",
+ "replaces": "/signout/MySignUpSignInPolicyId",
+ "description": "The global signout callback (use with IndividualB2C auth)."
+ },
"ResetPasswordPolicyId": {
"type": "parameter",
"datatype": "string",
- "defaultValue": "",
+ "defaultValue": "b2c_1_reset",
"replaces": "MyResetPasswordPolicyId",
"description": "The reset password policy ID for this project (use with IndividualB2C auth)."
},
"EditProfilePolicyId": {
"type": "parameter",
"datatype": "string",
- "defaultValue": "",
+ "defaultValue": "b2c_1_edit_profile",
"replaces": "MyEditProfilePolicyId",
"description": "The edit profile policy ID for this project (use with IndividualB2C auth)."
},
@@ -352,6 +405,37 @@
"format": "yyyy"
}
},
+ "CalledApiUrl": {
+ "type": "parameter",
+ "datatype": "string",
+ "replaces": "[WebApiUrl]",
+ "defaultValue" : "https://graph.microsoft.com/beta",
+ "description": "URL of the API to call from the web app. This option only applies if --auth SingleOrg, --auth MultiOrg or --auth IndividualB2C is specified."
+ },
+ "CallsMicrosoftGraph": {
+ "type": "parameter",
+ "datatype": "bool",
+ "defaultValue": "false",
+ "description": "Specifies if the web app calls Microsoft Graph. This option only applies if --auth SingleOrg or --auth MultiOrg is specified."
+ },
+ "CalledApiScopes": {
+ "type": "parameter",
+ "datatype": "string",
+ "replaces" : "user.read",
+ "description": "Scopes to request to call the API from the web app. This option only applies if --auth SingleOrg, --auth MultiOrg or --auth IndividualB2C is specified."
+ },
+ "GenerateApi": {
+ "type": "computed",
+ "value": "((IndividualB2CAuth || OrganizationalAuth) && (CalledApiUrl != \"https://graph.microsoft.com/beta\" || CalledApiScopes != \"user.read\"))"
+ },
+ "GenerateGraph": {
+ "type": "computed",
+ "value": "(OrganizationalAuth && CallsMicrosoftGraph)"
+ },
+ "GenerateApiOrGraph": {
+ "type": "computed",
+ "value": "(GenerateApi || GenerateGraph)"
+ },
"skipRestore": {
"type": "parameter",
"datatype": "bool",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/Pages/Shared/_LoginPartial.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/Pages/Shared/_LoginPartial.cshtml
index a4f854aac3..85263d76e2 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/Pages/Shared/_LoginPartial.cshtml
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Areas/Identity/Pages/Shared/_LoginPartial.cshtml
@@ -7,10 +7,10 @@
@if (SignInManager.IsSignedIn(User))
{
- Hello @User.Identity.Name!
+ Hello @User.Identity.Name!
-
@@ -18,10 +18,10 @@
else
{
- Register
+ Register
- Login
+ Login
}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/CallWebApi.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/CallWebApi.razor
new file mode 100644
index 0000000000..858bf6f09f
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/CallWebApi.razor
@@ -0,0 +1,37 @@
+@page "/callwebapi"
+
+@using BlazorServerWeb_CSharp
+@using Microsoft.Identity.Web
+
+@inject IDownstreamWebApi downstreamAPI
+@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
+
+Call an API
+
+This component demonstrates fetching data from a Web API.
+
+@if (apiResult == null)
+{
+ Loading...
+}
+else
+{
+ API Result
+ @apiResult
+}
+
+@code {
+ private string apiResult;
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ apiResult = await downstreamAPI.CallWebApiAsync("me");
+ }
+ catch (Exception ex)
+ {
+ ConsentHandler.HandleException(ex);
+ }
+ }
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/ShowProfile.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/ShowProfile.razor
new file mode 100644
index 0000000000..49e2027a8e
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/ShowProfile.razor
@@ -0,0 +1,84 @@
+@page "/showprofile"
+
+@using Microsoft.Identity.Web
+@using Microsoft.Graph
+@inject Microsoft.Graph.GraphServiceClient GraphServiceClient
+@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler
+
+Me
+
+This component demonstrates fetching data from a service.
+
+@if (user == null)
+{
+ Loading...
+}
+else
+{
+
+
+ | Property |
+ Value |
+
+
+ | Name |
+ @user.DisplayName |
+
+
+ | Photo |
+
+ @{
+ if (photo != null)
+ {
+
+ }
+ else
+ {
+ NO PHOTO
+ Check user profile in Azure Active Directory to add a photo.
+ }
+ }
+ |
+
+
+}
+
+@code {
+ User user;
+ string photo;
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ user = await GraphServiceClient.Me.Request().GetAsync();
+ photo = await GetPhoto();
+ }
+ catch (Exception ex)
+ {
+ ConsentHandler.HandleException(ex);
+ }
+ }
+
+ protected async Task GetPhoto()
+ {
+ string photo;
+
+ try
+ {
+ using (var photoStream = await GraphServiceClient.Me.Photo.Content.Request().GetAsync())
+ {
+ byte[] photoByte = ((System.IO.MemoryStream)photoStream).ToArray();
+ photo = Convert.ToBase64String(photoByte);
+ this.StateHasChanged();
+ }
+
+ }
+ catch (Exception)
+ {
+ photo = null;
+ }
+ return photo;
+ }
+
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Program.cs
index df82adf9a9..0132c0538a 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Program.cs
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Program.cs
@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Threading.Tasks;
-using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Services/DownstreamWebApi.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Services/DownstreamWebApi.cs
new file mode 100644
index 0000000000..fbb0de85f6
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Services/DownstreamWebApi.cs
@@ -0,0 +1,72 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Identity.Web;
+
+namespace BlazorServerWeb_CSharp
+{
+ public interface IDownstreamWebApi
+ {
+ Task CallWebApiAsync(string relativeEndpoint = "", string[] requiredScopes = null);
+ }
+
+ public static class DownstreamWebApiExtensions
+ {
+ public static void AddDownstreamWebApiService(this IServiceCollection services, IConfiguration configuration)
+ {
+ // https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
+ services.AddHttpClient();
+ }
+ }
+
+ public class DownstreamWebApi : IDownstreamWebApi
+ {
+ private readonly ITokenAcquisition _tokenAcquisition;
+
+ private readonly IConfiguration _configuration;
+
+ private readonly HttpClient _httpClient;
+
+ public DownstreamWebApi(
+ ITokenAcquisition tokenAcquisition,
+ IConfiguration configuration,
+ HttpClient httpClient)
+ {
+ _tokenAcquisition = tokenAcquisition;
+ _configuration = configuration;
+ _httpClient = httpClient;
+ }
+
+ ///
+ /// Calls the Web API with the required scopes
+ ///
+ /// [Optional] Scopes required to call the Web API. If
+ /// not specified, uses scopes from the configuration
+ /// Endpoint relative to the CalledApiUrl configuration
+ /// A JSON string representing the result of calling the Web API
+ public async Task CallWebApiAsync(string relativeEndpoint = "", string[] requiredScopes = null)
+ {
+ string[] scopes = requiredScopes ?? _configuration["CalledApi:CalledApiScopes"]?.Split(' ');
+ string apiUrl = (_configuration["CalledApi:CalledApiUrl"] as string)?.TrimEnd('/') + $"/{relativeEndpoint}";
+
+ string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
+ HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, apiUrl);
+ httpRequestMessage.Headers.Add("Authorization", $"bearer {accessToken}");
+
+ string apiResult;
+ var response = await _httpClient.SendAsync(httpRequestMessage);
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ apiResult = await response.Content.ReadAsStringAsync();
+ }
+ else
+ {
+ apiResult = $"Error calling the API '{apiUrl}'";
+ }
+
+ return apiResult;
+ }
+ }
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Services/MicrosoftGraphServiceExtensions.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Services/MicrosoftGraphServiceExtensions.cs
new file mode 100644
index 0000000000..f917fbed58
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Services/MicrosoftGraphServiceExtensions.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Graph;
+using Microsoft.Identity.Web;
+
+namespace BlazorServerWeb_CSharp
+{
+ public static class MicrosoftGraphServiceExtensions
+ {
+ ///
+ /// Adds the Microsoft Graph client as a singleton.
+ ///
+ /// Service collection.
+ /// Initial scopes.
+ /// Base URL for Microsoft graph. This can be
+ /// changed for instance for applications running in national clouds
+ public static IServiceCollection AddMicrosoftGraph(this IServiceCollection services,
+ IEnumerable initialScopes,
+ string graphBaseUrl = "https://graph.microsoft.com/v1.0")
+ {
+ services.AddTokenAcquisition(true);
+ services.AddSingleton(serviceProvider =>
+ {
+ var tokenAquisitionService = serviceProvider.GetService();
+ GraphServiceClient client = string.IsNullOrWhiteSpace(graphBaseUrl) ?
+ new GraphServiceClient(new TokenAcquisitionCredentialProvider(tokenAquisitionService, initialScopes)) :
+ new GraphServiceClient(graphBaseUrl, new TokenAcquisitionCredentialProvider(tokenAquisitionService, initialScopes));
+ return client;
+ });
+ return services;
+ }
+ }
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Services/TokenAcquisitionCredentialProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Services/TokenAcquisitionCredentialProvider.cs
new file mode 100644
index 0000000000..5d6c643ca4
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Services/TokenAcquisitionCredentialProvider.cs
@@ -0,0 +1,27 @@
+using Microsoft.Graph;
+using Microsoft.Identity.Web;
+using System.Collections;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace BlazorServerWeb_CSharp
+{
+ internal class TokenAcquisitionCredentialProvider : IAuthenticationProvider
+ {
+ public TokenAcquisitionCredentialProvider(ITokenAcquisition tokenAcquisition, IEnumerable initialScopes)
+ {
+ _tokenAcquisition = tokenAcquisition;
+ _initialScopes = initialScopes;
+ }
+
+ ITokenAcquisition _tokenAcquisition;
+ IEnumerable _initialScopes;
+
+ public async Task AuthenticateRequestAsync(HttpRequestMessage request)
+ {
+ request.Headers.Add("Authorization",
+ $"Bearer {await _tokenAcquisition.GetAccessTokenForUserAsync(_initialScopes)}");
+ }
+ }
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.IndividualB2CAuth.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.IndividualB2CAuth.razor
index 37159fc5a1..f185b33f6a 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.IndividualB2CAuth.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.IndividualB2CAuth.razor
@@ -1,21 +1,21 @@
-@using Microsoft.AspNetCore.Authentication.AzureADB2C.UI
+@using Microsoft.Identity.Web
@using Microsoft.Extensions.Options
-@inject IOptionsMonitor AzureADB2COptions
+@inject IOptionsMonitor microsoftIdentityOptions
@if (canEditProfile)
{
- Hello, @context.User.Identity.Name!
+ Hello, @context.User.Identity.Name!
}
else
{
Hello, @context.User.Identity.Name!
}
- Log out
+ Log out
- Log in
+ Log in
@@ -24,7 +24,7 @@
protected override void OnInitialized()
{
- var options = AzureADB2COptions.Get(AzureADB2CDefaults.AuthenticationScheme);
+ var options = microsoftIdentityOptions.CurrentValue;
canEditProfile = !string.IsNullOrEmpty(options.EditProfilePolicyId);
}
}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.OrganizationalAuth.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.OrganizationalAuth.razor
index fc4422a056..0fcc5bd960 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.OrganizationalAuth.razor
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/LoginDisplay.OrganizationalAuth.razor
@@ -1,9 +1,9 @@
Hello, @context.User.Identity.Name!
- Log out
+ Log out
- Log in
+ Log in
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.CallsMicrosoftGraph.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.CallsMicrosoftGraph.razor
new file mode 100644
index 0000000000..42fc97ba3e
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.CallsMicrosoftGraph.razor
@@ -0,0 +1,42 @@
+
+
+
+
+@code {
+ private bool collapseNavMenu = true;
+
+ private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
+
+ private void ToggleNavMenu()
+ {
+ collapseNavMenu = !collapseNavMenu;
+ }
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.CallsWebApi.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.CallsWebApi.razor
new file mode 100644
index 0000000000..b98dda3d7b
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.CallsWebApi.razor
@@ -0,0 +1,42 @@
+
+
+
+
+@code {
+ private bool collapseNavMenu = true;
+
+ private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
+
+ private void ToggleNavMenu()
+ {
+ collapseNavMenu = !collapseNavMenu;
+ }
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.NoGraphOrApi.razor
similarity index 100%
rename from src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.razor
rename to src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.NoGraphOrApi.razor
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs
index 03fc2cc98a..9b84922b31 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs
@@ -4,17 +4,16 @@ using System.Linq;
using System.Threading.Tasks;
#if (OrganizationalAuth || IndividualB2CAuth)
using Microsoft.AspNetCore.Authentication;
+using Microsoft.Identity.Web;
+using Microsoft.Identity.Web.UI;
+using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
#endif
#if (OrganizationalAuth)
-using Microsoft.AspNetCore.Authentication.AzureAD.UI;
#if (MultiOrgAuth)
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
#endif
using Microsoft.AspNetCore.Authorization;
#endif
-#if (IndividualB2CAuth)
-using Microsoft.AspNetCore.Authentication.AzureADB2C.UI;
-#endif
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
#if (IndividualLocalAuth)
@@ -42,6 +41,9 @@ using Microsoft.IdentityModel.Tokens;
using BlazorServerWeb_CSharp.Areas.Identity;
#endif
using BlazorServerWeb_CSharp.Data;
+#if (GenerateGraph)
+using Microsoft.Graph;
+#endif
namespace BlazorServerWeb_CSharp
{
@@ -70,59 +72,39 @@ namespace BlazorServerWeb_CSharp
services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores();
#elif (OrganizationalAuth)
-#pragma warning disable CS0618 // Type or member is obsolete
- services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
- .AddAzureAD(options => Configuration.Bind("AzureAd", options));
-#pragma warning restore CS0618 // Type or member is obsolete
-#if (MultiOrgAuth)
-
-#pragma warning disable CS0618 // Type or member is obsolete
- services.Configure(AzureADDefaults.OpenIdScheme, options =>
-#pragma warning restore CS0618 // Type or member is obsolete
- {
- options.TokenValidationParameters = new TokenValidationParameters
- {
- // Instead of using the default validation (validating against a single issuer value, as we do in
- // line of business apps), we inject our own multitenant validation logic
- ValidateIssuer = false,
-
- // If the app is meant to be accessed by entire organizations, add your issuer validation logic here.
- //IssuerValidator = (issuer, securityToken, validationParameters) => {
- // if (myIssuerValidationLogic(issuer)) return issuer;
- //}
- };
-
- options.Events = new OpenIdConnectEvents
- {
- OnTicketReceived = context =>
- {
- // If your authentication logic is based on users then add your logic here
- return Task.CompletedTask;
- },
- OnAuthenticationFailed = context =>
- {
- context.Response.Redirect("/Error");
- context.HandleResponse(); // Suppress the exception
- return Task.CompletedTask;
- },
- // If your application needs to authenticate single users, add your user validation below.
- //OnTokenValidated = context =>
- //{
- // return myUserValidationLogic(context.Ticket.Principal);
- //}
- };
- });
+#if (GenerateApiOrGraph)
+ string[] scopes = Configuration.GetValue("CalledApi:CalledApiScopes")?.Split(' ');
+#endif
+#if (GenerateApiOrGraph)
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAd")
+ .AddMicrosoftWebAppCallsWebApi(Configuration, scopes, "AzureAd")
+ .AddInMemoryTokenCaches();
+#else
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAd");
+#endif
+#if (GenerateApi)
+ services.AddDownstreamWebApiService(Configuration);
+#endif
+#if (GenerateGraph)
+ services.AddMicrosoftGraph(scopes, Configuration.GetValue("CalledApi:CalledApiUrl"));
#endif
-
#elif (IndividualB2CAuth)
-#pragma warning disable CS0618 // Type or member is obsolete
- services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
- .AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));
-#pragma warning restore CS0618 // Type or member is obsolete
-
+#if (GenerateApi)
+ string[] scopes = Configuration.GetValue("CalledApi:CalledApiScopes")?.Split(' ');
#endif
-#if (OrganizationalAuth)
- services.AddControllersWithViews();
+#if (GenerateApi)
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C")
+ .AddMicrosoftWebAppCallsWebApi(Configuration, scopes, "AzureAdB2C")
+ .AddInMemoryTokenCaches();
+
+ services.AddDownstreamWebApiService(Configuration);
+#else
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C");
+#endif
+#endif
+#if (OrganizationalAuth || IndividualB2CAuth)
+ services.AddControllersWithViews()
+ .AddMicrosoftIdentityUI();
services.AddAuthorization(options =>
{
@@ -132,7 +114,12 @@ namespace BlazorServerWeb_CSharp
#endif
services.AddRazorPages();
+#if (OrganizationalAuth || IndividualB2CAuth)
+ services.AddServerSideBlazor()
+ .AddMicrosoftIdentityConsentHandler();
+#else
services.AddServerSideBlazor();
+#endif
#if (IndividualLocalAuth)
services.AddScoped>();
#endif
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/appsettings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/appsettings.json
index 7210a062bf..b2b1a5fa42 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/appsettings.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/appsettings.json
@@ -5,6 +5,12 @@
// "ClientId": "11111111-1111-1111-11111111111111111",
// "CallbackPath": "/signin-oidc",
// "Domain": "qualified.domain.name",
+// "SignedOutCallbackPath": "/signout/MySignUpSignInPolicyId",
+//#if (GenerateApi)
+// "ClientSecret": "secret-from-app-registration",
+// "ClientCertificates" : [
+// ],
+//#endif
// "SignUpSignInPolicyId": "MySignUpSignInPolicyId",
// "ResetPasswordPolicyId": "MyResetPasswordPolicyId",
// "EditProfilePolicyId": "MyEditProfilePolicyId"
@@ -19,18 +25,36 @@
// "TenantId": "22222222-2222-2222-2222-222222222222",
//#endif
// "ClientId": "11111111-1111-1111-11111111111111111",
+//#if (GenerateApiOrGraph)
+// "ClientSecret": "secret-from-app-registration",
+// "ClientCertificates" : [
+// ],
+//#endif
// "CallbackPath": "/signin-oidc"
// },
-//#endif
+////#endif
+////#if (GenerateApiOrGraph)
+// "CalledApi": {
+// /*
+// 'CalledApiScopes' contains space separated scopes of the Web API you want to call. This can be:
+// - a scope for a V2 application (for instance api://b3682cc7-8b30-4bd2-aaba-080c6bf0fd31/access_as_user)
+// - a scope corresponding to a V1 application (for instance /.default, where is the
+// App ID URI of a legacy v1 Web application
+// Applications are registered in the https://portal.azure.com portal.
+// */
+// "CalledApiScopes": "user.read",
+// "CalledApiUrl": "[WebApiUrl]"
+// },
+////#endif
////#if (IndividualLocalAuth)
// "ConnectionStrings": {
-////#if (UseLocalDB)
+//#if (UseLocalDB)
// "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-BlazorServerWeb-CSharp-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true"
-////#else
+//#else
// "DefaultConnection": "DataSource=app.db;Cache=Shared"
//#endif
// },
-//#endif
+////#endif
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/dotnetcli.host.json
index 05caada2d0..f4f07149c8 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/dotnetcli.host.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/dotnetcli.host.json
@@ -77,6 +77,18 @@
"NoHttps": {
"longName": "no-https",
"shortName": ""
+ },
+ "CalledApiUrl": {
+ "longName": "called-api-url",
+ "shortName": ""
+ },
+ "CalledApiScopes": {
+ "longName": "called-api-scopes",
+ "shortName": ""
+ },
+ "CallsMicrosoftGraph": {
+ "longName": "calls-graph",
+ "shortName": ""
}
}
}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/template.json b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/template.json
index 30fec7a40b..74be1e1bff 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/.template.config/template.json
@@ -211,6 +211,19 @@
"Server/Controllers/OidcConfigurationController.cs",
"Server/Models/ApplicationUser.cs"
]
+ },
+ {
+ "condition": "(Hosted && !GenerateApi)",
+ "exclude": [
+ "Server/Services/DownstreamWebApi.cs"
+ ]
+ },
+ {
+ "condition": "(Hosted &&!GenerateGraph)",
+ "exclude": [
+ "Server/Services/MicrosoftGraphServiceExtensions.cs",
+ "Server/Services/TokenAcquisitionCredentialProvider.cs"
+ ]
}
]
}
@@ -297,7 +310,7 @@
"datatype": "string",
"defaultValue": "https://login.microsoftonline.com/",
"replaces": "https:////login.microsoftonline.com/",
- "description": "The Azure Active Directory instance to connect to (use with SingleOrg)."
+ "description": "The Azure Active Directory instance to connect to (use with SingleOrg auth)."
},
"ClientId": {
"type": "parameter",
@@ -452,6 +465,37 @@
"parameters": {
"format": "yyyy"
}
+ },
+ "CalledApiUrl": {
+ "type": "parameter",
+ "datatype": "string",
+ "replaces": "[WebApiUrl]",
+ "defaultValue" : "https://graph.microsoft.com/v1.0/me",
+ "description": "URL of the API to call from the web app. This option only applies if --auth SingleOrg, --auth MultiOrg or --auth IndividualB2C without and ASP.NET Core host is specified."
+ },
+ "CallsMicrosoftGraph": {
+ "type": "parameter",
+ "datatype": "bool",
+ "defaultValue": "false",
+ "description": "Specifies if the web app calls Microsoft Graph. This option only applies if --auth SingleOrg or --auth MultiOrg is specified."
+ },
+ "CalledApiScopes": {
+ "type": "parameter",
+ "datatype": "string",
+ "replaces" : "user.read",
+ "description": "Scopes to request to call the API from the web app. This option only applies if --auth SingleOrg, --auth MultiOrg or --auth IndividualB2C without and ASP.NET Core host is specified."
+ },
+ "GenerateApi": {
+ "type": "computed",
+ "value": "(( (IndividualB2CAuth && !Hosted) || OrganizationalAuth) && (CalledApiUrl != \"https://graph.microsoft.com/v1.0/me\" || CalledApiScopes != \"user.read\"))"
+ },
+ "GenerateGraph": {
+ "type": "computed",
+ "value": "(OrganizationalAuth && CallsMicrosoftGraph)"
+ },
+ "GenerateApiOrGraph": {
+ "type": "computed",
+ "value": "(GenerateApi || GenerateGraph)"
}
},
"tags": {
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Controllers/WeatherForecastController.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Controllers/WeatherForecastController.cs
index 117055576d..e39c9297a7 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Controllers/WeatherForecastController.cs
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Controllers/WeatherForecastController.cs
@@ -1,13 +1,25 @@
-using ComponentsWebAssembly_CSharp.Shared;
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
#if (!NoAuth)
using Microsoft.AspNetCore.Authorization;
#endif
+#if (GenerateApi)
+using Microsoft.Extensions.Configuration;
+using Microsoft.Identity.Web;
+using System.Net;
+using System.Net.Http;
+#endif
+#if (GenerateGraph)
+using Microsoft.Graph;
+#endif
using Microsoft.AspNetCore.Mvc;
+#if (OrganizationalAuth || IndividualB2CAuth)
+using Microsoft.Identity.Web.Resource;
+#endif
using Microsoft.Extensions.Logging;
+using ComponentsWebAssembly_CSharp.Shared;
namespace ComponentsWebAssembly_CSharp.Server.Controllers
{
@@ -23,16 +35,28 @@ namespace ComponentsWebAssembly_CSharp.Server.Controllers
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
- private readonly ILogger logger;
+ private readonly ILogger _logger;
- public WeatherForecastController(ILogger logger)
+ // The Web API will only accept tokens 1) for users, and 2) having the access_as_user scope for this API
+ static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };
+
+#if (GenerateApi)
+ private readonly IDownstreamWebApi _downstreamWebApi;
+
+ public WeatherForecastController(ILogger logger,
+ IDownstreamWebApi downstreamWebApi)
{
- this.logger = logger;
+ _logger = logger;
+ _downstreamWebApi = downstreamWebApi;
}
[HttpGet]
- public IEnumerable Get()
+ public async Task> Get()
{
+ HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
+
+ string downstreamApiResult = await _downstreamWebApi.CallWebApiAsync();
+
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
@@ -42,5 +66,54 @@ namespace ComponentsWebAssembly_CSharp.Server.Controllers
})
.ToArray();
}
+
+#elseif (GenerateGraph)
+ private readonly GraphServiceClient _graphServiceClient;
+
+ public WeatherForecastController(ILogger logger,
+ GraphServiceClient graphServiceClient)
+ {
+ _logger = logger;
+ _graphServiceClient = graphServiceClient;
+ }
+
+ [HttpGet]
+ public async Task> Get()
+ {
+ HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
+ var user = await _graphServiceClient.Me.Request().GetAsync();
+
+ var rng = new Random();
+ return Enumerable.Range(1, 5).Select(index => new WeatherForecast
+ {
+ Date = DateTime.Now.AddDays(index),
+ TemperatureC = rng.Next(-20, 55),
+ Summary = Summaries[rng.Next(Summaries.Length)]
+ })
+ .ToArray();
+ }
+#else
+ public WeatherForecastController(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ [HttpGet]
+ public IEnumerable Get()
+ {
+#if (OrganizationalAuth || IndividualB2CAuth)
+ HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
+
+#endif
+ var rng = new Random();
+ return Enumerable.Range(1, 5).Select(index => new WeatherForecast
+ {
+ Date = DateTime.Now.AddDays(index),
+ TemperatureC = rng.Next(-20, 55),
+ Summary = Summaries[rng.Next(Summaries.Length)]
+ })
+ .ToArray();
+ }
+#endif
}
}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Services/DownstreamWebApi.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Services/DownstreamWebApi.cs
new file mode 100644
index 0000000000..0c7d0fcb96
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Services/DownstreamWebApi.cs
@@ -0,0 +1,72 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Identity.Web;
+
+namespace ComponentsWebAssembly_CSharp.Server
+{
+ public interface IDownstreamWebApi
+ {
+ Task CallWebApiAsync(string relativeEndpoint = "", string[] requiredScopes = null);
+ }
+
+ public static class DownstreamWebApiExtensions
+ {
+ public static void AddDownstreamWebApiService(this IServiceCollection services, IConfiguration configuration)
+ {
+ // https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
+ services.AddHttpClient();
+ }
+ }
+
+ public class DownstreamWebApi : IDownstreamWebApi
+ {
+ private readonly ITokenAcquisition _tokenAcquisition;
+
+ private readonly IConfiguration _configuration;
+
+ private readonly HttpClient _httpClient;
+
+ public DownstreamWebApi(
+ ITokenAcquisition tokenAcquisition,
+ IConfiguration configuration,
+ HttpClient httpClient)
+ {
+ _tokenAcquisition = tokenAcquisition;
+ _configuration = configuration;
+ _httpClient = httpClient;
+ }
+
+ ///
+ /// Calls the Web API with the required scopes
+ ///
+ /// [Optional] Scopes required to call the Web API. If
+ /// not specified, uses scopes from the configuration
+ /// Endpoint relative to the CalledApiUrl configuration
+ /// A JSON string representing the result of calling the Web API
+ public async Task CallWebApiAsync(string relativeEndpoint = "", string[] requiredScopes = null)
+ {
+ string[] scopes = requiredScopes ?? _configuration["CalledApi:CalledApiScopes"]?.Split(' ');
+ string apiUrl = (_configuration["CalledApi:CalledApiUrl"] as string)?.TrimEnd('/') + $"/{relativeEndpoint}";
+
+ string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
+ HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, apiUrl);
+ httpRequestMessage.Headers.Add("Authorization", $"bearer {accessToken}");
+
+ string apiResult;
+ var response = await _httpClient.SendAsync(httpRequestMessage);
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ apiResult = await response.Content.ReadAsStringAsync();
+ }
+ else
+ {
+ apiResult = $"Error calling the API '{apiUrl}'";
+ }
+
+ return apiResult;
+ }
+ }
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Services/MicrosoftGraphServiceExtensions.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Services/MicrosoftGraphServiceExtensions.cs
new file mode 100644
index 0000000000..6702cc3371
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Services/MicrosoftGraphServiceExtensions.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Graph;
+using Microsoft.Identity.Web;
+
+namespace ComponentsWebAssembly_CSharp.Server
+{
+ public static class MicrosoftGraphServiceExtensions
+ {
+ ///
+ /// Adds the Microsoft Graph client as a singleton.
+ ///
+ /// Service collection.
+ /// Initial scopes.
+ /// Base URL for Microsoft graph. This can be
+ /// changed for instance for applications running in national clouds
+ public static IServiceCollection AddMicrosoftGraph(this IServiceCollection services,
+ IEnumerable initialScopes,
+ string graphBaseUrl = "https://graph.microsoft.com/v1.0")
+ {
+ services.AddTokenAcquisition(true);
+ services.AddSingleton(serviceProvider =>
+ {
+ var tokenAquisitionService = serviceProvider.GetService();
+ GraphServiceClient client = string.IsNullOrWhiteSpace(graphBaseUrl) ?
+ new GraphServiceClient(new TokenAcquisitionCredentialProvider(tokenAquisitionService, initialScopes)) :
+ new GraphServiceClient(graphBaseUrl, new TokenAcquisitionCredentialProvider(tokenAquisitionService, initialScopes));
+ return client;
+ });
+ return services;
+ }
+ }
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Services/TokenAcquisitionCredentialProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Services/TokenAcquisitionCredentialProvider.cs
new file mode 100644
index 0000000000..a6cc2b080e
--- /dev/null
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Services/TokenAcquisitionCredentialProvider.cs
@@ -0,0 +1,27 @@
+using Microsoft.Graph;
+using Microsoft.Identity.Web;
+using System.Collections;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace ComponentsWebAssembly_CSharp.Server
+{
+ internal class TokenAcquisitionCredentialProvider : IAuthenticationProvider
+ {
+ public TokenAcquisitionCredentialProvider(ITokenAcquisition tokenAcquisition, IEnumerable initialScopes)
+ {
+ _tokenAcquisition = tokenAcquisition;
+ _initialScopes = initialScopes;
+ }
+
+ ITokenAcquisition _tokenAcquisition;
+ IEnumerable _initialScopes;
+
+ public async Task AuthenticateRequestAsync(HttpRequestMessage request)
+ {
+ request.Headers.Add("Authorization",
+ $"Bearer {await _tokenAcquisition.GetAccessTokenForUserAsync(_initialScopes)}");
+ }
+ }
+}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Startup.cs
index e52120ff08..5899ece40e 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Startup.cs
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/Startup.cs
@@ -1,17 +1,10 @@
#if (OrganizationalAuth || IndividualB2CAuth || IndividualLocalAuth)
using Microsoft.AspNetCore.Authentication;
#endif
-#if (OrganizationalAuth)
-using Microsoft.AspNetCore.Authentication.AzureAD.UI;
-#endif
-#if (IndividualB2CAuth)
-using Microsoft.AspNetCore.Authentication.AzureADB2C.UI;
-#endif
using Microsoft.AspNetCore.Builder;
-#if (IndividualLocalAuth)
-using Microsoft.AspNetCore.Components.Authorization;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.AspNetCore.Identity.UI;
+#if (OrganizationalAuth || IndividualB2CAuth)
+using Microsoft.Identity.Web;
+using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
#endif
#if (RequiresHttps)
using Microsoft.AspNetCore.HttpsPolicy;
@@ -29,6 +22,9 @@ using System.Linq;
using ComponentsWebAssembly_CSharp.Server.Data;
using ComponentsWebAssembly_CSharp.Server.Models;
#endif
+#if (GenerateGraph)
+using Microsoft.Graph;
+#endif
namespace ComponentsWebAssembly_CSharp.Server
{
@@ -47,13 +43,13 @@ namespace ComponentsWebAssembly_CSharp.Server
{
#if (IndividualLocalAuth)
services.AddDbContext(options =>
- #if (UseLocalDB)
+#if (UseLocalDB)
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
- #else
+#else
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")));
- #endif
+#endif
services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores();
@@ -65,15 +61,32 @@ namespace ComponentsWebAssembly_CSharp.Server
.AddIdentityServerJwt();
#endif
#if (OrganizationalAuth)
-#pragma warning disable CS0618 // Type or member is obsolete
- services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
- .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
-#pragma warning restore CS0618 // Type or member is obsolete
+#if (GenerateApiOrGraph)
+ // Adds Microsoft Identity platform (AAD v2.0) support to protect this Api
+ services.AddMicrosoftWebApiAuthentication(Configuration, "AzureAd")
+ .AddMicrosoftWebApiCallsWebApi(Configuration, "AzureAd")
+ .AddInMemoryTokenCaches();
+#else
+ // Adds Microsoft Identity platform (AAD v2.0) support to protect this Api
+ services.AddMicrosoftWebApiAuthentication(Configuration, "AzureAd");
+#endif
+#if (GenerateApi)
+ services.AddDownstreamWebApiService(Configuration);
+#endif
+#if (GenerateGraph)
+ services.AddMicrosoftGraph(Configuration.GetValue("CalledApi:CalledApiScopes")?.Split(' '),
+ Configuration.GetValue("CalledApi:CalledApiUrl"));
+#endif
#elif (IndividualB2CAuth)
-#pragma warning disable CS0618 // Type or member is obsolete
- services.AddAuthentication(AzureADB2CDefaults.BearerAuthenticationScheme)
- .AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
-#pragma warning restore CS0618 // Type or member is obsolete
+#if (GenerateApi)
+ services.AddMicrosoftWebApiAuthentication(Configuration, "AzureAdB2C")
+ .AddMicrosoftWebApiCallsWebApi(Configuration, "AzureAdB2C")
+ .AddInMemoryTokenCaches();
+
+ services.AddDownstreamWebApiService(Configuration);
+#else
+ services.AddMicrosoftWebApiAuthentication(Configuration, "AzureAdB2C");
+#endif
#endif
services.AddControllersWithViews();
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/appsettings.json b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/appsettings.json
index fe7926d973..da1c94c1b9 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/appsettings.json
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Server/appsettings.json
@@ -12,23 +12,51 @@
// "Instance": "https:////aadB2CInstance.b2clogin.com/",
// "ClientId": "11111111-1111-1111-11111111111111111",
// "Domain": "qualified.domain.name",
+//#if (GenerateApi)
+// "ClientSecret": "secret-from-app-registration",
+// "ClientCertificates" : [
+// ],
+//#endif
// "SignUpSignInPolicyId": "MySignUpSignInPolicyId"
// },
////#elseif (OrganizationalAuth)
// "AzureAd": {
+//#if (!SingleOrgAuth)
+// "Instance": "https:////login.microsoftonline.com/common",
+//#else
// "Instance": "https:////login.microsoftonline.com/",
// "Domain": "qualified.domain.name",
// "TenantId": "22222222-2222-2222-2222-222222222222",
+//#endif
// "ClientId": "11111111-1111-1111-11111111111111111",
+//#if (GenerateApiOrGraph)
+// "ClientSecret": "secret-from-app-registration",
+// "ClientCertificates" : [
+// ],
+//#endif
+// "CallbackPath": "/signin-oidc"
+// },
+////#endif
+////#if (GenerateApiOrGraph)
+// "CalledApi": {
+// /*
+// 'CalledApiScopes' contains space separated scopes of the Web API you want to call. This can be:
+// - a scope for a V2 application (for instance api://b3682cc7-8b30-4bd2-aaba-080c6bf0fd31/access_as_user)
+// - a scope corresponding to a V1 application (for instance /.default, where is the
+// App ID URI of a legacy v1 Web application
+// Applications are registered in the https://portal.azure.com portal.
+// */
+// "CalledApiScopes": "user.read",
+// "CalledApiUrl": "[WebApiUrl]"
// },
////#endif
"Logging": {
- "LogLevel": {
+ "LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
- }
- },
+ }
+ },
////#if (IndividualLocalAuth)
// "IdentityServer": {
// "Clients": {
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs
index 242f93d1ee..d71d362b79 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorPagesWeb-CSharp/Startup.cs
@@ -71,13 +71,12 @@ namespace Company.WebApplication1
services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores();
#elif (OrganizationalAuth)
- services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAd")
#if (GenerateApiOrGraph)
- .AddMicrosoftWebAppCallsWebApi(Configuration,
- "AzureAd")
- .AddInMemoryTokenCaches();
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAd")
+ .AddMicrosoftWebAppCallsWebApi(Configuration, "AzureAd")
+ .AddInMemoryTokenCaches();
#else
- ;
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAd");
#endif
#if (GenerateApi)
services.AddDownstreamWebApiService(Configuration);
@@ -87,15 +86,14 @@ namespace Company.WebApplication1
Configuration.GetValue("CalledApi:CalledApiUrl"));
#endif
#elif (IndividualB2CAuth)
- services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C")
#if (GenerateApi)
- .AddMicrosoftWebAppCallsWebApi(Configuration,
- "AzureAdB2C")
- .AddInMemoryTokenCaches();
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C")
+ .AddMicrosoftWebAppCallsWebApi(Configuration, "AzureAdB2C")
+ .AddInMemoryTokenCaches();
services.AddDownstreamWebApiService(Configuration);
#else
- ;
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C");
#endif
#endif
#if (OrganizationalAuth)
@@ -106,11 +104,11 @@ namespace Company.WebApplication1
options.FallbackPolicy = options.DefaultPolicy;
});
services.AddRazorPages()
- .AddMvcOptions(options => {})
- .AddMicrosoftIdentityUI();
+ .AddMvcOptions(options => {})
+ .AddMicrosoftIdentityUI();
#elif (IndividualB2CAuth)
services.AddRazorPages()
- .AddMicrosoftIdentityUI();
+ .AddMicrosoftIdentityUI();
#else
services.AddRazorPages();
#endif
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs
index 72af4c5e51..dc4a413e8d 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Startup.cs
@@ -71,13 +71,12 @@ namespace Company.WebApplication1
services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores();
#elif (OrganizationalAuth)
- services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAd")
#if (GenerateApiOrGraph)
- .AddMicrosoftWebAppCallsWebApi(Configuration,
- "AzureAd")
- .AddInMemoryTokenCaches();
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAd")
+ .AddMicrosoftWebAppCallsWebApi(Configuration, "AzureAd")
+ .AddInMemoryTokenCaches();
#else
- ;
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAd");
#endif
#if (GenerateApi)
services.AddDownstreamWebApiService(Configuration);
@@ -87,15 +86,14 @@ namespace Company.WebApplication1
Configuration.GetValue("CalledApi:CalledApiUrl"));
#endif
#elif (IndividualB2CAuth)
- services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C")
#if (GenerateApi)
- .AddMicrosoftWebAppCallsWebApi(Configuration,
- "AzureAdB2C")
- .AddInMemoryTokenCaches();
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C")
+ .AddMicrosoftWebAppCallsWebApi(Configuration, "AzureAdB2C")
+ .AddInMemoryTokenCaches();
services.AddDownstreamWebApiService(Configuration);
#else
- ;
+ services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C");
#endif
#endif
#if (OrganizationalAuth)
@@ -112,7 +110,7 @@ namespace Company.WebApplication1
#endif
#if (OrganizationalAuth || IndividualB2CAuth)
services.AddRazorPages()
- .AddMicrosoftIdentityUI();
+ .AddMicrosoftIdentityUI();
#endif
}
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs
index a67f026b98..71568e5da0 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/WebApi-CSharp/Startup.cs
@@ -41,15 +41,15 @@ namespace Company.WebApplication1
public void ConfigureServices(IServiceCollection services)
{
#if (OrganizationalAuth)
+#if (GenerateApiOrGraph)
// Adds Microsoft Identity platform (AAD v2.0) support to protect this Api
services.AddMicrosoftWebApiAuthentication(Configuration, "AzureAd")
-#if (GenerateApiOrGraph)
- .AddMicrosoftWebApiCallsWebApi(Configuration,
- "AzureAd")
- .AddInMemoryTokenCaches();
+ .AddMicrosoftWebApiCallsWebApi(Configuration, "AzureAd")
+ .AddInMemoryTokenCaches();
#else
- ;
+ // Adds Microsoft Identity platform (AAD v2.0) support to protect this Api
+ services.AddMicrosoftWebApiAuthentication(Configuration, "AzureAd");
#endif
#if (GenerateApi)
services.AddDownstreamWebApiService(Configuration);
@@ -59,15 +59,14 @@ namespace Company.WebApplication1
Configuration.GetValue("CalledApi:CalledApiUrl"));
#endif
#elif (IndividualB2CAuth)
- services.AddMicrosoftWebApiAuthentication(Configuration, "AzureAdB2C")
#if (GenerateApi)
- .AddMicrosoftWebApiCallsWebApi(Configuration,
- "AzureAdB2C")
- .AddInMemoryTokenCaches();
+ services.AddMicrosoftWebApiAuthentication(Configuration, "AzureAdB2C")
+ .AddMicrosoftWebApiCallsWebApi(Configuration, "AzureAdB2C")
+ .AddInMemoryTokenCaches();
services.AddDownstreamWebApiService(Configuration);
#else
- ;
+ services.AddMicrosoftWebApiAuthentication(Configuration, "AzureAdB2C");
#endif
#endif