diff --git a/eng/Versions.props b/eng/Versions.props index 7793b47245..3116176cac 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -245,8 +245,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)) { @@ -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 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 +{ + + + + + + + + + + + + + +
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/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