Convert inline code to snippet references (#32003)

pull/32017/head
Tom Dykstra 2024-03-08 20:08:49 -08:00 committed by GitHub
parent 508b7ae321
commit 430914e9b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 234 additions and 82 deletions

View File

@ -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 <xref:Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityDbContext%601>:
```csharp
public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> 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<ApplicationDbContext>(
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 <xref:Microsoft.Extensions.DependencyInjection.AuthorizationServiceCollectionExtensions.AddAuthorization%2A> 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 <xref:Microsoft.Extensions.DependencyInjection.IdentityServiceCollectionExtensions.AddIdentityApiEndpoints%60%601(Microsoft.Extensions.DependencyInjection.IServiceCollection)> and <xref:Microsoft.Extensions.DependencyInjection.IdentityEntityFrameworkBuilderExtensions.AddEntityFrameworkStores%60%601(Microsoft.AspNetCore.Identity.IdentityBuilder)>.
```csharp
builder.Services.AddIdentityApiEndpoints<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
```
:::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 <xref:Microsoft.AspNetCore.Routing.IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi%60%601(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder)> to map the Identity endpoints:
```csharp
app.MapIdentityApi<IdentityUser>();
```
:::code language="csharp" source="~\security\authentication\identity-api-authorization\8samples\APIforSPA\Program.cs" id="snippetMapEndpoints":::
## Secure selected endpoints
To secure an endpoint, use the <xref:Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization%2A> 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<IdentityUser> 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<TUser>` 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 <xref:Microsoft.AspNetCore.Identity.SignInOptions.RequireConfirmedEmail>
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<IdentityOptions>(options =>
{
options.SignIn.RequireConfirmedEmail = true;
});
builder.Services.AddTransient<IEmailSender, EmailSender>();
```
:::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 <xref:security/authentication/accconfirm>.

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) :
base(options)
{ }
}

View File

@ -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<EmailSender> logger)
{
_logger = logger;
}
public List<Email> 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);

View File

@ -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);
// <snippetActivateAPIs>
builder.Services.AddIdentityApiEndpoints<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
// </snippetActivateAPIs>
// <snippetAppDbContext>
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseInMemoryDatabase("AppDb"));
// </snippetAppDbContext>
// <snippetAddAuthorization>
builder.Services.AddAuthorization();
// </snippetAddAuthorization>
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
#if Version2
// <snippetConfigureEmail>
builder.Services.Configure<IdentityOptions>(options =>
{
options.SignIn.RequireConfirmedEmail = true;
});
builder.Services.AddTransient<IEmailSender, EmailSender>();
// </snippetConfigureEmail>
#endif
var app = builder.Build();
// <snippetMapEndpoints>
app.MapIdentityApi<IdentityUser>();
// </snippetMapEndpoints>
// 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
// <snippetRequireAuthorization>
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();
// </snippetRequireAuthorization>
#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()
// <snippetRequireAdmin>
.RequireAuthorization("Admin");
// </snippetRequireAdmin>
#endif
#if Version4
// <snippetSwaggerAuth>
app.MapSwagger().RequireAuthorization();
// </snippetSwaggerAuth>
#endif
// <snippetLogout>
app.MapPost("/logout", async (SignInManager<IdentityUser> signInManager,
[FromBody] object empty) =>
{
if (empty != null)
{
await signInManager.SignOutAsync();
return Results.Ok();
}
return Results.Unauthorized();
})
.WithOpenApi()
.RequireAuthorization();
// </snippetLogout>
app.Run();
}
}

View File

@ -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; }
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}