diff --git a/Templating.sln b/Templating.sln index 6174b8ddd7..8fdd9e5dc6 100644 --- a/Templating.sln +++ b/Templating.sln @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Web.Spa.Pr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Web.Client.ItemTemplates", "src\Microsoft.DotNet.Web.Client.ItemTemplates\Microsoft.DotNet.Web.Client.ItemTemplates.csproj", "{1731F6D9-1DFC-49D6-8F28-471194B1962C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Web.ProjectTemplates.2.1", "src\Microsoft.DotNet.Web.ProjectTemplates.2.1\Microsoft.DotNet.Web.ProjectTemplates.2.1.csproj", "{260EBA09-DEF5-429C-99BF-90CA1456A576}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +61,10 @@ Global {1731F6D9-1DFC-49D6-8F28-471194B1962C}.Debug|Any CPU.Build.0 = Debug|Any CPU {1731F6D9-1DFC-49D6-8F28-471194B1962C}.Release|Any CPU.ActiveCfg = Release|Any CPU {1731F6D9-1DFC-49D6-8F28-471194B1962C}.Release|Any CPU.Build.0 = Release|Any CPU + {260EBA09-DEF5-429C-99BF-90CA1456A576}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {260EBA09-DEF5-429C-99BF-90CA1456A576}.Debug|Any CPU.Build.0 = Debug|Any CPU + {260EBA09-DEF5-429C-99BF-90CA1456A576}.Release|Any CPU.ActiveCfg = Release|Any CPU + {260EBA09-DEF5-429C-99BF-90CA1456A576}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -70,6 +76,7 @@ Global {01E12D5E-8540-4BC8-9A54-41EDD55E762E} = {0AD6E692-E423-408C-B523-DAFB19412E4B} {402E62D1-7FD0-4E07-812C-0E385D98D6D9} = {0AD6E692-E423-408C-B523-DAFB19412E4B} {1731F6D9-1DFC-49D6-8F28-471194B1962C} = {0AD6E692-E423-408C-B523-DAFB19412E4B} + {260EBA09-DEF5-429C-99BF-90CA1456A576} = {0AD6E692-E423-408C-B523-DAFB19412E4B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E9B27B0D-4F85-431B-9C26-80CFE4393D36} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/Microsoft.DotNet.Web.ProjectTemplates.2.1.csproj b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/Microsoft.DotNet.Web.ProjectTemplates.2.1.csproj new file mode 100644 index 0000000000..e59efc12c9 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/Microsoft.DotNet.Web.ProjectTemplates.2.1.csproj @@ -0,0 +1,8 @@ + + + + netcoreapp2.0 + Microsoft.DotNet.Web.ProjectTemplates.2.1.nuspec + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/Microsoft.DotNet.Web.ProjectTemplates.2.1.nuspec b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/Microsoft.DotNet.Web.ProjectTemplates.2.1.nuspec new file mode 100644 index 0000000000..e0bbc9cca1 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/Microsoft.DotNet.Web.ProjectTemplates.2.1.nuspec @@ -0,0 +1,22 @@ + + + + Microsoft.DotNet.Web.ProjectTemplates.2.1 + $version$ + Microsoft + ASP.NET Core Web Template Pack for Microsoft Template Engine + http://go.microsoft.com/fwlink/?LinkID=288859 + en-US + https://github.com/dotnet/templating + https://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm + Copyright © Microsoft Corporation + true + aspnet templates + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..8381117bc2 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "TargetFrameworkOverride": { + "isHidden": "true", + "longName": "target-framework-override", + "shortName": "" + }, + "Framework": { + "longName": "framework" + }, + "skipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "KestrelPort": { + "isHidden": true + }, + "IISExpressPort": { + "isHidden": true + }, + "IncludeLaunchSettings": { + "longName": "use-launch-settings", + "shortName": "" + }, + "RuntimeFrameworkVersion": { + "longName": "runtime-framework-version", + "shortName": "fv", + "isHidden": "true" + } + }, + "usageExamples": [ + "" + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/template.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/template.json new file mode 100644 index 0000000000..3958a03289 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/template.json @@ -0,0 +1,129 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": ["Web", "Empty"], + "name": "ASP.NET Core Empty", + "generatorVersions": "[1.0.0.0-*)", + "description": "An empty project template for creating an ASP.NET Core application. This template does not have any content in it.", + "groupIdentity": "Microsoft.Web.Empty", + "precedence": "3000", + "identity": "Microsoft.Web.Empty.CSharp.2.1", + "shortName": "web", + "thirdPartyNotices": "https://aka.ms/template-3pn", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Company.WebApplication1", + "preferNameDirectory": true, + "guids": [ "53bc9b9d-9d6a-45d4-8429-2a2761773502" ], + "sources": [ + { + "modifiers": [ + { + "condition": "(!IncludeLaunchSettings)", + "exclude": [ + "Properties/launchSettings.json" + ] + }, + ] + } + ], + "symbols": { + "RuntimeFrameworkVersion": { + "type": "parameter", + "replaces": "2.1.0-preview2-25624-02", + "datatype": "string", + "defaultValue": "2.1.0-preview2-25624-02" + }, + "IncludeLaunchSettings": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether to include launchSettings.json in the generated template." + }, + "KestrelPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure Kestrel in launchSettings.json." + }, + "KestrelPortGenerated": { + "type": "generated", + "generator": "port" + }, + "KestrelPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "KestrelPort", + "fallbackVariableName": "KestrelPortGenerated" + }, + "replaces": "5000" + }, + "IISExpressPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure IIS Express in launchSettings.json." + }, + "IISExpressPortGenerated": { + "type": "generated", + "generator": "port" + }, + "IISExpressPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "IISExpressPort", + "fallbackVariableName": "IISExpressPortGenerated" + }, + "replaces": "55556" + }, + "TargetFrameworkOverride": { + "type": "parameter", + "description": "Overrides the target framework", + "replaces": "TargetFrameworkOverride", + "datatype": "string", + "defaultValue": "" + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "netcoreapp2.1", + "description": "Target netcoreapp2.1" + } + ], + "replaces": "netcoreapp2.1", + "defaultValue": "netcoreapp2.1" + }, + "copyrightYear": { + "type": "generated", + "generator": "now", + "replaces": "1975", + "parameters": { + "format": "yyyy" + } + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "If specified, skips the automatic restore of the project on create.", + "defaultValue": "false" + } + }, + "primaryOutputs": [ { "path": "Company.WebApplication1.csproj" } ], + "defaultName": "WebApplication1", + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/vs-2017.3.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/vs-2017.3.host.json new file mode 100644 index 0000000000..3d9b518a3e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/vs-2017.3.host.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/vs-2017.3.host", + "name": { + "text": "Empty", + "package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}", + "id": "1011" + }, + "description": { + "text": "An empty project template for creating an ASP.NET Core application. This template does not have any content in it.", + "package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}", + "id": "1012" + }, + "order": 100, + "icon": "vs-2017.3/Empty.png", + "learnMoreLink": "https://go.microsoft.com/fwlink/?LinkID=784883", + "uiFilters": [ "oneaspnet" ], + "supportsDocker": true, + "legacyTemplateIdentity": "Microsoft.NetCore.CSharp.EmptyWeb", + "ports": [ + { + "name": "IISExpressPort", + "useHttps": false + }, + { + "name": "KestrelPort", + "useHttps": false + } + ], + "includeLaunchSettings": true, + "minFullFrameworkVersion": "4.6.1" +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/vs-2017.3/Empty.png b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/vs-2017.3/Empty.png new file mode 100644 index 0000000000..ea4b7e2492 Binary files /dev/null and b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/.template.config/vs-2017.3/Empty.png differ diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/Company.WebApplication1.csproj b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/Company.WebApplication1.csproj new file mode 100644 index 0000000000..c4eb1f6e64 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/Company.WebApplication1.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.1 + TargetFrameworkOverride + 2.1.0-preview2-25624-02 + 2.1.0-preview2-25624-02 + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/Program.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/Program.cs new file mode 100644 index 0000000000..13019a4fe2 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/Program.cs @@ -0,0 +1,25 @@ +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.Logging; + +namespace Company.WebApplication1 +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/Startup.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/Startup.cs new file mode 100644 index 0000000000..d039a38a68 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/Startup.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Company.WebApplication1 +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.Run(async (context) => + { + await context.Response.WriteAsync("Hello World!"); + }); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/wwwroot/-.- b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-CSharp/wwwroot/-.- new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/.template.config/dotnetcli.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..04017b2a43 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "TargetFrameworkOverride": { + "isHidden": "true", + "longName": "target-framework-override", + "shortName": "" + }, + "Framework": { + "longName": "framework" + }, + "skipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "RuntimeFrameworkVersion": { + "longName": "runtime-framework-version", + "shortName": "fv", + "isHidden": "true" + } + }, + "usageExamples": [ + "" + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/.template.config/template.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/.template.config/template.json new file mode 100644 index 0000000000..9e161cdcf2 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/.template.config/template.json @@ -0,0 +1,73 @@ +{ + "author": "Microsoft", + "classifications": ["Web", "Empty"], + "name": "ASP.NET Core Empty", + "generatorVersions": "[1.0.0.0-*)", + "description": "An empty project template for creating an ASP.NET Core application. This template does not have any content in it.", + "groupIdentity": "Microsoft.Web.Empty", + "precedence": "3000", + "identity": "Microsoft.Web.Empty.FSharp.2.1", + "shortName": "web", + "thirdPartyNotices": "https://aka.ms/template-3pn", + "tags": { + "language": "F#", + "type": "project" + }, + "sourceName": "Company.WebApplication1", + "preferNameDirectory": true, + "symbols": { + "RuntimeFrameworkVersion": { + "type": "parameter", + "replaces": "2.1.0-preview2-25624-02", + "datatype": "string", + "defaultValue": "2.1.0-preview2-25624-02" + }, + "TargetFrameworkOverride": { + "type": "parameter", + "description": "Overrides the target framework", + "replaces": "TargetFrameworkOverride", + "datatype": "string", + "defaultValue": "" + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "netcoreapp2.1", + "description": "Target netcoreapp2.1" + } + ], + "replaces": "netcoreapp2.1", + "defaultValue": "netcoreapp2.1" + }, + "copyrightYear": { + "type": "generated", + "generator": "now", + "replaces": "1975", + "parameters": { + "format": "yyyy" + } + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "If specified, skips the automatic restore of the project on create.", + "defaultValue": "false" + } + }, + "primaryOutputs": [ { "path": "Company.WebApplication1.fsproj" } ], + "defaultName": "WebApplication1", + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/Company.WebApplication1.fsproj b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/Company.WebApplication1.fsproj new file mode 100644 index 0000000000..e93e4f0381 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/Company.WebApplication1.fsproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.1 + TargetFrameworkOverride + 2.1.0-preview2-25624-02 + 2.1.0-preview2-25624-02 + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/Program.fs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/Program.fs new file mode 100644 index 0000000000..6b45d8eb71 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/Program.fs @@ -0,0 +1,26 @@ +namespace Company.WebApplication1 + +open System +open System.Collections.Generic +open System.IO +open System.Linq +open System.Threading.Tasks +open Microsoft.AspNetCore +open Microsoft.AspNetCore.Hosting +open Microsoft.Extensions.Configuration +open Microsoft.Extensions.Logging + +module Program = + let exitCode = 0 + + let BuildWebHost args = + WebHost + .CreateDefaultBuilder(args) + .UseStartup() + .Build() + + [] + let main args = + BuildWebHost(args).Run() + + exitCode diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/Startup.fs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/Startup.fs new file mode 100644 index 0000000000..525a7a2a05 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/EmptyWeb-FSharp/Startup.fs @@ -0,0 +1,19 @@ +namespace Company.WebApplication1 + +open System +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Hosting +open Microsoft.AspNetCore.Http +open Microsoft.Extensions.DependencyInjection + +type Startup() = + + member this.ConfigureServices(services: IServiceCollection) = + () + + member this.Configure(app: IApplicationBuilder, env: IHostingEnvironment) = + if env.IsDevelopment() then app.UseDeveloperExceptionPage() |> ignore + + app.Run(fun context -> context.Response.WriteAsync("Hello World!")) + + () \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.bowerrc b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.bowerrc new file mode 100644 index 0000000000..6406626abf --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "wwwroot/lib" +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..a726809b13 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,92 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "TargetFrameworkOverride": { + "isHidden": "true", + "longName": "target-framework-override", + "shortName": "" + }, + "UseLocalDB": { + "longName": "use-local-db" + }, + "AADInstance": { + "longName": "aad-instance", + "shortName": "" + }, + "AAdB2CInstance": { + "longName": "aad-b2c-instance", + "shortName": "" + }, + "SignUpSignInPolicyId": { + "longName": "susi-policy-id", + "shortName": "ssp" + }, + "ResetPasswordPolicyId": { + "longName": "reset-password-policy-id", + "shortName": "rp" + }, + "EditProfilePolicyId": { + "longName": "edit-profile-policy-id", + "shortName": "ep" + }, + "OrgReadAccess": { + "longName": "org-read-access", + "shortName": "r" + }, + "ClientId": { + "longName": "client-id", + "shortName": "" + }, + "CallbackPath": { + "longName": "callback-path", + "shortName": "" + }, + "Domain": { + "longName": "domain", + "shortName": "" + }, + "TenantId": { + "longName": "tenant-id", + "shortName": "" + }, + "Framework": { + "longName": "framework" + }, + "NoTools": { + "isHidden": "true", + "longName": "no-tools" + }, + "HttpsPort": { + "isHidden": true + }, + "KestrelPort": { + "isHidden": true + }, + "IISExpressPort": { + "isHidden": true + }, + "IncludeLaunchSettings": { + "longName": "use-launch-settings", + "shortName": "" + }, + "UserSecretsId": { + "isHidden": true + }, + "skipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "UseBrowserLink": { + "longName": "use-browserlink", + "shortName": "" + }, + "RuntimeFrameworkVersion": { + "longName": "runtime-framework-version", + "shortName": "fv", + "isHidden": "true" + } + }, + "usageExamples": [ + "--auth Individual" + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/template.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/template.json new file mode 100644 index 0000000000..58a8f79029 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/template.json @@ -0,0 +1,369 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ "Web", "MVC", "Razor Pages" ], + "name": "ASP.NET Core Web App", + "generatorVersions": "[1.0.0.0-*)", + "description": "A project template for creating an ASP.NET Core application with example ASP.NET Razor Pages content", + "groupIdentity": "Microsoft.Web.RazorPages", + "precedence": "3000", + "identity": "Microsoft.Web.RazorPages.CSharp.2.1", + "shortName": "razor", + "thirdPartyNotices": "https://aka.ms/template-3pn", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Company.WebApplication1", + "preferNameDirectory": true, + "guids": [ + "09732173-2cef-46b7-83db-1334bcb079d3", // Tenant ID + "0ce56475-d1db-490f-8af1-a881ea4fcd2d" // Client ID + ], + "sources": [ + { + "modifiers": [ + { + "condition": "(!IndividualAuth && !OrganizationalAuth)", + "exclude": [ + "Controllers/**", + "Pages/Account/**", + "Pages/_LoginPartial.cshtml" + ] + }, + { + "condition": "(!IndividualLocalAuth || UseLocalDB)", + "exclude": [ "app.db" ] + }, + { + "condition": "(!IndividualLocalAuth)", + "exclude": [ + "Pages/Account/ConfirmEmail.cshtml", + "Pages/Account/ConfirmEmail.cshtml.cs", + "Pages/Account/ExternalLogin.cshtml", + "Pages/Account/ExternalLogin.cshtml.cs", + "Pages/Account/ForgotPassword.cshtml", + "Pages/Account/ForgotPassword.cshtml.cs", + "Pages/Account/ForgotPasswordConfirmation.cshtml", + "Pages/Account/ForgotPasswordConfirmation.cshtml.cs", + "Pages/Account/Lockout.cshtml", + "Pages/Account/Lockout.cshtml.cs", + "Pages/Account/Login.cshtml", + "Pages/Account/Login.cshtml.cs", + "Pages/Account/LoginWith2fa.cshtml", + "Pages/Account/LoginWith2fa.cshtml.cs", + "Pages/Account/LoginWithRecoveryCode.cshtml", + "Pages/Account/LoginWithRecoveryCode.cshtml.cs", + "Pages/Account/Register.cshtml", + "Pages/Account/Register.cshtml.cs", + "Pages/Account/ResetPassword.cshtml", + "Pages/Account/ResetPassword.cshtml.cs", + "Pages/Account/ResetPasswordConfirmation.cshtml", + "Pages/Account/ResetPasswordConfirmation.cshtml.cs", + "Pages/Account/Manage/**", + "Extensions/EmailSenderExtensions.cs", + "Extensions/UrlHelperExtensions.cs", + "Services/**", + "Data/**" + ] + }, + { + "condition": "(!IncludeLaunchSettings && !WindowsAuth)", + "exclude": [ + "Properties/launchSettings.json" + ] + }, + { + "condition": "(!OrganizationalAuth)", + "exclude": [ + "Extensions/AzureAdAuthenticationBuilderExtensions.cs", + "Extensions/AzureAdOptions.cs" + ] + }, + { + "condition": "(!IndividualB2CAuth)", + "exclude": [ + "Extensions/AzureAdB2C*.cs" + ] + } + ] + } + ], + "symbols": { + "RuntimeFrameworkVersion": { + "type": "parameter", + "replaces": "2.1.0-preview2-25624-02", + "datatype": "string", + "defaultValue": "2.1.0-preview2-25624-02" + }, + "auth": { + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "None", + "description": "No authentication" + }, + { + "choice": "Individual", + "description": "Individual authentication" + }, + { + "choice": "IndividualB2C", + "description": "Individual authentication with Azure AD B2C" + }, + { + "choice": "SingleOrg", + "description": "Organizational authentication for a single tenant" + }, + { + "choice": "MultiOrg", + "description": "Organizational authentication for multiple tenants" + }, + { + "choice": "Windows", + "description": "Windows authentication" + } + ], + "defaultValue": "None", + "description": "The type of authentication to use" + }, + "AAdB2CInstance": { + "type": "parameter", + "datatype": "string", + "defaultValue": "https://login.microsoftonline.com/tfp/", + "replaces": "https:////login.microsoftonline.com/tfp/", + "description": "The Azure Active Directory B2C instance to connect to (use with IndividualB2C auth)." + }, + "SignUpSignInPolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MySignUpSignInPolicyId", + "description": "The sign-in and sign-up policy ID for this project (use with IndividualB2C auth)." + }, + "ResetPasswordPolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MyResetPasswordPolicyId", + "description": "The reset password policy ID for this project (use with IndividualB2C auth)." + }, + "EditProfilePolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MyEditProfilePolicyId", + "description": "The edit profile policy ID for this project (use with IndividualB2C auth)." + }, + "AADInstance": { + "type": "parameter", + "datatype": "string", + "defaultValue": "https://login.microsoftonline.com/", + "replaces": "https:////login.microsoftonline.com/", + "description": "The Azure Active Directory instance to connect to (use with SingleOrg or MultiOrg auth)." + }, + "ClientId": { + "type": "parameter", + "datatype": "string", + "replaces": "11111111-1111-1111-11111111111111111", + "description": "The Client ID for this project (use with IndividualB2C, SingleOrg or MultiOrg auth)." + }, + "Domain": { + "type": "parameter", + "datatype": "string", + "replaces": "qualified.domain.name", + "description": "The domain for the directory tenant (use with SingleOrg or IndividualB2C auth)." + }, + "TenantId": { + "type": "parameter", + "datatype": "string", + "replaces": "22222222-2222-2222-2222-222222222222", + "description": "The TenantId ID of the directory to connect to (use with SingleOrg auth)." + }, + "CallbackPath": { + "type": "parameter", + "datatype": "string", + "replaces": "/signin-oidc", + "defaultValue": "/signin-oidc", + "description": "The request path within the application's base path of the redirect URI (use with SingleOrg or IndividualB2C auth)." + }, + "OrgReadAccess": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether or not to allow this application read access to the directory (only applies to SingleOrg or MultiOrg auth)." + }, + "UserSecretsId": { + "type": "parameter", + "datatype": "string", + "replaces": "aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "defaultValue": "aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "description": "The ID to use for secrets (use with OrgReadAccess or Individual auth)." + }, + "IncludeLaunchSettings": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether to include launchSettings.json in the generated template." + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "If specified, skips the automatic restore of the project on create.", + "defaultValue": "false" + }, + "UseBrowserLink": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether or not to include BrowserLink in the project" + }, + "HttpsPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure SSL in launchSettings.json." + }, + "HttpsPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "low": 44300, + "high": 44399 + } + }, + "HttpsPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "HttpsPort", + "fallbackVariableName": "HttpsPortGenerated" + }, + "replaces": "43434" + }, + "KestrelPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure Kestrel in launchSettings.json." + }, + "KestrelPortGenerated": { + "type": "generated", + "generator": "port" + }, + "KestrelPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "KestrelPort", + "fallbackVariableName": "KestrelPortGenerated" + }, + "replaces": "5001" + }, + "IISExpressPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure IIS Express in launchSettings.json." + }, + "IISExpressPortGenerated": { + "type": "generated", + "generator": "port" + }, + "IISExpressPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "IISExpressPort", + "fallbackVariableName": "IISExpressPortGenerated" + }, + "replaces": "55556" + }, + "OrganizationalAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\" || auth == \"MultiOrg\")" + }, + "WindowsAuth": { + "type": "computed", + "value": "(auth == \"Windows\")" + }, + "MultiOrgAuth": { + "type": "computed", + "value": "(auth == \"MultiOrg\")" + }, + "SingleOrgAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\")" + }, + "IndividualLocalAuth": { + "type": "computed", + "value": "(auth == \"Individual\")" + }, + "IndividualAuth": { + "type": "computed", + "value": "(auth == \"Individual\" || auth == \"IndividualB2C\")" + }, + "IndividualB2CAuth": { + "type": "computed", + "value": "(auth == \"IndividualB2C\")" + }, + "NoAuth": { + "type": "computed", + "value": "(!(IndividualAuth || OrganizationalAuth || WindowsAuth))" + }, + "RequiresHttps": { + "type": "computed", + "value": "(OrganizationalAuth || IndividualAuth)" + }, + "UseLocalDB": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual or --auth IndividualB2C is specified." + }, + "TargetFrameworkOverride": { + "type": "parameter", + "description": "Overrides the target framework", + "replaces": "TargetFrameworkOverride", + "datatype": "string", + "defaultValue": "" + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "netcoreapp2.1", + "description": "Target netcoreapp2.1" + } + ], + "replaces": "netcoreapp2.1", + "defaultValue": "netcoreapp2.1" + }, + "copyrightYear": { + "type": "generated", + "generator": "now", + "replaces": "1975", + "parameters": { + "format": "yyyy" + } + }, + "NoTools": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + } + }, + "primaryOutputs": [ { "path": "Company.WebApplication1.csproj" } ], + "defaultName": "WebApplication1", + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3.host.json new file mode 100644 index 0000000000..5fbf904ed4 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3.host.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json.schemastore.org/vs-2017.3.host", + "name": { + "text": "Web Application", + "package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}", + "id": "1017" + }, + "description": { + "text": "A project template for creating an ASP.NET Core application with example ASP.NET Razor Pages content.", + "package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}", + "id": "1018" + }, + "order": 300, + "icon": "vs-2017.3/WebApplication.png", + "learnMoreLink": "https://go.microsoft.com/fwlink/?LinkID=784881", + "uiFilters": [ "oneaspnet" ], + "isApi": false, + "usesOidc": true, + "supportsDocker": true, + "useBrowserLink": true, + "legacyTemplateIdentity": "Microsoft.NetCore.CSharp.RazorPages", + "supportedAuthentications": [ + { + "auth": "None", + "authenticationType": "NoAuth" + }, + { + "auth": "Individual", + "authenticationType": "IndividualAuth", + "b2cAuthenticationOptions": "CloudExisting" + }, + { + "auth": "Individual", + "authenticationType": "IndividualAuth", + "b2cAuthenticationOptions": "Local" + }, + { + "auth": "SingleOrg", + "authenticationType": "OrgAuth", + "orgAuthenticationOptions": "SSO" + }, + { + "auth": "MultiOrg", + "authenticationType": "OrgAuth", + "orgAuthenticationOptions": "MultiOrg" + }, + { + "auth": "Windows", + "authenticationType": "WindowsAuth" + } + ], + "ports": [ + { + "name": "IISExpressPort", + "useHttps": false + }, + { + "name": "KestrelPort", + "useHttps": false + }, + { + "name": "HttpsPort", + "useHttps": true + } + ], + "azureReplyUrlPortName": "HttpsPort", + "includeLaunchSettings": true, + "minFullFrameworkVersion": "4.6.1" +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3/WebApplication.png b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3/WebApplication.png new file mode 100644 index 0000000000..572a095fa0 Binary files /dev/null and b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/.template.config/vs-2017.3/WebApplication.png differ diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Company.WebApplication1.csproj b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Company.WebApplication1.csproj new file mode 100644 index 0000000000..6ff5daedc1 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Company.WebApplication1.csproj @@ -0,0 +1,41 @@ + + + netcoreapp2.1 + TargetFrameworkOverride + 2.1.0-preview2-25624-02 + 2.1.0-preview2-25624-02 + + aspnet-Company.WebApplication1-0ce56475-d1db-490f-8af1-a881ea4fcd2d + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Controllers/AccountController.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Controllers/AccountController.cs new file mode 100644 index 0000000000..b3551c6f0d --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Controllers/AccountController.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +#endif +#if (IndividualLocalAuth) +using Microsoft.AspNetCore.Identity; +#endif +using Microsoft.AspNetCore.Mvc; +#if (IndividualLocalAuth) +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; +#endif +#if (IndividualB2CAuth) +using Microsoft.Extensions.Options; +#endif + +namespace Company.WebApplication1.Controllers +{ + [Route("[controller]/[action]")] + public class AccountController : Controller + { +#if (IndividualLocalAuth) + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public AccountController(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Logout() + { + await _signInManager.SignOutAsync(); + _logger.LogInformation("User logged out."); + return RedirectToPage("/Index"); + } +#elseif (OrganizationalAuth) + [HttpGet] + public IActionResult SignIn() + { + var redirectUrl = Url.Page("/Index"); + return Challenge( + new AuthenticationProperties { RedirectUri = redirectUrl }, + OpenIdConnectDefaults.AuthenticationScheme + ); + } + + [HttpGet] + public IActionResult SignOut() + { + var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme); + return SignOut( + new AuthenticationProperties { RedirectUri = callbackUrl }, + CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme + ); + } +#elseif (IndividualB2CAuth) + private readonly AzureAdB2COptions _options; + + public AccountController(IOptions b2cOptions) + { + _options = b2cOptions.Value; + } + + [HttpGet] + public IActionResult SignIn() + { + var redirectUrl = Url.Page("/Index"); + return Challenge( + new AuthenticationProperties { RedirectUri = redirectUrl }, + OpenIdConnectDefaults.AuthenticationScheme + ); + } + + [HttpGet] + public IActionResult ResetPassword() + { + var redirectUrl = Url.Page("/Index"); + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + properties.Items[AzureAdB2COptions.PolicyAuthenticationProperty] = _options.ResetPasswordPolicyId; + return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme); + } + + [HttpGet] + public IActionResult EditProfile() + { + var redirectUrl = Url.Page("/Index"); + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + properties.Items[AzureAdB2COptions.PolicyAuthenticationProperty] = _options.EditProfilePolicyId; + return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme); + } + + [HttpGet] + public IActionResult SignOut() + { + var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme); + return SignOut( + new AuthenticationProperties { RedirectUri = callbackUrl }, + CookieAuthenticationDefaults.AuthenticationScheme, + OpenIdConnectDefaults.AuthenticationScheme + ); + } +#endif + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/ApplicationDbContext.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/ApplicationDbContext.cs new file mode 100644 index 0000000000..64fdd804c7 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/ApplicationDbContext.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Company.WebApplication1.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + // Customize the ASP.NET Identity model and override the defaults if needed. + // For example, you can rename the ASP.NET Identity table names and more. + // Add your customizations after calling base.OnModelCreating(builder); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/ApplicationUser.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/ApplicationUser.cs new file mode 100644 index 0000000000..0fcfc519b1 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/ApplicationUser.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace Company.WebApplication1.Data +{ + // Add profile data for application users by adding properties to the ApplicationUser class + public class ApplicationUser : IdentityUser + { + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000000..483520ed85 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,246 @@ +using System; +#if (UseLocalDB) +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +#endif +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Company.WebApplication1.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder +#if (UseLocalDB) + .HasAnnotation("ProductVersion", "1.0.0-rc3") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); +#else + .HasAnnotation("ProductVersion", "1.0.2"); +#endif + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("NormalizedName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Company.WebApplication1.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("NormalizedUserName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 0000000000..93771c4379 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +#if (UseLocalDB) +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Metadata; +#endif +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Company.WebApplication1.Data.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + LockoutEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + PasswordHash = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + SecurityStamp = table.Column(nullable: true), + TwoFactorEnabled = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) +#if (UseLocalDB) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), +#else + .Annotation("Autoincrement", true), +#endif + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) +#if (UseLocalDB) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), +#else + .Annotation("Autoincrement", true), +#endif + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_UserId", + table: "AspNetUserRoles", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000000..01138f20da --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,245 @@ +using System; +#if (UseLocalDB) +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +#endif +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Company.WebApplication1.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder +#if (UseLocalDB) + .HasAnnotation("ProductVersion", "1.0.0-rc3") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); +#else + .HasAnnotation("ProductVersion", "1.0.2"); +#endif + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("NormalizedName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Company.WebApplication1.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("NormalizedUserName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdAuthenticationBuilderExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdAuthenticationBuilderExtensions.cs new file mode 100644 index 0000000000..82aea1a07e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdAuthenticationBuilderExtensions.cs @@ -0,0 +1,87 @@ +using System; +#if (MultiOrgAuth) +using System.Threading.Tasks; +#endif +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +#if (MultiOrgAuth) +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +#endif + +namespace Microsoft.AspNetCore.Authentication +{ + public static class AzureAdAuthenticationBuilderExtensions + { + public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder) + => builder.AddAzureAd(_ => { }); + + public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action configureOptions) + { + builder.Services.Configure(configureOptions); + builder.Services.AddSingleton, ConfigureAzureOptions>(); + builder.AddOpenIdConnect(); + return builder; + } + + private class ConfigureAzureOptions : IConfigureNamedOptions + { + private readonly AzureAdOptions _azureOptions; + + public ConfigureAzureOptions(IOptions azureOptions) + { + _azureOptions = azureOptions.Value; + } + + public void Configure(string name, OpenIdConnectOptions options) + { + options.ClientId = _azureOptions.ClientId; + options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}"; + options.UseTokenLifetime = true; + options.CallbackPath = _azureOptions.CallbackPath; + options.RequireHttpsMetadata = false; +#if (MultiOrgAuth) + + 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 do authenticate single users, add your user validation below. + //OnTokenValidated = context => + //{ + // return myUserValidationLogic(context.Ticket.Principal); + //} + }; +#endif + } + + public void Configure(OpenIdConnectOptions options) + { + Configure(Options.DefaultName, options); + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs new file mode 100644 index 0000000000..db7ed5227b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class AzureAdB2CAuthenticationBuilderExtensions + { + public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder) + => builder.AddAzureAdB2C(_ => { }); + + public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder, Action configureOptions) + { + builder.Services.Configure(configureOptions); + builder.Services.AddSingleton, ConfigureAzureOptions>(); + builder.AddOpenIdConnect(); + return builder; + } + + private class ConfigureAzureOptions: IConfigureNamedOptions + { + private readonly AzureAdB2COptions _azureOptions; + + public ConfigureAzureOptions(IOptions azureOptions) + { + _azureOptions = azureOptions.Value; + } + + public void Configure(string name, OpenIdConnectOptions options) + { + options.ClientId = _azureOptions.ClientId; + options.Authority = $"{_azureOptions.Instance}/{_azureOptions.Domain}/{_azureOptions.SignUpSignInPolicyId}/v2.0"; + options.UseTokenLifetime = true; + options.CallbackPath = _azureOptions.CallbackPath; + + options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name" }; + + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProvider = OnRedirectToIdentityProvider, + OnRemoteFailure = OnRemoteFailure + }; + } + + public void Configure(OpenIdConnectOptions options) + { + Configure(Options.DefaultName, options); + } + + public Task OnRedirectToIdentityProvider(RedirectContext context) + { + var defaultPolicy = _azureOptions.DefaultPolicy; + if (context.Properties.Items.TryGetValue(AzureAdB2COptions.PolicyAuthenticationProperty, out var policy) && + !policy.Equals(defaultPolicy)) + { + context.ProtocolMessage.Scope = OpenIdConnectScope.OpenIdProfile; + context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken; + context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress.ToLower() + .Replace($"/{defaultPolicy.ToLower()}/", $"/{policy.ToLower()}/"); + context.Properties.Items.Remove(AzureAdB2COptions.PolicyAuthenticationProperty); + } + return Task.CompletedTask; + } + + public Task OnRemoteFailure(RemoteFailureContext context) + { + context.HandleResponse(); + // Handle the error code that Azure AD B2C throws when trying to reset a password from the login page + // because password reset is not supported by a "sign-up or sign-in policy" + if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118")) + { + // If the user clicked the reset password link, redirect to the reset password route + context.Response.Redirect("/Account/ResetPassword"); + } + else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied")) + { + context.Response.Redirect("/"); + } + else + { + context.Response.Redirect("/Error"); + } + return Task.CompletedTask; + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdB2COptions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdB2COptions.cs new file mode 100644 index 0000000000..d998d3037a --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdB2COptions.cs @@ -0,0 +1,23 @@ +namespace Microsoft.AspNetCore.Authentication +{ + public class AzureAdB2COptions + { + public const string PolicyAuthenticationProperty = "Policy"; + + public string ClientId { get; set; } + + public string Instance { get; set; } + + public string Domain { get; set; } + + public string EditProfilePolicyId { get; set; } + + public string SignUpSignInPolicyId { get; set; } + + public string ResetPasswordPolicyId { get; set; } + + public string CallbackPath { get; set; } + + public string DefaultPolicy => SignUpSignInPolicyId; + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdOptions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdOptions.cs new file mode 100644 index 0000000000..e7cb904256 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/AzureAdOptions.cs @@ -0,0 +1,17 @@ +namespace Microsoft.AspNetCore.Authentication +{ + public class AzureAdOptions + { + public string ClientId { get; set; } + + public string ClientSecret { get; set; } + + public string Instance { get; set; } + + public string Domain { get; set; } + + public string TenantId { get; set; } + + public string CallbackPath { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/EmailSenderExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/EmailSenderExtensions.cs new file mode 100644 index 0000000000..d50d7f1d1c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/EmailSenderExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Company.WebApplication1.Services; + +namespace Company.WebApplication1.Services +{ + public static class EmailSenderExtensions + { + public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) + { + return emailSender.SendEmailAsync(email, "Confirm your email", + $"Please confirm your account by clicking here."); + } + + public static Task SendResetPasswordAsync(this IEmailSender emailSender, string email, string callbackUrl) + { + return emailSender.SendEmailAsync(email, "Reset Password", + $"Please reset your password by clicking here."); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/UrlHelperExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/UrlHelperExtensions.cs new file mode 100644 index 0000000000..4caa64486e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Extensions/UrlHelperExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc +{ + public static class UrlHelperExtensions + { + public static string GetLocalUrl(this IUrlHelper urlHelper, string localUrl) + { + if (!urlHelper.IsLocalUrl(localUrl)) + { + return urlHelper.Page("/Index"); + } + + return localUrl; + } + + public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + { + return urlHelper.Page( + "/Account/ConfirmEmail", + pageHandler: null, + values: new { userId, code }, + protocol: scheme); + } + + public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + { + return urlHelper.Page( + "/Account/ResetPassword", + pageHandler: null, + values: new { userId, code }, + protocol: scheme); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/About.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/About.cshtml new file mode 100644 index 0000000000..3c090d15f0 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/About.cshtml @@ -0,0 +1,9 @@ +@page +@model AboutModel +@{ + ViewData["Title"] = "About"; +} +

@ViewData["Title"]

+

@Model.Message

+ +

Use this area to provide additional information.

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/About.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/About.cshtml.cs new file mode 100644 index 0000000000..74a71cafa8 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/About.cshtml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Company.WebApplication1.Pages +{ + public class AboutModel : PageModel + { + public string Message { get; set; } + + public void OnGet() + { + Message = "Your application description page."; + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml new file mode 100644 index 0000000000..ba7d0215dc --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml @@ -0,0 +1,10 @@ +@page +@model AccessDeniedModel +@{ + ViewData["Title"] = "Access denied"; +} + +
+

ViewData["Title"]

+

You do not have access to this resource.

+
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml.cs new file mode 100644 index 0000000000..3bed2536e1 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/AccessDenied.cshtml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Company.WebApplication1.Pages.Account +{ + public class AccessDeniedModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ConfirmEmail.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000000..903896477c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ConfirmEmail.cshtml @@ -0,0 +1,12 @@ +@page +@model ConfirmEmailModel +@{ + ViewData["Title"] = "Confirm email"; +} + +

@ViewData["Title"]

+
+

+ Thank you for confirming your email. +

+
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ConfirmEmail.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ConfirmEmail.cshtml.cs new file mode 100644 index 0000000000..134f874024 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ConfirmEmail.cshtml.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account +{ + public class ConfirmEmailModel : PageModel + { + private readonly UserManager _userManager; + + public ConfirmEmailModel(UserManager userManager) + { + _userManager = userManager; + } + + public async Task OnGetAsync(string userId, string code) + { + if (userId == null || code == null) + { + return RedirectToPage("/Index"); + } + + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{userId}'."); + } + + var result = await _userManager.ConfirmEmailAsync(user, code); + if (!result.Succeeded) + { + throw new ApplicationException($"Error confirming email for user with ID '{userId}':"); + } + + return Page(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ExternalLogin.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ExternalLogin.cshtml new file mode 100644 index 0000000000..51c9b9264f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ExternalLogin.cshtml @@ -0,0 +1,33 @@ +@page +@model ExternalLoginModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+

Associate your @Model.LoginProvider account.

+
+ +

+ You've successfully authenticated with @Model.LoginProvider. + Please enter an email address for this site below and click the Register button to finish + logging in. +

+ +
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ExternalLogin.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ExternalLogin.cshtml.cs new file mode 100644 index 0000000000..fc19f502b9 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ExternalLogin.cshtml.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account +{ + public class ExternalLoginModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public ExternalLoginModel( + SignInManager signInManager, + UserManager userManager, + ILogger logger) + { + _signInManager = signInManager; + _userManager = userManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string LoginProvider { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } + + public IActionResult OnGetAsync() + { + return RedirectToPage("./Login"); + } + + public IActionResult OnPost(string provider, string returnUrl = null) + { + // Request a redirect to the external login provider. + var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return new ChallengeResult(provider, properties); + } + + public async Task OnGetCallbackAsync(string returnUrl = null, string remoteError = null) + { + if (remoteError != null) + { + ErrorMessage = $"Error from external provider: {remoteError}"; + return RedirectToPage("./Login"); + } + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + return RedirectToPage("./Login"); + } + + // Sign in the user with this external login provider if the user already has a login. + var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true); + if (result.Succeeded) + { + _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider); + return LocalRedirect(Url.GetLocalUrl(returnUrl)); + } + if (result.IsLockedOut) + { + return RedirectToPage("./Lockout"); + } + else + { + // If the user does not have an account, then ask the user to create an account. + ReturnUrl = returnUrl; + LoginProvider = info.LoginProvider; + if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) + { + Input = new InputModel + { + Email = info.Principal.FindFirstValue(ClaimTypes.Email) + }; + } + return Page(); + } + } + + public async Task OnPostConfirmationAsync(string returnUrl = null) + { + if (ModelState.IsValid) + { + // Get the information about the user from the external login provider + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + throw new ApplicationException("Error loading external login information during confirmation."); + } + var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email }; + var result = await _userManager.CreateAsync(user); + if (result.Succeeded) + { + result = await _userManager.AddLoginAsync(user, info); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); + return LocalRedirect(Url.GetLocalUrl(returnUrl)); + } + } + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + ReturnUrl = returnUrl; + return Page(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPassword.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPassword.cshtml new file mode 100644 index 0000000000..cd8e98cf44 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPassword.cshtml @@ -0,0 +1,26 @@ +@page +@model ForgotPasswordModel +@{ + ViewData["Title"] = "Forgot your password?"; +} + +

@ViewData["Title"]

+

Enter your email.

+
+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPassword.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPassword.cshtml.cs new file mode 100644 index 0000000000..63df7efd21 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPassword.cshtml.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Company.WebApplication1.Data; +using Company.WebApplication1.Services; + +namespace Company.WebApplication1.Pages.Account +{ + public class ForgotPasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly IEmailSender _emailSender; + + public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender) + { + _userManager = userManager; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } + + public async Task OnPostAsync() + { + if (ModelState.IsValid) + { + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + return RedirectToPage("./ForgotPasswordConfirmation"); + } + + // For more information on how to enable account confirmation and password reset please + // visit https://go.microsoft.com/fwlink/?LinkID=532713 + var code = await _userManager.GeneratePasswordResetTokenAsync(user); + var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); + await _emailSender.SendResetPasswordAsync(Input.Email, callbackUrl); + return RedirectToPage("./ForgotPasswordConfirmation"); + } + + return Page(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPasswordConfirmation.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000000..b1139309eb --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,10 @@ +@page +@model ForgotPasswordConfirmation +@{ + ViewData["Title"] = "Forgot password confirmation"; +} + +

@ViewData["Title"]

+

+ Please check your email to reset your password. +

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPasswordConfirmation.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPasswordConfirmation.cshtml.cs new file mode 100644 index 0000000000..7d51b89172 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ForgotPasswordConfirmation.cshtml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Company.WebApplication1.Pages.Account +{ + public class ForgotPasswordConfirmation : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Lockout.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Lockout.cshtml new file mode 100644 index 0000000000..4eded88208 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Lockout.cshtml @@ -0,0 +1,10 @@ +@page +@model LockoutModel +@{ + ViewData["Title"] = "Locked out"; +} + +
+

@ViewData["Title"]

+

This account has been locked out, please try again later.

+
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Lockout.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Lockout.cshtml.cs new file mode 100644 index 0000000000..b2a2aa0669 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Lockout.cshtml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Company.WebApplication1.Pages.Account +{ + public class LockoutModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Login.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Login.cshtml new file mode 100644 index 0000000000..a486f27545 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Login.cshtml @@ -0,0 +1,82 @@ +@page +@model LoginModel + +@{ + ViewData["Title"] = "Log in"; +} + +

@ViewData["Title"]

+
+
+
+
+

Use a local account to log in.

+
+
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+

Use another service to log in.

+
+ @{ + if ((Model.ExternalLogins?.Count ?? 0) == 0) + { +
+

+ There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
+
+

+ @foreach (var provider in Model.ExternalLogins) + { + + } +

+
+
+ } + } +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Login.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Login.cshtml.cs new file mode 100644 index 0000000000..9fd2f7c784 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Login.cshtml.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account +{ + public class LoginModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public IList ExternalLogins { get; set; } + + public string ReturnUrl { get; set; } + + [TempData] + public string ErrorMessage { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + if (!string.IsNullOrEmpty(ErrorMessage)) + { + ModelState.AddModelError(string.Empty, ErrorMessage); + } + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + + ReturnUrl = returnUrl; + } + + public async Task OnPostAsync(string returnUrl = null) + { + ReturnUrl = returnUrl; + + if (ModelState.IsValid) + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); + if (result.Succeeded) + { + _logger.LogInformation("User logged in."); + return LocalRedirect(Url.GetLocalUrl(returnUrl)); + } + if (result.RequiresTwoFactor) + { + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User account locked out."); + return RedirectToPage("./Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return Page(); + } + } + + // If we got this far, something failed, redisplay form + return Page(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWith2fa.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWith2fa.cshtml new file mode 100644 index 0000000000..032ecd98cd --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWith2fa.cshtml @@ -0,0 +1,41 @@ +@page +@model LoginWith2faModel +@{ + ViewData["Title"] = "Two-factor authentication"; +} + +

@ViewData["Title"]

+
+

Your login is protected with an authenticator app. Enter your authenticator code below.

+
+
+
+ +
+
+ + + +
+
+
+ +
+
+
+ +
+
+
+
+

+ Don't have access to your authenticator device? You can + log in with a recovery code. +

+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWith2fa.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWith2fa.cshtml.cs new file mode 100644 index 0000000000..7d117d567e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWith2fa.cshtml.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account +{ + public class LoginWith2faModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginWith2faModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public bool RememberMe { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Authenticator code")] + public string TwoFactorCode { get; set; } + + [Display(Name = "Remember this machine")] + public bool RememberMachine { get; set; } + } + + public async Task OnGetAsync(bool rememberMe, string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + ReturnUrl = returnUrl; + RememberMe = rememberMe; + + return Page(); + } + + public async Task OnPostAsync(bool rememberMe, string returnUrl = null) + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); + + var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id); + return LocalRedirect(Url.GetLocalUrl(returnUrl)); + } + else if (result.IsLockedOut) + { + _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + return RedirectToPage("./Lockout"); + } + else + { + _logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id); + ModelState.AddModelError(string.Empty, "Invalid authenticator code."); + return Page(); + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWithRecoveryCode.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWithRecoveryCode.cshtml new file mode 100644 index 0000000000..4b32c1d8ff --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWithRecoveryCode.cshtml @@ -0,0 +1,29 @@ +@page +@model LoginWithRecoveryCodeModel +@{ + ViewData["Title"] = "Recovery code verification"; +} + +

@ViewData["Title"]

+
+

+ You have requested to log in with a recovery code. This login will not be remembered until you provide + an authenticator app code at log in or disable 2FA and log in again. +

+
+
+
+
+
+ + + +
+ +
+
+
+ + @section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWithRecoveryCode.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWithRecoveryCode.cshtml.cs new file mode 100644 index 0000000000..1293bdc66a --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/LoginWithRecoveryCode.cshtml.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account +{ + public class LoginWithRecoveryCodeModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginWithRecoveryCodeModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [BindProperty] + [Required] + [DataType(DataType.Text)] + [Display(Name = "Recovery Code")] + public string RecoveryCode { get; set; } + } + + public async Task OnGetAsync(string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + ReturnUrl = returnUrl; + + return Page(); + } + + public async Task OnPostAsync(string returnUrl = null) + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); + + var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id); + return LocalRedirect(Url.GetLocalUrl(returnUrl)); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); + return RedirectToPage("./Lockout"); + } + else + { + _logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id); + ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); + return Page(); + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ChangePassword.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ChangePassword.cshtml new file mode 100644 index 0000000000..6aa47234d1 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ChangePassword.cshtml @@ -0,0 +1,35 @@ +@page +@model ChangePasswordModel +@{ + ViewData["Title"] = "Change password"; +} + +

@ViewData["Title"]

+@Html.Partial("_StatusMessage", Model.StatusMessage) +
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ChangePassword.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ChangePassword.cshtml.cs new file mode 100644 index 0000000000..07d97806d7 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ChangePassword.cshtml.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public class ChangePasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public ChangePasswordModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public class InputModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + if (!hasPassword) + { + return RedirectToPage("./SetPassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); + if (!changePasswordResult.Succeeded) + { + foreach (var error in changePasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation("User changed their password successfully."); + StatusMessage = "Your password has been changed."; + + return RedirectToPage(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Disable2fa.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Disable2fa.cshtml new file mode 100644 index 0000000000..954093ee3b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Disable2fa.cshtml @@ -0,0 +1,25 @@ +@page +@model Disable2faModel +@{ + ViewData["Title"] = "Disable two-factor authentication (2FA)"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + +

@ViewData["Title"]

+ + + +
+
+ +
+
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Disable2fa.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Disable2fa.cshtml.cs new file mode 100644 index 0000000000..be06576f2f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Disable2fa.cshtml.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public class Disable2faModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public Disable2faModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!await _userManager.GetTwoFactorEnabledAsync(user)) + { + throw new ApplicationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'."); + } + + _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); + + return RedirectToPage("./TwoFactorAuthentication"); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml new file mode 100644 index 0000000000..1d68558407 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml @@ -0,0 +1,53 @@ +@page +@model EnableAuthenticatorModel +@{ + ViewData["Title"] = "Configure authenticator app"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + +

@ViewData["Title"]

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Windows Phone, + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

    Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    +
    To enable QR code generation please read our documentation.
    +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
  6. +
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml.cs new file mode 100644 index 0000000000..09a6327de0 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -0,0 +1,138 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Text; +using System.Text.Encodings.Web; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public class EnableAuthenticatorModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly UrlEncoder _urlEncoder; + + private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + public EnableAuthenticatorModel( + UserManager userManager, + ILogger logger, + UrlEncoder urlEncoder) + { + _userManager = userManager; + _logger = logger; + _urlEncoder = urlEncoder; + } + + public string SharedKey { get; set; } + + public string AuthenticatorUri { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await LoadSharedKeyAndQrCodeUriAsync(user); + if (string.IsNullOrEmpty(SharedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + await LoadSharedKeyAndQrCodeUriAsync(user); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!ModelState.IsValid) + { + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + // Strip spaces and hypens + var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( + user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + ModelState.AddModelError("Input.Code", "Verification code is invalid."); + await LoadSharedKeyAndQrCodeUriAsync(user); + return Page(); + } + + await _userManager.SetTwoFactorEnabledAsync(user, true); + _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", user.Id); + return RedirectToPage("./GenerateRecoveryCodes"); + } + + private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) + { + // Load the authenticator key & QR code URI to display on the form + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (!string.IsNullOrEmpty(unformattedKey)) + { + SharedKey = FormatKey(unformattedKey); + AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); + } + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + AuthenicatorUriFormat, + _urlEncoder.Encode("Company.WebApplication1"), + _urlEncoder.Encode(email), + unformattedKey); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ExternalLogins.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ExternalLogins.cshtml new file mode 100644 index 0000000000..ccb66a6a49 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ExternalLogins.cshtml @@ -0,0 +1,52 @@ +@page +@model ExternalLoginsModel +@{ + ViewData["Title"] = "Manage your external logins"; +} + +@Html.Partial("_StatusMessage", Model.StatusMessage) +@if (Model.CurrentLogins?.Count > 0) +{ +

Registered Logins

+ + + @foreach (var login in Model.CurrentLogins) + { + + + + + } + +
@login.LoginProvider + @if (Model.ShowRemoveButton) + { +
+
+ + + +
+
+ } + else + { + @:   + } +
+} +@if (Model.OtherLogins?.Count > 0) +{ +

Add another service to log in.

+
+
+
+

+ @foreach (var provider in Model.OtherLogins) + { + + } +

+
+
+} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ExternalLogins.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ExternalLogins.cshtml.cs new file mode 100644 index 0000000000..804f84b967 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ExternalLogins.cshtml.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public class ExternalLoginsModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public ExternalLoginsModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + public IList CurrentLogins { get; set; } + + public IList OtherLogins { get; set; } + + public bool ShowRemoveButton { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + CurrentLogins = await _userManager.GetLoginsAsync(user); + OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + ShowRemoveButton = user.PasswordHash != null || CurrentLogins.Count > 1; + return Page(); + } + + public async Task OnPostRemoveLoginAsync(string loginProvider, string providerKey) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey); + if (!result.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'."); + } + + await _signInManager.SignInAsync(user, isPersistent: false); + StatusMessage = "The external login was removed."; + return RedirectToPage(); + } + + public async Task OnPostLinkLoginAsync(string provider) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback"); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); + return new ChallengeResult(provider, properties); + } + + public async Task OnGetLinkLoginCallbackAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user)); + if (info == null) + { + throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); + } + + var result = await _userManager.AddLoginAsync(user, info); + if (!result.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'."); + } + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + StatusMessage = "The external login was added."; + return RedirectToPage(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml new file mode 100644 index 0000000000..d05825429f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml @@ -0,0 +1,25 @@ +@page +@model GenerateRecoveryCodesModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + +

@ViewData["Title"]

+ +
+
+ @for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } +
+
\ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs new file mode 100644 index 0000000000..6839d275fc --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public class GenerateRecoveryCodesModel : PageModel + { + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public GenerateRecoveryCodesModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public string[] RecoveryCodes { get; set; } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!user.TwoFactorEnabled) + { + throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); + } + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + RecoveryCodes = recoveryCodes.ToArray(); + + _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", user.Id); + + return Page(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Index.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Index.cshtml new file mode 100644 index 0000000000..78a0383cd3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Index.cshtml @@ -0,0 +1,45 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Profile"; +} + +

@ViewData["Title"]

+@Html.Partial("_StatusMessage", Model.StatusMessage) +
+
+
+
+
+ + +
+
+ + @if (Model.IsEmailConfirmed) + { +
+ + +
+ } + else + { + + + } + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Index.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Index.cshtml.cs new file mode 100644 index 0000000000..cf02965dc8 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/Index.cshtml.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Company.WebApplication1.Data; +using Company.WebApplication1.Services; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public partial class IndexModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + + public IndexModel( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + } + + public string Username { get; set; } + + public bool IsEmailConfirmed { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + Username = user.UserName; + + Input = new InputModel + { + Email = user.Email, + PhoneNumber = user.PhoneNumber + }; + + IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user); + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (Input.Email != user.Email) + { + var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email); + if (!setEmailResult.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'."); + } + } + + if (Input.PhoneNumber != user.PhoneNumber) + { + var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber); + if (!setPhoneResult.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'."); + } + } + + StatusMessage = "Your profile has been updated"; + return RedirectToPage(); + } + public async Task OnPostSendVerificationEmailAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); + await _emailSender.SendEmailConfirmationAsync(user.Email, callbackUrl); + + StatusMessage = "Verification email sent. Please check your email."; + return RedirectToPage(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ManageNavPages.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ManageNavPages.cs new file mode 100644 index 0000000000..9f5b7521f0 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ManageNavPages.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public static class ManageNavPages + { + public static string Index => "Index"; + + public static string ChangePassword => "ChangePassword"; + + public static string ExternalLogins => "ExternalLogins"; + + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + public static string PageNavClass(ViewContext viewContext, string page) + { + var activePage = viewContext.ViewData["ActivePage"] as string + ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ResetAuthenticator.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ResetAuthenticator.cshtml new file mode 100644 index 0000000000..a9534ae257 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ResetAuthenticator.cshtml @@ -0,0 +1,23 @@ +@page +@model ResetAuthenticatorModel +@{ + ViewData["Title"] = "Reset authenticator key"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ResetAuthenticator.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ResetAuthenticator.cshtml.cs new file mode 100644 index 0000000000..f0ed397385 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/ResetAuthenticator.cshtml.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public class ResetAuthenticatorModel : PageModel + { + UserManager _userManager; + ILogger _logger; + + public ResetAuthenticatorModel( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + return Page(); + } + + public async Task OnPostAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); + + return RedirectToPage("./EnableAuthenticator"); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/SetPassword.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/SetPassword.cshtml new file mode 100644 index 0000000000..3f4544caf4 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/SetPassword.cshtml @@ -0,0 +1,35 @@ +@page +@model SetPasswordModel +@{ + ViewData["Title"] = "Set password"; + ViewData["ActivePage"] = "ChangePassword"; +} + +

Set your password

+@Html.Partial("_StatusMessage", Model.StatusMessage) +

+ You do not have a local username/password for this site. Add a local + account so you can log in without an external login. +

+
+
+
+
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/SetPassword.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/SetPassword.cshtml.cs new file mode 100644 index 0000000000..33ea8bf3c3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/SetPassword.cshtml.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public class SetPasswordModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public SetPasswordModel( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [BindProperty] + public InputModel Input { get; set; } + + [TempData] + public string StatusMessage { get; set; } + + public class InputModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public async Task OnGetAsync() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + + if (hasPassword) + { + return RedirectToPage("./ChangePassword"); + } + + return Page(); + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword); + if (!addPasswordResult.Succeeded) + { + foreach (var error in addPasswordResult.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + + await _signInManager.SignInAsync(user, isPersistent: false); + StatusMessage = "Your password has been set."; + + return RedirectToPage(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/TwoFactorAuthentication.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/TwoFactorAuthentication.cshtml new file mode 100644 index 0000000000..fabe1999e6 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/TwoFactorAuthentication.cshtml @@ -0,0 +1,49 @@ +@page +@model TwoFactorAuthenticationModel +@{ + ViewData["Title"] = "Two-factor authentication (2FA)"; +} + +

@ViewData["Title"]

+@if (Model.Is2faEnabled) +{ + if (Model.RecoveryCodesLeft == 0) + { +
+ You have no recovery codes left. +

You must generate a new set of recovery codes before you can log in with a recovery code.

+
+ } + else if (Model.RecoveryCodesLeft == 1) + { +
+ You have 1 recovery code left. +

You can generate a new set of recovery codes.

+
+ } + else if (Model.RecoveryCodesLeft <= 3) + { +
+ You have @Model.RecoveryCodesLeft recovery codes left. +

You should generate a new set of recovery codes.

+
+ } + + Disable 2FA + Reset recovery codes +} + +
Authenticator app
+@if (!Model.HasAuthenticator) +{ + Add authenticator app +} +else +{ + Configure authenticator app + Reset authenticator app +} + +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs new file mode 100644 index 0000000000..e91c080e1c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account.Manage +{ + public class TwoFactorAuthenticationModel : PageModel + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public TwoFactorAuthenticationModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + } + + public bool HasAuthenticator { get; set; } + + public int RecoveryCodesLeft { get; set; } + + [BindProperty] + public bool Is2faEnabled { get; set; } + + public async Task OnGet() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; + Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); + RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); + + return Page(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_Layout.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_Layout.cshtml new file mode 100644 index 0000000000..c3f24b6ef8 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_Layout.cshtml @@ -0,0 +1,23 @@ +@{ + Layout = "/Pages/_Layout.cshtml"; +} + +

Manage your account

+ +
+

Change your account settings

+
+
+
+ @await Html.PartialAsync("_ManageNav") +
+
+ @RenderBody() +
+
+
+ +@section Scripts { + @RenderSection("Scripts", required: false) +} + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_ManageNav.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_ManageNav.cshtml new file mode 100644 index 0000000000..16d50ddfa7 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_ManageNav.cshtml @@ -0,0 +1,15 @@ +@inject SignInManager SignInManager +@{ + var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); +} + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_StatusMessage.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_StatusMessage.cshtml new file mode 100644 index 0000000000..e996841309 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_ViewImports.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_ViewImports.cshtml new file mode 100644 index 0000000000..44fcb384f7 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Manage/_ViewImports.cshtml @@ -0,0 +1 @@ +@using Company.WebApplication1.Pages.Account.Manage diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Register.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Register.cshtml new file mode 100644 index 0000000000..64f2151518 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Register.cshtml @@ -0,0 +1,37 @@ +@page +@model RegisterModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+ +
+
+
+

Create a new account.

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Register.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Register.cshtml.cs new file mode 100644 index 0000000000..99fb88a1a8 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/Register.cshtml.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Company.WebApplication1.Data; +using Company.WebApplication1.Services; + +namespace Company.WebApplication1.Pages.Account +{ + public class RegisterModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + private readonly ILogger _logger; + private readonly IEmailSender _emailSender; + + public RegisterModel( + UserManager userManager, + SignInManager signInManager, + ILogger logger, + IEmailSender emailSender) + { + _userManager = userManager; + _signInManager = signInManager; + _logger = logger; + _emailSender = emailSender; + } + + [BindProperty] + public InputModel Input { get; set; } + + public string ReturnUrl { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public void OnGet(string returnUrl = null) + { + ReturnUrl = returnUrl; + } + + public async Task OnPostAsync(string returnUrl = null) + { + ReturnUrl = returnUrl; + if (ModelState.IsValid) + { + var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email }; + var result = await _userManager.CreateAsync(user, Input.Password); + if (result.Succeeded) + { + _logger.LogInformation("User created a new account with password."); + + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); + await _emailSender.SendEmailConfirmationAsync(Input.Email, callbackUrl); + + await _signInManager.SignInAsync(user, isPersistent: false); + return LocalRedirect(Url.GetLocalUrl(returnUrl)); + } + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + // If we got this far, something failed, redisplay form + return Page(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPassword.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPassword.cshtml new file mode 100644 index 0000000000..85a81b1ef5 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPassword.cshtml @@ -0,0 +1,37 @@ +@page +@model ResetPasswordModel +@{ + ViewData["Title"] = "Reset password"; +} + +

@ViewData["Title"]

+

Reset your password.

+
+
+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPassword.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPassword.cshtml.cs new file mode 100644 index 0000000000..b0cbc9922b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPassword.cshtml.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Company.WebApplication1.Data; + +namespace Company.WebApplication1.Pages.Account +{ + public class ResetPasswordModel : PageModel + { + private readonly UserManager _userManager; + + public ResetPasswordModel(UserManager userManager) + { + _userManager = userManager; + } + + [BindProperty] + public InputModel Input { get; set; } + + public class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + public string Code { get; set; } + } + + public IActionResult OnGet(string code = null) + { + if (code == null) + { + throw new ApplicationException("A code must be supplied for password reset."); + } + else + { + Input = new InputModel + { + Code = code + }; + return Page(); + } + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var user = await _userManager.FindByEmailAsync(Input.Email); + if (user == null) + { + // Don't reveal that the user does not exist + return RedirectToPage("./ResetPasswordConfirmation"); + } + + var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password); + if (result.Succeeded) + { + return RedirectToPage("./ResetPasswordConfirmation"); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Page(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPasswordConfirmation.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000000..a9972d4311 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,10 @@ +@page +@model ResetPasswordConfirmationModel +@{ + ViewData["Title"] = "Reset password confirmation"; +} + +

@ViewData["Title"]

+

+ Your password has been reset. Please click here to log in. +

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPasswordConfirmation.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPasswordConfirmation.cshtml.cs new file mode 100644 index 0000000000..fccee6c3e7 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/ResetPasswordConfirmation.cshtml.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Company.WebApplication1.Pages.Account +{ + public class ResetPasswordConfirmationModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/SignedOut.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/SignedOut.cshtml new file mode 100644 index 0000000000..66ee2c8595 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/SignedOut.cshtml @@ -0,0 +1,10 @@ +@page +@model SignedOutModel +@{ + ViewData["Title"] = "Signed out"; +} + +

@ViewData["Title"]

+

+ You have successfully signed out. +

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/SignedOut.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/SignedOut.cshtml.cs new file mode 100644 index 0000000000..ec39e5f35e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/SignedOut.cshtml.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +public class SignedOutModel : PageModel +{ + public IActionResult OnGet() + { + if (User.Identity.IsAuthenticated) + { + // Redirect to home page if the user is authenticated. + return RedirectToPage("/Index"); + } + + return Page(); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/_ViewImports.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/_ViewImports.cshtml new file mode 100644 index 0000000000..1f50da0bfa --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Account/_ViewImports.cshtml @@ -0,0 +1 @@ +@using Company.WebApplication1.Pages.Account diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml new file mode 100644 index 0000000000..b683c8216c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml @@ -0,0 +1,19 @@ +@page +@model ContactModel +@{ + ViewData["Title"] = "Contact"; +} +

@ViewData["Title"]

+

@Model.Message

+ +
+ One Microsoft Way
+ Redmond, WA 98052-6399
+ P: + 425.555.0100 +
+ +
+ Support: Support@example.com
+ Marketing: Marketing@example.com +
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml.cs new file mode 100644 index 0000000000..303ac65a8b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Contact.cshtml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Company.WebApplication1.Pages +{ + public class ContactModel : PageModel + { + public string Message { get; set; } + + public void OnGet() + { + Message = "Your contact page."; + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Error.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Error.cshtml new file mode 100644 index 0000000000..b1f3143a42 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Error.cshtml @@ -0,0 +1,23 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Error.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Error.cshtml.cs new file mode 100644 index 0000000000..7c5b9e4736 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Error.cshtml.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Company.WebApplication1.Pages +{ + public class ErrorModel : PageModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Index.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Index.cshtml new file mode 100644 index 0000000000..c7f588fa3f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Index.cshtml @@ -0,0 +1,110 @@ +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Index.cshtml.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Index.cshtml.cs new file mode 100644 index 0000000000..1c86e8c152 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/Index.cshtml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Company.WebApplication1.Pages +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_Layout.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_Layout.cshtml new file mode 100644 index 0000000000..fbfdb5f76d --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_Layout.cshtml @@ -0,0 +1,76 @@ + + + + + + @ViewData["Title"] - Company.WebApplication1 + + + + + + + + + + + + +
+ @RenderBody() +
+
+

© 2017 - Company.WebApplication1

+
+
+ + + + + + + + + + + + + @RenderSection("Scripts", required: false) + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_LoginPartial.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_LoginPartial.cshtml new file mode 100644 index 0000000000..015494e9d1 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_LoginPartial.cshtml @@ -0,0 +1,61 @@ +@*#if (IndividualLocalAuth) *@ +@using Microsoft.AspNetCore.Identity +@using Company.WebApplication1.Data +@inject SignInManager SignInManager +@inject UserManager UserManager +@*#else +@using System.Security.Principal +#endif *@ +@*#if (IndividualB2CAuth) +@using Microsoft.AspNetCore.Authentication +@using Microsoft.Extensions.Options +@inject IOptions AzureAdB2COptions +#endif *@ + +@*#if (IndividualLocalAuth) *@ +@if (SignInManager.IsSignedIn(User)) +@*#else +@if (User.Identity.IsAuthenticated) +#endif *@ +{ +@*#if (IndividualLocalAuth) *@ + +@*#elseif (IndividualB2CAuth) + +#else + +#endif *@ +} +else +{ + +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_ValidationScriptsPartial.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000000..a2b13b317f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_ViewImports.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_ViewImports.cshtml new file mode 100644 index 0000000000..7fdcd89164 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_ViewImports.cshtml @@ -0,0 +1,9 @@ +@*#if (IndividualLocalAuth) +@using Microsoft.AspNetCore.Identity +#endif*@ +@using Company.WebApplication1 +@*#if (IndividualLocalAuth) +@using Company.WebApplication1.Data +#endif*@ +@namespace Company.WebApplication1.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_ViewStart.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_ViewStart.cshtml new file mode 100644 index 0000000000..a5f10045db --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Program.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Program.cs new file mode 100644 index 0000000000..13019a4fe2 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Program.cs @@ -0,0 +1,25 @@ +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.Logging; + +namespace Company.WebApplication1 +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Services/EmailSender.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Services/EmailSender.cs new file mode 100644 index 0000000000..3c31d1b5c3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Services/EmailSender.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Services +{ + // This class is used by the application to send email for account confirmation and password reset. + // For more details see https://go.microsoft.com/fwlink/?LinkID=532713 + public class EmailSender : IEmailSender + { + public Task SendEmailAsync(string email, string subject, string message) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Services/IEmailSender.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Services/IEmailSender.cs new file mode 100644 index 0000000000..ce6e1e9e84 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Services/IEmailSender.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Services +{ + public interface IEmailSender + { + Task SendEmailAsync(string email, string subject, string message); + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Startup.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Startup.cs new file mode 100644 index 0000000000..da13981417 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/Startup.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +#endif +#if (OrganizationalAuth) +using Microsoft.AspNetCore.Authorization; +#endif +using Microsoft.AspNetCore.Builder; +#if (IndividualLocalAuth) +using Microsoft.AspNetCore.Identity; +#endif +#if (OrganizationalAuth || IndividualAuth) +using Microsoft.AspNetCore.Http; +#endif +using Microsoft.AspNetCore.Hosting; +#if (OrganizationalAuth) +using Microsoft.AspNetCore.Mvc.Authorization; +#endif +#if (IndividualLocalAuth) +using Microsoft.EntityFrameworkCore; +#endif +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +#if (OrganizationalAuth && OrgReadAccess) +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +#endif +#if (MultiOrgAuth) +using Microsoft.IdentityModel.Tokens; +#endif +#if (IndividualLocalAuth) +using Company.WebApplication1.Data; +using Company.WebApplication1.Services; +#endif + +namespace Company.WebApplication1 +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { +#if (IndividualLocalAuth) + services.AddDbContext(options => + #if (UseLocalDB) + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + #else + options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); + #endif + + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + +#elseif (OrganizationalAuth || IndividualB2CAuth) + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + #if (OrganizationalAuth) + .AddAzureAd(options => Configuration.Bind("AzureAd", options)) + #elseif (IndividualB2CAuth) + .AddAzureAdB2C(options => Configuration.Bind("AzureAdB2C", options)) + #endif + .AddCookie(); + +#endif +#if (IndividualLocalAuth) + services.AddMvc() + .AddRazorPagesOptions(options => + { + options.Conventions.AuthorizeFolder("/Account/Manage"); + options.Conventions.AuthorizePage("/Account/Logout"); + }); + + // Register no-op EmailSender used by account confirmation and password reset during development + // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713 + services.AddSingleton(); +#elseif (OrganizationalAuth) + services.AddMvc(options => + { + var policy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); + options.Filters.Add(new AuthorizeFilter(policy)); + }) + .AddRazorPagesOptions(options => + { + options.Conventions.AllowAnonymousToFolder("/Account"); + }); +#else + services.AddMvc(); +#endif + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); +#if (UseBrowserLink) + app.UseBrowserLink(); +#endif +#if (IndividualLocalAuth) + app.UseDatabaseErrorPage(); +#endif + } + else + { + app.UseExceptionHandler("/Error"); + } + + app.UseStaticFiles(); + +#if (OrganizationalAuth || IndividualAuth) + app.UseAuthentication(); + +#endif + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/app.db b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/app.db new file mode 100644 index 0000000000..ec163057f1 Binary files /dev/null and b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/app.db differ diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/appsettings.Development.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/appsettings.Development.json new file mode 100644 index 0000000000..fa8ce71a97 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/appsettings.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/appsettings.json new file mode 100644 index 0000000000..ce132995bc --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/appsettings.json @@ -0,0 +1,40 @@ +{ +////#if (IndividualB2CAuth) +// "AzureAdB2C": { +// "Instance": "https:////login.microsoftonline.com/tfp/", +// "ClientId": "11111111-1111-1111-11111111111111111", +// "CallbackPath": "/signin-oidc", +// "Domain": "qualified.domain.name", +// "SignUpSignInPolicyId": "MySignUpSignInPolicyId", +// "ResetPasswordPolicyId": "MyResetPasswordPolicyId", +// "EditProfilePolicyId": "MyEditProfilePolicyId" +// }, +////#elseif (OrganizationalAuth) +// "AzureAd": { +//#if (MultiOrgAuth) +// "Instance": "https:////login.microsoftonline.com/common", +//#elseif (SingleOrgAuth) +// "Instance": "https:////login.microsoftonline.com/", +// "Domain": "qualified.domain.name", +// "TenantId": "22222222-2222-2222-2222-222222222222", +//#endif +// "ClientId": "11111111-1111-1111-11111111111111111", +// "CallbackPath": "/signin-oidc" +// }, +//#endif +////#if (IndividualLocalAuth) +// "ConnectionStrings": { +////#if (UseLocalDB) +// "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true" +////#else +// "DefaultConnection": "DataSource=app.db" +//#endif +// }, +//#endif + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/bower.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/bower.json new file mode 100644 index 0000000000..b07e3cc5ae --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/bower.json @@ -0,0 +1,10 @@ +{ + "name": "asp.net", + "private": true, + "dependencies": { + "bootstrap": "3.3.7", + "jquery": "2.2.0", + "jquery-validation": "1.14.0", + "jquery-validation-unobtrusive": "3.2.6" + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/bundleconfig.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/bundleconfig.json new file mode 100644 index 0000000000..6d3f9a57ae --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/bundleconfig.json @@ -0,0 +1,24 @@ +// Configure bundling and minification for the project. +// More info at https://go.microsoft.com/fwlink/?LinkId=808241 +[ + { + "outputFileName": "wwwroot/css/site.min.css", + // An array of relative input file paths. Globbing patterns supported + "inputFiles": [ + "wwwroot/css/site.css" + ] + }, + { + "outputFileName": "wwwroot/js/site.min.js", + "inputFiles": [ + "wwwroot/js/site.js" + ], + // Optionally specify minification options + "minify": { + "enabled": true, + "renameLocals": true + }, + // Optionally generate .map file + "sourceMap": false + } +] diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/css/site.css b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/css/site.css new file mode 100644 index 0000000000..465ee540fa --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/css/site.css @@ -0,0 +1,35 @@ +body { + padding-top: 50px; + padding-bottom: 20px; +} + +/* Wrapping element */ +/* Set some basic padding to keep content from hitting the edges */ +.body-content { + padding-left: 15px; + padding-right: 15px; +} + +/* Carousel */ +.carousel-caption p { + font-size: 20px; + line-height: 1.4; +} + +/* Make .svg files in the carousel display properly in older browsers */ +.carousel-inner .item img[src$=".svg"] { + width: 100%; +} + +/* QR code generator */ +#qrCode { + margin: 15px; +} + +/* Hide/rearrange for smaller screens */ +@media screen and (max-width: 767px) { + /* Hide captions */ + .carousel-caption { + display: none; + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/css/site.min.css b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/css/site.min.css new file mode 100644 index 0000000000..5e93e30ae3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/css/site.min.css @@ -0,0 +1 @@ +body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/favicon.ico b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/favicon.ico new file mode 100644 index 0000000000..a3a799985c Binary files /dev/null and b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/favicon.ico differ diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner1.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner1.svg new file mode 100644 index 0000000000..1ab32b60b8 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner2.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner2.svg new file mode 100644 index 0000000000..9679c604d0 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner3.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner3.svg new file mode 100644 index 0000000000..9be2c2503c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner3.svg @@ -0,0 +1 @@ +banner3b \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner4.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner4.svg new file mode 100644 index 0000000000..38b3d7cd1f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/images/banner4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/js/site.js b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/js/site.js new file mode 100644 index 0000000000..82ecce7b4a --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/js/site.js @@ -0,0 +1 @@ +// Write your Javascript code. diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/js/site.min.js b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/RazorPagesWeb-CSharp/wwwroot/js/site.min.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.bowerrc b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.bowerrc new file mode 100644 index 0000000000..6406626abf --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "wwwroot/lib" +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..d4a66c0bca --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,92 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "TargetFrameworkOverride": { + "isHidden": "true", + "longName": "target-framework-override", + "shortName": "" + }, + "UseLocalDB": { + "longName": "use-local-db" + }, + "AADInstance": { + "longName": "aad-instance", + "shortName": "" + }, + "AAdB2CInstance": { + "longName": "aad-b2c-instance", + "shortName": "" + }, + "SignUpSignInPolicyId": { + "longName": "susi-policy-id", + "shortName": "ssp" + }, + "ResetPasswordPolicyId": { + "longName": "reset-password-policy-id", + "shortName": "rp" + }, + "EditProfilePolicyId": { + "longName": "edit-profile-policy-id", + "shortName": "ep" + }, + "OrgReadAccess": { + "longName": "org-read-access", + "shortName": "r" + }, + "ClientId": { + "longName": "client-id", + "shortName": "" + }, + "CallbackPath": { + "longName": "callback-path", + "shortName": "" + }, + "Domain": { + "longName": "domain", + "shortName": "" + }, + "TenantId": { + "longName": "tenant-id", + "shortName": "" + }, + "Framework": { + "longName": "framework" + }, + "NoTools": { + "isHidden": "true", + "longName": "no-tools" + }, + "skipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "HttpsPort": { + "isHidden": true + }, + "KestrelPort": { + "isHidden": true + }, + "IISExpressPort": { + "isHidden": true + }, + "IncludeLaunchSettings": { + "longName": "use-launch-settings", + "shortName": "" + }, + "UserSecretsId": { + "isHidden": true + }, + "UseBrowserLink": { + "longName": "use-browserlink", + "shortName": "" + }, + "RuntimeFrameworkVersion": { + "longName": "runtime-framework-version", + "shortName": "fv", + "isHidden": "true" + } + }, + "usageExamples": [ + "--auth Individual" + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/template.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/template.json new file mode 100644 index 0000000000..d19241a192 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/template.json @@ -0,0 +1,363 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": ["Web", "MVC"], + "name": "ASP.NET Core Web App (Model-View-Controller)", + "generatorVersions": "[1.0.0.0-*)", + "description": "A project template for creating an ASP.NET Core application with example ASP.NET Core MVC Views and Controllers. This template can also be used for RESTful HTTP services.", + "groupIdentity": "Microsoft.Web.Mvc", + "precedence": "3000", + "identity": "Microsoft.Web.Mvc.CSharp.2.1", + "shortName": "mvc", + "thirdPartyNotices": "https://aka.ms/template-3pn", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Company.WebApplication1", + "preferNameDirectory": true, + "guids": [ + "09732173-2cef-46b7-83db-1334bcb079d3", // Tenant ID + "53bc9b9d-9d6a-45d4-8429-2a2761773502" // Client ID + ], + "sources": [ + { + "modifiers": [ + { + "condition": "(!IndividualAuth && !OrganizationalAuth)", + "exclude": [ + "Controllers/AccountController.cs", + "Views/Account/**", + "Views/Shared/_LoginPartial.cshtml" + ] + }, + { + "condition": "(!IndividualAuth || UseLocalDB)", + "exclude": [ "app.db" ] + }, + { + "condition": "(!IndividualLocalAuth)", + "exclude": [ + "Controllers/ManageController.cs", + "Views/Account/ConfirmEmail.cshtml", + "Views/Account/ExternalLogin.cshtml", + "Views/Account/ForgotPassword.cshtml", + "Views/Account/ForgotPasswordConfirmation.cshtml", + "Views/Account/Lockout.cshtml", + "Views/Account/Login.cshtml", + "Views/Account/LoginWith2fa.cshtml", + "Views/Account/LoginWithRecoveryCode.cshtml", + "Views/Account/Register.cshtml", + "Views/Account/ResetPassword.cshtml", + "Views/Account/ResetPasswordConfirmation.cshtml", + "Views/Manage/**", + "Services/**", + "Data/**", + "Models/AccountViewModels/**", + "Models/ManageViewModels/**", + "Models/ApplicationUser.cs", + "Extensions/EmailSenderExtensions.cs", + "Extensions/UrlHelperExtensions.cs" + ] + }, + { + "condition": "(!IncludeLaunchSettings && !WindowsAuth)", + "exclude": [ + "Properties/launchSettings.json" + ] + }, + { + "condition": "(!OrganizationalAuth)", + "exclude": [ + "Extensions/AzureAdAuthenticationBuilderExtensions.cs", + "Extensions/AzureAdOptions.cs" + ] + }, + { + "condition": "(!IndividualB2CAuth)", + "exclude": [ + "Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs", + "Extensions/AzureAdB2COptions.cs" + ] + } + ] + } + ], + "symbols": { + "RuntimeFrameworkVersion": { + "type": "parameter", + "replaces": "2.1.0-preview2-25624-02", + "datatype": "string", + "defaultValue": "2.1.0-preview2-25624-02" + }, + "auth": { + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "None", + "description": "No authentication" + }, + { + "choice": "Individual", + "description": "Individual authentication" + }, + { + "choice": "IndividualB2C", + "description": "Individual authentication with Azure AD B2C" + }, + { + "choice": "SingleOrg", + "description": "Organizational authentication for a single tenant" + }, + { + "choice": "MultiOrg", + "description": "Organizational authentication for multiple tenants" + }, + { + "choice": "Windows", + "description": "Windows authentication" + } + ], + "defaultValue": "None", + "description": "The type of authentication to use" + }, + "AAdB2CInstance": { + "type": "parameter", + "datatype": "string", + "defaultValue": "https://login.microsoftonline.com/tfp/", + "replaces": "https:////login.microsoftonline.com/tfp/", + "description": "The Azure Active Directory B2C instance to connect to (use with IndividualB2C auth)." + }, + "SignUpSignInPolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MySignUpSignInPolicyId", + "description": "The sign-in and sign-up policy ID for this project (use with IndividualB2C auth)." + }, + "ResetPasswordPolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MyResetPasswordPolicyId", + "description": "The reset password policy ID for this project (use with IndividualB2C auth)." + }, + "EditProfilePolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MyEditProfilePolicyId", + "description": "The edit profile policy ID for this project (use with IndividualB2C auth)." + }, + "AADInstance": { + "type": "parameter", + "datatype": "string", + "defaultValue": "https://login.microsoftonline.com/", + "replaces": "https:////login.microsoftonline.com/", + "description": "The Azure Active Directory instance to connect to (use with SingleOrg or MultiOrg auth)." + }, + "ClientId": { + "type": "parameter", + "datatype": "string", + "replaces": "11111111-1111-1111-11111111111111111", + "description": "The Client ID for this project (use with IndividualB2C, SingleOrg or MultiOrg auth)." + }, + "Domain": { + "type": "parameter", + "datatype": "string", + "replaces": "qualified.domain.name", + "description": "The domain for the directory tenant (use with SingleOrg or IndividualB2C auth)." + }, + "TenantId": { + "type": "parameter", + "datatype": "string", + "replaces": "22222222-2222-2222-2222-222222222222", + "description": "The TenantId ID of the directory to connect to (use with SingleOrg auth)." + }, + "CallbackPath": { + "type": "parameter", + "datatype": "string", + "replaces": "/signin-oidc", + "defaultValue": "/signin-oidc", + "description": "The request path within the application's base path of the redirect URI (use with SingleOrg or IndividualB2C auth)." + }, + "OrgReadAccess": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether or not to allow this application read access to the directory (only applies to SingleOrg or MultiOrg auth)." + }, + "UserSecretsId": { + "type": "parameter", + "datatype": "string", + "replaces": "aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "defaultValue": "aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "description": "The ID to use for secrets (use with OrgReadAccess or Individual auth)." + }, + "IncludeLaunchSettings": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether to include launchSettings.json in the generated template." + }, + "UseBrowserLink": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether or not to include BrowserLink in the project" + }, + "HttpsPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure SSL in launchSettings.json." + }, + "HttpsPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "low": 44300, + "high": 44399 + } + }, + "HttpsPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "HttpsPort", + "fallbackVariableName": "HttpsPortGenerated" + }, + "replaces": "43434" + }, + "KestrelPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure Kestrel in launchSettings.json." + }, + "KestrelPortGenerated": { + "type": "generated", + "generator": "port" + }, + "KestrelPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "KestrelPort", + "fallbackVariableName": "KestrelPortGenerated" + }, + "replaces": "5001" + }, + "IISExpressPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure IIS Express in launchSettings.json." + }, + "IISExpressPortGenerated": { + "type": "generated", + "generator": "port" + }, + "IISExpressPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "IISExpressPort", + "fallbackVariableName": "IISExpressPortGenerated" + }, + "replaces": "55556" + }, + "OrganizationalAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\" || auth == \"MultiOrg\")" + }, + "WindowsAuth": { + "type": "computed", + "value": "(auth == \"Windows\")" + }, + "MultiOrgAuth": { + "type": "computed", + "value": "(auth == \"MultiOrg\")" + }, + "SingleOrgAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\")" + }, + "IndividualLocalAuth": { + "type": "computed", + "value": "(auth == \"Individual\")" + }, + "IndividualAuth": { + "type": "computed", + "value": "(auth == \"Individual\" || auth == \"IndividualB2C\")" + }, + "IndividualB2CAuth": { + "type": "computed", + "value": "(auth == \"IndividualB2C\")" + }, + "NoAuth": { + "type": "computed", + "value": "(!(IndividualAuth || OrganizationalAuth || WindowsAuth))" + }, + "RequiresHttps": { + "type": "computed", + "value": "(OrganizationalAuth || IndividualAuth)" + }, + "UseLocalDB": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual or --auth IndividualB2C is specified." + }, + "TargetFrameworkOverride": { + "type": "parameter", + "description": "Overrides the target framework", + "replaces": "TargetFrameworkOverride", + "datatype": "string", + "defaultValue": "" + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "netcoreapp2.1", + "description": "Target netcoreapp2.1" + } + ], + "replaces": "netcoreapp2.1", + "defaultValue": "netcoreapp2.1" + }, + "copyrightYear": { + "type": "generated", + "generator": "now", + "replaces": "1975", + "parameters": { + "format": "yyyy" + } + }, + "NoTools": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "If specified, skips the automatic restore of the project on create.", + "defaultValue": "false" + } + }, + "primaryOutputs": [ { "path": "Company.WebApplication1.csproj" } ], + "defaultName": "WebApplication1", + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/vs-2017.3.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/vs-2017.3.host.json new file mode 100644 index 0000000000..f1ee97d12c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/vs-2017.3.host.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json.schemastore.org/vs-2017.3.host", + "name": { + "text": "Web Application (Model-View-Controller)", + "package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}", + "id": "1015" + }, + "description": { + "text": "A project template for creating an ASP.NET Core application with example ASP.NET Core MVC Views and Controllers. This template can also be used for RESTful HTTP services.", + "package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}", + "id": "1016" + }, + "order": 300, + "icon": "vs-2017.3/WebApplication.png", + "learnMoreLink": "https://go.microsoft.com/fwlink/?LinkID=784881", + "uiFilters": [ "oneaspnet" ], + "supportsDocker": true, + "isApi": false, + "usesOidc": true, + "useBrowserLink": true, + "legacyTemplateIdentity": "Microsoft.NetCore.CSharp.StarterWeb", + "supportedAuthentications": [ + { + "auth": "None", + "authenticationType": "NoAuth" + }, + { + "auth": "Individual", + "authenticationType": "IndividualAuth", + "b2cAuthenticationOptions": "CloudExisting" + }, + { + "auth": "Individual", + "authenticationType": "IndividualAuth", + "b2cAuthenticationOptions": "Local" + }, + { + "auth": "SingleOrg", + "authenticationType": "OrgAuth", + "orgAuthenticationOptions": "SSO" + }, + { + "auth": "MultiOrg", + "authenticationType": "OrgAuth", + "orgAuthenticationOptions": "MultiOrg" + }, + { + "auth": "Windows", + "authenticationType": "WindowsAuth" + } + ], + "ports": [ + { + "name": "IISExpressPort", + "useHttps": false + }, + { + "name": "KestrelPort", + "useHttps": false + }, + { + "name": "HttpsPort", + "useHttps": true + } + ], + "azureReplyUrlPortName": "HttpsPort", + "includeLaunchSettings": true, + "minFullFrameworkVersion": "4.6.1" +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/vs-2017.3/WebApplication.png b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/vs-2017.3/WebApplication.png new file mode 100644 index 0000000000..572a095fa0 Binary files /dev/null and b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/.template.config/vs-2017.3/WebApplication.png differ diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Company.WebApplication1.csproj b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Company.WebApplication1.csproj new file mode 100644 index 0000000000..ce7913d34f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Company.WebApplication1.csproj @@ -0,0 +1,48 @@ + + + + netcoreapp2.1 + TargetFrameworkOverride + 2.1.0-preview2-25624-02 + 2.1.0-preview2-25624-02 + + aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Controllers/AccountController.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Controllers/AccountController.cs new file mode 100644 index 0000000000..fdef1e3a48 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Controllers/AccountController.cs @@ -0,0 +1,573 @@ +using System; +using System.Collections.Generic; +using System.Linq; +#if (IndividualAuth) +using System.Security.Claims; +#endif +using System.Threading.Tasks; +#if (OrganizationalAuth || IndividualB2CAuth || IndividualAuth) +using Microsoft.AspNetCore.Authentication; +#endif +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +#endif +#if (IndividualAuth) +using Microsoft.AspNetCore.Authorization; +#endif +#if (IndividualLocalAuth) +using Microsoft.AspNetCore.Identity; +#endif +using Microsoft.AspNetCore.Mvc; +#if (IndividualAuth) +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +#endif +#if (IndividualLocalAuth) +using Company.WebApplication1.Models; +using Company.WebApplication1.Models.AccountViewModels; +using Company.WebApplication1.Services; +#endif + +namespace Company.WebApplication1.Controllers +{ +#if (IndividualLocalAuth) + [Authorize] +#endif + [Route("[controller]/[action]")] + public class AccountController : Controller + { +#if (OrganizationalAuth) + [HttpGet] + public IActionResult SignIn() + { + var redirectUrl = Url.Action(nameof(HomeController.Index), "Home"); + return Challenge( + new AuthenticationProperties { RedirectUri = redirectUrl }, + OpenIdConnectDefaults.AuthenticationScheme); + } + + [HttpGet] + public IActionResult SignOut() + { + var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null, protocol: Request.Scheme); + return SignOut( + new AuthenticationProperties { RedirectUri = callbackUrl }, + CookieAuthenticationDefaults.AuthenticationScheme, + OpenIdConnectDefaults.AuthenticationScheme); + } + + [HttpGet] + public IActionResult SignedOut() + { + if (User.Identity.IsAuthenticated) + { + // Redirect to home page if the user is authenticated. + return RedirectToAction(nameof(HomeController.Index), "Home"); + } + + return View(); + } +#endif +#if (IndividualLocalAuth) + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + private readonly ILogger _logger; + + public AccountController( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender, + ILogger logger) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + _logger = logger; + } + + [TempData] + public string ErrorMessage { get; set; } + + [HttpGet] + [AllowAnonymous] + public async Task Login(string returnUrl = null) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + ViewData["ReturnUrl"] = returnUrl; + return View(); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Login(LoginViewModel model, string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + if (ModelState.IsValid) + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + _logger.LogInformation("User logged in."); + return RedirectToLocal(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToAction(nameof(LoginWith2fa), new { returnUrl, model.RememberMe }); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User account locked out."); + return RedirectToAction(nameof(Lockout)); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(model); + } + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + [HttpGet] + [AllowAnonymous] + public async Task LoginWith2fa(bool rememberMe, string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + var model = new LoginWith2faViewModel { RememberMe = rememberMe }; + ViewData["ReturnUrl"] = returnUrl; + + return View(model); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task LoginWith2fa(LoginWith2faViewModel model, bool rememberMe, string returnUrl = null) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); + + var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID {UserId} logged in with 2fa.", user.Id); + return RedirectToLocal(returnUrl); + } + else if (result.IsLockedOut) + { + _logger.LogWarning("User with ID {UserId} account locked out.", user.Id); + return RedirectToAction(nameof(Lockout)); + } + else + { + _logger.LogWarning("Invalid authenticator code entered for user with ID {UserId}.", user.Id); + ModelState.AddModelError(string.Empty, "Invalid authenticator code."); + return View(); + } + } + + [HttpGet] + [AllowAnonymous] + public async Task LoginWithRecoveryCode(string returnUrl = null) + { + // Ensure the user has gone through the username & password screen first + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + ViewData["ReturnUrl"] = returnUrl; + + return View(); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task LoginWithRecoveryCode(LoginWithRecoveryCodeViewModel model, string returnUrl = null) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + throw new ApplicationException($"Unable to load two-factor authentication user."); + } + + var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty); + + var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + + if (result.Succeeded) + { + _logger.LogInformation("User with ID {UserId} logged in with a recovery code.", user.Id); + return RedirectToLocal(returnUrl); + } + if (result.IsLockedOut) + { + _logger.LogWarning("User with ID {UserId} account locked out.", user.Id); + return RedirectToAction(nameof(Lockout)); + } + else + { + _logger.LogWarning("Invalid recovery code entered for user with ID {UserId}", user.Id); + ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); + return View(); + } + } + + [HttpGet] + [AllowAnonymous] + public IActionResult Lockout() + { + return View(); + } + + [HttpGet] + [AllowAnonymous] + public IActionResult Register(string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + return View(); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel model, string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + if (ModelState.IsValid) + { + var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; + var result = await _userManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + _logger.LogInformation("User created a new account with password."); + + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); + await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl); + + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation("User created a new account with password."); + return RedirectToLocal(returnUrl); + } + AddErrors(result); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Logout() + { + await _signInManager.SignOutAsync(); + _logger.LogInformation("User logged out."); + return RedirectToAction(nameof(HomeController.Index), "Home"); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public IActionResult ExternalLogin(string provider, string returnUrl = null) + { + // Request a redirect to the external login provider. + var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { returnUrl }); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return Challenge(properties, provider); + } + + [HttpGet] + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) + { + if (remoteError != null) + { + ErrorMessage = $"Error from external provider: {remoteError}"; + return RedirectToAction(nameof(Login)); + } + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + return RedirectToAction(nameof(Login)); + } + + // Sign in the user with this external login provider if the user already has a login. + var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); + if (result.Succeeded) + { + _logger.LogInformation("User logged in with {Name} provider.", info.LoginProvider); + return RedirectToLocal(returnUrl); + } + if (result.IsLockedOut) + { + return RedirectToAction(nameof(Lockout)); + } + else + { + // If the user does not have an account, then ask the user to create an account. + ViewData["ReturnUrl"] = returnUrl; + ViewData["LoginProvider"] = info.LoginProvider; + var email = info.Principal.FindFirstValue(ClaimTypes.Email); + return View("ExternalLogin", new ExternalLoginViewModel { Email = email }); + } + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ExternalLoginConfirmation(ExternalLoginViewModel model, string returnUrl = null) + { + if (ModelState.IsValid) + { + // Get the information about the user from the external login provider + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + throw new ApplicationException("Error loading external login information during confirmation."); + } + var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; + var result = await _userManager.CreateAsync(user); + if (result.Succeeded) + { + result = await _userManager.AddLoginAsync(user, info); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); + return RedirectToLocal(returnUrl); + } + } + AddErrors(result); + } + + ViewData["ReturnUrl"] = returnUrl; + return View(nameof(ExternalLogin), model); + } + + [HttpGet] + [AllowAnonymous] + public async Task ConfirmEmail(string userId, string code) + { + if (userId == null || code == null) + { + return RedirectToAction(nameof(HomeController.Index), "Home"); + } + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{userId}'."); + } + var result = await _userManager.ConfirmEmailAsync(user, code); + return View(result.Succeeded ? "ConfirmEmail" : "Error"); + } + + [HttpGet] + [AllowAnonymous] + public IActionResult ForgotPassword() + { + return View(); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ForgotPassword(ForgotPasswordViewModel model) + { + if (ModelState.IsValid) + { + var user = await _userManager.FindByEmailAsync(model.Email); + if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + return RedirectToAction(nameof(ForgotPasswordConfirmation)); + } + + // For more information on how to enable account confirmation and password reset please + // visit https://go.microsoft.com/fwlink/?LinkID=532713 + var code = await _userManager.GeneratePasswordResetTokenAsync(user); + var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); + await _emailSender.SendEmailAsync(model.Email, "Reset Password", + $"Please reset your password by clicking here: link"); + return RedirectToAction(nameof(ForgotPasswordConfirmation)); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + [HttpGet] + [AllowAnonymous] + public IActionResult ForgotPasswordConfirmation() + { + return View(); + } + + [HttpGet] + [AllowAnonymous] + public IActionResult ResetPassword(string code = null) + { + if (code == null) + { + throw new ApplicationException("A code must be supplied for password reset."); + } + var model = new ResetPasswordViewModel { Code = code }; + return View(model); + } + + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ResetPassword(ResetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await _userManager.FindByEmailAsync(model.Email); + if (user == null) + { + // Don't reveal that the user does not exist + return RedirectToAction(nameof(ResetPasswordConfirmation)); + } + var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password); + if (result.Succeeded) + { + return RedirectToAction(nameof(ResetPasswordConfirmation)); + } + AddErrors(result); + return View(); + } + + [HttpGet] + [AllowAnonymous] + public IActionResult ResetPasswordConfirmation() + { + return View(); + } + +#endif +#if (IndividualB2CAuth) + private readonly AzureAdB2COptions _options; + + public AccountController(IOptions b2cOptions) + { + _options = b2cOptions.Value; + } + + [HttpGet] + public IActionResult SignIn() + { + var redirectUrl = Url.Action(nameof(HomeController.Index), "Home"); + return Challenge( + new AuthenticationProperties { RedirectUri = redirectUrl }, + OpenIdConnectDefaults.AuthenticationScheme); + } + + [HttpGet] + public IActionResult ResetPassword() + { + var redirectUrl = Url.Action(nameof(HomeController.Index), "Home"); + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + properties.Items[AzureAdB2COptions.PolicyAuthenticationProperty] = _options.ResetPasswordPolicyId; + return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme); + } + + [HttpGet] + public IActionResult EditProfile() + { + var redirectUrl = Url.Action(nameof(HomeController.Index), "Home"); + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + properties.Items[AzureAdB2COptions.PolicyAuthenticationProperty] = _options.EditProfilePolicyId; + return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme); + } + + [HttpGet] + public IActionResult SignOut() + { + var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null, protocol: Request.Scheme); + return SignOut(new AuthenticationProperties { RedirectUri = callbackUrl }, + CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme); + } + + [HttpGet] + public IActionResult SignedOut() + { + if (User.Identity.IsAuthenticated) + { + // Redirect to home page if the user is authenticated. + return RedirectToAction(nameof(HomeController.Index), "Home"); + } + + return View(); + } +#endif + + [HttpGet] + public IActionResult AccessDenied() + { + return View(); + } +#if (IndividualLocalAuth) + + #region Helpers + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + private IActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return RedirectToAction(nameof(HomeController.Index), "Home"); + } + } + + #endregion +#endif + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Controllers/HomeController.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Controllers/HomeController.cs new file mode 100644 index 0000000000..50363a31ea --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Controllers/HomeController.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +#if (OrganizationalAuth) +using Microsoft.AspNetCore.Authorization; +#endif +using Microsoft.AspNetCore.Mvc; +using Company.WebApplication1.Models; + +namespace Company.WebApplication1.Controllers +{ +#if (OrganizationalAuth) + [Authorize] +#endif + public class HomeController : Controller + { + public IActionResult Index() + { + return View(); + } + + public IActionResult About() + { + ViewData["Message"] = "Your application description page."; + + return View(); + } + + public IActionResult Contact() + { + ViewData["Message"] = "Your contact page."; + + return View(); + } + +#if (OrganizationalAuth) + [AllowAnonymous] +#endif + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Controllers/ManageController.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Controllers/ManageController.cs new file mode 100644 index 0000000000..d840959dd3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Controllers/ManageController.cs @@ -0,0 +1,505 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Company.WebApplication1.Models; +using Company.WebApplication1.Models.ManageViewModels; +using Company.WebApplication1.Services; + +namespace Company.WebApplication1.Controllers +{ + [Authorize] + [Route("[controller]/[action]")] + public class ManageController : Controller + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + private readonly ILogger _logger; + private readonly UrlEncoder _urlEncoder; + + private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + public ManageController( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender, + ILogger logger, + UrlEncoder urlEncoder) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + _logger = logger; + _urlEncoder = urlEncoder; + } + + [TempData] + public string StatusMessage { get; set; } + + [HttpGet] + public async Task Index() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var model = new IndexViewModel + { + Username = user.UserName, + Email = user.Email, + PhoneNumber = user.PhoneNumber, + IsEmailConfirmed = user.EmailConfirmed, + StatusMessage = StatusMessage + }; + + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Index(IndexViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var email = user.Email; + if (model.Email != email) + { + var setEmailResult = await _userManager.SetEmailAsync(user, model.Email); + if (!setEmailResult.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'."); + } + } + + var phoneNumber = user.PhoneNumber; + if (model.PhoneNumber != phoneNumber) + { + var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, model.PhoneNumber); + if (!setPhoneResult.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'."); + } + } + + StatusMessage = "Your profile has been updated"; + return RedirectToAction(nameof(Index)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task SendVerificationEmail(IndexViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); + var email = user.Email; + await _emailSender.SendEmailConfirmationAsync(email, callbackUrl); + + StatusMessage = "Verification email sent. Please check your email."; + return RedirectToAction(nameof(Index)); + } + + [HttpGet] + public async Task ChangePassword() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + if (!hasPassword) + { + return RedirectToAction(nameof(SetPassword)); + } + + var model = new ChangePasswordViewModel { StatusMessage = StatusMessage }; + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ChangePassword(ChangePasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + if (!changePasswordResult.Succeeded) + { + AddErrors(changePasswordResult); + return View(model); + } + + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation("User changed their password successfully."); + StatusMessage = "Your password has been changed."; + + return RedirectToAction(nameof(ChangePassword)); + } + + [HttpGet] + public async Task SetPassword() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var hasPassword = await _userManager.HasPasswordAsync(user); + + if (hasPassword) + { + return RedirectToAction(nameof(ChangePassword)); + } + + var model = new SetPasswordViewModel { StatusMessage = StatusMessage }; + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task SetPassword(SetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword); + if (!addPasswordResult.Succeeded) + { + AddErrors(addPasswordResult); + return View(model); + } + + await _signInManager.SignInAsync(user, isPersistent: false); + StatusMessage = "Your password has been set."; + + return RedirectToAction(nameof(SetPassword)); + } + + [HttpGet] + public async Task ExternalLogins() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) }; + model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1; + model.StatusMessage = StatusMessage; + + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task LinkLogin(string provider) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Action(nameof(LinkLoginCallback)); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); + return new ChallengeResult(provider, properties); + } + + [HttpGet] + public async Task LinkLoginCallback() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var info = await _signInManager.GetExternalLoginInfoAsync(user.Id); + if (info == null) + { + throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); + } + + var result = await _userManager.AddLoginAsync(user, info); + if (!result.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'."); + } + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + StatusMessage = "The external login was added."; + return RedirectToAction(nameof(ExternalLogins)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task RemoveLogin(RemoveLoginViewModel model) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey); + if (!result.Succeeded) + { + throw new ApplicationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'."); + } + + await _signInManager.SignInAsync(user, isPersistent: false); + StatusMessage = "The external login was removed."; + return RedirectToAction(nameof(ExternalLogins)); + } + + [HttpGet] + public async Task TwoFactorAuthentication() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var model = new TwoFactorAuthenticationViewModel + { + HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null, + Is2faEnabled = user.TwoFactorEnabled, + RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user), + }; + + return View(model); + } + + [HttpGet] + public async Task Disable2faWarning() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!user.TwoFactorEnabled) + { + throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'."); + } + + return View(nameof(Disable2fa)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Disable2fa() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'."); + } + + _logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id); + return RedirectToAction(nameof(TwoFactorAuthentication)); + } + + [HttpGet] + public async Task EnableAuthenticator() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + var model = new EnableAuthenticatorViewModel + { + SharedKey = FormatKey(unformattedKey), + AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey) + }; + + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task EnableAuthenticator(EnableAuthenticatorViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + // Strip spaces and hypens + var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( + user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + ModelState.AddModelError("model.Code", "Verification code is invalid."); + return View(model); + } + + await _userManager.SetTwoFactorEnabledAsync(user, true); + _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); + return RedirectToAction(nameof(GenerateRecoveryCodes)); + } + + [HttpGet] + public IActionResult ResetAuthenticatorWarning() + { + return View(nameof(ResetAuthenticator)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ResetAuthenticator() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id); + + return RedirectToAction(nameof(EnableAuthenticator)); + } + + [HttpGet] + public async Task GenerateRecoveryCodes() + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); + } + + if (!user.TwoFactorEnabled) + { + throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); + } + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() }; + + _logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id); + + return View(model); + } + + #region Helpers + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.Substring(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + AuthenicatorUriFormat, + _urlEncoder.Encode("Company.WebApplication1"), + _urlEncoder.Encode(email), + unformattedKey); + } + + #endregion + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/ApplicationDbContext.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/ApplicationDbContext.cs new file mode 100644 index 0000000000..32da6ff1d1 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/ApplicationDbContext.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Company.WebApplication1.Models; + +namespace Company.WebApplication1.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + // Customize the ASP.NET Identity model and override the defaults if needed. + // For example, you can rename the ASP.NET Identity table names and more. + // Add your customizations after calling base.OnModelCreating(builder); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000000..483520ed85 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,246 @@ +using System; +#if (UseLocalDB) +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +#endif +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Company.WebApplication1.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder +#if (UseLocalDB) + .HasAnnotation("ProductVersion", "1.0.0-rc3") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); +#else + .HasAnnotation("ProductVersion", "1.0.2"); +#endif + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("NormalizedName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Company.WebApplication1.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("NormalizedUserName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 0000000000..93771c4379 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +#if (UseLocalDB) +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Metadata; +#endif +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Company.WebApplication1.Data.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + LockoutEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + PasswordHash = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + SecurityStamp = table.Column(nullable: true), + TwoFactorEnabled = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) +#if (UseLocalDB) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), +#else + .Annotation("Autoincrement", true), +#endif + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) +#if (UseLocalDB) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), +#else + .Annotation("Autoincrement", true), +#endif + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_UserId", + table: "AspNetUserRoles", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000000..01138f20da --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,245 @@ +using System; +#if (UseLocalDB) +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +#endif +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Company.WebApplication1.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder +#if (UseLocalDB) + .HasAnnotation("ProductVersion", "1.0.0-rc3") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); +#else + .HasAnnotation("ProductVersion", "1.0.2"); +#endif + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("NormalizedName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Company.WebApplication1.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("NormalizedUserName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") +#if (UseLocalDB) + .HasAnnotation("MaxLength", 256); +#else + .HasMaxLength(256); +#endif + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Company.WebApplication1.Models.ApplicationUser") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdAuthenticationBuilderExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdAuthenticationBuilderExtensions.cs new file mode 100644 index 0000000000..e6060f7f12 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdAuthenticationBuilderExtensions.cs @@ -0,0 +1,87 @@ +using System; +#if (MultiOrgAuth) +using System.Threading.Tasks; +#endif +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +#if (MultiOrgAuth) +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +#endif + +namespace Microsoft.AspNetCore.Authentication +{ + public static class AzureAdAuthenticationBuilderExtensions + { + public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder) + => builder.AddAzureAd(_ => { }); + + public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action configureOptions) + { + builder.Services.Configure(configureOptions); + builder.Services.AddSingleton, ConfigureAzureOptions>(); + builder.AddOpenIdConnect(); + return builder; + } + + private class ConfigureAzureOptions: IConfigureNamedOptions + { + private readonly AzureAdOptions _azureOptions; + + public ConfigureAzureOptions(IOptions azureOptions) + { + _azureOptions = azureOptions.Value; + } + + public void Configure(string name, OpenIdConnectOptions options) + { + options.ClientId = _azureOptions.ClientId; + options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}"; + options.UseTokenLifetime = true; + options.CallbackPath = _azureOptions.CallbackPath; + options.RequireHttpsMetadata = false; +#if (MultiOrgAuth) + + 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("/Home/Error"); + context.HandleResponse(); // Suppress the exception + return Task.CompletedTask; + }, + // If your application needs to do authenticate single users, add your user validation below. + //OnTokenValidated = context => + //{ + // return myUserValidationLogic(context.Ticket.Principal); + //} + }; +#endif + } + + public void Configure(OpenIdConnectOptions options) + { + Configure(Options.DefaultName, options); + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs new file mode 100644 index 0000000000..acfff43fd9 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class AzureAdB2CAuthenticationBuilderExtensions + { + public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder) + => builder.AddAzureAdB2C(_ => { }); + + public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder, Action configureOptions) + { + builder.Services.Configure(configureOptions); + builder.Services.AddSingleton, ConfigureAzureOptions>(); + builder.AddOpenIdConnect(); + return builder; + } + + private class ConfigureAzureOptions: IConfigureNamedOptions + { + private readonly AzureAdB2COptions _azureOptions; + + public ConfigureAzureOptions(IOptions azureOptions) + { + _azureOptions = azureOptions.Value; + } + + public void Configure(string name, OpenIdConnectOptions options) + { + options.ClientId = _azureOptions.ClientId; + options.Authority = $"{_azureOptions.Instance}/{_azureOptions.Domain}/{_azureOptions.SignUpSignInPolicyId}/v2.0"; + options.UseTokenLifetime = true; + options.CallbackPath = _azureOptions.CallbackPath; + + options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name" }; + + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProvider = OnRedirectToIdentityProvider, + OnRemoteFailure = OnRemoteFailure + }; + } + + public void Configure(OpenIdConnectOptions options) + { + Configure(Options.DefaultName, options); + } + + public Task OnRedirectToIdentityProvider(RedirectContext context) + { + var defaultPolicy = _azureOptions.DefaultPolicy; + if (context.Properties.Items.TryGetValue(AzureAdB2COptions.PolicyAuthenticationProperty, out var policy) && + !policy.Equals(defaultPolicy)) + { + context.ProtocolMessage.Scope = OpenIdConnectScope.OpenIdProfile; + context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken; + context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress.ToLower() + .Replace($"/{defaultPolicy.ToLower()}/", $"/{policy.ToLower()}/"); + context.Properties.Items.Remove(AzureAdB2COptions.PolicyAuthenticationProperty); + } + return Task.CompletedTask; + } + + public Task OnRemoteFailure(RemoteFailureContext context) + { + context.HandleResponse(); + // Handle the error code that Azure AD B2C throws when trying to reset a password from the login page + // because password reset is not supported by a "sign-up or sign-in policy" + if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118")) + { + // If the user clicked the reset password link, redirect to the reset password route + context.Response.Redirect("/Account/ResetPassword"); + } + else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied")) + { + context.Response.Redirect("/"); + } + else + { + context.Response.Redirect("/Home/Error"); + } + return Task.CompletedTask; + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdB2COptions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdB2COptions.cs new file mode 100644 index 0000000000..ea8c53297a --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdB2COptions.cs @@ -0,0 +1,23 @@ +namespace Microsoft.AspNetCore.Authentication +{ + public class AzureAdB2COptions + { + public const string PolicyAuthenticationProperty = "Policy"; + + public string ClientId { get; set; } + + public string Instance { get; set; } + + public string Domain { get; set; } + + public string EditProfilePolicyId { get; set; } + + public string SignUpSignInPolicyId { get; set; } + + public string ResetPasswordPolicyId { get; set; } + + public string CallbackPath { get; set; } + + public string DefaultPolicy => SignUpSignInPolicyId; + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdOptions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdOptions.cs new file mode 100644 index 0000000000..17fd16aa9e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/AzureAdOptions.cs @@ -0,0 +1,17 @@ +namespace Microsoft.AspNetCore.Authentication +{ + public class AzureAdOptions + { + public string ClientId { get; set; } + + public string ClientSecret { get; set; } + + public string Instance { get; set; } + + public string Domain { get; set; } + + public string TenantId { get; set; } + + public string CallbackPath { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/EmailSenderExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/EmailSenderExtensions.cs new file mode 100644 index 0000000000..6db948bc3a --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/EmailSenderExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Company.WebApplication1.Services; + +namespace Company.WebApplication1.Services +{ + public static class EmailSenderExtensions + { + public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) + { + return emailSender.SendEmailAsync(email, "Confirm your email", + $"Please confirm your account by clicking this link: link"); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/UrlHelperExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/UrlHelperExtensions.cs new file mode 100644 index 0000000000..6d1c3f6fce --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Extensions/UrlHelperExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Company.WebApplication1.Controllers; + +namespace Microsoft.AspNetCore.Mvc +{ + public static class UrlHelperExtensions + { + public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + { + return urlHelper.Action( + action: nameof(AccountController.ConfirmEmail), + controller: "Account", + values: new { userId, code }, + protocol: scheme); + } + + public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme) + { + return urlHelper.Action( + action: nameof(AccountController.ResetPassword), + controller: "Account", + values: new { userId, code }, + protocol: scheme); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/ExternalLoginViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/ExternalLoginViewModel.cs new file mode 100644 index 0000000000..66167fd25e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/ExternalLoginViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.AccountViewModels +{ + public class ExternalLoginViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/ForgotPasswordViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/ForgotPasswordViewModel.cs new file mode 100644 index 0000000000..dc6cddb9a8 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/ForgotPasswordViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.AccountViewModels +{ + public class ForgotPasswordViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/LoginViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/LoginViewModel.cs new file mode 100644 index 0000000000..d502be1bff --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/LoginViewModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.AccountViewModels +{ + public class LoginViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/LoginWith2faViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/LoginWith2faViewModel.cs new file mode 100644 index 0000000000..2f91ce39d4 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/LoginWith2faViewModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.AccountViewModels +{ + public class LoginWith2faViewModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Authenticator code")] + public string TwoFactorCode { get; set; } + + [Display(Name = "Remember this machine")] + public bool RememberMachine { get; set; } + + public bool RememberMe { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/LoginWithRecoveryCodeViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/LoginWithRecoveryCodeViewModel.cs new file mode 100644 index 0000000000..f2cc20de1d --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/LoginWithRecoveryCodeViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.AccountViewModels +{ + public class LoginWithRecoveryCodeViewModel + { + [Required] + [DataType(DataType.Text)] + [Display(Name = "Recovery Code")] + public string RecoveryCode { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/RegisterViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/RegisterViewModel.cs new file mode 100644 index 0000000000..76dbe7234c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/RegisterViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.AccountViewModels +{ + public class RegisterViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/ResetPasswordViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/ResetPasswordViewModel.cs new file mode 100644 index 0000000000..10f4dcc474 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/AccountViewModels/ResetPasswordViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.AccountViewModels +{ + public class ResetPasswordViewModel + { + [Required] + [EmailAddress] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + public string Code { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ApplicationUser.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ApplicationUser.cs new file mode 100644 index 0000000000..f4fcc44b85 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ApplicationUser.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace Company.WebApplication1.Models +{ + // Add profile data for application users by adding properties to the ApplicationUser class + public class ApplicationUser : IdentityUser + { + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ErrorViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ErrorViewModel.cs new file mode 100644 index 0000000000..592c52772e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ErrorViewModel.cs @@ -0,0 +1,11 @@ +using System; + +namespace Company.WebApplication1.Models +{ + public class ErrorViewModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/ChangePasswordViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/ChangePasswordViewModel.cs new file mode 100644 index 0000000000..710132efed --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/ChangePasswordViewModel.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.ManageViewModels +{ + public class ChangePasswordViewModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + public string StatusMessage { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/EnableAuthenticatorViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/EnableAuthenticatorViewModel.cs new file mode 100644 index 0000000000..3f3323f216 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/EnableAuthenticatorViewModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.ManageViewModels +{ + public class EnableAuthenticatorViewModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } + + [ReadOnly(true)] + public string SharedKey { get; set; } + + public string AuthenticatorUri { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/ExternalLoginsViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/ExternalLoginsViewModel.cs new file mode 100644 index 0000000000..0675aa0870 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/ExternalLoginsViewModel.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; + +namespace Company.WebApplication1.Models.ManageViewModels +{ + public class ExternalLoginsViewModel + { + public IList CurrentLogins { get; set; } + + public IList OtherLogins { get; set; } + + public bool ShowRemoveButton { get; set; } + + public string StatusMessage { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs new file mode 100644 index 0000000000..05f1fe8a70 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.ManageViewModels +{ + public class GenerateRecoveryCodesViewModel + { + public string[] RecoveryCodes { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/IndexViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/IndexViewModel.cs new file mode 100644 index 0000000000..1a5fe12d17 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/IndexViewModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.ManageViewModels +{ + public class IndexViewModel + { + public string Username { get; set; } + + public bool IsEmailConfirmed { get; set; } + + [Required] + [EmailAddress] + public string Email { get; set; } + + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } + + public string StatusMessage { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/RemoveLoginViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/RemoveLoginViewModel.cs new file mode 100644 index 0000000000..998a5bb122 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/RemoveLoginViewModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.ManageViewModels +{ + public class RemoveLoginViewModel + { + public string LoginProvider { get; set; } + public string ProviderKey { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/SetPasswordViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/SetPasswordViewModel.cs new file mode 100644 index 0000000000..6fcfe3ec9d --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/SetPasswordViewModel.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.ManageViewModels +{ + public class SetPasswordViewModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + public string StatusMessage { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/TwoFactorAuthenticationViewModel.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/TwoFactorAuthenticationViewModel.cs new file mode 100644 index 0000000000..b7847aff86 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Models/ManageViewModels/TwoFactorAuthenticationViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Models.ManageViewModels +{ + public class TwoFactorAuthenticationViewModel + { + public bool HasAuthenticator { get; set; } + + public int RecoveryCodesLeft { get; set; } + + public bool Is2faEnabled { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Program.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Program.cs new file mode 100644 index 0000000000..13019a4fe2 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Program.cs @@ -0,0 +1,25 @@ +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.Logging; + +namespace Company.WebApplication1 +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Services/EmailSender.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Services/EmailSender.cs new file mode 100644 index 0000000000..2521999578 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Services/EmailSender.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Services +{ + // This class is used by the application to send email for account confirmation and password reset. + // For more details see https://go.microsoft.com/fwlink/?LinkID=532713 + public class EmailSender : IEmailSender + { + public Task SendEmailAsync(string email, string subject, string message) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Services/IEmailSender.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Services/IEmailSender.cs new file mode 100644 index 0000000000..ce6e1e9e84 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Services/IEmailSender.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Company.WebApplication1.Services +{ + public interface IEmailSender + { + Task SendEmailAsync(string email, string subject, string message); + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Startup.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Startup.cs new file mode 100644 index 0000000000..ba511285a5 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Startup.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +#endif +using Microsoft.AspNetCore.Builder; +#if (IndividualLocalAuth) +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +#endif +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +#if (OrganizationalAuth && OrgReadAccess) +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +#endif +#if (MultiOrgAuth) +using Microsoft.IdentityModel.Tokens; +#endif +#if (IndividualLocalAuth) +using Company.WebApplication1.Data; +using Company.WebApplication1.Models; +using Company.WebApplication1.Services; +#endif + +namespace Company.WebApplication1 +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { +#if (IndividualLocalAuth) + services.AddDbContext(options => + #if (UseLocalDB) + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + #else + options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); + #endif + + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // Add application services. + services.AddTransient(); + +#elseif (OrganizationalAuth || IndividualB2CAuth) + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + }) + #if (OrganizationalAuth) + .AddAzureAd(options => Configuration.Bind("AzureAd", options)) + #elseif (IndividualB2CAuth) + .AddAzureAdB2C(options => Configuration.Bind("AzureAdB2C", options)) + #endif + .AddCookie(); + +#endif + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); +#if (UseBrowserLink) + app.UseBrowserLink(); +#endif +#if (IndividualLocalAuth) + app.UseDatabaseErrorPage(); +#endif + } + else + { + app.UseExceptionHandler("/Home/Error"); + } + + app.UseStaticFiles(); + +#if (OrganizationalAuth || IndividualAuth) + app.UseAuthentication(); + +#endif + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/AccessDenied.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/AccessDenied.cshtml new file mode 100644 index 0000000000..3a5a00852c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/AccessDenied.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Access denied"; +} + +
+

ViewData["Title"]

+

You do not have access to this resource.

+
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ConfirmEmail.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ConfirmEmail.cshtml new file mode 100644 index 0000000000..81125a426b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ConfirmEmail.cshtml @@ -0,0 +1,10 @@ +@{ + ViewData["Title"] = "Confirm email"; +} + +

@ViewData["Title"]

+
+

+ Thank you for confirming your email. +

+
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ExternalLogin.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ExternalLogin.cshtml new file mode 100644 index 0000000000..d08d86076c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ExternalLogin.cshtml @@ -0,0 +1,32 @@ +@model ExternalLoginViewModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+

Associate your @ViewData["LoginProvider"] account.

+
+ +

+ You've successfully authenticated with @ViewData["LoginProvider"]. + Please enter an email address for this site below and click the Register button to finish + logging in. +

+ +
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ForgotPassword.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ForgotPassword.cshtml new file mode 100644 index 0000000000..c5a8e7c7b5 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ForgotPassword.cshtml @@ -0,0 +1,25 @@ +@model ForgotPasswordViewModel +@{ + ViewData["Title"] = "Forgot your password?"; +} + +

@ViewData["Title"]

+

Enter your email.

+
+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ForgotPasswordConfirmation.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ForgotPasswordConfirmation.cshtml new file mode 100644 index 0000000000..ef1e54ed11 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ForgotPasswordConfirmation.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Forgot password confirmation"; +} + +

@ViewData["Title"]

+

+ Please check your email to reset your password. +

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/Lockout.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/Lockout.cshtml new file mode 100644 index 0000000000..f573a1f86b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/Lockout.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Locked out"; +} + +
+

@ViewData["Title"]

+

This account has been locked out, please try again later.

+
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/Login.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/Login.cshtml new file mode 100644 index 0000000000..e3529d714e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/Login.cshtml @@ -0,0 +1,87 @@ +@using System.Collections.Generic +@using System.Linq +@using Microsoft.AspNetCore.Http +@using Microsoft.AspNetCore.Http.Authentication +@model LoginViewModel +@inject SignInManager SignInManager + +@{ + ViewData["Title"] = "Log in"; +} + +

@ViewData["Title"]

+
+
+
+
+

Use a local account to log in.

+
+
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ +
+ +
+
+
+
+
+

Use another service to log in.

+
+ @{ + var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); + if (loginProviders.Count == 0) + { +
+

+ There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services. +

+
+ } + else + { +
+
+

+ @foreach (var provider in loginProviders) + { + + } +

+
+
+ } + } +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/LoginWith2fa.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/LoginWith2fa.cshtml new file mode 100644 index 0000000000..0f554e264f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/LoginWith2fa.cshtml @@ -0,0 +1,40 @@ +@model LoginWith2faViewModel +@{ + ViewData["Title"] = "Two-factor authentication"; +} + +

@ViewData["Title"]

+
+

Your login is protected with an authenticator app. Enter your authenticator code below.

+
+
+
+ +
+
+ + + +
+
+
+ +
+
+
+ +
+
+
+
+

+ Don't have access to your authenticator device? You can + log in with a recovery code. +

+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/LoginWithRecoveryCode.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/LoginWithRecoveryCode.cshtml new file mode 100644 index 0000000000..e2eb4c5a5b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/LoginWithRecoveryCode.cshtml @@ -0,0 +1,28 @@ +@model LoginWithRecoveryCodeViewModel +@{ + ViewData["Title"] = "Recovery code verification"; +} + +

@ViewData["Title"]

+
+

+ You have requested to login with a recovery code. This login will not be remembered until you provide + an authenticator app code at login or disable 2FA and login again. +

+
+
+
+
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/Register.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/Register.cshtml new file mode 100644 index 0000000000..05c6676541 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/Register.cshtml @@ -0,0 +1,36 @@ +@model RegisterViewModel +@{ + ViewData["Title"] = "Register"; +} + +

@ViewData["Title"]

+ +
+
+
+

Create a new account.

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ResetPassword.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ResetPassword.cshtml new file mode 100644 index 0000000000..d55c6793eb --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ResetPassword.cshtml @@ -0,0 +1,36 @@ +@model ResetPasswordViewModel +@{ + ViewData["Title"] = "Reset password"; +} + +

@ViewData["Title"]

+

Reset your password.

+
+
+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ResetPasswordConfirmation.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ResetPasswordConfirmation.cshtml new file mode 100644 index 0000000000..7967cf4374 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/ResetPasswordConfirmation.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Reset password confirmation"; +} + +

@ViewData["Title"]

+

+ Your password has been reset. Please click here to log in. +

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/SignedOut.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/SignedOut.cshtml new file mode 100644 index 0000000000..c7fb988cd4 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Account/SignedOut.cshtml @@ -0,0 +1,8 @@ +@{ + ViewData["Title"] = "Signed out"; +} + +

@ViewData["Title"]

+

+ You have successfully signed out. +

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Home/About.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Home/About.cshtml new file mode 100644 index 0000000000..3674e37a86 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Home/About.cshtml @@ -0,0 +1,7 @@ +@{ + ViewData["Title"] = "About"; +} +

@ViewData["Title"]

+

@ViewData["Message"]

+ +

Use this area to provide additional information.

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Home/Contact.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Home/Contact.cshtml new file mode 100644 index 0000000000..a11a1867cf --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Home/Contact.cshtml @@ -0,0 +1,17 @@ +@{ + ViewData["Title"] = "Contact"; +} +

@ViewData["Title"]

+

@ViewData["Message"]

+ +
+ One Microsoft Way
+ Redmond, WA 98052-6399
+ P: + 425.555.0100 +
+ +
+ Support: Support@example.com
+ Marketing: Marketing@example.com +
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Home/Index.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Home/Index.cshtml new file mode 100644 index 0000000000..00afab6a0c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Home/Index.cshtml @@ -0,0 +1,108 @@ +@{ + ViewData["Title"] = "Home Page"; +} + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ChangePassword.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ChangePassword.cshtml new file mode 100644 index 0000000000..0f7b041c96 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ChangePassword.cshtml @@ -0,0 +1,35 @@ +@model ChangePasswordViewModel +@{ + ViewData["Title"] = "Change password"; + ViewData.AddActivePage(ManageNavPages.ChangePassword); +} + +

@ViewData["Title"]

+@Html.Partial("_StatusMessage", Model.StatusMessage) +
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/Disable2fa.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/Disable2fa.cshtml new file mode 100644 index 0000000000..cdd79cb62c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/Disable2fa.cshtml @@ -0,0 +1,24 @@ +@{ + ViewData["Title"] = "Disable two-factor authentication (2FA)"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+ + + +
+
+ +
+
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/EnableAuthenticator.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/EnableAuthenticator.cshtml new file mode 100644 index 0000000000..79693d78e3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/EnableAuthenticator.cshtml @@ -0,0 +1,52 @@ +@model EnableAuthenticatorViewModel +@{ + ViewData["Title"] = "Enable authenticator"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Windows Phone, + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

    Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    +
    To enable QR code generation please read our documentation.
    +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
  6. +
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ExternalLogins.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ExternalLogins.cshtml new file mode 100644 index 0000000000..e6f5687269 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ExternalLogins.cshtml @@ -0,0 +1,52 @@ +@model ExternalLoginsViewModel +@{ + ViewData["Title"] = "Manage your external logins"; + ViewData.AddActivePage(ManageNavPages.ExternalLogins); +} + +@Html.Partial("_StatusMessage", Model.StatusMessage) +@if (Model.CurrentLogins?.Count > 0) +{ +

Registered Logins

+ + + @foreach (var login in Model.CurrentLogins) + { + + + + + } + +
@login.LoginProvider + @if (Model.ShowRemoveButton) + { +
+
+ + + +
+
+ } + else + { + @:   + } +
+} +@if (Model.OtherLogins?.Count > 0) +{ +

Add another service to log in.

+
+
+
+

+ @foreach (var provider in Model.OtherLogins) + { + + } +

+
+
+} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/GenerateRecoveryCodes.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/GenerateRecoveryCodes.cshtml new file mode 100644 index 0000000000..669d13ef93 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/GenerateRecoveryCodes.cshtml @@ -0,0 +1,24 @@ +@model GenerateRecoveryCodesViewModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+ +
+
+ @for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } +
+
\ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/Index.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/Index.cshtml new file mode 100644 index 0000000000..c141c956d7 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/Index.cshtml @@ -0,0 +1,45 @@ +@model IndexViewModel +@{ + ViewData["Title"] = "Profile"; + ViewData.AddActivePage(ManageNavPages.Index); +} + +

@ViewData["Title"]

+@Html.Partial("_StatusMessage", Model.StatusMessage) +
+
+
+
+
+ + +
+
+ + @if (Model.IsEmailConfirmed) + { +
+ + +
+ } + else + { + + + } + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ManageNavPages.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ManageNavPages.cs new file mode 100644 index 0000000000..6b9bbe9dc7 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ManageNavPages.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace Company.WebApplication1.Views.Manage +{ + public static class ManageNavPages + { + public static string ActivePageKey => "ActivePage"; + + public static string Index => "Index"; + + public static string ChangePassword => "ChangePassword"; + + public static string ExternalLogins => "ExternalLogins"; + + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + public static string PageNavClass(ViewContext viewContext, string page) + { + var activePage = viewContext.ViewData["ActivePage"] as string; + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + } + + public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage; + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ResetAuthenticator.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ResetAuthenticator.cshtml new file mode 100644 index 0000000000..54a23613c9 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/ResetAuthenticator.cshtml @@ -0,0 +1,21 @@ +@{ + ViewData["Title"] = "Reset authenticator key"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+ +
+
+ +
+
\ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/SetPassword.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/SetPassword.cshtml new file mode 100644 index 0000000000..56c359935a --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/SetPassword.cshtml @@ -0,0 +1,34 @@ +@model SetPasswordViewModel +@{ + ViewData["Title"] = "Set password"; + ViewData.AddActivePage(ManageNavPages.ChangePassword); +} + +

Set your password

+@Html.Partial("_StatusMessage", Model.StatusMessage) +

+ You do not have a local username/password for this site. Add a local + account so you can log in without an external login. +

+
+
+
+
+
+ + + +
+
+ + + +
+ +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/TwoFactorAuthentication.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/TwoFactorAuthentication.cshtml new file mode 100644 index 0000000000..a2b52ac5b4 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/TwoFactorAuthentication.cshtml @@ -0,0 +1,49 @@ +@model TwoFactorAuthenticationViewModel +@{ + ViewData["Title"] = "Two-factor authentication"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

@ViewData["Title"]

+@if (Model.Is2faEnabled) +{ + if (Model.RecoveryCodesLeft == 0) + { +
+ You have no recovery codes left. +

You must generate a new set of recovery codes before you can log in with a recovery code.

+
+ } + else if (Model.RecoveryCodesLeft == 1) + { +
+ You have 1 recovery code left. +

You can generate a new set of recovery codes.

+
+ } + else if (Model.RecoveryCodesLeft <= 3) + { +
+ You have @Model.RecoveryCodesLeft recovery codes left. +

You should generate a new set of recovery codes.

+
+ } + + Disable 2FA + Reset recovery codes +} + +
Authenticator app
+@if (!Model.HasAuthenticator) +{ + Add authenticator app +} +else +{ + Configure authenticator app + Reset authenticator key +} + +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_Layout.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_Layout.cshtml new file mode 100644 index 0000000000..95dc87d1d9 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_Layout.cshtml @@ -0,0 +1,23 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; +} + +

Manage your account

+ +
+

Change your account settings

+
+
+
+ @await Html.PartialAsync("_ManageNav") +
+
+ @RenderBody() +
+
+
+ +@section Scripts { + @RenderSection("Scripts", required: false) +} + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_ManageNav.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_ManageNav.cshtml new file mode 100644 index 0000000000..5c9b54bccb --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_ManageNav.cshtml @@ -0,0 +1,16 @@ +@using Company.WebApplication1.Views.Manage +@inject SignInManager SignInManager +@{ + var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); +} + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_StatusMessage.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_StatusMessage.cshtml new file mode 100644 index 0000000000..e996841309 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_StatusMessage.cshtml @@ -0,0 +1,10 @@ +@model string + +@if (!String.IsNullOrEmpty(Model)) +{ + var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; + +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_ViewImports.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_ViewImports.cshtml new file mode 100644 index 0000000000..ad58b13e67 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Manage/_ViewImports.cshtml @@ -0,0 +1 @@ +@using Company.WebApplication1.Views.Manage \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/Error.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/Error.cshtml new file mode 100644 index 0000000000..ec2ea6bd03 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/Error.cshtml @@ -0,0 +1,22 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000000..1e1b736e1e --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/_Layout.cshtml @@ -0,0 +1,76 @@ + + + + + + @ViewData["Title"] - Company.WebApplication1 + + + + + + + + + + + + +
+ @RenderBody() +
+
+

© 1975 - Company.WebApplication1

+
+
+ + + + + + + + + + + + + @RenderSection("Scripts", required: false) + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.cshtml new file mode 100644 index 0000000000..2883a2e821 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/_LoginPartial.cshtml @@ -0,0 +1,68 @@ +@*#if (IndividualB2CAuth) +@using Microsoft.AspNetCore.Authentication +@using Microsoft.Extensions.Options + +#endif +@*#if (IndividualB2CAuth) +@inject IOptions AzureAdB2COptions + +#endif *@ +@*#if (IndividualLocalAuth) *@ +@using Microsoft.AspNetCore.Identity +@using Company.WebApplication1.Models +@*#else +@using System.Security.Principal +#endif *@ + +@*#if (IndividualLocalAuth) *@ +@inject SignInManager SignInManager +@inject UserManager UserManager +@*#endif *@ +@*#if (IndividualLocalAuth) *@ + +@if (SignInManager.IsSignedIn(User)) +@*#else +@if (User.Identity.IsAuthenticated) +#endif *@ +{ +@*#if (IndividualLocalAuth) *@ + +@*#elseif (IndividualB2CAuth) + +#else + +#endif *@ +} +else +{ + +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/_ValidationScriptsPartial.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000000..a699aafa97 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/_ViewImports.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/_ViewImports.cshtml new file mode 100644 index 0000000000..29aa8da2b3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/_ViewImports.cshtml @@ -0,0 +1,10 @@ +@*#if (IndividualLocalAuth) +@using Microsoft.AspNetCore.Identity +#endif*@ +@using Company.WebApplication1 +@using Company.WebApplication1.Models +@*#if (IndividualLocalAuth) +@using Company.WebApplication1.Models.AccountViewModels +@using Company.WebApplication1.Models.ManageViewModels +#endif*@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/_ViewStart.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/_ViewStart.cshtml new file mode 100644 index 0000000000..a5f10045db --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/app.db b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/app.db new file mode 100644 index 0000000000..ec163057f1 Binary files /dev/null and b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/app.db differ diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/appsettings.Development.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/appsettings.Development.json new file mode 100644 index 0000000000..fa8ce71a97 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/appsettings.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/appsettings.json new file mode 100644 index 0000000000..ce132995bc --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/appsettings.json @@ -0,0 +1,40 @@ +{ +////#if (IndividualB2CAuth) +// "AzureAdB2C": { +// "Instance": "https:////login.microsoftonline.com/tfp/", +// "ClientId": "11111111-1111-1111-11111111111111111", +// "CallbackPath": "/signin-oidc", +// "Domain": "qualified.domain.name", +// "SignUpSignInPolicyId": "MySignUpSignInPolicyId", +// "ResetPasswordPolicyId": "MyResetPasswordPolicyId", +// "EditProfilePolicyId": "MyEditProfilePolicyId" +// }, +////#elseif (OrganizationalAuth) +// "AzureAd": { +//#if (MultiOrgAuth) +// "Instance": "https:////login.microsoftonline.com/common", +//#elseif (SingleOrgAuth) +// "Instance": "https:////login.microsoftonline.com/", +// "Domain": "qualified.domain.name", +// "TenantId": "22222222-2222-2222-2222-222222222222", +//#endif +// "ClientId": "11111111-1111-1111-11111111111111111", +// "CallbackPath": "/signin-oidc" +// }, +//#endif +////#if (IndividualLocalAuth) +// "ConnectionStrings": { +////#if (UseLocalDB) +// "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true" +////#else +// "DefaultConnection": "DataSource=app.db" +//#endif +// }, +//#endif + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/bower.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/bower.json new file mode 100644 index 0000000000..b07e3cc5ae --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/bower.json @@ -0,0 +1,10 @@ +{ + "name": "asp.net", + "private": true, + "dependencies": { + "bootstrap": "3.3.7", + "jquery": "2.2.0", + "jquery-validation": "1.14.0", + "jquery-validation-unobtrusive": "3.2.6" + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/bundleconfig.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/bundleconfig.json new file mode 100644 index 0000000000..6d3f9a57ae --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/bundleconfig.json @@ -0,0 +1,24 @@ +// Configure bundling and minification for the project. +// More info at https://go.microsoft.com/fwlink/?LinkId=808241 +[ + { + "outputFileName": "wwwroot/css/site.min.css", + // An array of relative input file paths. Globbing patterns supported + "inputFiles": [ + "wwwroot/css/site.css" + ] + }, + { + "outputFileName": "wwwroot/js/site.min.js", + "inputFiles": [ + "wwwroot/js/site.js" + ], + // Optionally specify minification options + "minify": { + "enabled": true, + "renameLocals": true + }, + // Optionally generate .map file + "sourceMap": false + } +] diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/css/site.css b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/css/site.css new file mode 100644 index 0000000000..6d0f6e44ec --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/css/site.css @@ -0,0 +1,35 @@ +body { + padding-top: 50px; + padding-bottom: 20px; +} + +/* Wrapping element */ +/* Set some basic padding to keep content from hitting the edges */ +.body-content { + padding-left: 15px; + padding-right: 15px; +} + +/* Carousel */ +.carousel-caption p { + font-size: 20px; + line-height: 1.4; +} + +/* Make .svg files in the carousel display properly in older browsers */ +.carousel-inner .item img[src$=".svg"] { + width: 100%; +} + +/* QR code generator */ +#qrCode { + margin: 15px; +} + +/* Hide/rearrange for smaller screens */ +@media screen and (max-width: 767px) { + /* Hide captions */ + .carousel-caption { + display: none; + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/css/site.min.css b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/css/site.min.css new file mode 100644 index 0000000000..5e93e30ae3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/css/site.min.css @@ -0,0 +1 @@ +body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/favicon.ico b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/favicon.ico new file mode 100644 index 0000000000..a3a799985c Binary files /dev/null and b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/favicon.ico differ diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner1.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner1.svg new file mode 100644 index 0000000000..1ab32b60b8 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner2.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner2.svg new file mode 100644 index 0000000000..9679c604d0 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner3.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner3.svg new file mode 100644 index 0000000000..9be2c2503c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner3.svg @@ -0,0 +1 @@ +banner3b \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner4.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner4.svg new file mode 100644 index 0000000000..38b3d7cd1f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/images/banner4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/js/site.js b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/js/site.js new file mode 100644 index 0000000000..0f3411a45b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/js/site.js @@ -0,0 +1 @@ +// Write your JavaScript code. diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/js/site.min.js b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-CSharp/wwwroot/js/site.min.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/.bowerrc b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/.bowerrc new file mode 100644 index 0000000000..6406626abf --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "wwwroot/lib" +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/.template.config/dotnetcli.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..db2415c17b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,25 @@ +{ +"$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "TargetFrameworkOverride": { + "isHidden": "true", + "longName": "target-framework-override", + "shortName": "" + }, + "Framework": { + "longName": "framework" + }, + "skipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "RuntimeFrameworkVersion": { + "longName": "runtime-framework-version", + "shortName": "fv", + "isHidden": "true" + } + }, + "usageExamples": [ + "" + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/.template.config/template.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/.template.config/template.json new file mode 100644 index 0000000000..4e7067b84a --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/.template.config/template.json @@ -0,0 +1,74 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": ["Web", "MVC"], + "name": "ASP.NET Core Web App (Model-View-Controller)", + "generatorVersions": "[1.0.0.0-*)", + "description": "A project template for creating an ASP.NET Core application with example ASP.NET Core MVC Views and Controllers. This template can also be used for RESTful HTTP services.", + "groupIdentity": "Microsoft.Web.Mvc", + "precedence": "3000", + "identity": "Microsoft.Web.Mvc.FSharp.2.1", + "shortName": "mvc", + "thirdPartyNotices": "https://aka.ms/template-3pn", + "tags": { + "language": "F#", + "type": "project" + }, + "sourceName": "Company.WebApplication1", + "preferNameDirectory": true, + "symbols":{ + "RuntimeFrameworkVersion": { + "type": "parameter", + "replaces": "2.1.0-preview2-25624-02", + "datatype": "string", + "defaultValue": "2.1.0-preview2-25624-02" + }, + "TargetFrameworkOverride": { + "type": "parameter", + "description": "Overrides the target framework", + "replaces": "TargetFrameworkOverride", + "datatype": "string", + "defaultValue": "" + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "netcoreapp2.1", + "description": "Target netcoreapp2.1" + } + ], + "replaces": "netcoreapp2.1", + "defaultValue": "netcoreapp2.1" + }, + "copyrightYear": { + "type": "generated", + "generator": "now", + "replaces": "1975", + "parameters": { + "format": "yyyy" + } + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "If specified, skips the automatic restore of the project on create.", + "defaultValue": "false" + } + }, + "primaryOutputs": [ { "path": "Company.WebApplication1.fsproj" } ], + "defaultName": "WebApplication1", + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Company.WebApplication1.fsproj b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Company.WebApplication1.fsproj new file mode 100644 index 0000000000..7ad25e4813 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Company.WebApplication1.fsproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.1 + TargetFrameworkOverride + 2.1.0-preview2-25624-02 + 2.1.0-preview2-25624-02 + + true + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Controllers/HomeController.fs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Controllers/HomeController.fs new file mode 100644 index 0000000000..13271b6136 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Controllers/HomeController.fs @@ -0,0 +1,24 @@ +namespace Company.WebApplication1.Controllers + +open System +open System.Collections.Generic +open System.Linq +open System.Threading.Tasks +open Microsoft.AspNetCore.Mvc + +type HomeController () = + inherit Controller() + + member this.Index () = + this.View() + + member this.About () = + this.ViewData.["Message"] <- "Your application description page." + this.View() + + member this.Contact () = + this.ViewData.["Message"] <- "Your contact page." + this.View() + + member this.Error () = + this.View(); diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Models/ErrorViewModel.fs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Models/ErrorViewModel.fs new file mode 100644 index 0000000000..b028e7d4e4 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Models/ErrorViewModel.fs @@ -0,0 +1,8 @@ +namespace Company.WebApplication1 + +open System + +type ErrorViewModel private () = + member val RequestId : string = null with get, set + + member val ShowRequestId : bool = true with get, set diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Program.fs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Program.fs new file mode 100644 index 0000000000..2ba6d636b3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Program.fs @@ -0,0 +1,26 @@ +namespace Company.WebApplication1 + +open System +open System.Collections.Generic +open System.IO +open System.Linq +open System.Threading.Tasks +open Microsoft.AspNetCore +open Microsoft.AspNetCore.Hosting +open Microsoft.Extensions.Configuration +open Microsoft.Extensions.Logging + +module Program = + let exitCode = 0 + + let BuildWebHost args = + WebHost + .CreateDefaultBuilder(args) + .UseStartup() + .Build() + + [] + let main args = + BuildWebHost(args).Run() + + exitCode diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Startup.fs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Startup.fs new file mode 100644 index 0000000000..0ff184dc1c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Startup.fs @@ -0,0 +1,39 @@ +namespace Company.WebApplication1 + +open System +open System.Collections.Generic +open System.Linq +open System.Threading.Tasks +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Hosting +open Microsoft.Extensions.Configuration +open Microsoft.Extensions.DependencyInjection + + +type Startup private () = + new (configuration: IConfiguration) as this = + Startup() then + this.Configuration <- configuration + + // This method gets called by the runtime. Use this method to add services to the container. + member this.ConfigureServices(services: IServiceCollection) = + // Add framework services. + services.AddMvc() |> ignore + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + member this.Configure(app: IApplicationBuilder, env: IHostingEnvironment) = + + if (env.IsDevelopment()) then + app.UseDeveloperExceptionPage() |> ignore + else + app.UseExceptionHandler("/Home/Error") |> ignore + + app.UseStaticFiles() |> ignore + + app.UseMvc(fun routes -> + routes.MapRoute( + name = "default", + template = "{controller=Home}/{action=Index}/{id?}") |> ignore + ) |> ignore + + member val Configuration : IConfiguration = null with get, set diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Home/About.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Home/About.cshtml new file mode 100644 index 0000000000..50476d1fbd --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Home/About.cshtml @@ -0,0 +1,7 @@ +@{ + ViewData["Title"] = "About"; +} +

@ViewData["Title"].

+

@ViewData["Message"]

+ +

Use this area to provide additional information.

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Home/Contact.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Home/Contact.cshtml new file mode 100644 index 0000000000..15c12c6d12 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Home/Contact.cshtml @@ -0,0 +1,17 @@ +@{ + ViewData["Title"] = "Contact"; +} +

@ViewData["Title"].

+

@ViewData["Message"]

+ +
+ One Microsoft Way
+ Redmond, WA 98052-6399
+ P: + 425.555.0100 +
+ +
+ Support: Support@example.com
+ Marketing: Marketing@example.com +
diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Home/Index.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Home/Index.cshtml new file mode 100644 index 0000000000..00afab6a0c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Home/Index.cshtml @@ -0,0 +1,108 @@ +@{ + ViewData["Title"] = "Home Page"; +} + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Shared/Error.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Shared/Error.cshtml new file mode 100644 index 0000000000..ec2ea6bd03 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Shared/Error.cshtml @@ -0,0 +1,22 @@ +@model ErrorViewModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Shared/_Layout.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000000..9b7545903b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Shared/_Layout.cshtml @@ -0,0 +1,71 @@ + + + + + + @ViewData["Title"] - Company.WebApplication1 + + + + + + + + + + + + +
+ @RenderBody() +
+
+

© 1975 - Company.WebApplication1

+
+
+ + + + + + + + + + + + + @RenderSection("Scripts", required: false) + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Shared/_ValidationScriptsPartial.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Shared/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000000..a2b13b317f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/Shared/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/_ViewImports.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/_ViewImports.cshtml new file mode 100644 index 0000000000..e3f167d1f5 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using Company.WebApplication1 +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/_ViewStart.cshtml b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/_ViewStart.cshtml new file mode 100644 index 0000000000..a5f10045db --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/appsettings.Development.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/appsettings.Development.json new file mode 100644 index 0000000000..fa8ce71a97 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/appsettings.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/appsettings.json new file mode 100644 index 0000000000..26bb0ac7ac --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/bower.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/bower.json new file mode 100644 index 0000000000..b07e3cc5ae --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/bower.json @@ -0,0 +1,10 @@ +{ + "name": "asp.net", + "private": true, + "dependencies": { + "bootstrap": "3.3.7", + "jquery": "2.2.0", + "jquery-validation": "1.14.0", + "jquery-validation-unobtrusive": "3.2.6" + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/bundleconfig.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/bundleconfig.json new file mode 100644 index 0000000000..6d3f9a57ae --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/bundleconfig.json @@ -0,0 +1,24 @@ +// Configure bundling and minification for the project. +// More info at https://go.microsoft.com/fwlink/?LinkId=808241 +[ + { + "outputFileName": "wwwroot/css/site.min.css", + // An array of relative input file paths. Globbing patterns supported + "inputFiles": [ + "wwwroot/css/site.css" + ] + }, + { + "outputFileName": "wwwroot/js/site.min.js", + "inputFiles": [ + "wwwroot/js/site.js" + ], + // Optionally specify minification options + "minify": { + "enabled": true, + "renameLocals": true + }, + // Optionally generate .map file + "sourceMap": false + } +] diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/css/site.css b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/css/site.css new file mode 100644 index 0000000000..e31abdefd3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/css/site.css @@ -0,0 +1,37 @@ +body { + padding-top: 50px; + padding-bottom: 20px; +} + +/* Wrapping element */ +/* Set some basic padding to keep content from hitting the edges */ +.body-content { + padding-left: 15px; + padding-right: 15px; +} + +/* Set widths on the form inputs since otherwise they're 100% wide */ +input, +select, +textarea { + max-width: 280px; +} + +/* Carousel */ +.carousel-caption p { + font-size: 20px; + line-height: 1.4; +} + +/* Make .svg files in the carousel display properly in older browsers */ +.carousel-inner .item img[src$=".svg"] { + width: 100%; +} + +/* Hide/rearrange for smaller screens */ +@media screen and (max-width: 767px) { + /* Hide captions */ + .carousel-caption { + display: none; + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/css/site.min.css b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/css/site.min.css new file mode 100644 index 0000000000..3beb45f52f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/css/site.min.css @@ -0,0 +1 @@ +body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}input,select,textarea{max-width:280px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}@media screen and (max-width:767px){.carousel-caption{display:none}} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/favicon.ico b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/favicon.ico new file mode 100644 index 0000000000..a3a799985c Binary files /dev/null and b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/favicon.ico differ diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner1.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner1.svg new file mode 100644 index 0000000000..1ab32b60b8 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner2.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner2.svg new file mode 100644 index 0000000000..9679c604d0 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner3.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner3.svg new file mode 100644 index 0000000000..9be2c2503c --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner3.svg @@ -0,0 +1 @@ +banner3b \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner4.svg b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner4.svg new file mode 100644 index 0000000000..38b3d7cd1f --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/images/banner4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/js/site.js b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/js/site.js new file mode 100644 index 0000000000..82ecce7b4a --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/js/site.js @@ -0,0 +1 @@ +// Write your Javascript code. diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/js/site.min.js b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/StarterWeb-FSharp/wwwroot/js/site.min.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/dotnetcli.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..4cd9a7a737 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,72 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "TargetFrameworkOverride": { + "isHidden": "true", + "longName": "target-framework-override", + "shortName": "" + }, + "UseLocalDB": { + "longName": "use-local-db" + }, + "AADInstance": { + "longName": "aad-instance", + "shortName": "" + }, + "AAdB2CInstance": { + "longName": "aad-b2c-instance", + "shortName": "" + }, + "SignUpSignInPolicyId": { + "longName": "susi-policy-id", + "shortName": "ssp" + }, + "OrgReadAccess": { + "longName": "org-read-access", + "shortName": "r" + }, + "ClientId": { + "longName": "client-id", + "shortName": "" + }, + "Domain": { + "longName": "domain", + "shortName": "" + }, + "TenantId": { + "longName": "tenant-id", + "shortName": "" + }, + "Framework": { + "longName": "framework" + }, + "skipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "HttpsPort": { + "isHidden": true + }, + "KestrelPort": { + "isHidden": true + }, + "IISExpressPort": { + "isHidden": true + }, + "IncludeLaunchSettings": { + "longName": "use-launch-settings", + "shortName": "" + }, + "UserSecretsId": { + "isHidden": true + }, + "RuntimeFrameworkVersion": { + "longName": "runtime-framework-version", + "shortName": "fv", + "isHidden": "true" + } + }, + "usageExamples": [ + "" + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/template.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/template.json new file mode 100644 index 0000000000..152435cbbf --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/template.json @@ -0,0 +1,284 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": ["Web", "WebAPI"], + "name": "ASP.NET Core Web API", + "generatorVersions": "[1.0.0.0-*)", + "description": "A project template for creating an ASP.NET Core application with an example Controller for a RESTful HTTP service. This template can also be used for ASP.NET Core MVC Views and Controllers.", + "groupIdentity": "Microsoft.Web.WebApi", + "precedence": "3000", + "identity": "Microsoft.Web.WebApi.CSharp.2.1", + "shortName": "webapi", + "thirdPartyNotices": "https://aka.ms/template-3pn", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Company.WebApplication1", + "preferNameDirectory": true, + "guids": [ + "09732173-2cef-46b7-83db-1334bcb079d3", // Tenant ID + "53bc9b9d-9d6a-45d4-8429-2a2761773502" // Client ID + ], + "sources": [ + { + "modifiers": [ + { + "condition": "(windir == 'C:\\Windows')", + "exclude": [ + "Properties/launchSettings.json" + ] + }, + { + "condition": "(!IncludeLaunchSettings && !WindowsAuth)", + "exclude": [ + "Properties/launchSettings.json" + ] + }, + { + "condition": "(!OrganizationalAuth)", + "exclude": [ + "Extensions/AzureAdAuthenticationBuilderExtensions.cs", + "Extensions/AzureAdOptions.cs" + ] + }, + { + "condition": "(!IndividualB2CAuth)", + "exclude": [ + "Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs", + "Extensions/AzureAdB2COptions.cs" + ] + } + ] + } + ], + "symbols": { + "RuntimeFrameworkVersion": { + "type": "parameter", + "replaces": "2.1.0-preview2-25624-02", + "datatype": "string", + "defaultValue": "2.1.0-preview2-25624-02" + }, + "auth": { + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "None", + "description": "No authentication" + }, + { + "choice": "IndividualB2C", + "description": "Individual authentication with Azure AD B2C" + }, + { + "choice": "SingleOrg", + "description": "Organizational authentication for a single tenant" + }, + { + "choice": "Windows", + "description": "Windows authentication" + } + ], + "defaultValue": "None", + "description": "The type of authentication to use" + }, + "AAdB2CInstance": { + "type": "parameter", + "datatype": "string", + "defaultValue": "https://login.microsoftonline.com/tfp/", + "replaces": "https:////login.microsoftonline.com/tfp/", + "description": "The Azure Active Directory B2C instance to connect to (use with IndividualB2C auth)." + }, + "SignUpSignInPolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MySignUpSignInPolicyId", + "description": "The sign-in and sign-up policy ID for this project (use with IndividualB2C auth)." + }, + "AADInstance": { + "type": "parameter", + "datatype": "string", + "defaultValue": "https://login.microsoftonline.com/", + "replaces": "https:////login.microsoftonline.com/", + "description": "The Azure Active Directory instance to connect to (use with SingleOrg auth)." + }, + "ClientId": { + "type": "parameter", + "datatype": "string", + "replaces": "11111111-1111-1111-11111111111111111", + "description": "The Client ID for this project (use with SingleOrg or IndividualB2C auth)." + }, + "Domain": { + "type": "parameter", + "datatype": "string", + "replaces": "qualified.domain.name", + "description": "The domain for the directory tenant (use with SingleOrg or IndividualB2C auth)." + }, + "TenantId": { + "type": "parameter", + "datatype": "string", + "replaces": "22222222-2222-2222-2222-222222222222", + "description": "The TenantId ID of the directory to connect to (use with SingleOrg auth)." + }, + "OrgReadAccess": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether or not to allow this application read access to the directory (only applies to SingleOrg auth)." + }, + "UserSecretsId": { + "type": "parameter", + "datatype": "string", + "replaces": "aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "defaultValue": "aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "description": "The ID to use for secrets (use with OrgReadAccess or Individual auth)." + }, + "IncludeLaunchSettings": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether to include launchSettings.json in the generated template." + }, + "HttpsPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure SSL in launchSettings.json." + }, + "HttpsPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "low": 44300, + "high": 44399 + } + }, + "HttpsPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "HttpsPort", + "fallbackVariableName": "HttpsPortGenerated" + }, + "replaces": "43434" + }, + "KestrelPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure Kestrel in launchSettings.json." + }, + "KestrelPortGenerated": { + "type": "generated", + "generator": "port" + }, + "KestrelPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "KestrelPort", + "fallbackVariableName": "KestrelPortGenerated" + }, + "replaces": "5000" + }, + "IISExpressPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure IIS Express in launchSettings.json." + }, + "IISExpressPortGenerated": { + "type": "generated", + "generator": "port" + }, + "IISExpressPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "IISExpressPort", + "fallbackVariableName": "IISExpressPortGenerated" + }, + "replaces": "55556" + }, + "OrganizationalAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\")" + }, + "WindowsAuth": { + "type": "computed", + "value": "(auth == \"Windows\")" + }, + "SingleOrgAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\")" + }, + "IndividualAuth": { + "type": "computed", + "value": "(auth == \"IndividualB2C\")" + }, + "IndividualB2CAuth": { + "type": "computed", + "value": "(auth == \"IndividualB2C\")" + }, + "NoAuth": { + "type": "computed", + "value": "(!(IndividualAuth || OrganizationalAuth || WindowsAuth))" + }, + "RequiresHttps": { + "type": "computed", + "value": "(OrganizationalAuth || IndividualAuth)" + }, + "UseLocalDB": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether to use LocalDB instead of SQLite. This option only applies if --auth Individual or --auth IndividualB2C is specified." + }, + "TargetFrameworkOverride": { + "type": "parameter", + "description": "Overrides the target framework", + "replaces": "TargetFrameworkOverride", + "datatype": "string", + "defaultValue": "" + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "netcoreapp2.1", + "description": "Target netcoreapp2.1" + } + ], + "replaces": "netcoreapp2.1", + "defaultValue": "netcoreapp2.1" + }, + "copyrightYear": { + "type": "generated", + "generator": "now", + "replaces": "1975", + "parameters": { + "format": "yyyy" + } + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "If specified, skips the automatic restore of the project on create.", + "defaultValue": "false" + } + }, + "primaryOutputs": [ { "path": "Company.WebApplication1.csproj" } ], + "defaultName": "WebApplication1", + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/vs-2017.3.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/vs-2017.3.host.json new file mode 100644 index 0000000000..6a1ebc6e41 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/vs-2017.3.host.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json.schemastore.org/vs-2017.3.host", + "name": { + "text": "Web API", + "package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}", + "id": "1013" + }, + "description": { + "text": "A project template for creating an ASP.NET Core application with an example Controller for a RESTful HTTP service. This template can also be used for ASP.NET Core MVC Views and Controllers.", + "package": "{0CD94836-1526-4E85-87D3-FB5274C5AFC9}", + "id": "1014" + }, + "order": 200, + "icon": "vs-2017.3/WebAPI.png", + "learnMoreLink": "https://go.microsoft.com/fwlink/?LinkID=784882", + "uiFilters": [ "oneaspnet" ], + "supportsDocker": true, + "isApi": true, + "usesOidc": true, + "legacyTemplateIdentity": "Microsoft.NetCore.CSharp.WebAPI", + "supportedAuthentications": [ + { + "auth": "None", + "authenticationType": "NoAuth" + }, + { + "auth": "Individual", + "authenticationType": "IndividualAuth", + "b2cAuthenticationOptions": "CloudExisting" + }, + { + "auth": "SingleOrg", + "authenticationType": "OrgAuth", + "orgAuthenticationOptions": "SSO" + }, + { + "auth": "Windows", + "authenticationType": "WindowsAuth" + } + ], + "ports": [ + { + "name": "IISExpressPort", + "useHttps": false + }, + { + "name": "KestrelPort", + "useHttps": false + }, + { + "name": "HttpsPort", + "useHttps": true + } + ], + "azureReplyUrlPortName": "HttpsPort", + "includeLaunchSettings": true, + "minFullFrameworkVersion": "4.6.1" +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/vs-2017.3/WebAPI.png b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/vs-2017.3/WebAPI.png new file mode 100644 index 0000000000..61ba1afdce Binary files /dev/null and b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/.template.config/vs-2017.3/WebAPI.png differ diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Company.WebApplication1.csproj b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Company.WebApplication1.csproj new file mode 100644 index 0000000000..4ab1fb19d3 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Company.WebApplication1.csproj @@ -0,0 +1,32 @@ + + + + netcoreapp2.1 + TargetFrameworkOverride + 2.1.0-preview2-25624-02 + 2.1.0-preview2-25624-02 + + aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502 + 0 + 1 + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Controllers/ValuesController.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Controllers/ValuesController.cs new file mode 100644 index 0000000000..612508ce71 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Controllers/ValuesController.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +#if (!NoAuth) +using Microsoft.AspNetCore.Authorization; +#endif +using Microsoft.AspNetCore.Mvc; + +namespace Company.WebApplication1.Controllers +{ +#if (!NoAuth) + [Authorize] +#endif + [Route("api/[controller]")] + public class ValuesController : Controller + { + // GET api/values + [HttpGet] + public IEnumerable Get() + { + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + public string Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody]string value) + { +#if (OrganizationalAuth || WindowsAuth) + // For more information on protecting this API from Cross Site Request Forgery (CSRF) attacks, see https://go.microsoft.com/fwlink/?LinkID=717803 +#endif + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody]string value) + { +#if (OrganizationalAuth || WindowsAuth) + // For more information on protecting this API from Cross Site Request Forgery (CSRF) attacks, see https://go.microsoft.com/fwlink/?LinkID=717803 +#endif + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { +#if (OrganizationalAuth || WindowsAuth) + // For more information on protecting this API from Cross Site Request Forgery (CSRF) attacks, see https://go.microsoft.com/fwlink/?LinkID=717803 +#endif + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdAuthenticationBuilderExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdAuthenticationBuilderExtensions.cs new file mode 100644 index 0000000000..b8c4e08f0b --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdAuthenticationBuilderExtensions.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class AzureAdServiceCollectionExtensions + { + public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder) + => builder.AddAzureAdBearer(_ => { }); + + public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action configureOptions) + { + builder.Services.Configure(configureOptions); + builder.Services.AddSingleton, ConfigureAzureOptions>(); + builder.AddJwtBearer(); + return builder; + } + + private class ConfigureAzureOptions: IConfigureNamedOptions + { + private readonly AzureAdOptions _azureOptions; + + public ConfigureAzureOptions(IOptions azureOptions) + { + _azureOptions = azureOptions.Value; + } + + public void Configure(string name, JwtBearerOptions options) + { + options.Audience = _azureOptions.ClientId; + options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}"; + } + + public void Configure(JwtBearerOptions options) + { + Configure(Options.DefaultName, options); + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs new file mode 100644 index 0000000000..62e5c580d0 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdB2CAuthenticationBuilderExtensions.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class AzureAdServiceCollectionExtensions + { + public static AuthenticationBuilder AddAzureAdB2CBearer(this AuthenticationBuilder builder) + => builder.AddAzureAdB2CBearer(_ => { }); + + public static AuthenticationBuilder AddAzureAdB2CBearer(this AuthenticationBuilder builder, Action configureOptions) + { + builder.Services.Configure(configureOptions); + builder.Services.AddSingleton, ConfigureAzureOptions>(); + builder.AddJwtBearer(); + return builder; + } + + private class ConfigureAzureOptions: IConfigureNamedOptions + { + private readonly AzureAdB2COptions _azureOptions; + + public ConfigureAzureOptions(IOptions azureOptions) + { + _azureOptions = azureOptions.Value; + } + + public void Configure(string name, JwtBearerOptions options) + { + options.Audience = _azureOptions.ClientId; + options.Authority = $"{_azureOptions.Instance}/{_azureOptions.Domain}/{_azureOptions.SignUpSignInPolicyId}/v2.0"; + } + + public void Configure(JwtBearerOptions options) + { + Configure(Options.DefaultName, options); + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdB2COptions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdB2COptions.cs new file mode 100644 index 0000000000..11c46c5592 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdB2COptions.cs @@ -0,0 +1,10 @@ +namespace Microsoft.AspNetCore.Authentication +{ + public class AzureAdB2COptions + { + public string ClientId { get; set; } + public string Instance { get; set; } + public string Domain { get; set; } + public string SignUpSignInPolicyId { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdOptions.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdOptions.cs new file mode 100644 index 0000000000..8a2b051165 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Extensions/AzureAdOptions.cs @@ -0,0 +1,11 @@ +namespace Microsoft.AspNetCore.Authentication +{ + public class AzureAdOptions + { + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string Instance { get; set; } + public string Domain { get; set; } + public string TenantId { get; set; } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Program.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Program.cs new file mode 100644 index 0000000000..13019a4fe2 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Program.cs @@ -0,0 +1,25 @@ +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.Logging; + +namespace Company.WebApplication1 +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Startup.cs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Startup.cs new file mode 100644 index 0000000000..13a6989a81 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/Startup.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +#if (OrganizationalAuth || IndividualB2CAuth) +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +#endif +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Company.WebApplication1 +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { +#if (OrganizationalAuth || IndividualB2CAuth) + services.AddAuthentication(sharedOptions => + { + sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + #if (IndividualB2CAuth) + .AddAzureAdB2CBearer(options => Configuration.Bind("AzureAdB2C", options)); + #elseif (OrganizationalAuth) + .AddAzureAdBearer(options => Configuration.Bind("AzureAd", options)); + #endif + +#endif + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + +#if (OrganizationalAuth || IndividualAuth) + app.UseAuthentication(); +#endif + app.UseMvc(); + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/appsettings.Development.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/appsettings.Development.json new file mode 100644 index 0000000000..fa8ce71a97 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/appsettings.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/appsettings.json new file mode 100644 index 0000000000..aa6ce15e45 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/appsettings.json @@ -0,0 +1,34 @@ +{ +////#if (IndividualB2CAuth) +// "AzureAdB2C": { +// "Instance": "https:////login.microsoftonline.com/tfp/", +// "ClientId": "11111111-1111-1111-11111111111111111", +// "Domain": "qualified.domain.name", +// "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" +// }, +//#endif + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/wwwroot/-.- b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-CSharp/wwwroot/-.- new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/.template.config/dotnetcli.host.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..04017b2a43 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "TargetFrameworkOverride": { + "isHidden": "true", + "longName": "target-framework-override", + "shortName": "" + }, + "Framework": { + "longName": "framework" + }, + "skipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "RuntimeFrameworkVersion": { + "longName": "runtime-framework-version", + "shortName": "fv", + "isHidden": "true" + } + }, + "usageExamples": [ + "" + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/.template.config/template.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/.template.config/template.json new file mode 100644 index 0000000000..b4a3ef5706 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/.template.config/template.json @@ -0,0 +1,73 @@ +{ + "author": "Microsoft", + "classifications": ["Web", "WebAPI"], + "name": "ASP.NET Core Web API", + "generatorVersions": "[1.0.0.0-*)", + "description": "A project template for creating an ASP.NET Core application with an example Controller for a RESTful HTTP service. This template can also be used for ASP.NET Core MVC Views and Controllers.", + "groupIdentity": "Microsoft.Web.WebApi", + "precedence": "3000", + "identity": "Microsoft.Web.WebApi.FSharp.2.1", + "shortName": "webapi", + "thirdPartyNotices": "https://aka.ms/template-3pn", + "tags": { + "language": "F#", + "type": "project" + }, + "sourceName": "Company.WebApplication1", + "preferNameDirectory": true, + "symbols": { + "RuntimeFrameworkVersion": { + "type": "parameter", + "replaces": "2.1.0-preview2-25624-02", + "datatype": "string", + "defaultValue": "2.1.0-preview2-25624-02" + }, + "TargetFrameworkOverride": { + "type": "parameter", + "description": "Overrides the target framework", + "replaces": "TargetFrameworkOverride", + "datatype": "string", + "defaultValue": "" + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "netcoreapp2.1", + "description": "Target netcoreapp2.1" + } + ], + "replaces": "netcoreapp2.1", + "defaultValue": "netcoreapp2.1" + }, + "copyrightYear": { + "type": "generated", + "generator": "now", + "replaces": "1975", + "parameters": { + "format": "yyyy" + } + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "If specified, skips the automatic restore of the project on create.", + "defaultValue": "false" + } + }, + "primaryOutputs": [ { "path": "Company.WebApplication1.fsproj" } ], + "defaultName": "WebApplication1", + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Company.WebApplication1.fsproj b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Company.WebApplication1.fsproj new file mode 100644 index 0000000000..887b701c79 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Company.WebApplication1.fsproj @@ -0,0 +1,24 @@ + + + + netcoreapp2.1 + TargetFrameworkOverride + 2.1.0-preview2-25624-02 + 2.1.0-preview2-25624-02 + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Controllers/ValuesController.fs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Controllers/ValuesController.fs new file mode 100644 index 0000000000..b26a809bb4 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Controllers/ValuesController.fs @@ -0,0 +1,31 @@ +namespace Company.WebApplication1.Controllers + +open System +open System.Collections.Generic +open System.Linq +open System.Threading.Tasks +open Microsoft.AspNetCore.Mvc + +[] +type ValuesController () = + inherit Controller() + + [] + member this.Get() = + [|"value1"; "value2"|] + + [] + member this.Get(id:int) = + "value" + + [] + member this.Post([]value:string) = + () + + [] + member this.Put(id:int, []value:string ) = + () + + [] + member this.Delete(id:int) = + () diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Program.fs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Program.fs new file mode 100644 index 0000000000..6b45d8eb71 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Program.fs @@ -0,0 +1,26 @@ +namespace Company.WebApplication1 + +open System +open System.Collections.Generic +open System.IO +open System.Linq +open System.Threading.Tasks +open Microsoft.AspNetCore +open Microsoft.AspNetCore.Hosting +open Microsoft.Extensions.Configuration +open Microsoft.Extensions.Logging + +module Program = + let exitCode = 0 + + let BuildWebHost args = + WebHost + .CreateDefaultBuilder(args) + .UseStartup() + .Build() + + [] + let main args = + BuildWebHost(args).Run() + + exitCode diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Startup.fs b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Startup.fs new file mode 100644 index 0000000000..075193a4aa --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/Startup.fs @@ -0,0 +1,27 @@ +namespace Company.WebApplication1 + +open System +open System.Collections.Generic +open System.Linq +open System.Threading.Tasks +open Microsoft.AspNetCore.Builder +open Microsoft.AspNetCore.Hosting +open Microsoft.Extensions.Configuration +open Microsoft.Extensions.DependencyInjection + + +type Startup private () = + new (configuration: IConfiguration) as this = + Startup() then + this.Configuration <- configuration + + // This method gets called by the runtime. Use this method to add services to the container. + member this.ConfigureServices(services: IServiceCollection) = + // Add framework services. + services.AddMvc() |> ignore + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + member this.Configure(app: IApplicationBuilder, env: IHostingEnvironment) = + app.UseMvc() |> ignore + + member val Configuration : IConfiguration = null with get, set \ No newline at end of file diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/appsettings.Development.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/appsettings.Development.json new file mode 100644 index 0000000000..fa8ce71a97 --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/appsettings.json b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/appsettings.json new file mode 100644 index 0000000000..5fff67bacc --- /dev/null +++ b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/wwwroot/-.- b/src/Microsoft.DotNet.Web.ProjectTemplates.2.1/content/WebApi-FSharp/wwwroot/-.- new file mode 100644 index 0000000000..e69de29bb2