web-dev-qa-db-ja.com

IDを使用しないASP.NET Core 2.0 Bearer認証

1日前に.NETコア2.0に自己完結型のベアラ認証webapiを実装しようとしたときに、かなり単純な目標を念頭に置いていたと思いましたが、リモートで動作するものはまだありません。ここに私がやろうとしていることのリストがあります:

  • ベアラトークンで保護されたwebapiを実装する
  • 同じプロジェクトのエンドポイントからトークンを発行してトークンを更新する
  • [Authorize]属性を使用して、APIサーフェスへのアクセスを制御します
  • ASP.Net Identityを使用しない(ユーザー/メンバーシップの要件がはるかに軽い)

ログインでID /クレーム/プリンシパルを構築し、それをリクエストコンテキストに追加することはまったく問題ありませんが、IDなしのCore 2.0 webapiで認証/更新トークンを発行および使用する方法に関する単一の例を見たことはありません。 Identityを使用しない1.x MSDNのCookieの例を見てきましたが、上記の要件を満たすために十分な理解が得られませんでした。

これは一般的なシナリオであり、それほど難しいことではないように感じます(おそらくそうではなく、ドキュメント/例が足りないだけでしょうか?)。私が知る限り、IdentityServer4はCore 2.0 Authと互換性がなく、opendiddictはIdentityを必要とするようです。また、トークンエンドポイントを別のプロセスでホストするのではなく、同じwebapiインスタンス内でホストする必要があります。

誰でも具体的な例を教えてもらえますか、少なくとも最良のステップ/オプションは何かについてのガイダンスを提供できますか?

24
pseabury

ASP.NET Core 2.0と互換性があるように編集しました。


まず、いくつかのNugetパッケージ:

  • Microsoft.AspNetCore.Authentication.JwtBearer
  • Microsoft.AspNetCore.Identity
  • System.IdentityModel.Tokens.Jwt
  • System.Security.Cryptography.Csp

次に、いくつかの基本的なデータ転送オブジェクト。

_// Presumably you will have an equivalent user account class with a user name.
public class User
{
    public string UserName { get; set; }
}

public class JsonWebToken
{
    public string access_token { get; set; }

    public string token_type { get; set; } = "bearer";

    public int expires_in { get; set; }

    public string refresh_token { get; set; }
}
_

適切な機能を使用するには、実際に認証トークンをユーザーに送信するためのログイン/トークンWebメソッドが必要です。

_[Route("api/token")]
public class TokenController : Controller
{
    private ITokenProvider _tokenProvider;

    public TokenController(ITokenProvider tokenProvider) // We'll create this later, don't worry.
    {
        _tokenProvider = tokenProvider;
    }

    public JsonWebToken Get([FromQuery] string grant_type, [FromQuery] string username, [FromQuery] string password, [FromQuery] string refresh_token)
    {
        // Authenticate depending on the grant type.
        User user = grant_type == "refresh_token" ? GetUserByToken(refresh_token) : GetUserByCredentials(username, password);

        if (user == null)
            throw new UnauthorizedAccessException("No!");

        int ageInMinutes = 20;  // However long you want...

        DateTime expiry = DateTime.UtcNow.AddMinutes(ageInMinutes);

        var token = new JsonWebToken {
            access_token = _tokenProvider.CreateToken(user, expiry),
            expires_in   = ageInMinutes * 60
        };

        if (grant_type != "refresh_token")
            token.refresh_token = GenerateRefreshToken(user);

        return token;
    }

    private User GetUserByToken(string refreshToken)
    {
        // TODO: Check token against your database.
        if (refreshToken == "test")
            return new User { UserName = "test" };

        return null;
    }

    private User GetUserByCredentials(string username, string password)
    {
        // TODO: Check username/password against your database.
        if (username == password)
            return new User { UserName = username };

        return null;
    }

    private string GenerateRefreshToken(User user)
    {
        // TODO: Create and persist a refresh token.
        return "test";
    }
}
_

トークンの作成は、想像上のITokenProviderによってパススルーされた「魔法」であることに気付いたでしょう。トークンプロバイダーインターフェイスを定義します。

_public interface ITokenProvider
{
    string CreateToken(User user, DateTime expiry);

    // TokenValidationParameters is from Microsoft.IdentityModel.Tokens
    TokenValidationParameters GetValidationParameters();
}
_

