diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 6a2e61c56114..a22a67342820 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -305,9 +305,9 @@ https://github.com/dotnet/runtime 0e0e648770e54b12c2fa81a77538ce1a72fca8af - + https://github.com/dotnet/arcade - ff5d4b6c8dbdaeacb6e6159d3f8185118dffd915 + 22d6355c4f3c9ac00b0e3abf9d85f2fb07e4787b https://github.com/dotnet/arcade diff --git a/eng/Versions.props b/eng/Versions.props index 5c29d43ef54f..180dc2aee61b 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -256,8 +256,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/global.json b/global.json index 492d16af5d8d..6496dffe47d9 100644 --- a/global.json +++ b/global.json @@ -30,7 +30,7 @@ }, "msbuild-sdks": { "Yarn.MSBuild": "1.15.2", - "Microsoft.DotNet.Arcade.Sdk": "5.0.0-beta.20364.3", + "Microsoft.DotNet.Arcade.Sdk": "5.0.0-beta.20377.2", "Microsoft.DotNet.Helix.Sdk": "5.0.0-beta.20364.3" } } diff --git a/src/ProjectTemplates/BlazorTemplates.Tests/BlazorServerTemplateTest.cs b/src/ProjectTemplates/BlazorTemplates.Tests/BlazorServerTemplateTest.cs index 426647d481a4..7a47f9b84acf 100644 --- a/src/ProjectTemplates/BlazorTemplates.Tests/BlazorServerTemplateTest.cs +++ b/src/ProjectTemplates/BlazorTemplates.Tests/BlazorServerTemplateTest.cs @@ -30,6 +30,9 @@ public BlazorServerTemplateTest(ProjectFactoryFixture projectFactory, BrowserFix [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/20172")] public async Task BlazorServerTemplateWorks_NoAuth() { + // Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278 + Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true"); + Project = await ProjectFactory.GetOrCreateProject("blazorservernoauth", Output); var createResult = await Project.RunDotNetNewAsync("blazorserver"); @@ -88,6 +91,9 @@ public async Task BlazorServerTemplateWorks_NoAuth() [QuarantinedTest] public async Task BlazorServerTemplateWorks_IndividualAuth(bool useLocalDB) { + // Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278 + Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true"); + Project = await ProjectFactory.GetOrCreateProject("blazorserverindividual" + (useLocalDB ? "uld" : ""), Output); var createResult = await Project.RunDotNetNewAsync("blazorserver", auth: "Individual", useLocalDB: useLocalDB); @@ -182,5 +188,30 @@ private void TestBasicNavigation() 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 1a72d92d54ff..c087fc3d0748 100644 --- a/src/ProjectTemplates/BlazorTemplates.Tests/BlazorWasmTemplateTest.cs +++ b/src/ProjectTemplates/BlazorTemplates.Tests/BlazorWasmTemplateTest.cs @@ -43,6 +43,9 @@ public override Task InitializeAsync() [Fact] public async Task BlazorWasmStandaloneTemplate_Works() { + // Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278 + Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true"); + var project = await ProjectFactory.GetOrCreateProject("blazorstandalone", Output); project.RuntimeIdentifier = "browser-wasm"; @@ -81,6 +84,9 @@ public async Task BlazorWasmStandaloneTemplate_Works() [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/20172")] public async Task BlazorWasmHostedTemplate_Works() { + // Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278 + Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true"); + var project = await ProjectFactory.GetOrCreateProject("blazorhosted", Output); var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--hosted" }); @@ -135,6 +141,9 @@ private static async Task AssertCompressionFormat(AspNetProcess aspNetProcess, s [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23992")] public async Task BlazorWasmStandalonePwaTemplate_Works() { + // Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278 + Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true"); + var project = await ProjectFactory.GetOrCreateProject("blazorstandalonepwa", Output); project.RuntimeIdentifier = "browser-wasm"; @@ -174,6 +183,9 @@ public async Task BlazorWasmStandalonePwaTemplate_Works() [Fact] public async Task BlazorWasmHostedPwaTemplate_Works() { + // Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278 + Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true"); + var project = await ProjectFactory.GetOrCreateProject("blazorhostedpwa", Output); var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--hosted", "--pwa" }); @@ -269,6 +281,9 @@ public Task BlazorWasmHostedTemplate_IndividualAuth_Works_WithOutLocalDB() private async Task BlazorWasmHostedTemplate_IndividualAuth_Works(bool useLocalDb) { + // Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278 + Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true"); + var project = await ProjectFactory.GetOrCreateProject("blazorhostedindividual" + (useLocalDb ? "uld" : ""), Output); var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--hosted", "-au", "Individual", useLocalDb ? "-uld" : "" }); @@ -336,6 +351,9 @@ private async Task BlazorWasmHostedTemplate_IndividualAuth_Works(bool useLocalDb [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23639")] public async Task BlazorWasmStandaloneTemplate_IndividualAuth_Works() { + // Additional arguments are needed. See: https://github.com/dotnet/aspnetcore/issues/24278 + Environment.SetEnvironmentVariable("EnableDefaultScopedCssItems", "true"); + var project = await ProjectFactory.GetOrCreateProject("blazorstandaloneindividual", Output); project.RuntimeIdentifier = "browser-wasm"; @@ -403,6 +421,27 @@ public async Task BlazorWasmStandaloneTemplate_IndividualAuth_Works() "--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 868f60d4af11..c47e9753cd6d 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 1c350c70aae5..20efa3e9e2ab 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 247382756857..3a5e6c90a856 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 484e81863cd8..463e0eb7ba2b 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 a4f854aac31f..85263d76e2e4 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)) { @@ -18,10 +18,10 @@ else { } 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 000000000000..858bf6f09f6a --- /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 000000000000..49e2027a8e73 --- /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 +{ + + + + + + + + + + + + + +
PropertyValue
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/Pages/_Host.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml index 767bcb005f26..f32e81bd404b 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml @@ -14,11 +14,10 @@ + - - - +
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Program.cs index df82adf9a960..0132c0538aa3 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 000000000000..fbb0de85f661 --- /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 000000000000..f917fbed586f --- /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 000000000000..5d6c643ca455 --- /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 37159fc5a186..f185b33f6a79 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 fc4422a0565e..0fcc5bd960fa 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/MainLayout.Auth.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.Auth.razor index fafa2f55f105..a6763b9aaafe 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.Auth.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.Auth.razor @@ -1,16 +1,18 @@ @inherits LayoutComponentBase - - -
-
- - About +
+ -
- @Body +
+
+ + About +
+ +
+ @Body +
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.NoAuth.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.NoAuth.razor index 74820a0b75cf..2281c4f7834a 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.NoAuth.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.NoAuth.razor @@ -1,15 +1,17 @@ @inherits LayoutComponentBase - - -
-
- About +
+ -
- @Body +
+
+ About +
+ +
+ @Body +
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.razor.css b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.razor.css new file mode 100644 index 000000000000..61a993539a38 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/MainLayout.razor.css @@ -0,0 +1,70 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +.main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + } + + .top-row a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 767.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } + + .top-row a, .top-row .btn-link { + margin-left: 0; + } +} + +@media (min-width: 768px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .main > div { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} 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 000000000000..42fc97ba3eda --- /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 000000000000..b98dda3d7b21 --- /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/Shared/NavMenu.razor.css b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.razor.css new file mode 100644 index 000000000000..622671ec4c87 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Shared/NavMenu.razor.css @@ -0,0 +1,62 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.oi { + width: 2rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.25); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +@media (min-width: 768px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Startup.cs index 03fc2cc98a65..9b84922b3143 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.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 BlazorServerWeb_CSharp.Areas.Identity; #endif using BlazorServerWeb_CSharp.Data; +#if (GenerateGraph) +using Microsoft.Graph; +#endif namespace BlazorServerWeb_CSharp { @@ -70,59 +72,39 @@ public void ConfigureServices(IServiceCollection services) 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 (GenerateApi) + services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C") + .AddMicrosoftWebAppCallsWebApi(Configuration, scopes, "AzureAdB2C") + .AddInMemoryTokenCaches(); + services.AddDownstreamWebApiService(Configuration); +#else + services.AddMicrosoftWebAppAuthentication(Configuration, "AzureAdB2C"); #endif -#if (OrganizationalAuth) - services.AddControllersWithViews(); +#endif +#if (OrganizationalAuth || IndividualB2CAuth) + services.AddControllersWithViews() + .AddMicrosoftIdentityUI(); services.AddAuthorization(options => { @@ -132,7 +114,12 @@ public void ConfigureServices(IServiceCollection services) #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 7210a062bf06..b2b1a5fa42d9 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/BlazorServerWeb-CSharp/wwwroot/css/site.css b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/wwwroot/css/site.css index dd1b08bd8598..caebf2a4630d 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/wwwroot/css/site.css +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/wwwroot/css/site.css @@ -14,98 +14,10 @@ a, .btn-link { border-color: #1861ac; } -app { - position: relative; - display: flex; - flex-direction: column; -} - -.top-row { - height: 3.5rem; - display: flex; - align-items: center; - z-index: 10; -} - -.main { - flex: 1; -} - - .main .top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - } - - .main .top-row > a, .main .top-row .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - } - -.main .top-row a:first-child { - overflow: hidden; - text-overflow: ellipsis; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - - .sidebar .top-row { - background-color: rgba(0,0,0,0.4); - } - - .sidebar .navbar-brand { - font-size: 1.1rem; - } - - .sidebar .oi { - width: 2rem; - font-size: 1.1rem; - vertical-align: text-top; - top: -2px; - } - - .sidebar .nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; - } - - .sidebar .nav-item:first-of-type { - padding-top: 1rem; - } - - .sidebar .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .sidebar .nav-item a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - } - - .sidebar .nav-item a.active { - background-color: rgba(255,255,255,0.25); - color: white; - } - - .sidebar .nav-item a:hover { - background-color: rgba(255,255,255,0.1); - color: white; - } - .content { padding-top: 1.1rem; } -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); -} - .valid.modified:not([type=checkbox]) { outline: 1px solid #26b050; } @@ -136,50 +48,3 @@ app { right: 0.75rem; top: 0.5rem; } - -@media (max-width: 767.98px) { - .main .top-row:not(.auth) { - display: none; - } - - .main .top-row.auth { - justify-content: space-between; - } - - .main .top-row a, .main .top-row .btn-link { - margin-left: 0; - } -} - -@media (min-width: 768px) { - app { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .main .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .main > div { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } - - .navbar-toggler { - display: none; - } - - .sidebar .collapse { - /* Never collapse the sidebar for wide screens */ - display: block; - } -} 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 05caada2d06b..f4f07149c87a 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 30fec7a40b01..74be1e1bff6b 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/Client/Program.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs index 02dac7f0d3ee..8d71e2bf47f9 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs @@ -22,7 +22,7 @@ public class Program public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); - builder.RootComponents.Add("app"); + builder.RootComponents.Add("#app"); #if (!Hosted || NoAuth) builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.Auth.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.Auth.razor index fafa2f55f105..a6763b9aaafe 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.Auth.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.Auth.razor @@ -1,16 +1,18 @@ @inherits LayoutComponentBase - - -
-
- - About +
+ -
- @Body +
+
+ + About +
+ +
+ @Body +
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.NoAuth.razor b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.NoAuth.razor index 0f4e22a9434d..b416cb95698c 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.NoAuth.razor +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.NoAuth.razor @@ -1,15 +1,17 @@ @inherits LayoutComponentBase - - -
-
- About +
+ -
- @Body +
+
+ About +
+ +
+ @Body +
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.razor.css b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.razor.css new file mode 100644 index 000000000000..61a993539a38 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/MainLayout.razor.css @@ -0,0 +1,70 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +.main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + } + + .top-row a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 767.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } + + .top-row a, .top-row .btn-link { + margin-left: 0; + } +} + +@media (min-width: 768px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .main > div { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/NavMenu.razor.css b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/NavMenu.razor.css new file mode 100644 index 000000000000..622671ec4c87 --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Shared/NavMenu.razor.css @@ -0,0 +1,62 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.oi { + width: 2rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.25); + color: white; +} + +.nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +@media (min-width: 768px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/wwwroot/css/app.css b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/wwwroot/css/app.css index 76b9667ff54a..82fc22a39385 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/wwwroot/css/app.css +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/wwwroot/css/app.css @@ -14,98 +14,10 @@ a, .btn-link { border-color: #1861ac; } -app { - position: relative; - display: flex; - flex-direction: column; -} - -.top-row { - height: 3.5rem; - display: flex; - align-items: center; - z-index: 10; -} - -.main { - flex: 1; -} - - .main .top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - } - - .main .top-row > a, .main .top-row .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - } - -.main .top-row a:first-child { - overflow: hidden; - text-overflow: ellipsis; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - - .sidebar .top-row { - background-color: rgba(0,0,0,0.4); - } - - .sidebar .navbar-brand { - font-size: 1.1rem; - } - - .sidebar .oi { - width: 2rem; - font-size: 1.1rem; - vertical-align: text-top; - top: -2px; - } - - .sidebar .nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; - } - - .sidebar .nav-item:first-of-type { - padding-top: 1rem; - } - - .sidebar .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .sidebar .nav-item a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - } - - .sidebar .nav-item a.active { - background-color: rgba(255,255,255,0.25); - color: white; - } - - .sidebar .nav-item a:hover { - background-color: rgba(255,255,255,0.1); - color: white; - } - .content { padding-top: 1.1rem; } -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); -} - .valid.modified:not([type=checkbox]) { outline: 1px solid #26b050; } @@ -130,56 +42,9 @@ app { z-index: 1000; } -#blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; -} - -@media (max-width: 767.98px) { - .main .top-row:not(.auth) { - display: none; - } - - .main .top-row.auth { - justify-content: space-between; - } - - .main .top-row a, .main .top-row .btn-link { - margin-left: 0; - } -} - -@media (min-width: 768px) { - app { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .main .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .main > div { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } - - .navbar-toggler { - display: none; + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; } - - .sidebar .collapse { - /* Never collapse the sidebar for wide screens */ - display: block; - } -} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/wwwroot/index.html b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/wwwroot/index.html index 7e39788a581e..d29be69affc5 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/wwwroot/index.html +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/wwwroot/index.html @@ -8,6 +8,7 @@ + @@ -15,7 +16,7 @@ - Loading... +
Loading...
An unhandled error has occurred. 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 117055576dec..e39c9297a7aa 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,76 @@ public class WeatherForecastController : ControllerBase "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; - private readonly ILogger logger; + private readonly 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) + { + _logger = logger; + _downstreamWebApi = downstreamWebApi; + } + + [HttpGet] + 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 + { + Date = DateTime.Now.AddDays(index), + TemperatureC = rng.Next(-20, 55), + Summary = Summaries[rng.Next(Summaries.Length)] + }) + .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) { - this.logger = 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 { @@ -42,5 +114,6 @@ public IEnumerable Get() }) .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 000000000000..0c7d0fcb9674 --- /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 000000000000..6702cc33714f --- /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 000000000000..a6cc2b080ec4 --- /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 e52120ff08ce..5899ece40e8a 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 ComponentsWebAssembly_CSharp.Server.Data; using ComponentsWebAssembly_CSharp.Server.Models; #endif +#if (GenerateGraph) +using Microsoft.Graph; +#endif namespace ComponentsWebAssembly_CSharp.Server { @@ -47,13 +43,13 @@ public void ConfigureServices(IServiceCollection services) { #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 @@ public void ConfigureServices(IServiceCollection services) .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 fe7926d97302..da1c94c1b9a7 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 242f93d1ee1b..d71d362b793a 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 @@ public void ConfigureServices(IServiceCollection services) 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 @@ public void ConfigureServices(IServiceCollection services) 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 @@ public void ConfigureServices(IServiceCollection services) 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 72af4c5e5112..dc4a413e8dab 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 @@ public void ConfigureServices(IServiceCollection services) 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 @@ public void ConfigureServices(IServiceCollection services) 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 @@ public void ConfigureServices(IServiceCollection services) #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 a67f026b9881..71568e5da054 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 @@ public Startup(IConfiguration configuration) 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 @@ public void ConfigureServices(IServiceCollection services) 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 diff --git a/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in b/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in index c3e1a9634d09..be883d274c29 100644 --- a/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in +++ b/src/ProjectTemplates/test/Infrastructure/TemplateTests.props.in @@ -2,7 +2,6 @@ true ${RestoreAdditionalProjectSources} - $(MSBuildThisFileDirectory)runtimeconfig.norollforward.json diff --git a/src/ProjectTemplates/test/template-baselines.json b/src/ProjectTemplates/test/template-baselines.json index bf2f88c6a7cc..e77037842c72 100644 --- a/src/ProjectTemplates/test/template-baselines.json +++ b/src/ProjectTemplates/test/template-baselines.json @@ -921,7 +921,9 @@ "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", + "Shared/MainLayout.razor.css", "Shared/NavMenu.razor", + "Shared/NavMenu.razor.css", "Shared/SurveyPrompt.razor", "wwwroot/favicon.ico", "wwwroot/css/site.css", @@ -958,7 +960,9 @@ "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", + "Shared/MainLayout.razor.css", "Shared/NavMenu.razor", + "Shared/NavMenu.razor.css", "Shared/SurveyPrompt.razor", "wwwroot/favicon.ico", "wwwroot/css/site.css", @@ -995,7 +999,9 @@ "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", + "Shared/MainLayout.razor.css", "Shared/NavMenu.razor", + "Shared/NavMenu.razor.css", "Shared/SurveyPrompt.razor", "wwwroot/favicon.ico", "wwwroot/css/site.css", @@ -1031,7 +1037,9 @@ "Pages/_Host.cshtml", "Properties/launchSettings.json", "Shared/MainLayout.razor", + "Shared/MainLayout.razor.css", "Shared/NavMenu.razor", + "Shared/NavMenu.razor.css", "Shared/SurveyPrompt.razor", "wwwroot/favicon.ico", "wwwroot/css/site.css", @@ -1068,7 +1076,9 @@ "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", + "Shared/MainLayout.razor.css", "Shared/NavMenu.razor", + "Shared/NavMenu.razor.css", "Shared/SurveyPrompt.razor", "wwwroot/favicon.ico", "wwwroot/css/site.css", @@ -1105,7 +1115,9 @@ "Properties/launchSettings.json", "Shared/LoginDisplay.razor", "Shared/MainLayout.razor", + "Shared/MainLayout.razor.css", "Shared/NavMenu.razor", + "Shared/NavMenu.razor.css", "Shared/SurveyPrompt.razor", "wwwroot/favicon.ico", "wwwroot/css/site.css", diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.ScopedCss.targets b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.ScopedCss.targets index 5a20c17ad101..379958af8072 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.ScopedCss.targets +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.ScopedCss.targets @@ -58,6 +58,7 @@ Integration with static web assets: <_ScopedCssExtension>.rz.scp.css $(ResolveStaticWebAssetsInputsDependsOn);_CollectAllScopedCssAssets;AddScopedCssBundle $(ResolveCurrentProjectStaticWebAssetsInputsDependsOn);_AddGeneratedScopedCssFiles + $(GetCurrentProjectStaticWebAssetsDependsOn);IncludeScopedCssBundle; @@ -68,7 +69,7 @@ Integration with static web assets: NOTE: This target is called as part of an incremental build scenario in VS. Do not perform any work outside of calculating RazorComponent items in this target. --> - + @@ -207,6 +208,33 @@ Integration with static web assets: + + + <_ScopedCssOutputPath>$(_ScopedCssIntermediatePath)_framework\scoped.styles.css + <_ScopedCssOutputFullPath>$([System.IO.Path]::Combine('$(MSBuildProjectFileDirectory)', '$(_ScopedCssIntermediatePath)_framework\scoped.styles.css')) + + + + + + + + $(PackageId) + $(_ScopedCssIntermediatePath) + $(StaticWebAssetBasePath) + _framework/scoped.styles.css + + <_ExternalStaticWebAsset Include="$(_ScopedCssOutputPath)" Condition="@(_AllScopedCss) != ''"> + generated + $(PackageId) + $(_ScopedCssIntermediatePath) + $(StaticWebAssetBasePath) + _framework/scoped.styles.css + + + +