web-dev-qa-db-ja.com

ASP.NET Web APIのJWT認証

私は私のWeb APIアプリケーションでJWTベアラトークン(JSON Web Token)をサポートしようとしていますが、迷子になります。

.NET CoreとOWINアプリケーションがサポートされています。
現在IISでアプリケーションをホストしています。

アプリケーションでこの認証モジュールを実現するにはどうすればいいですか。フォーム/ Windows認証を使用するのと同じように、<authentication>構成を使用する方法はありますか?

193
Amir Popovich

私はこの質問に答えました: 4年前にHMACを使用してASP.NET Web APIをセキュアにする方法

現在、セキュリティに関して多くのことが変更され、特にJWTが普及しています。ここでは、私ができる最も簡単で基本的な方法でJWTを使用する方法を説明しようとするので、OWIN、Oauth2、ASP.NET Identityなどのジャングルから迷子になることはありません。

JWTトークンがわからない場合は、少し見てみる必要があります。

https://tools.ietf.org/html/rfc7519

基本的に、JWTトークンは以下のようになります。

<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>

例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

JWTトークンには3つのセクションがあります。

  1. ヘッダー:base64としてエンコードされているJSON形式
  2. 要求:base64としてエンコードされているJSON形式。
  3. 署名:base64としてエンコードされているHeaderおよびClaimsに基づいて作成および署名されています。

上記のトークンを使用してWebサイト jwt.io を使用すると、以下のようにデコードしてトークンを見ることができます。

enter image description here

技術的には、JWTはヘッダーから署名された署名を使用し、ヘッダーで指定されたセキュリティアルゴリズムを使用します(例:HMACSHA256)。したがって、クレームに機密情報を格納している場合は、JWTをHTTP経由で転送する必要があります。

今、JWT認証を使用するために、あなたがレガシーWeb Apiシステムを持っているなら、あなたは本当にOWINミドルウェアを必要としません。単純な概念は、要求が来たときにJWTトークンを提供する方法とトークンを検証する方法です。それでおしまい。

デモに戻ると、JWTトークンを軽量に保つために、JWTにはusernameexpiration timeのみを格納しています。しかし、このようにして、ロールの承認をしたい場合は、新たなローカルID(プリンシパル)を再構築して、ロールのような情報を追加する必要があります。しかし、JWTにもっと情報を追加したいのであれば、それはあなた次第です、非常に柔軟です。

OWINミドルウェアを使用する代わりに、コントローラーからのアクションを使用してJWTトークン・エンドポイントを単に提供することができます。

public class TokenController : ApiController
{
    // This is naive endpoint for demo, it should use Basic authentication to provide token or POST request
    [AllowAnonymous]
    public string Get(string username, string password)
    {
        if (CheckUser(username, password))
        {
            return JwtManager.GenerateToken(username);
        }

        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    public bool CheckUser(string username, string password)
    {
        // should check in the database
        return true;
    }
}

本番環境では、JWTトークンを提供するためにPOST requestまたは基本認証エンドポイントを使用する必要があります。

usernameに基づいてトークンを生成する方法

トークンを生成するのにMSからのSystem.IdentityModel.Tokens.Jwtと呼ばれるNuGetパッケージ、あるいはあなたが望むなら別のパッケージさえも使うことができます。デモでは、SymmetricKeyと共にHMACSHA256を使用します。

    /// <summary>
    /// Use the below code to generate symmetric Secret Key
    ///     var hmac = new HMACSHA256();
    ///     var key = Convert.ToBase64String(hmac.Key);
    /// </summary>
    private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";

    public static string GenerateToken(string username, int expireMinutes = 20)
    {
        var symmetricKey = Convert.FromBase64String(Secret);
        var tokenHandler = new JwtSecurityTokenHandler();

        var now = DateTime.UtcNow;
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
                    {
                        new Claim(ClaimTypes.Name, username)
                    }),

            Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),

            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(symmetricKey), SecurityAlgorithms.HmacSha256Signature)
        };

        var stoken = tokenHandler.CreateToken(tokenDescriptor);
        var token = tokenHandler.WriteToken(stoken);

        return token;
    }