JWTでRSAセキュリティキーを使用してトークン作成を実装しました。そう...

_public class RsaJwtTokenProvider : ITokenProvider
{
    private RsaSecurityKey _key;
    private string _algorithm;
    private string _issuer;
    private string _audience;

    public RsaJwtTokenProvider(string issuer, string audience, string keyName)
    {
        var parameters = new CspParameters { KeyContainerName = keyName };
        var provider = new RSACryptoServiceProvider(2048, parameters);

        _key = new RsaSecurityKey(provider);

        _algorithm = SecurityAlgorithms.RsaSha256Signature;
        _issuer = issuer;
        _audience = audience;
    }

    public string CreateToken(User user, DateTime expiry)
    {
        JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

        ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user.UserName, "jwt"));

        // TODO: Add whatever claims the user may have...

        SecurityToken token = tokenHandler.CreateJwtSecurityToken(new SecurityTokenDescriptor
        {
            Audience = _audience,
            Issuer = _issuer,
            SigningCredentials = new SigningCredentials(_key, _algorithm),
            Expires = expiry.ToUniversalTime(),
            Subject = identity
        });

        return tokenHandler.WriteToken(token);
    }

    public TokenValidationParameters GetValidationParameters()
    {
        return new TokenValidationParameters
        {
            IssuerSigningKey = _key,
            ValidAudience = _audience,
            ValidIssuer = _issuer,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromSeconds(0) // Identity and resource servers are the same.
        };
    }
}
_

トークンを生成しています。実際にそれらを検証して接続する時間です。 Startup.csに移動します。

ConfigureServices()

_var tokenProvider = new RsaJwtTokenProvider("issuer", "audience", "mykeyname");
services.AddSingleton<ITokenProvider>(tokenProvider);

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => {
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = tokenProvider.GetValidationParameters();
    });

// This is for the [Authorize] attributes.
services.AddAuthorization(auth => {
    auth.DefaultPolicy = new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .Build();
});
_

次にConfigure()

_public void Configure(IApplicationBuilder app)
{
    app.UseAuthentication();

    // Whatever else you're putting in here...

    app.UseMvc();
}
_

それはあなたが必要とするすべてについてでなければなりません。うまくいけば、私は何も見逃していない。

幸せな結果は...

_[Authorize] // Yay!
[Route("api/values")]
public class ValuesController : Controller
{
    // ...
}
_
18
Mitch

@Mitchの回答に続いて:認証スタックは、.NET Core 2.0に移行するためにかなり変更されました。以下の回答では、新しい実装を使用しています。

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

namespace JwtWithoutIdentity
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(cfg =>
                {
                    cfg.RequireHttpsMetadata = false;
                    cfg.SaveToken = true;

                    cfg.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidIssuer = "me",
                        ValidAudience = "you",
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")) //Secret
                    };

                });

            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseAuthentication();

            app.UseMvc();
        }
    }
}

トークンコントローラー

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JwtWithoutIdentity.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

namespace JwtWithoutIdentity.Controllers
{
    public class TokenController : Controller
    {

        [AllowAnonymous]
        [Route("api/token")]
        [HttpPost]
        public async Task<IActionResult> Token(LoginViewModel model)
        {

            if (!ModelState.IsValid) return BadRequest("Token failed to generate");

            var user = (model.Password == "password" && model.Username == "username");

            if (!user) return Unauthorized();

            //Add Claims
            var claims = new[]
            {
                new Claim(JwtRegisteredClaimNames.UniqueName, "data"),
                new Claim(JwtRegisteredClaimNames.Sub, "data"),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("rlyaKithdrYVl6Z80ODU350md")); //Secret
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            var token = new JwtSecurityToken("me",
                "you",
                claims,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: creds);

            return Ok(new JsonWebToken()
            {
                access_token = new JwtSecurityTokenHandler().WriteToken(token),
                expires_in = 600000,
                token_type = "bearer"
            });
        }
    }
}

値コントローラー

using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace JwtWithoutIdentity.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [Authorize]
        [HttpGet]
        public IEnumerable<string> Get()
        {
            var name = User.Identity.Name;
            var claims = User.Claims;

            return new string[] { "value1", "value2" };
        }
    }
}

お役に立てれば!

14
Sethles