質問
ASP.NET Core Webアプリケーションでカスタムメンバーシップを使用して基本認証を実装するにはどうすればよいですか?
注
MVC 5では、この 記事 の指示を使用していましたが、WebConfig
にモジュールを追加する必要があります。
IIS
に新しいMVC Core
applicationをデプロイしていますが、このアプローチは機能していないようです。
また、IIS Windows認証情報を使用するため、基本認証の組み込みサポートを使用しません。
ASP.NETセキュリティには、潜在的な不安定性とパフォーマンスの問題のため、基本認証ミドルウェアは含まれません。
テスト目的で基本認証ミドルウェアが必要な場合は、 https://github.com/blowdart/idunno.Authentication をご覧ください。
ASP.NET Core 2.0では、認証とIDに重大な変更が導入されました。
1.xでは、ミドルウェアを介して認証プロバイダーが構成されました(受け入れられた回答の実装として)。 2.0では、サービスに基づいています。
MSドキュメントの詳細: https://docs.Microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x
ASP.NET Core 2.0の基本認証実装を作成し、NuGetに公開しました: https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic
ActionFilterを使用して、内部サービスにダイジェストセキュリティを実装しました。
public class DigestAuthenticationFilterAttribute : ActionFilterAttribute
{
private const string AUTH_HEADER_NAME = "Authorization";
private const string AUTH_METHOD_NAME = "Digest ";
private AuthenticationSettings _settings;
public DigestAuthenticationFilterAttribute(IOptions<AuthenticationSettings> settings)
{
_settings = settings.Value;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
ValidateSecureChannel(context?.HttpContext?.Request);
ValidateAuthenticationHeaders(context?.HttpContext?.Request);
base.OnActionExecuting(context);
}
private void ValidateSecureChannel(HttpRequest request)
{
if (_settings.RequireSSL && !request.IsHttps)
{
throw new AuthenticationException("This service must be called using HTTPS");
}
}
private void ValidateAuthenticationHeaders(HttpRequest request)
{
string authHeader = GetRequestAuthorizationHeaderValue(request);
string digest = (authHeader != null && authHeader.StartsWith(AUTH_METHOD_NAME)) ? authHeader.Substring(AUTH_METHOD_NAME.Length) : null;
if (string.IsNullOrEmpty(digest))
{
throw new AuthenticationException("You must send your credentials using Authorization header");
}
if (digest != CalculateSHA1($"{_settings.UserName}:{_settings.Password}"))
{
throw new AuthenticationException("Invalid credentials");
}
}
private string GetRequestAuthorizationHeaderValue(HttpRequest request)
{
return request.Headers.Keys.Contains(AUTH_HEADER_NAME) ? request.Headers[AUTH_HEADER_NAME].First() : null;
}
public static string CalculateSHA1(string text)
{
var sha1 = System.Security.Cryptography.SHA1.Create();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(text));
return Convert.ToBase64String(hash);
}
}
その後、ダイジェストセキュリティを使用して、アクセスするコントローラーまたはメソッドに注釈を付けることができます。
[Route("api/xxxx")]
[ServiceFilter(typeof(DigestAuthenticationFilterAttribute))]
public class MyController : Controller
{
[HttpGet]
public string Get()
{
return "HELLO";
}
}
Basicセキュリティを実装するには、DigestAuthenticationFilterAttributeを変更して、SHA1ではなく、AuthorizationヘッダーのBase64デコードを直接使用します。
ASP.NET Core認証ミドルウェアの設計には失望しています。フレームワークとしては、単純化して生産性を向上させる必要がありますが、ここではそうではありません。
とにかく、シンプルでありながら安全なアプローチは、承認フィルターに基づいています。 IAsyncAuthorizationFilter
。 MVCが特定のコントローラーアクションを選択してフィルター処理に移行すると、他のミドルウェアの後に承認フィルターが実行されることに注意してください。ただし、フィルター内では、最初に許可フィルターが実行されます( details )。
私はヘクターの答えに対するクレイのコメントにコメントするつもりでしたが、ヘクターズの例が例外を投げてチャレンジメカニズムを持たないのが好きではなかったので、ここに実例があります。
留意してください:
それを念頭に置いて、基本認証に関するFUDを購入しないでください。基本認証と同じくらい基本的なものをスキップすることは、意見を重視し、実質を低くします。コメント here で、この設計に関するフラストレーションを確認できます。
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace BasicAuthFilterDemo
{
public class BasicAuthenticationFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public string Realm { get; set; }
public const string AuthTypeName = "Basic ";
private const string _authHeaderName = "Authorization";
public BasicAuthenticationFilterAttribute(string realm = null)
{
Realm = realm;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
try
{
var request = context?.HttpContext?.Request;
var authHeader = request.Headers.Keys.Contains(_authHeaderName) ? request.Headers[_authHeaderName].First() : null;
string encodedAuth = (authHeader != null && authHeader.StartsWith(AuthTypeName)) ? authHeader.Substring(AuthTypeName.Length).Trim() : null;
if (string.IsNullOrEmpty(encodedAuth))
{
context.Result = new BasicAuthChallengeResult(Realm);
return;
}
var (username, password) = DecodeUserIdAndPassword(encodedAuth);
// Authenticate credentials against database
var db = (ApplicationDbContext)context.HttpContext.RequestServices.GetService(typeof(ApplicationDbContext));
var userManager = (UserManager<User>)context.HttpContext.RequestServices.GetService(typeof(UserManager<User>));
var founduser = await db.Users.Where(u => u.Email == username).FirstOrDefaultAsync();
if (!await userManager.CheckPasswordAsync(founduser, password))
{
// writing to the Result property aborts rest of the pipeline
// see https://docs.Microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.0#cancellation-and-short-circuiting
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
// Populate user: adjust claims as needed
var claims = new[] { new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, AuthTypeName) };
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthTypeName));
context.HttpContext.User = principal;
}
catch
{
// log and reject
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
}
private static (string userid, string password) DecodeUserIdAndPassword(string encodedAuth)
{
var userpass = Encoding.UTF8.GetString(Convert.FromBase64String(encodedAuth));
var separator = userpass.IndexOf(':');
if (separator == -1)
return (null, null);
return (userpass.Substring(0, separator), userpass.Substring(separator + 1));
}
}
}
そして、これらはサポートするクラスです
public class StatusCodeOnlyResult : ActionResult
{
protected int StatusCode;
public StatusCodeOnlyResult(int statusCode)
{
StatusCode = statusCode;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
return base.ExecuteResultAsync(context);
}
}
public class BasicAuthChallengeResult : StatusCodeOnlyResult
{
private string _realm;
public BasicAuthChallengeResult(string realm = "") : base(StatusCodes.Status401Unauthorized)
{
_realm = realm;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
context.HttpContext.Response.Headers.Add("WWW-Authenticate", $"{BasicAuthenticationFilterAttribute.AuthTypeName} Realm=\"{_realm}\"");
return base.ExecuteResultAsync(context);
}
}