ASP.NET Web APIのJWT認証


264

Web APIアプリケーションでJWTベアラートークン(JSON Webトークン)をサポートしようとしていますが、道に迷ってしまいます。

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

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

回答:


611

私はこの質問に答えました:HMACを使用して4年前に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.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCWYQYQYQMQYQYQMQYQYQMQYQYQMQYQYQMQQYQYQYQMQQYQMQQYQM

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

  1. ヘッダー:Base64でエンコードされたJSON形式
  2. クレーム:Base64でエンコードされたJSON形式。
  3. 署名:Base64でエンコードされたヘッダーとクレームに基づいて作成および署名されます。

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

ここに画像の説明を入力してください

技術的には、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;
    }
}

これは単純なアクションです。本番環境では、POSTリクエストまたは基本認証エンドポイントを使用してJWTトークンを提供する必要があります。

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

System.IdentityModel.Tokens.JwtMicrosoftから呼び出されたNuGetパッケージを使用してトークンを生成したり、必要に応じて別のパッケージを使用したりすることもできます。デモでは、私はHMACSHA256と一緒に使用しSymmetricKeyます:

/// <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(認証フィルターの詳細はこちら)。

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

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

WebController(コントローラーまたはアクションに固有ではない)のすべての着信要求を検証する場合は、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


5
@Cuong Leでよく説明されていますが、さらに追加したいと思います。OWINを使用している場合は、Microsoft.Owin.Security.Jwtで利用可能なUseJwtBearerAuthenticationをチェックして、WebAPIでこのowinミドルウェアを使用して、すべての受信リクエストを自動的に検証できます。owinスタートアップクラスを使用してミドルウェアを登録する
Jek

5
@AmirPopovich応答にトークンを設定する必要はありません。トークンはクライアント側の別の場所に保存する必要があります。Webの場合、ローカルストレージに配置できます。HTTPリクエストを送信するときはいつでも、トークンをヘッダーに配置します。
cuongle

7
これは、私が長い間見た中で最も単純な説明です。+100できれば
餃子クドール2017年

4
@Homam:この遅い答えで申し訳ありませんが、生成する最良の方法は次のとおりです:varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
cuongle

4
CuongLeのリポジトリのデモコードを使用している人は誰でも、認証ヘッダーのないリクエストが処理されないというバグがあることに気づくでしょう。つまり、クエリがないとクエリを通過できません(エンドポイントのセキュリティが不十分です)。この問題を修正するための@magicleonからのプルリクエストがあります:github.com/cuongle/WebApi.Jwt/pull/4
Chucky

11

最小限の労力で(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を有効にした後、スタートアップNSWagライブラリが機能しなくなりました(たとえば、Angularアプリの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;
    }
}

もう一つの重要な側面-私は経由JWTトークンを送信し、承認、次のように、ヘッダ私にとってとても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')


I replaced NSWag with Swashbuckle and didn't have any further issues.Swashbuckleにはtypescriptファイルを生成する機能がありますか、それとも自分で追加したものですか?
クラッシュ

@crush swashbucleは、nuget nswagライブラリのようなjsonを提供するバックエンドライブラリです。typescriptファイルを作成するには、npmのnswagパッケージを使用する必要があります。
Alex Herman

ええ、私はしばらくの間、既にプロジェクトにスワッシュバックルを持っています。それは、nswagではなくTypeScriptモデルを生成できるとおっしゃっていたようです。私はnswagのファンではありません...それは重いです。私はSwashbuckleにフックされる独自のC#-> TypeScript変換を作成しました-ビルド後のプロセスとしてファイルを生成し、それらをプロジェクトのnpmフィードに公開します。私は、すでに同じことをしているSwashbuckleプロジェクトを見逃していないことを確認したかっただけです。
クラッシュ

8

これは、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");
        }

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

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

これで、次のように承認したいメソッドのいずれかにポリシーフィルターを設定できます。

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

お役に立てれば


3

JWTトークンをサポートするには、いくつかの3Dパーティーサーバーを使用する必要があると思います。WEBAPI 2では、すぐに使えるJWTサポートはありません。

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

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

かなり長いですが、ほとんどの部分は、コントローラーとASP.NET Identityの詳細であり、まったく必要ない場合があります。最も重要なのは

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

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

そこで、フロントエンドからアクセスできるエンドポイント(「/ token」など)のセットアップ方法(およびリクエストのフォーマットの詳細)を読むことができます。

他の手順では、そのエンドポイントをデータベースなどに接続する方法の詳細が提供され、必要な部分を選択できます。


2

私の場合、JWTは別のAPIによって作成されるため、ASP.NETはそれをデコードして検証するだけで済みます。受け入れられた回答とは対照的に、非対称アルゴリズムであるRSAを使用しているため、SymmetricSecurityKey上記のクラスは機能しません。

結果は次のとおりです。

using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;

    public static async Task<JwtSecurityToken> VerifyAndDecodeJwt(string accessToken)
    {
        try
        {
            var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{securityApiOrigin}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
            var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
            var validationParameters = new TokenValidationParameters()
            {
                ValidateLifetime = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                RequireSignedTokens = true,
                IssuerSigningKeys = openIdConfig.SigningKeys,
            };
            new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var validToken);
            // threw on invalid, so...
            return validToken as JwtSecurityToken;
        }
        catch (Exception ex)
        {
            logger.Info(ex.Message);
            return null;
        }
    }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.