JWTトークンを提供するためのエンドポイントは、リクエストが来たときにJWTを検証する方法です。デモでは、JwtAuthenticationAttributeから継承するIAuthenticationFilterを構築しました。認証フィルターの詳細については、 here を参照してください。

この属性を使用すると、任意のアクションを認証できます。そのアクションにこの属性を追加するだけです。

public class ValueController : ApiController
{
    [JwtAuthentication]
    public string Get()
    {
        return "value";
    }
}

WebApiへのすべての着信要求を検証したい場合は、OWINミドルウェアまたはDelegateHanderを使用することもできます(コントローラーまたはアクションに限定されない)。

以下は認証フィルタからのコアメソッドです:

    private static bool ValidateToken(string token, out string username)
    {
        username = null;

        var simplePrinciple = JwtManager.GetPrincipal(token);
        var identity = simplePrinciple.Identity as ClaimsIdentity;

        if (identity == null)
            return false;

        if (!identity.IsAuthenticated)
            return false;

        var usernameClaim = identity.FindFirst(ClaimTypes.Name);
        username = usernameClaim?.Value;

        if (string.IsNullOrEmpty(username))
            return false;

        // More validate to check whether username exists in system

        return true;
    }

    protected Task<IPrincipal> AuthenticateJwtToken(string token)
    {
        string username;

        if (ValidateToken(token, out username))
        {
            // based on username to get more information from database in order to build local identity
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, username)
                // Add more claims if needed: Roles, ...
            };

            var identity = new ClaimsIdentity(claims, "Jwt");
            IPrincipal user = new ClaimsPrincipal(identity);

            return Task.FromResult(user);
        }

        return Task.FromResult<IPrincipal>(null);
    }

ワークフローは、JWTライブラリ(上記のNuGetパッケージ)を使用してJWTトークンを検証してからClaimsPrincipalを返すことです。ユーザーがシステムに存在するかどうかを確認し、必要に応じて他のカスタム検証を追加するなど、さらに検証を実行できます。 JWTトークンを検証してプリンシパルを取り戻すためのコード。

   public static ClaimsPrincipal GetPrincipal(string token)
    {
        try
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;

            if (jwtToken == null)
                return null;

            var symmetricKey = Convert.FromBase64String(Secret);

            var validationParameters = new TokenValidationParameters()
            {
               RequireExpirationTime = true,
               ValidateIssuer = false,
               ValidateAudience = false,
               IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
            };

            SecurityToken securityToken;
            var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);

            return principal;
        }

        catch (Exception)
        {
            //should write log
            return null;
        }
    }

JWTトークンが検証され、プリンシパルが返却された場合は、新しいローカルIDを作成し、それにロール許可を確認するための詳細情報を追加する必要があります。

リソースへの匿名の要求を防ぐために、グローバルスコープでconfig.Filters.Add(new AuthorizeAttribute());(デフォルトの承認)を追加することを忘れないでください。

Postmanを使ってデモをテストすることができます。

トークンを要求します(上記のとおり、単純にデモ用です)。

GET http://localhost:{port}/api/token?username=cuong&password=1

許可された要求のヘッダーにJWTトークンを入れます。例えば、

GET http://localhost:{port}/api/value

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

デモはここに置かれています: https://github.com/cuongle/WebApi.Jwt

487
cuongle

私は、最小限の努力でそれを達成することができました(ASP.NET Coreと同じくらい簡単です)。

そのためにはOWINのStartup.csファイルとMicrosoft.Owin.Security.Jwtライブラリを使います。

アプリがStartup.csをヒットするためには、Web.configを修正する必要があります。

