diff --git a/aspnetcore/fundamentals/logging/loggermessage.md b/aspnetcore/fundamentals/logging/loggermessage.md index 19d2cb9cf5..c9614dc263 100644 --- a/aspnetcore/fundamentals/logging/loggermessage.md +++ b/aspnetcore/fundamentals/logging/loggermessage.md @@ -29,15 +29,7 @@ The sample app demonstrates `LoggerMessage` features with a basic quote tracking [Define(LogLevel, EventId, String)](/dotnet/api/microsoft.extensions.logging.loggermessage.define) creates an `Action` delegate for logging a message. `Define` overloads permit passing up to six type parameters to a named format string (template). -## LoggerMessage.DefineScope - -[DefineScope(String)](/dotnet/api/microsoft.extensions.logging.loggermessage.definescope) creates a `Func` delegate for defining a [log scope](xref:fundamentals/logging/index#log-scopes). `DefineScope` overloads permit passing up to three type parameters to a named format string (template). - -## Message template (named format string) - -The string provided to the `Define` and `DefineScope` methods is a template and not an interpolated string. Placeholders are filled in the order that the types are specified. Placeholder names in the template should be descriptive and consistent across templates. They serve as property names within structured log data. We recommend [Pascal casing](/dotnet/standard/design-guidelines/capitalization-conventions) for placeholder names. For example, `{Count}`, `{FirstName}`. - -## Implementing LoggerMessage.Define +The string provided to the `Define` method is a template and not an interpolated string. Placeholders are filled in the order that the types are specified. Placeholder names in the template should be descriptive and consistent across templates. They serve as property names within structured log data. We recommend [Pascal casing](/dotnet/standard/design-guidelines/capitalization-conventions) for placeholder names. For example, `{Count}`, `{FirstName}`. Each log message is an `Action` held in a static field created by `LoggerMessage.Define`. For example, the sample app creates a field to describe a log message for a GET request for the Index page (*Internal/LoggerExtensions.cs*): @@ -136,7 +128,11 @@ Parameter name: entity \sample\Pages\Index.cshtml.cs:line 87 ``` -## Implementing LoggerMessage.DefineScope +## LoggerMessage.DefineScope + +[DefineScope(String)](/dotnet/api/microsoft.extensions.logging.loggermessage.definescope) creates a `Func` delegate for defining a [log scope](xref:fundamentals/logging/index#log-scopes). `DefineScope` overloads permit passing up to three type parameters to a named format string (template). + +As is the case with the `Define` method, the string provided to the `DefineScope` method is a template and not an interpolated string. Placeholders are filled in the order that the types are specified. Placeholder names in the template should be descriptive and consistent across templates. They serve as property names within structured log data. We recommend [Pascal casing](/dotnet/standard/design-guidelines/capitalization-conventions) for placeholder names. For example, `{Count}`, `{FirstName}`. Define a [log scope](xref:fundamentals/logging/index#log-scopes) to apply to a series of log messages using the [DefineScope(String)](/dotnet/api/microsoft.extensions.logging.loggermessage.definescope) method. diff --git a/aspnetcore/host-and-deploy/azure-apps/index.md b/aspnetcore/host-and-deploy/azure-apps/index.md index 2877a41700..928ec3dd2c 100644 --- a/aspnetcore/host-and-deploy/azure-apps/index.md +++ b/aspnetcore/host-and-deploy/azure-apps/index.md @@ -1,11 +1,11 @@ --- title: Host ASP.NET Core on Azure App Service author: guardrex -description: Discover links to resources for learning how to host ASP.NET Core apps in Azure App Service. +description: Discover how to host ASP.NET Core apps in Azure App Service with links to helpful resources. manager: wpickett ms.author: riande ms.custom: mvc -ms.date: 01/08/2018 +ms.date: 01/29/2018 ms.prod: asp.net-core ms.technology: aspnet ms.topic: article @@ -13,13 +13,25 @@ uid: host-and-deploy/azure-apps/index --- # Host ASP.NET Core on Azure App Service -The following articles are available for learning about hosting ASP.NET Core apps in Azure App Service: +[Azure App Service](https://azure.microsoft.com/services/app-service/) is a [Microsoft cloud computing platform service](https://azure.microsoft.com/) for hosting web apps, including ASP.NET Core. + +## Useful resources + +The Azure [Web Apps Documentation](/azure/app-service/) is the home for Azure Apps documentation, tutorials, samples, how-to guides, and other resources. Two notable tutorials that pertain to hosting ASP.NET Core apps are: + +[Quickstart: Create an ASP.NET Core web app in Azure](/azure/app-service/app-service-web-get-started-dotnet) +Use Visual Studio to create and deploy an ASP.NET Core web app to Azure App Service on Windows. + +[Quickstart: Create a .NET Core web app in App Service on Linux](/azure/app-service/containers/quickstart-dotnetcore) +Use the command line to create and deploy an ASP.NET Core web app to Azure App Service on Linux. + +The following articles are available in ASP.NET Core documentation: [Publish to Azure with Visual Studio](xref:tutorials/publish-to-azure-webapp-using-vs) Learn how to publish an ASP.NET Core app to Azure App Service using Visual Studio. [Publish to Azure with CLI tools](xref:tutorials/publish-to-azure-webapp-using-cli) -Learn how to publish an ASP.NET Core app to Azure App Service using the Git command line client. +Learn how to publish an ASP.NET Core app to Azure App Service using the Git command-line client. [Continuous deployment to Azure with Visual Studio and Git](xref:host-and-deploy/azure-apps/azure-continuous-deployment) Learn how to create an ASP.NET Core web app using Visual Studio and deploy it to Azure App Service using Git for continuous deployment. @@ -27,5 +39,57 @@ Learn how to create an ASP.NET Core web app using Visual Studio and deploy it to [Continuous deployment to Azure with VSTS](https://www.visualstudio.com/docs/build/aspnet/core/quick-to-azure) Set up a CI build for an ASP.NET Core app, then create a continuous deployment release to Azure App Service. -[Troubleshoot ASP.NET Core on Azure App Service](xref:host-and-deploy/azure-apps/troubleshoot) (*Topic under development*) -Learn how to diagnose problems with ASP.NET Core Azure App Service deployments. +## Application configuration + +With ASP.NET Core 2.0 and later, three packages in the [Microsoft.AspNetCore.All metapackage](xref:fundamentals/metapackage) provide automatic logging features for apps deployed to Azure App Service: + +* [Microsoft.AspNetCore.AzureAppServices.HostingStartup](https://www.nuget.org/packages/Microsoft.AspNetCore.AzureAppServices.HostingStartup/) uses [IHostingStartup](xref:host-and-deploy/ihostingstartup) to provide ASP.NET Core lightup integration with Azure App Service. The added logging features are provided by the `Microsoft.AspNetCore.AzureAppServicesIntegration` package. +* [Microsoft.AspNetCore.AzureAppServicesIntegration](https://www.nuget.org/packages/Microsoft.AspNetCore.AzureAppServicesIntegration/) executes [AddAzureWebAppDiagnostics](/dotnet/api/microsoft.extensions.logging.azureappservicesloggerfactoryextensions.addazurewebappdiagnostics) to add Azure App Service diagnostics logging providers in the `Microsoft.Extensions.Logging.AzureAppServices` package. +* [Microsoft.Extensions.Logging.AzureAppServices](https://www.nuget.org/packages/Microsoft.Extensions.Logging.AzureAppServices/) provides logger implementations to support Azure App Service diagnostics logs and log streaming features. + +## Monitoring and logging + +For monitoring, logging, and troubleshooting information, see the following articles: + +[How to: Monitor Apps in Azure App Service](/azure/app-service/web-sites-monitor) +Learn how to review quotas and metrics for apps and App Service plans. + +[Enable diagnostics logging for web apps in Azure App Service](/azure/app-service/web-sites-enable-diagnostic-log) +Discover how to enable and access diagnostic logging for HTTP status codes, failed requests, and web server activity. + +[Introduction to Error Handling in ASP.NET Core](xref:fundamentals/error-handling) +Understand common appoaches to handling errors in ASP.NET Core apps. + +[Troubleshoot ASP.NET Core on Azure App Service](xref:host-and-deploy/azure-apps/troubleshoot) +Learn how to diagnose issues with Azure App Service deployments with ASP.NET Core apps. + +[Common errors reference for Azure App Service and IIS with ASP.NET Core](xref:host-and-deploy/azure-iis-errors-reference) +See the common deployment configuration errors for apps hosted by Azure App Service/IIS with troubleshooting advice. + +## Data Protection key ring and deployment slots + +[Data Protection keys](xref:security/data-protection/implementation/key-management#data-protection-implementation-key-management) are persisted to the *%HOME%\ASP.NET\DataProtection-Keys* folder. This folder is backed by network storage and is synchronized across all machines hosting the app. Keys aren't protected at rest. This folder supplies the key ring to all instances of an app in a single deployment slot. Separate deployment slots, such as Staging and Production, don't share a key ring. + +When swapping between deployment slots, any system using data protection won't be able to decrypt stored data using the key ring inside the previous slot. ASP.NET Cookie Middleware uses data protection to protect its cookies. This leads to users being signed out of an app that uses the standard ASP.NET Cookie Middleware. For a slot-independent key ring solution, use an external key ring provider, such as: + +* Azure Blob Storage +* Azure Key Vault +* SQL store +* Redis cache + +For more information, see [Key storage providers](xref:security/data-protection/implementation/key-storage-providers). + +## Additional resources + +* [Web Apps overview (5-minute overview video)](/azure/app-service/app-service-web-overview) +* [Azure App Service: The Best Place to Host your .NET Apps (55-minute overview video)](https://channel9.msdn.com/events/dotnetConf/2017/T222) +* [Azure Friday: Azure App Service Diagnostic and Troubleshooting Experience (12-minute video)](https://channel9.msdn.com/Shows/Azure-Friday/Azure-App-Service-Diagnostic-and-Troubleshooting-Experience) +* [Azure App Service diagnostics overview](/azure/app-service/app-service-diagnostics) + +Azure App Service on Windows Server uses [Internet Information Services (IIS)](https://www.iis.net/). The following topics pertain to the underlying IIS technology: + +* [Host ASP.NET Core on Windows with IIS](xref:host-and-deploy/iis/index) +* [Introduction to ASP.NET Core Module](xref:fundamentals/servers/aspnet-core-module) +* [ASP.NET Core Module configuration reference](xref:host-and-deploy/aspnet-core-module) +* [Using IIS Modules with ASP.NET Core](xref:host-and-deploy/iis/modules) +* [Microsoft TechNet Library: Windows Server](https://docs.microsoft.com/windows-server/windows-server-versions) diff --git a/aspnetcore/host-and-deploy/azure-apps/troubleshoot.md b/aspnetcore/host-and-deploy/azure-apps/troubleshoot.md index 41921fee1c..dacb80e4f4 100644 --- a/aspnetcore/host-and-deploy/azure-apps/troubleshoot.md +++ b/aspnetcore/host-and-deploy/azure-apps/troubleshoot.md @@ -5,7 +5,7 @@ description: Learn how to diagnose problems with ASP.NET Core Azure App Service manager: wpickett ms.author: riande ms.custom: mvc -ms.date: 01/08/2018 +ms.date: 01/31/2018 ms.prod: asp.net-core ms.technology: aspnet ms.topic: article @@ -13,4 +13,158 @@ uid: host-and-deploy/azure-apps/troubleshoot --- # Troubleshoot ASP.NET Core on Azure App Service -This topic is under development the week of January 8, 2018 and will appear soon. +By [Luke Latham](https://github.com/guardrex) + +This article provides instructions on how to diagnose an ASP.NET Core app startup issue using Azure App Service's diagnostic tools. For additional troubleshooting advice, see [Azure App Service diagnostics overview](/azure/app-service/app-service-diagnostics) and [How to: Monitor Apps in Azure App Service](/azure/app-service/web-sites-monitor) in the Azure documentation. + +## App startup errors + +**502.5 Process Failure** +The worker process fails. The app doesn't start. + +The [ASP.NET Core Module](xref:fundamentals/servers/aspnet-core-module) attempts to start the worker process but it fails to start. Examining the Application Event Log often helps troubleshoot this type of problem. Accessing the log is explained in the [Event log](#event-log) section. + +The *502.5 Process Failure* error page is returned when a misconfigured app causes the worker process to fail: + +![Browser window showing the 502.5 Process Failure page](troubleshoot/_static/process-failure-page.png) + +**500 Internal Server Error** +The app starts, but an error prevents the server from fulfilling the request. + +This error occurs within the app's code during startup or while creating a response. The response may contain no content, or the response may appear as a *500 Internal Server Error* in the browser. The Application Event Log usually states that the app started normally. From the server's perspective, that's correct. The app did start, but it can't generate a valid response. [Run the app in the Kudu console](#run-the-app-in-the-kudu-console) or [enable the ASP.NET Core Module stdout log](#aspnet-core-module-stdout-log) to troubleshoot the problem. + +## Troubleshoot app startup errors + +### Application Event Log + +To access the Application Event Log, use the **Diagnose and solve problems** blade in the Azure portal : + +1. In the Azure portal, open the app's blade in the **App Services** blade. +1. Select the **Diagnose and solve problems** blade. +1. Under **SELECT PROBLEM CATEGORY**, select the **Web App Down** button. +1. Under **Suggested Solutions**, open the pane for **Open Application Event Logs**. Select the **Open Application Event Logs** button. +1. Examine the latest error provided by the *IIS AspNetCoreModule* in the **Source** column. + +An alternative to using the **Diagnose and solve problems** blade is to examine the Application Event Log file directly using [Kudu](https://github.com/projectkudu/kudu/wiki): + +1. Select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window. +1. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**. +1. Open the **LogFiles** folder. +1. Select the pencil icon next to the *eventlog.xml* file. +1. Examine the log. Scroll to the bottom of the log to see the most recent events. + +### Run the app in the Kudu console + +Many startup errors don't produce useful information in the Application Event Log. You can run the app in the [Kudu](https://github.com/projectkudu/kudu/wiki) Remote Execution Console to discover the error: + +1. Select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window. +1. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**. +1. Open the folders to the path **site** > **wwwroot**. +1. In the console, run the app by executing the app's assembly with *dotnet.exe*. In the following command, substitute the name of the app's assembly for ``: + ```console + dotnet .\.dll + ``` +1. The console output from the app, showing any errors, is piped to the Kudu console. + +### ASP.NET Core Module stdout log + +The ASP.NET Core Module stdout log often records useful error messages not found in the Application Event Log. To enable and view stdout logs: + +1. Navigate to the **Diagnose and solve problems** blade in the Azure portal. +1. Under **SELECT PROBLEM CATEGORY**, select the **Web App Down** button. +1. Under **Suggested Solutions** > **Enable Stdout Log Redirection**, select the button to **Open Kudu Console to edit Web.Config**. +1. In the Kudu **Diagnostic Console**, open the folders to the path **site** > **wwwroot**. Scroll down to reveal the *web.config* file at the bottom of the list. +1. Click the pencil icon next to the *web.config* file. +1. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to: `\\?\%home%\LogFiles\stdout`. +1. Select **Save** to save the updated *web.config* file. +1. Make a request to the app. +1. Return to the Azure portal. Select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window. +1. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**. +1. Select the **LogFiles** folder. +1. Inspect the **Modified** column and select the pencil icon to edit the stdout log with the latest modification date. +1. When the log file opens, the error is displayed. + +**Important!** Disable stdout logging when troubleshooting is complete. + +1. In the Kudu **Diagnostic Console**, return to the path **site** > **wwwroot** to reveal the *web.config* file. Open the **web.config** file again by selecting the pencil icon. +1. Set **stdoutLogEnabled** to `false`. +1. Select **Save** to save the file. + +> [!WARNING] +> Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created. +> +> For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](xref:fundamentals/logging/index#third-party-logging-providers). + +## Common startup errors + +See the [ASP.NET Core common errors reference](xref:host-and-deploy/azure-iis-errors-reference). Most of the common problems that prevent app startup are covered in the reference topic. + +## Process dump for a slow or hanging app + +When an app responds slowly or hangs on a request, see [Troubleshoot slow web app performance issues in Azure App Service](/azure/app-service/app-service-web-troubleshoot-performance-degradation) for debugging guidance. + +## Remote debugging + +See [Remote debugging web apps section of Troubleshoot a web app in Azure App Service using Visual Studio](/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio#remotedebug) in the Azure documentation. + +## Application Insights + +[Application Insights](https://azure.microsoft.com/services/application-insights/) provides telemetry from apps hosted in the Azure App Service, including error logging and reporting features. Application Insights can only report on errors that occur after the app starts when the app's logging features become available. For more information, see [Application Insights for ASP.NET Core](/azure/application-insights/app-insights-asp-net-core). + +## Monitoring blades + +Monitoring blades provide an alternative troubleshooting experience to the methods described earlier in the topic. These blades can be used to diagnose 500-series errors. + +Confirm that the ASP.NET Core Extensions are installed. If the extensions aren't installed, install them manually: + +1. In the **DEVELOPMENT TOOLS** blade section, select the **Extensions** blade. +1. The **ASP.NET Core Extensions** should appear in the list. +1. If the extensions aren't installed, select the **Add** button. +1. Choose the **ASP.NET Core Extensions** from the list. +1. Select **OK** to accept the legal terms. +1. Select **OK** on the **Add extension** blade. +1. An informational pop-up message indicates when the extensions are successfully installed. + +If stdout logging isn't enabled, follow these steps: + +1. In the Azure portal, select the **Advanced Tools** blade in the **DEVELOPMENT TOOLS** area. Select the **Go→** button. The Kudu console opens in a new browser tab or window. +1. Using the navigation bar at the top of the page, open **Debug console** and select **CMD**. +1. Open the folders to the path **site** > **wwwroot** and scroll down to reveal the *web.config* file at the bottom of the list. +1. Click the pencil icon next to the *web.config* file. +1. Set **stdoutLogEnabled** to `true` and change the **stdoutLogFile** path to: `\\?\%home%\LogFiles\stdout`. +1. Select **Save** to save the updated *web.config* file. + +Proceed to activate diagnostic logging: + +1. In the Azure portal, select the **Diagnostics logs** blade. +1. Select the **On** switch for **Application Logging (Filesystem)** and **Detailed error messages**. Select the **Save** button at the top of the blade. +1. To include failed request tracing, also known as Failed Request Event Buffering (FREB) logging, select the **On** switch for **Failed request tracing**. +1. Select the **Log stream** blade, which is listed immediately under the **Diagnostics logs** blade in the portal. +1. Make a request to the app. +1. Within the log stream data, the cause of the error is indicated. + +**Important!** Be sure to disable stdout logging when troubleshooting is complete. See the instructions in the [ASP.NET Core Module stdout log](#aspnet-core-module-stdout-log) section. + +To view the failed request tracing logs (FREB logs): + +1. Navigate to the **Diagnose and solve problems** blade in the Azure portal. +1. Select **Failed Request Tracing Logs** from the **SUPPORT TOOLS** area of the sidebar. + +See [Failed request traces section of the Enable diagnostics logging for web apps in Azure App Service topic](/azure/app-service/web-sites-enable-diagnostic-log#failed-request-traces) and the [Application performance FAQs for Web Apps in Azure: How do I turn on failed request tracing?](/azure/app-service/app-service-web-availability-performance-application-issues-faq#how-do-i-turn-on-failed-request-tracing) for more information. + +For more information, see [Enable diagnostics logging for web apps in Azure App Service](/azure/app-service/web-sites-enable-diagnostic-log). + +> [!WARNING] +> Failure to disable the stdout log can lead to app or server failure. There's no limit on log file size or the number of log files created. +> +> For routine logging in an ASP.NET Core app, use a logging library that limits log file size and rotates logs. For more information, see [third-party logging providers](xref:fundamentals/logging/index#third-party-logging-providers). + +## Additional resources + +* [Introduction to Error Handling in ASP.NET Core](xref:fundamentals/error-handling) +* [Common errors reference for Azure App Service and IIS with ASP.NET Core](xref:host-and-deploy/azure-iis-errors-reference) +* [Troubleshoot a web app in Azure App Service using Visual Studio](/azure/app-service/web-sites-dotnet-troubleshoot-visual-studio) +* [Troubleshoot HTTP errors of "502 bad gateway" and "503 service unavailable" in your Azure web apps](/app-service/app-service-web-troubleshoot-http-502-http-503) +* [Troubleshoot slow web app performance issues in Azure App Service](/azure/app-service/app-service-web-troubleshoot-performance-degradation) +* [Application performance FAQs for Web Apps in Azure](/azure/app-service/app-service-web-availability-performance-application-issues-faq) +* [Azure Friday: Azure App Service Diagnostic and Troubleshooting Experience (12-minute video)](https://channel9.msdn.com/Shows/Azure-Friday/Azure-App-Service-Diagnostic-and-Troubleshooting-Experience) diff --git a/aspnetcore/host-and-deploy/azure-apps/troubleshoot/_static/process-failure-page.png b/aspnetcore/host-and-deploy/azure-apps/troubleshoot/_static/process-failure-page.png new file mode 100644 index 0000000000..6f78830b18 Binary files /dev/null and b/aspnetcore/host-and-deploy/azure-apps/troubleshoot/_static/process-failure-page.png differ diff --git a/aspnetcore/host-and-deploy/azure-iis-errors-reference.md b/aspnetcore/host-and-deploy/azure-iis-errors-reference.md index a557bb60f8..824fde5ff8 100644 --- a/aspnetcore/host-and-deploy/azure-iis-errors-reference.md +++ b/aspnetcore/host-and-deploy/azure-iis-errors-reference.md @@ -17,6 +17,14 @@ By [Luke Latham](https://github.com/guardrex) The following isn't a complete list of errors. If you encounter an error not listed here, [open a new issue](https://github.com/aspnet/Docs/issues/new) with detailed instructions to reproduce the error. +Collect the following information: + +* Browser behavior +* Application Event Log entries +* ASP.NET Core Module stdout log entries + +Compare the information to the following common errors. If a match is found, follow the troubleshooting advice. + ## Installer unable to obtain VC++ Redistributable * **Installer Exception:** 0x80072efd or 0x80072f76 - Unspecified error diff --git a/aspnetcore/index.md b/aspnetcore/index.md index 864e63b3b3..bda7981fd2 100644 --- a/aspnetcore/index.md +++ b/aspnetcore/index.md @@ -60,6 +60,7 @@ ASP.NET Core integrates seamlessly with popular client-side frameworks and libra For more information, see the following resources: +* [Get started with Razor Pages](xref:tutorials/razor-pages/razor-pages-start) * [ASP.NET Core tutorials](xref:tutorials/index) * [ASP.NET Core fundamentals](xref:fundamentals/index) * [The weekly ASP.NET community standup](https://live.asp.net/) covers the team's progress and plans. It features new blogs and third-party software. diff --git a/aspnetcore/mvc/views/razor.md b/aspnetcore/mvc/views/razor.md index d0362c89a8..0a6c56c03e 100644 --- a/aspnetcore/mvc/views/razor.md +++ b/aspnetcore/mvc/views/razor.md @@ -466,7 +466,7 @@ Razor exposes a `Model` property for accessing the model passed to the view:
The Login Email: @Model.Email
``` -The `@model` directive specifies the type of this property. The directive specifies the `T` in `RazorPage` that the generated class that the view derives from. If the `@model` directive iisn't specified, the `Model` property is of type `dynamic`. The value of the model is passed from the controller to the view. For more information, see [Strongly typed models and the @model keyword. +The `@model` directive specifies the type of this property. The directive specifies the `T` in `RazorPage` that the generated class that the view derives from. If the `@model` directive isn't specified, the `Model` property is of type `dynamic`. The value of the model is passed from the controller to the view. For more information, see [Strongly typed models and the @model keyword. ### @inherits diff --git a/aspnetcore/mvc/views/tag-helpers/built-in/anchor-tag-helper.md b/aspnetcore/mvc/views/tag-helpers/built-in/anchor-tag-helper.md index a6f0ac4d79..057f292a0f 100644 --- a/aspnetcore/mvc/views/tag-helpers/built-in/anchor-tag-helper.md +++ b/aspnetcore/mvc/views/tag-helpers/built-in/anchor-tag-helper.md @@ -15,7 +15,7 @@ uid: mvc/views/tag-helpers/builtin-th/anchor-tag-helper By [Peter Kellner](http://peterkellner.net) and [Scott Addie](https://github.com/scottaddie) -[View or download sample code](https://github.com/aspnet/Docs/tree/master/aspnetcore/tag-helpers/built-in/samples/TagHelpersBuiltInAspNetCore) ([how to download](xref:tutorials/index#how-to-download-a-sample)) +[View or download sample code](https://github.com/aspnet/Docs/tree/master/aspnetcore/mvc/views/tag-helpers/built-in/samples) ([how to download](xref:tutorials/index#how-to-download-a-sample)) The [Anchor Tag Helper](/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper) enhances the standard HTML anchor (``) tag by adding new attributes. By convention, the attribute names are prefixed with `asp-`. The rendered anchor element's `href` attribute value is determined by the values of the `asp-` attributes. @@ -117,7 +117,7 @@ If either `asp-controller` or `asp-action` aren't specified, then the same defau The [asp-route](/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.route) attribute is used for creating a URL linking directly to a named route. Using [routing attributes](xref:mvc/controllers/routing#attribute-routing), a route can be named as shown in the `SpeakerController` and used in its `Evaluations` action: -[!code-cshtml[](samples/TagHelpersBuiltInAspNetCore/Controllers/SpeakerController.cs?range=22-24)] +[!code-csharp[](samples/TagHelpersBuiltInAspNetCore/Controllers/SpeakerController.cs?range=22-24)] In the following markup, the `asp-route` attribute references the named route: @@ -200,7 +200,7 @@ The generated HTML: The [asp-protocol](/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.anchortaghelper.protocol) attribute is for specifying a protocol (such as `https`) in your URL. For example: -[!code-cshtml[samples/TagHelpersBuiltInAspNetCore/Views/Index.cshtml?name=snippet_AspProtocol]] +[!code-cshtml[](samples/TagHelpersBuiltInAspNetCore/Views/Home/Index.cshtml?name=snippet_AspProtocol)] The generated HTML: diff --git a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Controllers/ManageController.cs b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Controllers/ManageController.cs index 532a9f537a..5474bda67c 100644 --- a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Controllers/ManageController.cs +++ b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Controllers/ManageController.cs @@ -26,7 +26,8 @@ namespace IdentityDemo.Controllers private readonly ILogger _logger; private readonly UrlEncoder _urlEncoder; - private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + private const string RecoveryCodesKey = nameof(RecoveryCodesKey); public ManageController( UserManager userManager, @@ -371,18 +372,8 @@ namespace IdentityDemo.Controllers 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) - }; + var model = new EnableAuthenticatorViewModel(); + await LoadSharedKeyAndQrCodeUriAsync(user, model); return View(model); } @@ -391,17 +382,18 @@ namespace IdentityDemo.Controllers [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)}'."); } + if (!ModelState.IsValid) + { + await LoadSharedKeyAndQrCodeUriAsync(user, model); + return View(model); + } + // Strip spaces and hypens var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty); @@ -410,13 +402,30 @@ namespace IdentityDemo.Controllers if (!is2faTokenValid) { - ModelState.AddModelError("model.Code", "Verification code is invalid."); + ModelState.AddModelError("Code", "Verification code is invalid."); + await LoadSharedKeyAndQrCodeUriAsync(user, model); 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)); + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + TempData[RecoveryCodesKey] = recoveryCodes.ToArray(); + + return RedirectToAction(nameof(ShowRecoveryCodes)); + } + + [HttpGet] + public IActionResult ShowRecoveryCodes() + { + var recoveryCodes = (string[])TempData[RecoveryCodesKey]; + if (recoveryCodes == null) + { + return RedirectToAction(nameof(TwoFactorAuthentication)); + } + + var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes }; + return View(model); } [HttpGet] @@ -443,6 +452,24 @@ namespace IdentityDemo.Controllers } [HttpGet] + public async Task GenerateRecoveryCodesWarning() + { + 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}' because they do not have 2FA enabled."); + } + + return View(nameof(GenerateRecoveryCodes)); + } + + [HttpPost] + [ValidateAntiForgeryToken] public async Task GenerateRecoveryCodes() { var user = await _userManager.GetUserAsync(User); @@ -457,11 +484,11 @@ namespace IdentityDemo.Controllers } 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); + var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() }; + + return View(nameof(ShowRecoveryCodes), model); } #region Helpers @@ -494,12 +521,25 @@ namespace IdentityDemo.Controllers private string GenerateQrCodeUri(string email, string unformattedKey) { return string.Format( - AuthenicatorUriFormat, + AuthenticatorUriFormat, _urlEncoder.Encode("IdentityDemo"), _urlEncoder.Encode(email), unformattedKey); } + private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user, EnableAuthenticatorViewModel model) + { + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + model.SharedKey = FormatKey(unformattedKey); + model.AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); + } + #endregion } } diff --git a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/EnableAuthenticatorViewModel.cs b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/EnableAuthenticatorViewModel.cs index 4a85ce5a5e..25e6bed6af 100644 --- a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/EnableAuthenticatorViewModel.cs +++ b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/EnableAuthenticatorViewModel.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace IdentityDemo.Models.ManageViewModels { @@ -15,9 +16,10 @@ namespace IdentityDemo.Models.ManageViewModels [Display(Name = "Verification Code")] public string Code { get; set; } - [ReadOnly(true)] + [BindNever] public string SharedKey { get; set; } + [BindNever] public string AuthenticatorUri { get; set; } } } diff --git a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/ShowRecoveryCodesViewModel.cs similarity index 84% rename from aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs rename to aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/ShowRecoveryCodesViewModel.cs index eb1d77a3e9..eeeb27992c 100644 --- a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs +++ b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Models/ManageViewModels/ShowRecoveryCodesViewModel.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace IdentityDemo.Models.ManageViewModels { - public class GenerateRecoveryCodesViewModel + public class ShowRecoveryCodesViewModel { public string[] RecoveryCodes { get; set; } } diff --git a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/EnableAuthenticator.cshtml b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/EnableAuthenticator.cshtml index 79693d78e3..4cbe57304f 100644 --- a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/EnableAuthenticator.cshtml +++ b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/EnableAuthenticator.cshtml @@ -23,7 +23,7 @@

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.
-
+
  • diff --git a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/GenerateRecoveryCodes.cshtml b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/GenerateRecoveryCodes.cshtml index 669d13ef93..996967b3a2 100644 --- a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/GenerateRecoveryCodes.cshtml +++ b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/GenerateRecoveryCodes.cshtml @@ -1,24 +1,26 @@ -@model GenerateRecoveryCodesViewModel -@{ - ViewData["Title"] = "Recovery codes"; +@{ + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); } -

    @ViewData["Title"]

    +

    @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/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/ShowRecoveryCodes.cshtml b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 0000000000..c2be067710 --- /dev/null +++ b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/ShowRecoveryCodes.cshtml @@ -0,0 +1,24 @@ +@model ShowRecoveryCodesViewModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

    @ViewData["Title"]

    + +
    +
    + @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
    + } +
    +
    \ No newline at end of file diff --git a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/TwoFactorAuthentication.cshtml b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/TwoFactorAuthentication.cshtml index a2b52ac5b4..9286c0821e 100644 --- a/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/TwoFactorAuthentication.cshtml +++ b/aspnetcore/security/authentication/identity/sample/src/ASPNETCore-IdentityDemoComplete/IdentityDemo/Views/Manage/TwoFactorAuthentication.cshtml @@ -30,7 +30,7 @@ } Disable 2FA - Reset recovery codes + Reset recovery codes }
    Authenticator app
    diff --git a/aspnetcore/security/authorization/resourcebased.md b/aspnetcore/security/authorization/resourcebased.md index 19c9a961df..cebef220f0 100644 --- a/aspnetcore/security/authorization/resourcebased.md +++ b/aspnetcore/security/authorization/resourcebased.md @@ -14,14 +14,14 @@ uid: security/authorization/resourcebased --- # Resource-based authorization -By [Scott Addie](https://twitter.com/Scott_Addie) - Authorization strategy depends upon the resource being accessed. Consider a document which has an author property. Only the author is allowed to update the document. Consequently, the document must be retrieved from the data store before authorization evaluation can occur. Attribute evaluation occurs before data binding and before execution of the page handler or action which loads the document. For these reasons, declarative authorization with an `[Authorize]` attribute won't suffice. Instead, you can invoke a custom authorization method—a style known as imperative authorization. Use the [sample apps](https://github.com/aspnet/Docs/tree/master/aspnetcore/security/authorization/resourcebased/samples) ([how to download](xref:tutorials/index#how-to-download-a-sample)) to explore the features described in this topic. +[Create an ASP.NET Core app with user data protected by authorization](xref:security/authorization/secure-data) contains a sample app that uses resource-based authorization. + ## Use imperative authorization Authorization is implemented as an [IAuthorizationService](/dotnet/api/microsoft.aspnetcore.authorization.iauthorizationservice) service and is registered in the service collection within the `Startup` class. The service is made available via [dependency injection](xref:fundamentals/dependency-injection#fundamentals-dependency-injection) to page handlers or actions. diff --git a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Controllers/ManageController.cs b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Controllers/ManageController.cs index 2068d6df25..b3a763aa34 100644 --- a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Controllers/ManageController.cs +++ b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Controllers/ManageController.cs @@ -26,7 +26,8 @@ namespace ResourceBasedAuthApp2.Controllers private readonly ILogger _logger; private readonly UrlEncoder _urlEncoder; - private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + private const string RecoveryCodesKey = nameof(RecoveryCodesKey); public ManageController( UserManager userManager, @@ -371,18 +372,8 @@ namespace ResourceBasedAuthApp2.Controllers 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) - }; + var model = new EnableAuthenticatorViewModel(); + await LoadSharedKeyAndQrCodeUriAsync(user, model); return View(model); } @@ -391,17 +382,18 @@ namespace ResourceBasedAuthApp2.Controllers [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)}'."); } + if (!ModelState.IsValid) + { + await LoadSharedKeyAndQrCodeUriAsync(user, model); + return View(model); + } + // Strip spaces and hypens var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty); @@ -410,13 +402,30 @@ namespace ResourceBasedAuthApp2.Controllers if (!is2faTokenValid) { - ModelState.AddModelError("model.Code", "Verification code is invalid."); + ModelState.AddModelError("Code", "Verification code is invalid."); + await LoadSharedKeyAndQrCodeUriAsync(user, model); 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)); + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + TempData[RecoveryCodesKey] = recoveryCodes.ToArray(); + + return RedirectToAction(nameof(ShowRecoveryCodes)); + } + + [HttpGet] + public IActionResult ShowRecoveryCodes() + { + var recoveryCodes = (string[])TempData[RecoveryCodesKey]; + if (recoveryCodes == null) + { + return RedirectToAction(nameof(TwoFactorAuthentication)); + } + + var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes }; + return View(model); } [HttpGet] @@ -443,6 +452,24 @@ namespace ResourceBasedAuthApp2.Controllers } [HttpGet] + public async Task GenerateRecoveryCodesWarning() + { + 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}' because they do not have 2FA enabled."); + } + + return View(nameof(GenerateRecoveryCodes)); + } + + [HttpPost] + [ValidateAntiForgeryToken] public async Task GenerateRecoveryCodes() { var user = await _userManager.GetUserAsync(User); @@ -457,11 +484,11 @@ namespace ResourceBasedAuthApp2.Controllers } 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); + var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() }; + + return View(nameof(ShowRecoveryCodes), model); } #region Helpers @@ -494,12 +521,25 @@ namespace ResourceBasedAuthApp2.Controllers private string GenerateQrCodeUri(string email, string unformattedKey) { return string.Format( - AuthenicatorUriFormat, + AuthenticatorUriFormat, _urlEncoder.Encode("ResourceBasedAuthApp2"), _urlEncoder.Encode(email), unformattedKey); } + private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user, EnableAuthenticatorViewModel model) + { + var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); + } + + model.SharedKey = FormatKey(unformattedKey); + model.AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); + } + #endregion } } diff --git a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/EnableAuthenticatorViewModel.cs b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/EnableAuthenticatorViewModel.cs index 74a1260f46..e21c410a5d 100644 --- a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/EnableAuthenticatorViewModel.cs +++ b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/EnableAuthenticatorViewModel.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace ResourceBasedAuthApp2.Models.ManageViewModels { @@ -15,9 +16,10 @@ namespace ResourceBasedAuthApp2.Models.ManageViewModels [Display(Name = "Verification Code")] public string Code { get; set; } - [ReadOnly(true)] + [BindNever] public string SharedKey { get; set; } + [BindNever] public string AuthenticatorUri { get; set; } } } diff --git a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/ShowRecoveryCodesViewModel.cs similarity index 84% rename from aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs rename to aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/ShowRecoveryCodesViewModel.cs index 680110000a..81a0445a34 100644 --- a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/GenerateRecoveryCodesViewModel.cs +++ b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Models/ManageViewModels/ShowRecoveryCodesViewModel.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace ResourceBasedAuthApp2.Models.ManageViewModels { - public class GenerateRecoveryCodesViewModel + public class ShowRecoveryCodesViewModel { public string[] RecoveryCodes { get; set; } } diff --git a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/EnableAuthenticator.cshtml b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/EnableAuthenticator.cshtml index 79693d78e3..4cbe57304f 100644 --- a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/EnableAuthenticator.cshtml +++ b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/EnableAuthenticator.cshtml @@ -23,7 +23,7 @@

    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.
    -
    +
  • diff --git a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/GenerateRecoveryCodes.cshtml b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/GenerateRecoveryCodes.cshtml index 669d13ef93..996967b3a2 100644 --- a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/GenerateRecoveryCodes.cshtml +++ b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/GenerateRecoveryCodes.cshtml @@ -1,24 +1,26 @@ -@model GenerateRecoveryCodesViewModel -@{ - ViewData["Title"] = "Recovery codes"; +@{ + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); } -

    @ViewData["Title"]

    +

    @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/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/ShowRecoveryCodes.cshtml b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 0000000000..c2be067710 --- /dev/null +++ b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/ShowRecoveryCodes.cshtml @@ -0,0 +1,24 @@ +@model ShowRecoveryCodesViewModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication); +} + +

    @ViewData["Title"]

    + +
    +
    + @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
    + } +
    +
    \ No newline at end of file diff --git a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/TwoFactorAuthentication.cshtml b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/TwoFactorAuthentication.cshtml index a2b52ac5b4..9286c0821e 100644 --- a/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/TwoFactorAuthentication.cshtml +++ b/aspnetcore/security/authorization/resourcebased/samples/ResourceBasedAuthApp2/Views/Manage/TwoFactorAuthentication.cshtml @@ -30,7 +30,7 @@ } Disable 2FA - Reset recovery codes + Reset recovery codes }
    Authenticator app
    diff --git a/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/EnableAuthenticator.cshtml b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/EnableAuthenticator.cshtml index 1d68558407..9d1113d779 100644 --- a/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/EnableAuthenticator.cshtml +++ b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/EnableAuthenticator.cshtml @@ -24,7 +24,7 @@

    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.
    -
    +
  • diff --git a/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/EnableAuthenticator.cshtml.cs index 2a08a16c2c..1b84d5d16f 100644 --- a/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/EnableAuthenticator.cshtml.cs +++ b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -20,7 +20,7 @@ namespace ContactManager.Pages.Account.Manage private readonly ILogger _logger; private readonly UrlEncoder _urlEncoder; - private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; public EnableAuthenticatorModel( UserManager userManager, @@ -57,11 +57,6 @@ namespace ContactManager.Pages.Account.Manage } await LoadSharedKeyAndQrCodeUriAsync(user); - if (string.IsNullOrEmpty(SharedKey)) - { - await _userManager.ResetAuthenticatorKeyAsync(user); - await LoadSharedKeyAndQrCodeUriAsync(user); - } return Page(); } @@ -95,18 +90,24 @@ namespace ContactManager.Pages.Account.Manage await _userManager.SetTwoFactorEnabledAsync(user, true); _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", user.Id); - return RedirectToPage("./GenerateRecoveryCodes"); + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + TempData["RecoveryCodes"] = recoveryCodes.ToArray(); + return RedirectToPage("./ShowRecoveryCodes"); } 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)) + if (string.IsNullOrEmpty(unformattedKey)) { - SharedKey = FormatKey(unformattedKey); - AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); } + + SharedKey = FormatKey(unformattedKey); + AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); } private string FormatKey(string unformattedKey) @@ -129,7 +130,7 @@ namespace ContactManager.Pages.Account.Manage private string GenerateQrCodeUri(string email, string unformattedKey) { return string.Format( - AuthenicatorUriFormat, + AuthenticatorUriFormat, _urlEncoder.Encode("ContactManager"), _urlEncoder.Encode(email), unformattedKey); diff --git a/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml index d05825429f..3738b9237c 100644 --- a/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml +++ b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml @@ -1,7 +1,7 @@ @page @model GenerateRecoveryCodesModel @{ - ViewData["Title"] = "Recovery codes"; + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; ViewData["ActivePage"] = "TwoFactorAuthentication"; } @@ -9,17 +9,19 @@

    + +
    +
    + +
    -
    -
    - @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/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs index ebea0c571b..bc6023b63b 100644 --- a/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs +++ b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -23,8 +23,6 @@ namespace ContactManager.Pages.Account.Manage _logger = logger; } - public string[] RecoveryCodes { get; set; } - public async Task OnGetAsync() { var user = await _userManager.GetUserAsync(User); @@ -33,17 +31,33 @@ namespace ContactManager.Pages.Account.Manage 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}' because they do not have 2FA 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)}'."); + } + 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(); + TempData["RecoveryCodes"] = recoveryCodes.ToArray(); _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", user.Id); - return Page(); + return RedirectToPage("./ShowRecoveryCodes"); } } } \ No newline at end of file diff --git a/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/ShowRecoveryCodes.cshtml b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 0000000000..a7225dfa23 --- /dev/null +++ b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/ShowRecoveryCodes.cshtml @@ -0,0 +1,25 @@ +@page +@model ShowRecoveryCodesModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + +

    @ViewData["Title"]

    + +
    +
    + @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
    + } +
    +
    \ No newline at end of file diff --git a/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs new file mode 100644 index 0000000000..2ae5429512 --- /dev/null +++ b/aspnetcore/security/authorization/secure-data/samples/final2/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace ContactManager.Pages.Account.Manage +{ + public class ShowRecoveryCodesModel : PageModel + { + public string[] RecoveryCodes { get; private set; } + + public IActionResult OnGet() + { + RecoveryCodes = (string[])TempData["RecoveryCodes"]; + if (RecoveryCodes == null) + { + return RedirectToPage("TwoFactorAuthentication"); + } + + return Page(); + } + } +} \ No newline at end of file diff --git a/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/EnableAuthenticator.cshtml b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/EnableAuthenticator.cshtml index 1d68558407..9d1113d779 100644 --- a/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/EnableAuthenticator.cshtml +++ b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/EnableAuthenticator.cshtml @@ -24,7 +24,7 @@

    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.
    -
    +
  • diff --git a/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/EnableAuthenticator.cshtml.cs index 2a08a16c2c..1b84d5d16f 100644 --- a/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/EnableAuthenticator.cshtml.cs +++ b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -20,7 +20,7 @@ namespace ContactManager.Pages.Account.Manage private readonly ILogger _logger; private readonly UrlEncoder _urlEncoder; - private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; public EnableAuthenticatorModel( UserManager userManager, @@ -57,11 +57,6 @@ namespace ContactManager.Pages.Account.Manage } await LoadSharedKeyAndQrCodeUriAsync(user); - if (string.IsNullOrEmpty(SharedKey)) - { - await _userManager.ResetAuthenticatorKeyAsync(user); - await LoadSharedKeyAndQrCodeUriAsync(user); - } return Page(); } @@ -95,18 +90,24 @@ namespace ContactManager.Pages.Account.Manage await _userManager.SetTwoFactorEnabledAsync(user, true); _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", user.Id); - return RedirectToPage("./GenerateRecoveryCodes"); + + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + TempData["RecoveryCodes"] = recoveryCodes.ToArray(); + return RedirectToPage("./ShowRecoveryCodes"); } 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)) + if (string.IsNullOrEmpty(unformattedKey)) { - SharedKey = FormatKey(unformattedKey); - AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); + await _userManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); } + + SharedKey = FormatKey(unformattedKey); + AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); } private string FormatKey(string unformattedKey) @@ -129,7 +130,7 @@ namespace ContactManager.Pages.Account.Manage private string GenerateQrCodeUri(string email, string unformattedKey) { return string.Format( - AuthenicatorUriFormat, + AuthenticatorUriFormat, _urlEncoder.Encode("ContactManager"), _urlEncoder.Encode(email), unformattedKey); diff --git a/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml index d05825429f..3738b9237c 100644 --- a/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml +++ b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml @@ -1,7 +1,7 @@ @page @model GenerateRecoveryCodesModel @{ - ViewData["Title"] = "Recovery codes"; + ViewData["Title"] = "Generate two-factor authentication (2FA) recovery codes"; ViewData["ActivePage"] = "TwoFactorAuthentication"; } @@ -9,17 +9,19 @@

    + +
    +
    + +
    -
    -
    - @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/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs index ebea0c571b..bc6023b63b 100644 --- a/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs +++ b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs @@ -23,8 +23,6 @@ namespace ContactManager.Pages.Account.Manage _logger = logger; } - public string[] RecoveryCodes { get; set; } - public async Task OnGetAsync() { var user = await _userManager.GetUserAsync(User); @@ -33,17 +31,33 @@ namespace ContactManager.Pages.Account.Manage 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}' because they do not have 2FA 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)}'."); + } + 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(); + TempData["RecoveryCodes"] = recoveryCodes.ToArray(); _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", user.Id); - return Page(); + return RedirectToPage("./ShowRecoveryCodes"); } } } \ No newline at end of file diff --git a/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/ShowRecoveryCodes.cshtml b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/ShowRecoveryCodes.cshtml new file mode 100644 index 0000000000..a7225dfa23 --- /dev/null +++ b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/ShowRecoveryCodes.cshtml @@ -0,0 +1,25 @@ +@page +@model ShowRecoveryCodesModel +@{ + ViewData["Title"] = "Recovery codes"; + ViewData["ActivePage"] = "TwoFactorAuthentication"; +} + +

    @ViewData["Title"]

    + +
    +
    + @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
    + } +
    +
    \ No newline at end of file diff --git a/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs new file mode 100644 index 0000000000..2ae5429512 --- /dev/null +++ b/aspnetcore/security/authorization/secure-data/samples/starter2/Pages/Account/Manage/ShowRecoveryCodes.cshtml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace ContactManager.Pages.Account.Manage +{ + public class ShowRecoveryCodesModel : PageModel + { + public string[] RecoveryCodes { get; private set; } + + public IActionResult OnGet() + { + RecoveryCodes = (string[])TempData["RecoveryCodes"]; + if (RecoveryCodes == null) + { + return RedirectToPage("TwoFactorAuthentication"); + } + + return Page(); + } + } +} \ No newline at end of file