Skip to content

Commit b858128

Browse files
authored
Update client IP address safelist doc and app for ASP.NET Core 3.1 (#17281)
* Update client IP address safelist doc and sample app for ASP.NET Core 3.1 * Add solution files * Remove unused region * Replace HTTP 401 with 403 * Add class-level regions for filters * More work * More tweaks * More edits * More work * Sample app refactoring * minor edit * Fix code snippet include * more edits * Remove 4 moniker ranges * Display doc for 2.1+ only * Remove 2 more moniker ranges
1 parent ad8c504 commit b858128

27 files changed

Lines changed: 449 additions & 274 deletions

aspnetcore/security/ip-safelist.md

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,119 @@
11
---
22
title: Client IP safelist for ASP.NET Core
33
author: damienbod
4-
description: Learn how to write Middleware or action filters to validate remote IP addresses against a list of approved IP addresses.
4+
description: Learn how to write middleware or action filters to validate remote IP addresses against a list of approved IP addresses.
5+
monikerRange: '>= aspnetcore-2.1'
56
ms.author: riande
67
ms.custom: mvc
7-
ms.date: 08/31/2018
8+
ms.date: 03/12/2020
89
uid: security/ip-safelist
910
---
1011
# Client IP safelist for ASP.NET Core
1112

1213
By [Damien Bowden](https://twitter.com/damien_bod) and [Tom Dykstra](https://github.com/tdykstra)
1314

14-
This article shows three ways to implement an IP safelist (also known as a whitelist) in an ASP.NET Core app. You can use:
15+
This article shows three ways to implement an IP address safelist (also known as an allow list) in an ASP.NET Core app. An accompanying sample app demonstrates all three approaches. You can use:
1516

1617
* Middleware to check the remote IP address of every request.
17-
* Action filters to check the remote IP address of requests for specific controllers or action methods.
18+
* MVC action filters to check the remote IP address of requests for specific controllers or action methods.
1819
* Razor Pages filters to check the remote IP address of requests for Razor pages.
1920

20-
In each case, a string containing approved client IP addresses is stored in an app setting. The middleware or filter parses the string into a list and checks if the remote IP is in the list. If not, an HTTP 403 Forbidden status code is returned.
21+
In each case, a string containing approved client IP addresses is stored in an app setting. The middleware or filter:
2122

22-
[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/security/ip-safelist/samples/2.x/ClientIpAspNetCore) ([how to download](xref:index#how-to-download-a-sample))
23+
* Parses the string into an array.
24+
* Checks if the remote IP address exists in the array.
2325

24-
## The safelist
26+
Access is allowed if the array contains the IP address. Otherwise, an HTTP 403 Forbidden status code is returned.
2527

26-
The list is configured in the *appsettings.json* file. It's a semicolon-delimited list and can contain IPv4 and IPv6 addresses.
28+
[View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/security/ip-safelist/samples) ([how to download](xref:index#how-to-download-a-sample))
2729

28-
[!code-json[](ip-safelist/samples/2.x/ClientIpAspNetCore/appsettings.json?highlight=2)]
30+
## IP address safelist
31+
32+
In the sample app, the IP address safelist is:
33+
34+
* Defined by the `AdminSafeList` property in the *appsettings.json* file.
35+
* A semicolon-delimited string that may contain both [Internet Protocol version 4 (IPv4)](https://wikipedia.org/wiki/IPv4) and [Internet Protocol version 6 (IPv6)](https://wikipedia.org/wiki/IPv6) addresses.
36+
37+
[!code-json[](ip-safelist/samples/3.x/ClientIpAspNetCore/appsettings.json?range=1-3&highlight=2)]
38+
39+
In the preceding example, the IPv4 addresses of `127.0.0.1` and `192.168.1.5` and the IPv6 loopback address of `::1` (compressed format for `0:0:0:0:0:0:0:1`) are allowed.
2940

3041
## Middleware
3142

32-
The `Configure` method adds the middleware and passes the safelist string to it in a constructor parameter.
43+
The `Startup.Configure` method adds the custom `AdminSafeListMiddleware` middleware type to the app's request pipeline. The safelist is retrieved with the .NET Core configuration provider and is passed as a constructor parameter.
3344

34-
[!code-csharp[](ip-safelist/samples/2.x/ClientIpAspNetCore/Startup.cs?name=snippet_Configure&highlight=10)]
45+
[!code-csharp[](ip-safelist/samples/3.x/ClientIpAspNetCore/Startup.cs?name=snippet_ConfigureAddMiddleware)]
3546

36-
The middleware parses the string into an array and looks for the remote IP address in the array. If the remote IP address is not found, the middleware returns HTTP 401 Forbidden. This validation process is bypassed for HTTP Get requests.
47+
The middleware parses the string into an array and searches for the remote IP address in the array. If the remote IP address isn't found, the middleware returns HTTP 403 Forbidden. This validation process is bypassed for HTTP GET requests.
3748

38-
[!code-csharp[](ip-safelist/samples/2.x/ClientIpAspNetCore/AdminSafeListMiddleware.cs?name=snippet_ClassOnly)]
49+
[!code-csharp[](ip-safelist/samples/Shared/ClientIpSafelistComponents/Middlewares/AdminSafeListMiddleware.cs?name=snippet_ClassOnly)]
3950

4051
## Action filter
4152

42-
If you want a safelist only for specific controllers or action methods, use an action filter. Here's an example:
53+
If you want safelist-driven access control for specific MVC controllers or action methods, use an action filter. For example:
54+
55+
[!code-csharp[](ip-safelist/samples/Shared/ClientIpSafelistComponents/Filters/ClientIpCheckActionFilter.cs?name=snippet_ClassOnly)]
56+
57+
In `Startup.ConfigureServices`, add the action filter to the MVC filters collection. In the following example, a `ClientIpCheckActionFilter` action filter is added. A safelist and a console logger instance are passed as constructor parameters.
58+
59+
::: moniker range=">= aspnetcore-3.0"
60+
61+
[!code-csharp[](ip-safelist/samples/3.x/ClientIpAspNetCore/Startup.cs?name=snippet_ConfigureServicesActionFilter)]
62+
63+
::: moniker-end
64+
65+
::: moniker range="<= aspnetcore-2.2"
66+
67+
[!code-csharp[](ip-safelist/samples/2.x/ClientIpAspNetCore/Startup.cs?name=snippet_ConfigureServicesActionFilter)]
68+
69+
::: moniker-end
70+
71+
The action filter can then be applied to a controller or action method with the [[ServiceFilter]](xref:Microsoft.AspNetCore.Mvc.ServiceFilterAttribute) attribute:
72+
73+
[!code-csharp[](ip-safelist/samples/3.x/ClientIpAspNetCore/Controllers/ValuesController.cs?name=snippet_ActionFilter&highlight=1)]
74+
75+
In the sample app, the action filter is applied to the controller's `Get` action method. When you test the app by sending:
76+
77+
* An HTTP GET request, the `[ServiceFilter]` attribute validates the client IP address. If access is allowed to the `Get` action method, a variation of the following console output is produced by the action filter and action method:
78+
79+
```
80+
dbug: ClientIpSafelistComponents.Filters.ClientIpCheckActionFilter[0]
81+
Remote IpAddress: ::1
82+
dbug: ClientIpAspNetCore.Controllers.ValuesController[0]
83+
successful HTTP GET
84+
```
85+
86+
* An HTTP request verb other than GET, the `AdminSafeListMiddleware` middleware validates the client IP address.
4387
44-
[!code-csharp[](ip-safelist/samples/2.x/ClientIpAspNetCore/Filters/ClientIpCheckFilter.cs)]
88+
## Razor Pages filter
4589
46-
The action filter is added to the services container.
90+
If you want safelist-driven access control for a Razor Pages app, use a Razor Pages filter. For example:
4791
48-
[!code-csharp[](ip-safelist/samples/2.x/ClientIpAspNetCore/Startup.cs?name=snippet_ConfigureServices&highlight=3)]
92+
[!code-csharp[](ip-safelist/samples/Shared/ClientIpSafelistComponents/Filters/ClientIpCheckPageFilter.cs?name=snippet_ClassOnly)]
4993
50-
The filter can then be used on a controller or action method.
94+
In `Startup.ConfigureServices`, enable the Razor Pages filter by adding it to the MVC filters collection. In the following example, a `ClientIpCheckPageFilter` Razor Pages filter is added. A safelist and a console logger instance are passed as constructor parameters.
5195
52-
[!code-csharp[](ip-safelist/samples/2.x/ClientIpAspNetCore/Controllers/ValuesController.cs?name=snippet_Filter&highlight=1)]
96+
::: moniker range=">= aspnetcore-3.0"
5397
54-
In the sample app, the filter is applied to the `Get` method. So when you test the app by sending a `Get` API request, the attribute is validating the client IP address. When you test by calling the API with any other HTTP method, the middleware is validating the client IP.
98+
[!code-csharp[](ip-safelist/samples/3.x/ClientIpAspNetCore/Startup.cs?name=snippet_ConfigureServicesPageFilter)]
5599
56-
## Razor Pages filter
100+
::: moniker-end
57101
58-
If you want a safelist for a Razor Pages app, use a Razor Pages filter. Here's an example:
102+
::: moniker range="<= aspnetcore-2.2"
59103
60-
[!code-csharp[](ip-safelist/samples/2.x/ClientIpAspNetCore/Filters/ClientIpCheckPageFilter.cs)]
104+
[!code-csharp[](ip-safelist/samples/2.x/ClientIpAspNetCore/Startup.cs?name=snippet_ConfigureServicesPageFilter)]
61105
62-
This filter is enabled by adding it to the MVC Filters collection.
106+
::: moniker-end
63107
64-
[!code-csharp[](ip-safelist/samples/2.x/ClientIpAspNetCore/Startup.cs?name=snippet_ConfigureServices&highlight=7-9)]
108+
When the sample app's *Index* Razor page is requested, the Razor Pages filter validates the client IP address. The filter produces a variation of the following console output:
65109
66-
When you run the app and request a Razor page, the Razor Pages filter is validating the client IP.
110+
```
111+
dbug: ClientIpSafelistComponents.Filters.ClientIpCheckPageFilter[0]
112+
Remote IpAddress: ::1
113+
```
67114
68-
## Next steps
115+
## Additional resources
69116
70-
[Learn more about ASP.NET Core Middleware](xref:fundamentals/middleware/index).
117+
* <xref:fundamentals/middleware/index>
118+
* [Action filters](xref:mvc/controllers/filters#action-filters)
119+
* <xref:razor-pages/filter>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.29806.167
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientIpAspNetCore", "ClientIpAspNetCore\ClientIpAspNetCore.csproj", "{51B901C7-2081-4CC2-A970-1C48A33E5507}"
7+
EndProject
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientIpSafelistComponents", "..\Shared\ClientIpSafelistComponents\ClientIpSafelistComponents.csproj", "{419CE413-A0F4-4415-B93E-F2D95B272921}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{51B901C7-2081-4CC2-A970-1C48A33E5507}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{51B901C7-2081-4CC2-A970-1C48A33E5507}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{51B901C7-2081-4CC2-A970-1C48A33E5507}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{51B901C7-2081-4CC2-A970-1C48A33E5507}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{419CE413-A0F4-4415-B93E-F2D95B272921}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{419CE413-A0F4-4415-B93E-F2D95B272921}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{419CE413-A0F4-4415-B93E-F2D95B272921}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{419CE413-A0F4-4415-B93E-F2D95B272921}.Release|Any CPU.Build.0 = Release|Any CPU
24+
EndGlobalSection
25+
GlobalSection(SolutionProperties) = preSolution
26+
HideSolutionNode = FALSE
27+
EndGlobalSection
28+
GlobalSection(ExtensibilityGlobals) = postSolution
29+
SolutionGuid = {DE6B1697-AE16-4BE0-ACF1-F31CD304CCCF}
30+
EndGlobalSection
31+
EndGlobal

aspnetcore/security/ip-safelist/samples/2.x/ClientIpAspNetCore/ClientIpAspNetCore.csproj

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,15 @@
22

33
<PropertyGroup>
44
<TargetFramework>netcoreapp2.1</TargetFramework>
5+
<ProjectUISubcaption>ASP.NET Core 2.x</ProjectUISubcaption>
56
</PropertyGroup>
67

78
<ItemGroup>
89
<PackageReference Include="Microsoft.AspNetCore.App" />
9-
<PackageReference Include="NLog.Extensions.Logging" Version="1.2.1" />
10-
<PackageReference Include="NLog.Web.AspNetCore" Version="4.6.0" />
11-
<PackageReference Include="NLog" Version="4.5.9" />
1210
</ItemGroup>
1311

1412
<ItemGroup>
15-
<Content Update="nlog.config">
16-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
17-
</Content>
18-
<Content Update="nlog.file.config">
19-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
20-
</Content>
13+
<ProjectReference Include="..\..\Shared\ClientIpSafelistComponents\ClientIpSafelistComponents.csproj" />
2114
</ItemGroup>
2215

2316
</Project>

aspnetcore/security/ip-safelist/samples/2.x/ClientIpAspNetCore/Controllers/ValuesController.cs

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,40 @@
1-
using System.Collections.Generic;
2-
using ClientIpAspNetCore.Filters;
1+
using ClientIpSafelistComponents.Filters;
32
using Microsoft.AspNetCore.Mvc;
43
using Microsoft.Extensions.Logging;
4+
using System.Collections.Generic;
55

66
namespace ClientIpAspNetCore.Controllers
77
{
8-
[Route("api/[controller]")]
9-
public class ValuesController : Controller
8+
[ApiController]
9+
[Route("[controller]")]
10+
public class ValuesController : ControllerBase
1011
{
11-
private ILogger<ValuesController> _logger;
12+
private readonly ILogger<ValuesController> _logger;
1213

1314
public ValuesController(ILogger<ValuesController> logger)
1415
{
1516
_logger = logger;
1617
}
1718

18-
// GET api/values
19-
#region snippet_Filter
20-
[ServiceFilter(typeof(ClientIpCheckFilter))]
19+
#region snippet_ActionFilter
20+
[ServiceFilter(typeof(ClientIpCheckActionFilter))]
2121
[HttpGet]
2222
public IEnumerable<string> Get()
23-
#endregion
23+
#endregion snippet_ActionFilter
2424
{
25-
_logger.LogDebug("successful get.");
25+
_logger.LogDebug("successful HTTP GET");
26+
2627
return new string[] { "value1", "value2" };
2728
}
2829

29-
// GET api/values/5
3030
[HttpGet("{id}")]
3131
public string Get(int id)
3232
{
3333
return "value";
3434
}
3535

36-
// POST api/values
3736
[HttpPost]
38-
public void Post([FromBody]string value)
39-
{
40-
}
41-
42-
// PUT api/values/5
43-
[HttpPut("{id}")]
44-
public void Put(int id, [FromBody]string value)
45-
{
46-
}
47-
48-
// DELETE api/values/5
49-
[HttpDelete("{id}")]
50-
public void Delete(int id)
37+
public void Post(string value)
5138
{
5239
}
5340
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@page
2-
@model IndexModel
2+
@model ClientIpAspNetCore.Pages.IndexModel
33
@{
4-
ViewData["Title"] = "ClientIpAspNetCore page";
4+
ViewData["Title"] = "Index";
55
}
66

77
Empty Page to use Filter

aspnetcore/security/ip-safelist/samples/2.x/ClientIpAspNetCore/Pages/Index.cshtml.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Threading.Tasks;
5-
using Microsoft.AspNetCore.Mvc;
6-
using Microsoft.AspNetCore.Mvc.RazorPages;
1+
using Microsoft.AspNetCore.Mvc.RazorPages;
72

83
namespace ClientIpAspNetCore.Pages
94
{

aspnetcore/security/ip-safelist/samples/2.x/ClientIpAspNetCore/Pages/Shared/_Layout.cshtml

Lines changed: 0 additions & 21 deletions
This file was deleted.

aspnetcore/security/ip-safelist/samples/2.x/ClientIpAspNetCore/Pages/_ViewImports.cshtml

Lines changed: 0 additions & 3 deletions
This file was deleted.

aspnetcore/security/ip-safelist/samples/2.x/ClientIpAspNetCore/Pages/_ViewStart.cshtml

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using Microsoft.AspNetCore;
22
using Microsoft.AspNetCore.Hosting;
3-
using System;
4-
using NLog.Web;
53
using Microsoft.Extensions.Logging;
64

75
namespace ClientIpAspNetCore
@@ -10,34 +8,17 @@ public class Program
108
{
119
public static void Main(string[] args)
1210
{
13-
var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
14-
try
15-
{
16-
logger.Debug("init main");
17-
BuildWebHost(args).Run();
18-
}
19-
catch (Exception ex)
20-
{
21-
//NLog: catch setup errors
22-
logger.Error(ex, "Stopped program because of exception");
23-
throw;
24-
}
25-
finally
26-
{
27-
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
28-
NLog.LogManager.Shutdown();
29-
}
11+
BuildWebHost(args).Run();
3012
}
3113

3214
public static IWebHost BuildWebHost(string[] args) =>
33-
WebHost.CreateDefaultBuilder(args)
34-
.UseStartup<Startup>()
35-
.ConfigureLogging(logging =>
36-
{
37-
logging.ClearProviders();
38-
logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
39-
})
40-
.UseNLog() // NLog: setup NLog for Dependency injection
41-
.Build();
15+
WebHost.CreateDefaultBuilder(args)
16+
.UseStartup<Startup>()
17+
.ConfigureLogging(logging =>
18+
{
19+
logging.ClearProviders();
20+
logging.AddConsole();
21+
})
22+
.Build();
4223
}
4324
}

0 commit comments

Comments
 (0)