From e1e65779323f457c10e8b5c8e7a056262bfe5de8 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:14:34 -0400 Subject: [PATCH 1/2] =?UTF-8?q?Authorization=20tutorial=20Blazorfication?= =?UTF-8?q?=E2=84=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/authorization/secure-data.md | 759 ++++++++++++++++++ .../security/authorization/secure-data.md | 18 +- aspnetcore/toc.yml | 8 +- 3 files changed, 773 insertions(+), 12 deletions(-) create mode 100644 aspnetcore/razor-pages/security/authorization/secure-data.md diff --git a/aspnetcore/razor-pages/security/authorization/secure-data.md b/aspnetcore/razor-pages/security/authorization/secure-data.md new file mode 100644 index 000000000000..97480b1e87c9 --- /dev/null +++ b/aspnetcore/razor-pages/security/authorization/secure-data.md @@ -0,0 +1,759 @@ +--- +title: Create an ASP.NET Core app with user data protected by authorization +author: tdykstra +description: Learn how to create an ASP.NET Core web app with user data protected by authorization. Includes HTTPS, authentication, security, ASP.NET Core Identity. +ms.author: tdykstra +ms.custom: mvc, sfi-image-nochange +ms.date: 12/5/2021 +ms.sfi.ropc: t +uid: razor-pages/security/authorization/secure-data +--- +# Create an ASP.NET Core web app with user data protected by authorization + +By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Joe Audette](https://twitter.com/joeaudette) + +:::moniker range=">= aspnetcore-6.0" + +This tutorial shows how to create an ASP.NET Core web app with user data protected by authorization. It displays a list of contacts that authenticated (registered) users have created. There are three security groups: + +* **Registered users** can view all the approved data and can edit/delete their own data. +* **Managers** can approve or reject contact data. Only approved contacts are visible to users. +* **Administrators** can approve/reject and edit/delete any data. + +The images in this document don't exactly match the latest templates. + +In the following image, user Rick (`rick@example.com`) is signed in. Rick can only view approved contacts and **Edit**/**Delete**/**Create New** links for his contacts. Only the last record, created by Rick, displays **Edit** and **Delete** links. Other users won't see the last record until a manager or administrator changes the status to "Approved". + +![Screenshot showing Rick signed in](secure-data/_static/rick.png) + +In the following image, `manager@contoso.com` is signed in and in the manager's role: + +![Screenshot showing manager@contoso.com signed in](secure-data/_static/manager1.png) + +The following image shows the managers details view of a contact: + +![Manager's view of a contact](secure-data/_static/manager.png) + +The **Approve** and **Reject** buttons are only displayed for managers and administrators. + +In the following image, `admin@contoso.com` is signed in and in the administrator's role: + +![Screenshot showing admin@contoso.com signed in](secure-data/_static/admin.png) + +The administrator has all privileges. She can read, edit, or delete any contact and change the status of contacts. + +The app was created by [scaffolding](xref:tutorials/first-mvc-app/adding-model#scaffold-movie-pages) the following `Contact` model: + +[!code-csharp[](secure-data/samples/starter2.1/Models/Contact.cs?name=snippet1)] + +The sample contains the following authorization handlers: + +* `ContactIsOwnerAuthorizationHandler`: Ensures that a user can only edit their data. +* `ContactManagerAuthorizationHandler`: Allows managers to approve or reject contacts. +* `ContactAdministratorsAuthorizationHandler`: Allows administrators to approve or reject contacts and to edit/delete contacts. + +## Prerequisites + +This tutorial is advanced. You should be familiar with: + +* [ASP.NET Core](xref:tutorials/first-mvc-app/start-mvc) +* [Authentication](xref:security/authentication/identity) +* [Account Confirmation and Password Recovery](xref:security/authentication/accconfirm) +* [Authorization](xref:security/authorization/introduction) +* [Entity Framework Core](xref:data/ef-mvc/intro) + +## The starter and completed app + +[Download](xref:fundamentals/index#how-to-download-a-sample) the [completed](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/security/authorization/secure-data/samples) app. [Test](#test-the-completed-app) the completed app so you become familiar with its security features. + +> [!TIP] +> Use [`git sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) to only download the sample subfolder. +For example: +``` +git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.git --sparse +cd AspNetCore.Docs +git sparse-checkout init --cone +git sparse-checkout set aspnetcore/razor-pages/security/authorization/secure-data/samples +``` + +### The starter app + +[Download](xref:fundamentals/index#how-to-download-a-sample) the [starter](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/security/authorization/secure-data/samples/) app. + +Run the app, tap the **ContactManager** link, and verify you can create, edit, and delete a contact. To create the starter app, see [Create the starter app](#create-the-starter-app). + +## Secure user data + +The following sections have all the major steps to create the secure user data app. You may find it helpful to refer to the completed project. + +### Tie the contact data to the user + +Use the ASP.NET [Identity](xref:security/authentication/identity) user ID to ensure users can edit their data, but not other users data. Add `OwnerID` and `ContactStatus` to the `Contact` model: + +[!code-csharp[](secure-data/samples/final6/Models/Contact.cs?name=snippet1&highlight=5-6,16-999)] + +`OwnerID` is the user's ID from the `AspNetUser` table in the [Identity](xref:security/authentication/identity) database. The `Status` field determines if a contact is viewable by general users. + +Create a new migration and update the database: + +```dotnetcli +dotnet ef migrations add userID_Status +dotnet ef database update +``` + +### Add Role services to Identity + +Append to add Role services: + +[!code-csharp[](secure-data/samples/final6/Program.cs?name=snippet&highlight=10)] + + + +### Require authenticated users + +Set the fallback authorization policy to require users to be authenticated: + +[!code-csharp[](secure-data/samples/final6/Program.cs?name=snippet2&highlight=15-99)] + +The preceding highlighted code sets the [fallback authorization policy](xref:Microsoft.AspNetCore.Authorization.AuthorizationOptions.FallbackPolicy). The fallback authorization policy requires ***all*** users to be authenticated, except for Razor Pages, controllers, or action methods with an authorization attribute. For example, Razor Pages, controllers, or action methods with `[AllowAnonymous]` or `[Authorize(PolicyName="MyPolicy")]` use the applied authorization attribute rather than the fallback authorization policy. + + adds to the current instance, which enforces that the current user is authenticated. + +The fallback authorization policy: + +* Is applied to all requests that don't explicitly specify an authorization policy. For requests served by endpoint routing, this includes any endpoint that doesn't specify an authorization attribute. For requests served by other middleware after the authorization middleware, such as [static files](xref:fundamentals/static-files), this applies the policy to all requests. + +Setting the fallback authorization policy to require users to be authenticated protects newly added Razor Pages and controllers. Having authorization required by default is more secure than relying on new controllers and Razor Pages to include the `[Authorize]` attribute. + +The class also contains . The `DefaultPolicy` is the policy used with the `[Authorize]` attribute when no policy is specified. `[Authorize]` doesn't contain a named policy, unlike `[Authorize(PolicyName="MyPolicy")]`. + +For more information on policies, see . + +An alternative way for MVC controllers and Razor Pages to require all users be authenticated is adding an authorization filter: + +[!code-csharp[](secure-data/samples/final6/Program.cs?name=snippet3&highlight=21-27)] + +The preceding code uses an authorization filter, setting the fallback policy uses endpoint routing. Setting the fallback policy is the preferred way to require all users be authenticated. + +Add [AllowAnonymous](xref:Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute) to the `Index` and `Privacy` pages so anonymous users can get information about the site before they register: + +[!code-csharp[](secure-data/samples/final6/Pages/Index.cshtml.cs?highlight=1,6)] + +### Configure the test account + +The `SeedData` class creates two accounts: administrator and manager. Use the [Secret Manager tool](xref:security/app-secrets) to set a password for these accounts. Set the password from the project directory (the directory containing `Program.cs`): + +```dotnetcli +dotnet user-secrets set SeedUserPW +``` + +If a weak password is specified, an exception is thrown when `SeedData.Initialize` is called. + +Update the app to use the test password: + +[!code-csharp[](secure-data/samples/final6/Program.cs?name=snippet4&highlight=34-99)] + +### Create the test accounts and update the contacts + +Update the `Initialize` method in the `SeedData` class to create the test accounts: + +[!code-csharp[](secure-data/samples/final6/Data/SeedData.cs?name=snippet_Initialize)] + +Add the administrator user ID and `ContactStatus` to the contacts. Make one of the contacts "Submitted" and one "Rejected". Add the user ID and status to all the contacts. Only one contact is shown: + +[!code-csharp[](secure-data/samples/final6/Data/SeedData.cs?name=snippet1&highlight=17,18)] + +## Create owner, manager, and administrator authorization handlers + +Create a `ContactIsOwnerAuthorizationHandler` class in the *Authorization* folder. The `ContactIsOwnerAuthorizationHandler` verifies that the user acting on a resource owns the resource. + +[!code-csharp[](secure-data/samples/final6/Authorization/ContactIsOwnerAuthorizationHandler.cs)] + +The `ContactIsOwnerAuthorizationHandler` calls [context.Succeed](xref:Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext.Succeed%2A) if the current authenticated user is the contact owner. Authorization handlers generally: + +* Call `context.Succeed` when the requirements are met. +* Return `Task.CompletedTask` when requirements aren't met. Returning `Task.CompletedTask` without a prior call to `context.Success` or `context.Fail`, is not a success or failure, it allows other authorization handlers to run. + +If you need to explicitly fail, call [context.Fail](xref:Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext.Fail%2A). + +The app allows contact owners to edit/delete/create their own data. `ContactIsOwnerAuthorizationHandler` doesn't need to check the operation passed in the requirement parameter. + +### Create a manager authorization handler + +Create a `ContactManagerAuthorizationHandler` class in the *Authorization* folder. The `ContactManagerAuthorizationHandler` verifies the user acting on the resource is a manager. Only managers can approve or reject content changes (new or changed). + +[!code-csharp[](secure-data/samples/final6/Authorization/ContactManagerAuthorizationHandler.cs)] + +### Create an administrator authorization handler + +Create a `ContactAdministratorsAuthorizationHandler` class in the *Authorization* folder. The `ContactAdministratorsAuthorizationHandler` verifies the user acting on the resource is an administrator. Administrator can do all operations. + +[!code-csharp[](secure-data/samples/final6/Authorization/ContactAdministratorsAuthorizationHandler.cs)] + +## Register the authorization handlers + +Services using Entity Framework Core must be registered for [dependency injection](xref:fundamentals/dependency-injection) using . The `ContactIsOwnerAuthorizationHandler` uses ASP.NET Core [Identity](xref:security/authentication/identity), which is built on Entity Framework Core. Register the handlers with the service collection so they're available to the `ContactsController` through [dependency injection](xref:fundamentals/dependency-injection). Add the following code to the end of `ConfigureServices`: + +[!code-csharp[](secure-data/samples/final6/Program.cs?name=snippet4&highlight=22-30)] + +`ContactAdministratorsAuthorizationHandler` and `ContactManagerAuthorizationHandler` are added as singletons. They're singletons because they don't use EF and all the information needed is in the `Context` parameter of the `HandleRequirementAsync` method. + +## Support authorization + +In this section, you update the Razor Pages and add an operations requirements class. + +### Review the contact operations requirements class + +Review the `ContactOperations` class. This class contains the requirements the app supports: + +[!code-csharp[](secure-data/samples/final3/Authorization/ContactOperations.cs)] + +### Create a base class for the Contacts Razor Pages + +Create a base class that contains the services used in the contacts Razor Pages. The base class puts the initialization code in one location: + +[!code-csharp[](secure-data/samples/final3/Pages/Contacts/DI_BasePageModel.cs)] + +The preceding code: + +* Adds the `IAuthorizationService` service to access to the authorization handlers. +* Adds the Identity `UserManager` service. +* Add the `ApplicationDbContext`. + +### Update the CreateModel + +Update the create page model: + +* Constructor to use the `DI_BasePageModel` base class. +* `OnPostAsync` method to: + * Add the user ID to the `Contact` model. + * Call the authorization handler to verify the user has permission to create contacts. + +[!code-csharp[](secure-data/samples/final6/Pages/Contacts/Create.cshtml.cs?name=snippet)] + +### Update the IndexModel + +Update the `OnGetAsync` method so only approved contacts are shown to general users: + +[!code-csharp[](secure-data/samples/final6/Pages/Contacts/Index.cshtml.cs?name=snippet)] + +### Update the EditModel + +Add an authorization handler to verify the user owns the contact. Because resource authorization is being validated, the `[Authorize]` attribute is not enough. The app doesn't have access to the resource when attributes are evaluated. Resource-based authorization must be imperative. Checks must be performed once the app has access to the resource, either by loading it in the page model or by loading it within the handler itself. You frequently access the resource by passing in the resource key. + +[!code-csharp[](secure-data/samples/final6/Pages/Contacts/Edit.cshtml.cs?name=snippet)] + +### Update the DeleteModel + +Update the delete page model to use the authorization handler to verify the user has delete permission on the contact. + +[!code-csharp[](secure-data/samples/final6/Pages/Contacts/Delete.cshtml.cs?name=snippet)] + +## Inject the authorization service into the views + +Currently, the UI shows edit and delete links for contacts the user can't modify. + +Inject the authorization service in the `Pages/_ViewImports.cshtml` file so it's available to all views: + +[!code-cshtml[](secure-data/samples/final6/Pages/_ViewImports.cshtml?highlight=6-99)] + +The preceding markup adds several `using` statements. + +Update the **Edit** and **Delete** links in `Pages/Contacts/Index.cshtml` so they're only rendered for users with the appropriate permissions: + +[!code-cshtml[](secure-data/samples/final6/Pages/Contacts/Index.cshtml?highlight=34-36,62-999)] + +> [!WARNING] +> Hiding links from users that don't have permission to change data doesn't secure the app. Hiding links makes the app more user-friendly by displaying only valid links. Users can hack the generated URLs to invoke edit and delete operations on data they don't own. The Razor Page or controller must enforce access checks to secure the data. + +### Update Details + +Update the details view so managers can approve or reject contacts: + +[!code-cshtml[](secure-data/samples/final6/Pages/Contacts/Details.cshtml?name=snippet)] + +### Update the details page model + +[!code-csharp[](secure-data/samples/final6/Pages/Contacts/Details.cshtml.cs?name=snippet)] + +## Add or remove a user to a role + +See [this issue](https://github.com/dotnet/AspNetCore.Docs/issues/8502) for information on: + +* Removing privileges from a user. For example, muting a user in a chat app. +* Adding privileges to a user. + + + +## Differences between Challenge and Forbid + +This app sets the default policy to [require authenticated users](#require-authenticated-users). The following code allows anonymous users. Anonymous users are allowed to show the differences between Challenge vs Forbid. + +[!code-csharp[](secure-data/samples/final6/Pages/Contacts/Details2.cshtml.cs?name=snippet)] + +In the preceding code: + +* When the user is **not** authenticated, a `ChallengeResult` is returned. When a `ChallengeResult` is returned, the user is redirected to the sign-in page. +* When the user is authenticated, but not authorized, a `ForbidResult` is returned. When a `ForbidResult` is returned, the user is redirected to the access denied page. + +## Test the completed app + +> [!WARNING] +> This article uses the [Secret Manager tool](xref:security/app-secrets) to store the password for the seeded user accounts. The Secret Manager tool is used to store sensitive data during local development. For information on authentication procedures that can be used when an app is deployed to a test or production environment, see [Secure authentication flows](xref:security/index#secure-authentication-flows). + +If you haven't already set a password for seeded user accounts, use the [Secret Manager tool](xref:security/app-secrets#secret-manager) to set a password: + +* Choose a [strong password](https://support.microsoft.com/windows/create-and-use-strong-passwords-c5cebb49-8c53-4f5e-2bc4-fe357ca048eb): + * At least 12 characters long but 14 or more is better. + * A combination of uppercase letters, lowercase letters, numbers, and symbols. + * Not a word that can be found in a dictionary or the name of a person, character, product, or organization. + * Significantly different from your previous passwords. + * Easy for you to remember but difficult for others to guess. Consider using a memorable phrase like "6MonkeysRLooking^". +* Execute the following command from the project's folder, where `` is the password: + + ```dotnetcli + dotnet user-secrets set SeedUserPW + ``` + +If the app has contacts: + +* Delete all of the records in the `Contact` table. +* Restart the app to seed the database. + +An easy way to test the completed app is to launch three different browsers (or incognito/InPrivate sessions). In one browser, register a new user (for example, `test@example.com`). Sign in to each browser with a different user. Verify the following operations: + +* Registered users can view all of the approved contact data. +* Registered users can edit/delete their own data. +* Managers can approve/reject contact data. The `Details` view shows **Approve** and **Reject** buttons. +* Administrators can approve/reject and edit/delete all data. + +| User | Approve or reject contacts| Options | +| ------------------- | :---------------: | ---------------------------------------- | +| test@example.com | No | Edit and delete their data. | +| manager@contoso.com | Yes | Edit and delete their data. | +| admin@contoso.com | Yes | Edit and delete ***all*** data. | + +Create a contact in the administrator's browser. Copy the URL for delete and edit from the administrator contact. Paste these links into the test user's browser to verify the test user can't perform these operations. + +## Create the starter app + +* Create a Razor Pages app named "ContactManager" + * Create the app with **Individual Accounts**. + * Name it "ContactManager" so the namespace matches the namespace used in the sample. + * `-uld` specifies LocalDB instead of SQLite + + ```dotnetcli + dotnet new webapp -o ContactManager -au Individual -uld + ``` + +* Add `Models/Contact.cs`: + secure-data\samples\starter6\ContactManager\Models\Contact.cs + [!code-csharp[](secure-data/samples/starter6/Models/Contact.cs)] + +* Scaffold the `Contact` model. +* Create initial migration and update the database: + +```dotnetcli +dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design +dotnet tool install -g dotnet-aspnet-codegenerator +dotnet-aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries +dotnet ef database drop -f +dotnet ef migrations add initial +dotnet ef database update +``` + +[!INCLUDE[](~/includes/dotnet-tool-install-arch-options.md)] + +* Update the **ContactManager** anchor in the `Pages/Shared/_Layout.cshtml` file: + + ```cshtml + Contact Manager + ``` + +* Test the app by creating, editing, and deleting a contact + +### Seed the database + + +Add the [SeedData](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/security/authorization/secure-data/samples/starter6/Data/SeedData.cs) class to the *Data* folder: + +[!code-csharp[](secure-data/samples/starter6/Data/SeedData.cs)] + +Call `SeedData.Initialize` from `Program.cs`: + +[!code-csharp[](secure-data/samples/starter6/Program.cs?highlight=18-23)] + +Test that the app seeded the database. If there are any rows in the contact DB, the seed method doesn't run. + +:::moniker-end + +:::moniker range="< aspnetcore-6.0" + +This tutorial shows how to create an ASP.NET Core web app with user data protected by authorization. It displays a list of contacts that authenticated (registered) users have created. There are three security groups: + +* **Registered users** can view all the approved data and can edit/delete their own data. +* **Managers** can approve or reject contact data. Only approved contacts are visible to users. +* **Administrators** can approve/reject and edit/delete any data. + +The images in this document don't exactly match the latest templates. + +In the following image, user Rick (`rick@example.com`) is signed in. Rick can only view approved contacts and **Edit**/**Delete**/**Create New** links for his contacts. Only the last record, created by Rick, displays **Edit** and **Delete** links. Other users won't see the last record until a manager or administrator changes the status to "Approved". + +![Screenshot showing Rick signed in](secure-data/_static/rick.png) + +In the following image, `manager@contoso.com` is signed in and in the manager's role: + +![Screenshot showing manager@contoso.com signed in](secure-data/_static/manager1.png) + +The following image shows the managers details view of a contact: + +![Manager's view of a contact](secure-data/_static/manager.png) + +The **Approve** and **Reject** buttons are only displayed for managers and administrators. + +In the following image, `admin@contoso.com` is signed in and in the administrator's role: + +![Screenshot showing admin@contoso.com signed in](secure-data/_static/admin.png) + +The administrator has all privileges. She can read/edit/delete any contact and change the status of contacts. + +The app was created by [scaffolding](xref:tutorials/first-mvc-app/adding-model#scaffold-movie-pages) the following `Contact` model: + +[!code-csharp[](secure-data/samples/starter2.1/Models/Contact.cs?name=snippet1)] + +The sample contains the following authorization handlers: + +* `ContactIsOwnerAuthorizationHandler`: Ensures that a user can only edit their data. +* `ContactManagerAuthorizationHandler`: Allows managers to approve or reject contacts. +* `ContactAdministratorsAuthorizationHandler`: Allows administrators to: + * Approve or reject contacts + * Edit and delete contacts + +## Prerequisites + +This tutorial is advanced. You should be familiar with: + +* [ASP.NET Core](xref:tutorials/first-mvc-app/start-mvc) +* [Authentication](xref:security/authentication/identity) +* [Account Confirmation and Password Recovery](xref:security/authentication/accconfirm) +* [Authorization](xref:security/authorization/introduction) +* [Entity Framework Core](xref:data/ef-mvc/intro) + +## The starter and completed app + +[Download](xref:fundamentals/index#how-to-download-a-sample) the [completed](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/security/authorization/secure-data/samples) app. [Test](#test-the-completed-app) the completed app so you become familiar with its security features. + +### The starter app + +[Download](xref:fundamentals/index#how-to-download-a-sample) the [starter](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/razor-pages/security/authorization/secure-data/samples/) app. + +Run the app, tap the **ContactManager** link, and verify you can create, edit, and delete a contact. To create the starter app, see [Create the starter app](#create-the-starter-app). + +## Secure user data + +The following sections have all the major steps to create the secure user data app. You may find it helpful to refer to the completed project. + +### Tie the contact data to the user + +Use the ASP.NET [Identity](xref:security/authentication/identity) user ID to ensure users can edit their data, but not other users data. Add `OwnerID` and `ContactStatus` to the `Contact` model: + +[!code-csharp[](secure-data/samples/final3/Models/Contact.cs?name=snippet1&highlight=5-6,16-999)] + +`OwnerID` is the user's ID from the `AspNetUser` table in the [Identity](xref:security/authentication/identity) database. The `Status` field determines if a contact is viewable by general users. + +Create a new migration and update the database: + +```dotnetcli +dotnet ef migrations add userID_Status +dotnet ef database update +``` + +### Add Role services to Identity + +Append to add Role services: + +[!code-csharp[](secure-data/samples/final3/Startup.cs?name=snippet2&highlight=8)] + + + +### Require authenticated users + +Set the fallback authentication policy to require users to be authenticated: + +[!code-csharp[](secure-data/samples/final3/Startup.cs?name=snippet&highlight=13-99)] + +The preceding highlighted code sets the [fallback authentication policy](xref:Microsoft.AspNetCore.Authorization.AuthorizationOptions.FallbackPolicy). The fallback authentication policy requires ***all*** users to be authenticated, except for Razor Pages, controllers, or action methods with an authentication attribute. For example, Razor Pages, controllers, or action methods with `[AllowAnonymous]` or `[Authorize(PolicyName="MyPolicy")]` use the applied authentication attribute rather than the fallback authentication policy. + + adds to the current instance, which enforces that the current user is authenticated. + +The fallback authentication policy: + +* Is applied to all requests that do not explicitly specify an authentication policy. For requests served by endpoint routing, this would include any endpoint that does not specify an authorization attribute. For requests served by other middleware after the authorization middleware, such as [static files](xref:fundamentals/static-files), this would apply the policy to all requests. + +Setting the fallback authentication policy to require users to be authenticated protects newly added Razor Pages and controllers. Having authentication required by default is more secure than relying on new controllers and Razor Pages to include the `[Authorize]` attribute. + +The class also contains . The `DefaultPolicy` is the policy used with the `[Authorize]` attribute when no policy is specified. `[Authorize]` doesn't contain a named policy, unlike `[Authorize(PolicyName="MyPolicy")]`. + +For more information on policies, see . + +An alternative way for MVC controllers and Razor Pages to require all users be authenticated is adding an authorization filter: + +[!code-csharp[](secure-data/samples/final3/Startup2.cs?name=snippet&highlight=14-99)] + +The preceding code uses an authorization filter, setting the fallback policy uses endpoint routing. Setting the fallback policy is the preferred way to require all users be authenticated. + +Add [AllowAnonymous](xref:Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute) to the `Index` and `Privacy` pages so anonymous users can get information about the site before they register: + +[!code-csharp[](secure-data/samples/final3/Pages/Index.cshtml.cs?highlight=1,7)] + +### Configure the test account + +The `SeedData` class creates two accounts: administrator and manager. Use the [Secret Manager tool](xref:security/app-secrets) to set a password for these accounts. Set the password from the project directory (the directory containing `Program.cs`): + +```dotnetcli +dotnet user-secrets set SeedUserPW +``` + +If a strong password is not specified, an exception is thrown when `SeedData.Initialize` is called. + +Update `Main` to use the test password: + +[!code-csharp[](secure-data/samples/final3/Program.cs?name=snippet)] + +### Create the test accounts and update the contacts + +Update the `Initialize` method in the `SeedData` class to create the test accounts: + +[!code-csharp[](secure-data/samples/final6/Data/SeedData.cs?name=snippet_Initialize)] + +Add the administrator user ID and `ContactStatus` to the contacts. Make one of the contacts "Submitted" and one "Rejected". Add the user ID and status to all the contacts. Only one contact is shown: + +[!code-csharp[](secure-data/samples/final6/Data/SeedData.cs?name=snippet1&highlight=17,18)] + +## Create owner, manager, and administrator authorization handlers + +Create a `ContactIsOwnerAuthorizationHandler` class in the *Authorization* folder. The `ContactIsOwnerAuthorizationHandler` verifies that the user acting on a resource owns the resource. + +[!code-csharp[](secure-data/samples/final6/Authorization/ContactIsOwnerAuthorizationHandler.cs)] + +The `ContactIsOwnerAuthorizationHandler` calls [context.Succeed](xref:Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext.Succeed%2A) if the current authenticated user is the contact owner. Authorization handlers generally: + +* Call `context.Succeed` when the requirements are met. +* Return `Task.CompletedTask` when requirements aren't met. Returning `Task.CompletedTask` without a prior call to `context.Success` or `context.Fail`, is not a success or failure, it allows other authorization handlers to run. + +If you need to explicitly fail, call [context.Fail](xref:Microsoft.AspNetCore.Authorization.AuthorizationHandlerContext.Fail%2A). + +The app allows contact owners to edit/delete/create their own data. `ContactIsOwnerAuthorizationHandler` doesn't need to check the operation passed in the requirement parameter. + +### Create a manager authorization handler + +Create a `ContactManagerAuthorizationHandler` class in the *Authorization* folder. The `ContactManagerAuthorizationHandler` verifies the user acting on the resource is a manager. Only managers can approve or reject content changes (new or changed). + +[!code-csharp[](secure-data/samples/final6/Authorization/ContactManagerAuthorizationHandler.cs)] + +### Create an administrator authorization handler + +Create a `ContactAdministratorsAuthorizationHandler` class in the *Authorization* folder. The `ContactAdministratorsAuthorizationHandler` verifies the user acting on the resource is an administrator. Administrator can do all operations. + +[!code-csharp[](secure-data/samples/final6/Authorization/ContactAdministratorsAuthorizationHandler.cs)] + +## Register the authorization handlers + +Services using Entity Framework Core must be registered for [dependency injection](xref:fundamentals/dependency-injection) using . The `ContactIsOwnerAuthorizationHandler` uses ASP.NET Core [Identity](xref:security/authentication/identity), which is built on Entity Framework Core. Register the handlers with the service collection so they're available to the `ContactsController` through [dependency injection](xref:fundamentals/dependency-injection). Add the following code to the end of `ConfigureServices`: + +[!code-csharp[](secure-data/samples/final3/Startup.cs?name=snippet_defaultPolicy&highlight=23-99)] + +`ContactAdministratorsAuthorizationHandler` and `ContactManagerAuthorizationHandler` are added as singletons. They're singletons because they don't use EF and all the information needed is in the `Context` parameter of the `HandleRequirementAsync` method. + +## Support authorization + +In this section, you update the Razor Pages and add an operations requirements class. + +### Review the contact operations requirements class + +Review the `ContactOperations` class. This class contains the requirements the app supports: + +[!code-csharp[](secure-data/samples/final3/Authorization/ContactOperations.cs)] + +### Create a base class for the Contacts Razor Pages + +Create a base class that contains the services used in the contacts Razor Pages. The base class puts the initialization code in one location: + +[!code-csharp[](secure-data/samples/final3/Pages/Contacts/DI_BasePageModel.cs)] + +The preceding code: + +* Adds the `IAuthorizationService` service to access to the authorization handlers. +* Adds the Identity `UserManager` service. +* Add the `ApplicationDbContext`. + +### Update the CreateModel + +Update the create page model constructor to use the `DI_BasePageModel` base class: + +[!code-csharp[](secure-data/samples/final3/Pages/Contacts/Create.cshtml.cs?name=snippetCtor)] + +Update the `CreateModel.OnPostAsync` method to: + +* Add the user ID to the `Contact` model. +* Call the authorization handler to verify the user has permission to create contacts. + +[!code-csharp[](secure-data/samples/final3/Pages/Contacts/Create.cshtml.cs?name=snippet_Create)] + +### Update the IndexModel + +Update the `OnGetAsync` method so only approved contacts are shown to general users: + +[!code-csharp[](secure-data/samples/final3/Pages/Contacts/Index.cshtml.cs?name=snippet)] + +### Update the EditModel + +Add an authorization handler to verify the user owns the contact. Because resource authorization is being validated, the `[Authorize]` attribute is not enough. The app doesn't have access to the resource when attributes are evaluated. Resource-based authorization must be imperative. Checks must be performed once the app has access to the resource, either by loading it in the page model or by loading it within the handler itself. You frequently access the resource by passing in the resource key. + +[!code-csharp[](secure-data/samples/final3/Pages/Contacts/Edit.cshtml.cs?name=snippet)] + +### Update the DeleteModel + +Update the delete page model to use the authorization handler to verify the user has delete permission on the contact. + +[!code-csharp[](secure-data/samples/final3/Pages/Contacts/Delete.cshtml.cs?name=snippet)] + +## Inject the authorization service into the views + +Currently, the UI shows edit and delete links for contacts the user can't modify. + +Inject the authorization service in the `Pages/_ViewImports.cshtml` file so it's available to all views: + +[!code-cshtml[](secure-data/samples/final3/Pages/_ViewImports.cshtml?highlight=6-99)] + +The preceding markup adds several `using` statements. + +Update the **Edit** and **Delete** links in `Pages/Contacts/Index.cshtml` so they're only rendered for users with the appropriate permissions: + +[!code-cshtml[](secure-data/samples/final3/Pages/Contacts/Index.cshtml?highlight=34-36,62-999)] + +> [!WARNING] +> Hiding links from users that don't have permission to change data doesn't secure the app. Hiding links makes the app more user-friendly by displaying only valid links. Users can hack the generated URLs to invoke edit and delete operations on data they don't own. The Razor Page or controller must enforce access checks to secure the data. + +### Update Details + +Update the details view so managers can approve or reject contacts: + +[!code-cshtml[](secure-data/samples/final3/Pages/Contacts/Details.cshtml?name=snippet)] + +Update the details page model: + +[!code-csharp[](secure-data/samples/final3/Pages/Contacts/Details.cshtml.cs?name=snippet)] + +## Add or remove a user to a role + +See [this issue](https://github.com/dotnet/AspNetCore.Docs/issues/8502) for information on: + +* Removing privileges from a user. For example, muting a user in a chat app. +* Adding privileges to a user. + + + +## Differences between Challenge and Forbid + +This app sets the default policy to [require authenticated users](#require-authenticated-users). The following code allows anonymous users. Anonymous users are allowed to show the differences between Challenge vs Forbid. + +[!code-csharp[](secure-data/samples/final3/Pages/Contacts/Details2.cshtml.cs?name=snippet)] + +In the preceding code: + +* When the user is **not** authenticated, a `ChallengeResult` is returned. When a `ChallengeResult` is returned, the user is redirected to the sign-in page. +* When the user is authenticated, but not authorized, a `ForbidResult` is returned. When a `ForbidResult` is returned, the user is redirected to the access denied page. + +## Test the completed app + +If you haven't already set a password for seeded user accounts, use the [Secret Manager tool](xref:security/app-secrets#secret-manager) to set a password: + +* Choose a strong password: Use eight or more characters and at least one upper-case character, number, and symbol. For example, `Passw0rd!` meets the strong password requirements. +* Execute the following command from the project's folder, where `` is the password: + + ```dotnetcli + dotnet user-secrets set SeedUserPW + ``` + +If the app has contacts: + +* Delete all of the records in the `Contact` table. +* Restart the app to seed the database. + +An easy way to test the completed app is to launch three different browsers (or incognito/InPrivate sessions). In one browser, register a new user (for example, `test@example.com`). Sign in to each browser with a different user. Verify the following operations: + +* Registered users can view all of the approved contact data. +* Registered users can edit/delete their own data. +* Managers can approve/reject contact data. The `Details` view shows **Approve** and **Reject** buttons. +* Administrators can approve/reject and edit/delete all data. + +| User | Seeded by the app | Options | +| ------------------- | :---------------: | ---------------------------------------- | +| test@example.com | No | Edit/delete the own data. | +| manager@contoso.com | Yes | Approve/reject and edit/delete own data. | +| admin@contoso.com | Yes | Approve/reject and edit/delete all data. | + +Create a contact in the administrator's browser. Copy the URL for delete and edit from the administrator contact. Paste these links into the test user's browser to verify the test user can't perform these operations. + +## Create the starter app + +* Create a Razor Pages app named "ContactManager" + * Create the app with **Individual Accounts**. + * Name it "ContactManager" so the namespace matches the namespace used in the sample. + * `-uld` specifies LocalDB instead of SQLite + + ```dotnetcli + dotnet new webapp -o ContactManager -au Individual -uld + ``` + +* Add `Models/Contact.cs`: + + [!code-csharp[](secure-data/samples/starter2.1/Models/Contact.cs?name=snippet1)] + +* Scaffold the `Contact` model. +* Create initial migration and update the database: + +```dotnetcli +dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design +dotnet tool install -g dotnet-aspnet-codegenerator +dotnet aspnet-codegenerator razorpage -m Contact -udl -dc ApplicationDbContext -outDir Pages\Contacts --referenceScriptLibraries +dotnet ef database drop -f +dotnet ef migrations add initial +dotnet ef database update +``` + +[!INCLUDE[](~/includes/dotnet-tool-install-arch-options.md)] + +If you experience a bug with the `dotnet aspnet-codegenerator razorpage` command, see [this GitHub issue](https://github.com/aspnet/Scaffolding/issues/984). + +* Update the **ContactManager** anchor in the `Pages/Shared/_Layout.cshtml` file: + + ```cshtml +ContactManager + ``` + +* Test the app by creating, editing, and deleting a contact + +### Seed the database + +Add the [SeedData](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authorization/secure-data/razor-pages/samples/starter3/Data/SeedData.cs) class to the *Data* folder: + +[!code-csharp[](secure-data/samples/starter3/Data/SeedData.cs)] + +Call `SeedData.Initialize` from `Main`: + +[!code-csharp[](secure-data/samples/starter3/Program.cs)] + +Test that the app seeded the database. If there are any rows in the contact DB, the seed method doesn't run. + +:::moniker-end + + + +### Additional resources + +* [Tutorial: Build an ASP.NET Core and Azure SQL Database app in Azure App Service](/azure/app-service/tutorial-dotnetcore-sqldb-app) +* [ASP.NET Core Authorization Lab](https://github.com/blowdart/AspNetAuthorizationWorkshop). This lab goes into more detail on the security features introduced in this tutorial. +* +* [Custom policy-based authorization](xref:security/authorization/policies) diff --git a/aspnetcore/security/authorization/secure-data.md b/aspnetcore/security/authorization/secure-data.md index f206a2f65935..a2452f5c45cf 100644 --- a/aspnetcore/security/authorization/secure-data.md +++ b/aspnetcore/security/authorization/secure-data.md @@ -8,11 +8,8 @@ ms.date: 12/5/2021 ms.sfi.ropc: t uid: security/authorization/secure-data --- - # Create an ASP.NET Core web app with user data protected by authorization -By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Joe Audette](https://twitter.com/joeaudette) - :::moniker range=">= aspnetcore-6.0" This tutorial shows how to create an ASP.NET Core web app with user data protected by authorization. It displays a list of contacts that authenticated (registered) users have created. There are three security groups: @@ -68,14 +65,13 @@ This tutorial is advanced. You should be familiar with: [Download](xref:fundamentals/index#how-to-download-a-sample) the [completed](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/security/authorization/secure-data/samples) app. [Test](#test-the-completed-app) the completed app so you become familiar with its security features. > [!TIP] -> Use [`git sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) to only download the sample subfolder. -For example: -``` -git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.git --sparse -cd AspNetCore.Docs -git sparse-checkout init --cone -git sparse-checkout set aspnetcore/security/authorization/secure-data/samples -``` +> Use [`git sparse-checkout`](https://git-scm.com/docs/git-sparse-checkout) to only download the sample subfolder. For example: +> ``` +> git clone --depth 1 --filter=blob:none https://github.com/dotnet/AspNetCore.Docs.git --sparse +> cd AspNetCore.Docs +> git sparse-checkout init --cone +> git sparse-checkout set aspnetcore/security/authorization/secure-data/samples +> ``` ### The starter app diff --git a/aspnetcore/toc.yml b/aspnetcore/toc.yml index 27ef9ec5ddc5..2f0a89713ba8 100644 --- a/aspnetcore/toc.yml +++ b/aspnetcore/toc.yml @@ -566,12 +566,16 @@ items: items: - name: Authorization items: + - name: Create a web app with authorization + uid: razor-pages/security/authorization/secure-data - name: Simple authorization uid: razor-pages/security/authorization/simple - name: Authorization conventions uid: razor-pages/security/authorization/conventions - name: Role-based authorization uid: razor-pages/security/authorization/roles + - name: Claim-based authorization + uid: razor-pages/security/authorization/claims - name: MVC items: - name: Overview @@ -622,6 +626,8 @@ items: uid: mvc/security/authorization/iard - name: Role-based authorization uid: mvc/security/authorization/roles + - name: Claim-based authorization + uid: mvc/security/authorization/claims - name: Blazor items: - name: Overview @@ -2083,7 +2089,7 @@ items: uid: security/authorization/iard - name: Role-based authorization uid: security/authorization/roles - - name: Claims-based authorization + - name: Claim-based authorization uid: security/authorization/claims - name: Policy-based authorization uid: security/authorization/policies From 28636249f7ccebbb2397f2d38140c12cbdc5cdf9 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:00:45 -0400 Subject: [PATCH 2/2] Updates --- .../security/authorization/secure-data.md | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/aspnetcore/security/authorization/secure-data.md b/aspnetcore/security/authorization/secure-data.md index a2452f5c45cf..108151824aa2 100644 --- a/aspnetcore/security/authorization/secure-data.md +++ b/aspnetcore/security/authorization/secure-data.md @@ -10,6 +10,224 @@ uid: security/authorization/secure-data --- # Create an ASP.NET Core web app with user data protected by authorization +Scaffold Identity into the app and create the database with existing movie data migrations + +Install NuGet package: `Microsoft.AspNetCore.Identity.EntityFrameworkCore` + +Add an application user (`ApplicationUser`) that implements `IdentityUser`. + +`Data/ApplicationUser.cs`: + +```csharp +using Microsoft.AspNetCore.Identity; + +namespace BlazorWebAppMovies.Data; + +public class ApplicationUser : IdentityUser +{ + // Add custom user properties here +} +``` + +Change `BlazorWebAppMoviesContext` to inherit from `IdentityDbContext`: + +```diff +- public class BlazorWebAppMoviesContext : DbContext ++ public class BlazorWebAppMoviesContext : IdentityDbContext + +``` + +In **Solution Explorer**, right-click the project and select **Add** > **New Scaffolded Item**. + +In the **Add New Scaffolded Item** dialog, select **Identity** > **Blazor Identity**. Select the **Add** button. + +In the **Add Blazor Identity** dialog, select `BlazorWebAppMoviesContext` for the **DbContext class**. Select the **Add** button to accept the suggested database context, which is the existing context for the existing database. Select the **Add** button to scaffold Identity into the app. Wait until the scaffolding process is complete. + +Due to a temporary [problem with Identity scaffolding (`dotnet/Scaffolding` #3716)](https://github.com/dotnet/Scaffolding/issues/3716), use the following instructions to move the generated Identity assets: + +* Move the contents of the `BlazorWebAppMovies` > `BlazorWebAppMovies` > `Components` > `Account` > `Pages` folder to the app's `BlazorWebAppMovies` > `Components` > `Account` folder. +* Move the component files in the `BlazorWebAppMovies` > `BlazorWebAppMovies` > `Components` > `Account` > `Shared` folder to the app's `BlazorWebAppMovies` > `Components` > `Account` > `Shared` folder. +* Move the C# files from the `BlazorWebAppMovies` > `BlazorWebAppMovies` > `Components` > `Account` folder to the app's `BlazorWebAppMovies` > `Components` > `Account` folder. +* Delete the `BlazorWebAppMovies` > `BlazorWebAppMovies` folder. + +Next, update the seed data for the app to include seeded user accounts, which include role claims for test users. + +Locate the `SeedData` class (`Data/SeedData.cs`). Add the following class at the bottom of the file just inside the closing brace for the namespace. The `SeedUser` represents a seeded test user for the app and includes properties to hold the user's roles and claims: + +```csharp +private class SeedUser : ApplicationUser +{ + public string[]? Roles { get; set; } + public List>? Claims { get; set; } +} +``` + +At the top of the `SeedData` class, add the following `seedUsers` field. This field holds the test users to seed into the database: + +```csharp +private static readonly IEnumerable seedUsers = +[ + new SeedUser() + { + Email = "leela@contoso.com", + NormalizedEmail = "LEELA@CONTOSO.COM", + NormalizedUserName = "LEELA@CONTOSO.COM", + Roles = [ "Administrator" ], + Claims = [], + UserName = "leela@contoso.com" + }, + new SeedUser() + { + Email = "harry@contoso.com", + NormalizedEmail = "HARRY@CONTOSO.COM", + NormalizedUserName = "HARRY@CONTOSO.COM", + Roles = [ "Manager" ], + Claims = [], + UserName = "harry@contoso.com" + }, +]; +``` + +Change the method signature of the `Initialize` method because calls added to the method in the next step are asynchronous: + +```diff +- public static void Initialize(IServiceProvider serviceProvider) ++ public static async Task Initialize(IServiceProvider serviceProvider) +``` + +Immediately above the line `context.SaveChanges();` in the `Initialize` method, add the following C# code. The code adds roles to the database using the `RoleManager` service, adds the test users in the `seedUsers` field to the Identity store using the `UserManager` service, and assigns any roles and claims to the users with a common test user password of `Passw0rd!`: + +```csharp +var userStore = new UserStore(context); +var password = new PasswordHasher(); + +using var roleManager = serviceProvider.GetRequiredService>(); + +string[] roles = ["Administrator", "Manager"]; + +foreach (var role in roles) +{ + if (!await roleManager.RoleExistsAsync(role)) + { + await roleManager.CreateAsync(new IdentityRole(role)); + } +} + +using var userManager = serviceProvider.GetRequiredService>(); + +foreach (var user in seedUsers) +{ + var hashed = password.HashPassword(user, "Passw0rd!"); + user.PasswordHash = hashed; + await userStore.CreateAsync(user); + + if (user.Email is not null) + { + var appUser = await userManager.FindByEmailAsync(user.Email); + + if (appUser is not null) + { + if (user.Roles is not null) + { + await userManager.AddToRolesAsync(appUser, user.Roles); + } + + if (user.Claims is not null) + { + foreach (var claim in user.Claims) + { + await userManager.AddClaimAsync(appUser, new Claim(claim.Key, claim.Value)); + } + } + } + } +} +``` + +In the app's `Program` file, add role-related services for the app's users by calling `AddRoles` with the `ApplicationUser` type: + +```diff +builder.Services.AddIdentityCore(options => + { + options.SignIn.RequireConfirmedAccount = true; + options.Stores.SchemaVersion = IdentitySchemaVersions.Version3; + }) ++ .AddRoles() + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders(); +``` + +Change the line that seeds movies and users to await the call: + +```diff +- SeedData.Initialize(services); ++ await SeedData.Initialize(services); +``` + +In the app's main layout component (`Components/Layout/MainLayout.razor`), replace the link to the `About` page with the user's name from Identity inside an [`AuthorizeView` component]() that only displays its contents for an authenticated user: + +```diff +- About ++ ++ @context.User.Identity?.Name ++ +``` + +To inspect the claims of authenticated users, add the following `UserClaims` component to the app's `Pages` folder: + +`Components/Pages/UserClaims.razor`: + +```razor +@page "/user-claims" +@using System.Security.Claims +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize] + +User Claims + +

User Claims

+ +@if (claims.Any()) +{ +
    + @foreach (var claim in claims) + { +
  • @claim.Type: @claim.Value
  • + } +
+} + +@code { + private IEnumerable claims = []; + + [CascadingParameter] + private Task? AuthState { get; set; } + + protected override async Task OnInitializedAsync() + { + if (AuthState == null) + { + return; + } + + var authState = await AuthState; + claims = authState.User.Claims; + } +} +``` + +Open the `NavMenu` component (`Components/Layout/NavMenu.razor`). Locate the `` tag inside the `` markup. Add an entry to the `` block to show a link (`NavLink`) for the `UserClaims` component. The link only appears for authenticated users: + +```razor + +``` + + :::moniker range=">= aspnetcore-6.0" This tutorial shows how to create an ASP.NET Core web app with user data protected by authorization. It displays a list of contacts that authenticated (registered) users have created. There are three security groups: