diff --git a/aspnetcore/security/authentication/identity-api-authorization.md b/aspnetcore/security/authentication/identity-api-authorization.md index bbc98dd82a88..b7e8412281ed 100644 --- a/aspnetcore/security/authentication/identity-api-authorization.md +++ b/aspnetcore/security/authentication/identity-api-authorization.md @@ -4,7 +4,7 @@ author: JeremyLikness description: Learn how to use Identity to secure a Web API backend for single page applications (SPAs). monikerRange: '>= aspnetcore-3.0' ms.author: tdykstra -ms.date: 12/15/2023 +ms.date: 03/08/2024 uid: security/authentication/identity/spa --- # How to use Identity to secure a Web API backend for SPAs @@ -23,7 +23,6 @@ The steps shown in this article add authentication and authorization to an ASP.N * Isn't already configured for authentication. * Targets `net8.0` or later. -* Includes OpenAPI support. * Can be either minimal API or controller-based API. Some of the testing instructions in this article use the [Swagger UI](/aspnet/core/tutorials/web-api-help-pages-using-swagger) that's included with the project template. The Swagger UI isn't required to use Identity with a Web API backend. @@ -48,13 +47,7 @@ Install these packages by using the [NuGet package manager in Visual Studio](/nu Add a class named `ApplicationDbContext` that inherits from : -```csharp -public class ApplicationDbContext : IdentityDbContext -{ - public ApplicationDbContext(DbContextOptions options) : - base(options) { } -} -``` +:::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\ApplicationDbContext.cs"::: The code shown provides a special constructor that makes it possible to configure the database for different environments. @@ -70,10 +63,7 @@ using Microsoft.EntityFrameworkCore; As noted earlier, the simplest way to get started is to use the in-memory database. With in-memory each run starts with a fresh database, and there's no need to use migrations. After the call to `WebApplication.CreateBuilder(args)`, add the following code to configure Identity to use an in-memory database: -```csharp -builder.Services.AddDbContext( - options => options.UseInMemoryDatabase("AppDb")); -``` +:::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\Program.cs" id="snippetAppDbContext"::: To save user data between sessions when testing or for production use, change the database later to SQLite or SQL Server. @@ -81,18 +71,13 @@ To save user data between sessions when testing or for production use, change th After the call to `WebApplication.CreateBuilder(args)`, call to add services to the dependency injection (DI) container: -```csharp -builder.Services.AddAuthorization(); -``` +:::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\Program.cs" id="snippetAddAuthorization"::: ## Activate Identity APIs After the call to `WebApplication.CreateBuilder(args)`, call and . -```csharp -builder.Services.AddIdentityApiEndpoints() - .AddEntityFrameworkStores(); -``` +:::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\Program.cs" id="snippetActivateAPIs"::: By default, both cookies and proprietary tokens are activated. Cookies and tokens are issued at login if the `useCookies` query string parameter in the login endpoint is `true`. @@ -100,45 +85,23 @@ By default, both cookies and proprietary tokens are activated. Cookies and token After the call to `builder.Build()`, call to map the Identity endpoints: -```csharp -app.MapIdentityApi(); -``` +:::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\Program.cs" id="snippetMapEndpoints"::: ## Secure selected endpoints To secure an endpoint, use the extension method on the `Map{Method}` call that defines the route. For example: -```csharp -app.MapGet("/weatherforecast", (HttpContext httpContext) => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = summaries[Random.Shared.Next(summaries.Length)] - }) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast") -.WithOpenApi() -.RequireAuthorization(); -``` +:::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\Program.cs" id="snippetRequireAuthorization" highlight="15"::: The `RequireAuthorization` method can also be used to: * Secure Swagger UI endpoints, as shown in the following example: - ```csharp - app.MapSwagger().RequireAuthorization(); - ``` - + :::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\Program.cs" id="snippetSwaggerAuth"::: + * Secure with a specific claim or permission, as shown in the following example: -```csharp -RequiresAuthorization("Admin") -``` + :::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\Program.cs" id="snippetRequireAdmin"::: In a controller-based web API project, secure endpoints by applying the [[`Authorize`]](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute) attribute to a controller or action. @@ -233,9 +196,9 @@ Some web clients might not include cookies in the header by default: * The JavaScript `fetch` API doesn't include cookies by default. Enable them by setting `credentials` to the value `include` in the options. * An `HttpClient` running in a Blazor WebAssembly app needs the `HttpRequestMessage` to include credentials, like the following example: -```csharp -request.SetBrowserRequestCredential(BrowserRequestCredentials.Include); -``` + ```csharp + request.SetBrowserRequestCredential(BrowserRequestCredentials.Include); + ``` ## Use token-based authentication @@ -249,20 +212,7 @@ To use token-based authentication, set the `useCookies` query string parameter t To provide a way for the user to log out, define a `/logout` endpoint like the following example: -```csharp -app.MapPost("/logout", async (SignInManager signInManager, - [FromBody]object empty) => -{ - if (empty != null) - { - await signInManager.SignOutAsync(); - return Results.Ok(); - } - return Results.Unauthorized(); -}) -.WithOpenApi() -.RequireAuthorization(); -``` +:::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\Program.cs" id="snippetLogout"::: Provide an empty JSON object (`{}`) in the request body when calling this endpoint. The following code is an example of a call to the logout endpoint: @@ -278,16 +228,16 @@ public signOut() { The call to `MapIdentityApi` adds the following endpoints to the app: -* [Use the `POST /register`](#use-the-post-register-endpoint) -* [Use the `POST /login`](#use-the-post-login-endpoint) -* [Use the `POST /refresh`](#use-the-post-refresh-endpoint) -* [Use the `GET /confirmEmail`](#use-the-get-confirmemail-endpoint) -* [Use the `POST /resendConfirmationEmail`](#use-the-post-resendconfirmationemail-endpoint) -* [Use the `POST /forgotPassword`](#use-the-post-forgotpassword-endpoint) -* [Use the `POST /reset Password`](#use-the-post-resetpassword-endpoint) -* [Use the `POST /manage/2fa`](#use-the-post-manage2fa-endpoint) -* [Use the `GET /manage/info`](#use-the-get-manageinfo-endpoint) -* [Use the `POST /manage/info`](#use-the-post-manageinfo-endpoint) +* [`POST /register`](#use-the-post-register-endpoint) +* [`POST /login`](#use-the-post-login-endpoint) +* [`POST /refresh`](#use-the-post-refresh-endpoint) +* [`GET /confirmEmail`](#use-the-get-confirmemail-endpoint) +* [`POST /resendConfirmationEmail`](#use-the-post-resendconfirmationemail-endpoint) +* [`POST /forgotPassword`](#use-the-post-forgotpassword-endpoint) +* [`POST /reset Password`](#use-the-post-resetpassword-endpoint) +* [`POST /manage/2fa`](#use-the-post-manage2fa-endpoint) +* [`GET /manage/info`](#use-the-get-manageinfo-endpoint) +* [`POST /manage/info`](#use-the-post-manageinfo-endpoint) ## Use the `POST /register` endpoint @@ -409,14 +359,7 @@ If the To set up Identity for email confirmation, add code in `Program.cs` to set `RequireConfirmedEmail` to `true` and add a class that implements `IEmailSender` to the DI container. For example: -```csharp -builder.Services.Configure(options => -{ - options.SignIn.RequireConfirmedEmail = true; -}); - -builder.Services.AddTransient(); -``` +:::code language="csharp" source="~/security/authentication/identity-api-authorization/8samples/APIforSPA/Program.cs" id="snippetConfigureEmail"::: In the preceding example, `EmailSender` is a class that implements `IEmailSender`. For more information, including an example of a class that implements `IEmailSender`, see . diff --git a/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/APIforSPA.csproj b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/APIforSPA.csproj new file mode 100644 index 000000000000..39df6fa81f39 --- /dev/null +++ b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/APIforSPA.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/ApplicationDbContext.cs b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/ApplicationDbContext.cs new file mode 100644 index 000000000000..994dac12e69e --- /dev/null +++ b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/ApplicationDbContext.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +public class ApplicationDbContext : IdentityDbContext +{ + public ApplicationDbContext(DbContextOptions options) : + base(options) + { } +} diff --git a/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/EmailSender.cs b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/EmailSender.cs new file mode 100644 index 000000000000..d624cd376933 --- /dev/null +++ b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/EmailSender.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.Extensions.Options; + +namespace APIforSPA; + +sealed class EmailSender : IEmailSender +{ + private readonly ILogger _logger; + + public EmailSender(ILogger logger) + { + _logger = logger; + } + public List Emails { get; set; } = new(); + + public Task SendEmailAsync(string email, string subject, string htmlMessage) + { + _logger.LogWarning($"{email} {subject} {htmlMessage}"); + Emails.Add(new(email, subject, htmlMessage)); + return Task.CompletedTask; + } +} +sealed record Email(string Address, string Subject, string HtmlMessage); diff --git a/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/Program.cs b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/Program.cs new file mode 100644 index 000000000000..118b16d4cd50 --- /dev/null +++ b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/Program.cs @@ -0,0 +1,129 @@ +#define Version1 // Version1 / Version2 / Version3 / Version4 +// Version2 = require email confirmation +// Version3 = require admin role +// Version4 = require authorization for Swagger +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace APIforSPA; + +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + // + builder.Services.AddIdentityApiEndpoints() + .AddEntityFrameworkStores(); + // + + // + builder.Services.AddDbContext( + options => options.UseInMemoryDatabase("AppDb")); + // + + // + builder.Services.AddAuthorization(); + // + + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + +#if Version2 + // + builder.Services.Configure(options => + { + options.SignIn.RequireConfirmedEmail = true; + }); + + builder.Services.AddTransient(); + // +#endif + + var app = builder.Build(); + + // + app.MapIdentityApi(); + // + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + + app.UseAuthorization(); + + var summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + +#if Version1 || Version2 || Version4 + // + app.MapGet("/weatherforecast", (HttpContext httpContext) => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }) + .ToArray(); + return forecast; + }) + .WithName("GetWeatherForecast") + .WithOpenApi() + .RequireAuthorization(); + // +#endif +#if Version3 + app.MapGet("/weatherforecast", (HttpContext httpContext) => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }) + .ToArray(); + return forecast; + }) + .WithName("GetWeatherForecast") + .WithOpenApi() + // + .RequireAuthorization("Admin"); + // +#endif + +#if Version4 + // + app.MapSwagger().RequireAuthorization(); + // +#endif + + // + app.MapPost("/logout", async (SignInManager signInManager, + [FromBody] object empty) => + { + if (empty != null) + { + await signInManager.SignOutAsync(); + return Results.Ok(); + } + return Results.Unauthorized(); + }) + .WithOpenApi() + .RequireAuthorization(); + // + + app.Run(); + } +} diff --git a/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/WeatherForecast.cs b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/WeatherForecast.cs new file mode 100644 index 000000000000..140b057a78e1 --- /dev/null +++ b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace APIforSPA; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/appsettings.Development.json b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/appsettings.Development.json new file mode 100644 index 000000000000..0c208ae9181e --- /dev/null +++ b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/appsettings.json b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/appsettings.json new file mode 100644 index 000000000000..10f68b8c8b4f --- /dev/null +++ b/aspnetcore/security/authentication/identity-api-authorization/8samples/APIforSPA/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}