<configuration>
  <appSettings>
    <add key="owin:AutomaticAppStartup" value="true" />
    ...

これがStartup.csの外観です。

using MyApp.Helpers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
using Owin;

[Assembly: OwinStartup(typeof(MyApp.App_Start.Startup))]

namespace MyApp.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseJwtBearerAuthentication(
                new JwtBearerAuthenticationOptions
                {
                    AuthenticationMode = AuthenticationMode.Active,
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = ConfigHelper.GetAudience(),
                        ValidIssuer = ConfigHelper.GetIssuer(),
                        IssuerSigningKey = ConfigHelper.GetSymmetricSecurityKey(),
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true
                    }
                });
        }
    }
}

皆さんの多くは、今日ASP.NET Coreを使用していますので、ご覧のとおり、それは私たちが持っているものとそれほど違いはありません。

最初は私を困惑させました。私はカスタムプロバイダなどを実装しようとしていましたが、それほど単純であるとは思っていませんでした。 OWINはただ揺れ動きます!

1つだけ言及する必要があります - 私がOWIN Startup NSWagライブラリを有効にした後で動作しなくなりました(たとえば、Angular app用にTypeScript HTTPプロキシを自動生成したい場合があります)。

解決策も非常に簡単でした - 私はNSWagSwashbuckleに置き換えましたが、それ以上の問題はありませんでした。


さて、今ConfigHelperコードを共有しています:

public class ConfigHelper
{
    public static string GetIssuer()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Issuer"];
        return result;
    }

    public static string GetAudience()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Audience"];
        return result;
    }

    public static SigningCredentials GetSigningCredentials()
    {
        var result = new SigningCredentials(GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
        return result;
    }

    public static string GetSecurityKey()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["SecurityKey"];
        return result;
    }

    public static byte[] GetSymmetricSecurityKeyAsBytes()
    {
        var issuerSigningKey = GetSecurityKey();
        byte[] data = Encoding.UTF8.GetBytes(issuerSigningKey);
        return data;
    }

    public static SymmetricSecurityKey GetSymmetricSecurityKey()
    {
        byte[] data = GetSymmetricSecurityKeyAsBytes();
        var result = new SymmetricSecurityKey(data);
        return result;
    }

    public static string GetCorsOrigins()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["CorsOrigins"];
        return result;
    }
}

もう一つの重要な側面 - 私は Authorization headerを通してJWT Tokenを送ったので、TypeScriptコードは私を次のように探します。

(以下のコードは NSWag によって生成されます)

@Injectable()
export class TeamsServiceProxy {
    private http: HttpClient;
    private baseUrl: string;
    protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;

    constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
        this.http = http;
        this.baseUrl = baseUrl ? baseUrl : "https://localhost:44384";
    }

    add(input: TeamDto | null): Observable<boolean> {
        let url_ = this.baseUrl + "/api/Teams/Add";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = JSON.stringify(input);

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json", 
                "Accept": "application/json",
                "Authorization": "Bearer " + localStorage.getItem('token')
            })
        };

ヘッダー部分を参照 - "Authorization": "Bearer " + localStorage.getItem('token')

4
Alex Herman

私はあなたがJWTトークンをサポートするためにあなたが何らかのパーティーサーバを使用するべきであると思います、そしてWEB API 2には箱から出してすぐに使えるJWTサポートはありません。

しかし、何らかの形式の署名付きトークン(JWTではない)をサポートするためのOWINプロジェクトがあります。それはWebサイトのための認証の簡単な形式を提供するために縮小OAuthプロトコルとして機能します。

あなたはそれについてもっと読むことができます例えば ここに

それはかなり長いですが、ほとんどの部分はあなたがまったく必要としないかもしれないコントローラとASP.NETアイデンティティの詳細です。最も重要なのは

ステップ9:OAuthベアラトークン生成のサポートを追加する

ステップ12:バックエンドAPIのテスト

フロントエンドからアクセスできるエンドポイント(例: "/ token")の設定方法(およびリクエストのフォーマットの詳細)を読むことができます。

他の手順では、そのエンドポイントをデータベースなどに接続する方法について詳しく説明しています。必要な部分を選択できます。

1

これは、ASP.NET Core Web APIでJWTトークンを使用したクレームベース認証の非常に最小限で安全な実装です。

まず第一に、ユーザーに割り当てられたクレームを含むJWTトークンを返すエンドポイントを公開する必要があります。

 /// <summary>
        /// Login provides API to verify user and returns authentication token.
        /// API Path:  api/account/login
        /// </summary>
        /// <param name="paramUser">Username and Password</param>
        /// <returns>{Token: [Token] }</returns>
        [HttpPost("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] UserRequestVM paramUser, CancellationToken ct)
        {

            var result = await UserApplication.PasswordSignInAsync(paramUser.Email, paramUser.Password, false, lockoutOnFailure: false);

            if (result.Succeeded)
            {
                UserRequestVM request = new UserRequestVM();
                request.Email = paramUser.Email;


                ApplicationUser UserDetails = await this.GetUserByEmail(request);
                List<ApplicationClaim> UserClaims = await this.ClaimApplication.GetListByUser(UserDetails);

                var Claims = new ClaimsIdentity(new Claim[]
                                {
                                    new Claim(JwtRegisteredClaimNames.Sub, paramUser.Email.ToString()),
                                    new Claim(UserId, UserDetails.UserId.ToString())
                                });


                //Adding UserClaims to JWT claims
                foreach (var item in UserClaims)
                {
                    Claims.AddClaim(new Claim(item.ClaimCode, string.Empty));
                }

                var tokenHandler = new JwtSecurityTokenHandler();
                  // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                var encryptionkey = Configuration["Jwt:Encryptionkey"];
                var key = Encoding.ASCII.GetBytes(encryptionkey);
                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Issuer = Configuration["Jwt:Issuer"],
                    Subject = Claims,

                // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                    Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(Configuration["Jwt:ExpiryTimeInMinutes"])),

                    //algorithm to sign the token
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)

                };

                var token = tokenHandler.CreateToken(tokenDescriptor);
                var tokenString = tokenHandler.WriteToken(token);

                return Ok(new
                {
                    token = tokenString
                });
            }

            return BadRequest("Wrong Username or password");
        }

このようにデフォルトの認証サービスとしてJWT認証を追加するには、 startup.cs 内のConfigureServicesでサービスに認証を追加する必要があります。

services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
             .AddJwtBearer(cfg =>
             {
                 cfg.RequireHttpsMetadata = false;
                 cfg.SaveToken = true;
                 cfg.TokenValidationParameters = new TokenValidationParameters()
                 {
                     //ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Encryptionkey"])),
                     ValidateAudience = false,
                     ValidateLifetime = true,
                     ValidIssuer = configuration["Jwt:Issuer"],
                     //ValidAudience = Configuration["Jwt:Audience"],
                     //IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
                 };
             });

これで、認可サービスにポリシーを追加できます。

services.AddAuthorization(options =>
            {
                options.AddPolicy("YourPolicyNameHere",
                                policy => policy.RequireClaim("YourClaimNameHere"));
            });

_あるいは_ 、これはあなたのアプリケーションの起動時に一度だけ実行され、次のようなポリシーに追加されるのであなたはあなたのデータベースからあなたのすべての要求を投入することもできます。

  services.AddAuthorization(async options =>
            {
                var ClaimList = await claimApplication.GetList(applicationClaim);
                foreach (var item in ClaimList)
                {                        
                    options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));                       
                }
            });

これで、許可したいメソッドのいずれかにPolicyフィルターを設定できます。

 [HttpPost("update")]
        [Authorize(Policy = "ACC_UP")]
        public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
        {
//your logic goes here
}

お役に立てれば

0
Zeeshan Adil