Otentikasi JWT untuk ASP.NET Web API

264

Saya mencoba untuk mendukung token pembawa JWT (JSON Web Token) di aplikasi API web saya dan saya tersesat.

Saya melihat dukungan untuk .NET Core dan untuk aplikasi OWIN.
Saat ini saya menjadi hosting aplikasi saya di IIS.

Bagaimana saya bisa mencapai modul otentikasi ini di aplikasi saya? Apakah ada cara saya dapat menggunakan <authentication>konfigurasi yang mirip dengan cara saya menggunakan formulir / otentikasi Windows?

Amir Popovich
sumber

Jawaban:

611

Saya menjawab pertanyaan ini: Bagaimana cara mengamankan API Web ASP.NET 4 tahun yang lalu menggunakan HMAC.

Sekarang, banyak hal berubah dalam keamanan, terutama JWT semakin populer. Di sini, saya akan mencoba menjelaskan cara menggunakan JWT dengan cara paling sederhana dan dasar yang saya bisa, jadi kita tidak akan tersesat dari hutan OWIN, Oauth2, ASP.NET Identity ... :).

Jika Anda tidak tahu token JWT, Anda perlu melihatnya sedikit di:

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

Pada dasarnya, token JWT terlihat seperti:

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

Contoh:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

Token JWT memiliki tiga bagian:

  1. Header: Format JSON yang disandikan dalam Base64
  2. Klaim: Format JSON yang disandikan di Base64.
  3. Tanda tangan: Dibuat dan ditandatangani berdasarkan Header dan Klaim yang disandikan di Base64.

Jika Anda menggunakan situs web jwt.io dengan token di atas, Anda dapat mendekode token dan melihatnya seperti di bawah ini:

masukkan deskripsi gambar di sini

Secara teknis, JWT menggunakan tanda tangan yang ditandatangani dari header dan klaim dengan algoritma keamanan yang ditentukan dalam header (contoh: HMACSHA256). Oleh karena itu, JWT harus ditransfer melalui HTTP jika Anda menyimpan informasi sensitif apa pun dalam klaim.

Sekarang, untuk menggunakan otentikasi JWT, Anda tidak benar-benar membutuhkan middleware OWIN jika Anda memiliki sistem Web Api yang lama. Konsep sederhananya adalah bagaimana menyediakan token JWT dan bagaimana memvalidasi token ketika permintaan datang. Itu dia.

Kembali ke demo, agar token JWT tetap ringan, saya hanya menyimpan usernamedan expiration timedi JWT. Tetapi dengan cara ini, Anda harus membangun kembali identitas lokal baru (kepala sekolah) untuk menambahkan lebih banyak informasi seperti: peran .. jika Anda ingin melakukan otorisasi peran. Tetapi, jika Anda ingin menambahkan lebih banyak informasi ke JWT, terserah Anda: sangat fleksibel.

Alih-alih menggunakan middleware OWIN, Anda cukup memberikan titik akhir JWT token dengan menggunakan aksi dari controller:

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

Ini adalah tindakan naif; dalam produksi, Anda harus menggunakan permintaan POST atau titik akhir Otentikasi Dasar untuk memberikan token JWT.

Bagaimana cara menghasilkan token username?

Anda dapat menggunakan paket NuGet yang dipanggil System.IdentityModel.Tokens.Jwtdari Microsoft untuk menghasilkan token, atau bahkan paket lain jika Anda mau. Dalam demo, saya gunakan HMACSHA256dengan 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;
}

Titik akhir untuk memberikan token JWT telah selesai. Sekarang, bagaimana memvalidasi JWT ketika permintaan datang? Dalam demo saya telah membangun JwtAuthenticationAttributeyang mewarisi dari IAuthenticationFilter(lebih detail tentang filter otentikasi di sini ).

Dengan atribut ini, Anda dapat mengautentikasi tindakan apa pun: Anda hanya perlu menempatkan atribut ini pada tindakan itu.

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

Anda juga dapat menggunakan middleware OWIN atau DelegateHander jika Anda ingin memvalidasi semua permintaan yang masuk untuk WebAPI Anda (tidak khusus untuk Kontroler atau tindakan)

Di bawah ini adalah metode inti dari filter otentikasi:

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

Alur kerjanya adalah, menggunakan perpustakaan JWT (paket NuGet di atas) untuk memvalidasi token JWT dan kemudian kembali ClaimsPrincipal. Anda dapat melakukan lebih banyak validasi seperti memeriksa apakah pengguna ada di sistem Anda dan menambahkan validasi khusus lainnya jika Anda mau. Kode untuk memvalidasi token JWT dan mendapatkan kembali pokok:

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

Jika token JWT divalidasi dan kepala sekolah kembali, Anda harus membangun identitas lokal baru dan memasukkan lebih banyak informasi ke dalamnya untuk memeriksa otorisasi peran.

Ingatlah untuk menambahkan config.Filters.Add(new AuthorizeAttribute());(otorisasi default) pada lingkup global untuk mencegah permintaan anonim ke sumber daya Anda.

Anda dapat menggunakan tukang pos untuk menguji demo:

Permintaan token (naif seperti yang saya sebutkan di atas, hanya untuk demo):

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

Masukkan token JWT di header untuk permintaan resmi, contoh:

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

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

Demo diletakkan di sini: https://github.com/cuongle/WebApi.Jwt

cuongle
sumber
5
Diterangkan dengan baik oleh @Cuong Le tetapi saya ingin menambahkan lebih banyak: Jika Anda menggunakan OWIN periksa UseJwtBearerAuthentication yang tersedia di Microsoft.Owin.Security.Jwt Anda dapat menggunakan middleware owin ini di WebAPI untuk memvalidasi setiap permintaan yang masuk secara otomatis. gunakan kelas startup owin untuk mendaftar middleware
Jek
5
@AmirPopovich Anda tidak perlu mengatur token pada respons, token harus disimpan di tempat lain di sisi klien, untuk web, Anda dapat memasukkan ke penyimpanan lokal, setiap kali Anda mengirim permintaan HTTP, letakkan token di header.
cuongle
7
Wow ini adalah penjelasan paling sederhana yang pernah saya lihat dalam waktu yang lama. 100 jika saya bisa
gyozo kudor
4
@Homam: Maaf dari jawaban yang terlambat ini, cara terbaik untuk menghasilkan adalah: varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
cuongle
4
Siapa pun yang menggunakan kode demo dari repo CuongLe akan melihat ada bug di mana permintaan tanpa tajuk otorisasi tidak ditangani, artinya kueri apa pun yang tidak dapat dilewati (tidak ada titik akhir yang aman!). Ada permintaan tarik dari @magicleon untuk memperbaiki masalah ini di sini: github.com/cuongle/WebApi.Jwt/pull/4
Chucky
11

Saya telah berhasil mencapainya dengan upaya minimal (sesederhana dengan ASP.NET Core).

Untuk itu saya menggunakan Startup.csfile dan Microsoft.Owin.Security.Jwtperpustakaan OWIN .

Agar aplikasi berhasil, Startup.cskita perlu mengubah Web.config:

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

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

Banyak dari Anda menggunakan ASP.NET Core saat ini, sehingga Anda dapat melihatnya tidak berbeda jauh dari yang kami miliki di sana.

Itu benar-benar membuat saya bingung pertama, saya mencoba menerapkan penyedia kustom, dll. Tapi saya tidak berharap itu menjadi sangat sederhana. OWINhanya bebatuan!

Hanya satu hal lagi - setelah saya mengaktifkan pustaka OWIN Startup NSWagberhenti bekerja untuk saya (mis. Beberapa dari Anda mungkin ingin secara otomatis membuat proxy HTTP skrip naskah untuk aplikasi Angular).

Solusinya juga sangat sederhana - saya diganti NSWagdengan Swashbuckledan tidak memiliki masalah lebih lanjut.


Oke, sekarang bagikan ConfigHelperkode:

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

Aspek penting lainnya - Saya mengirim JWT Token melalui tajuk Otorisasi , sehingga kode skrip mencari saya sebagai berikut:

(kode di bawah ini dihasilkan oleh 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')
            })
        };

Lihat bagian tajuk - "Authorization": "Bearer " + localStorage.getItem('token')

Alex Herman
sumber
I replaced NSWag with Swashbuckle and didn't have any further issues.Apakah Swashbuckle memiliki kemampuan untuk menghasilkan file skrip atau apakah itu sesuatu yang Anda tambahkan sendiri?
naksir
@crush swashbucle adalah perpustakaan backend yang menyediakan json, seperti perpustakaan nuget nswag hanya lebih baik. Untuk menghasilkan file naskah Anda masih harus menggunakan paket nswag dari npm.
Alex Herman
Benar, saya sudah punya swashbuckle di proyek saya untuk beberapa waktu, sepertinya Anda menyarankan itu dapat menghasilkan model TypeScript bukan nswag. Saya bukan penggemar nswag ... ini berat. Saya telah membuat C # -> konversi TypeScript saya yang terhubung ke Swashbuckle - menghasilkan file sebagai proses post-build, dan menerbitkannya ke feed npm untuk proyek kami. Saya hanya ingin memastikan bahwa saya tidak mengabaikan proyek Swashbuckle yang sudah melakukan hal yang sama.
naksir
8

Berikut adalah implementasi Otentikasi berbasis Klaim yang sangat minimal dan aman menggunakan token JWT dalam API Web Inti ASP.NET.

pertama-tama, Anda perlu mengekspos titik akhir yang mengembalikan token JWT dengan klaim yang ditetapkan untuk pengguna:

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

sekarang Anda perlu Menambahkan Otentikasi ke layanan Anda di ConfigureServicesdalam startup.cs Anda untuk menambahkan otentikasi JWT sebagai layanan otentikasi default Anda seperti ini:

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"])),
                 };
             });

sekarang Anda dapat menambahkan kebijakan ke layanan otorisasi Anda seperti ini:

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

ALTERNATIF , Anda juga dapat (tidak perlu) mengisi semua klaim Anda dari basis data karena ini hanya akan berjalan satu kali saat startup aplikasi dan menambahkannya ke kebijakan seperti ini:

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

sekarang Anda dapat menempatkan filter Kebijakan pada salah satu metode yang Anda ingin diotorisasi seperti ini:

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

Semoga ini membantu

Zeeshan Adil
sumber
3

Saya pikir Anda harus menggunakan server pihak 3d untuk mendukung token JWT dan tidak ada dukungan JWT out of the box di WEB API 2.

Namun ada proyek OWIN untuk mendukung beberapa format token yang ditandatangani (bukan JWT). Ini berfungsi sebagai protokol OAuth yang diperkecil untuk menyediakan hanya bentuk otentikasi sederhana untuk situs web.

Anda dapat membaca lebih lanjut misalnya di sini .

Ini agak panjang, tetapi sebagian besar adalah rincian dengan pengontrol dan ASP.NET Identity yang mungkin tidak Anda perlukan sama sekali. Yang terpenting adalah

Langkah 9: Tambahkan dukungan untuk OAuth Bearer Tokens Generation

Langkah 12: Menguji API Back-end

Di sana Anda dapat membaca cara mengatur titik akhir (mis. "/ Token") yang dapat Anda akses dari frontend (dan perincian tentang format permintaan).

Langkah-langkah lain memberikan perincian tentang bagaimana menghubungkan titik akhir itu ke database, dll. Dan Anda dapat memilih bagian yang Anda butuhkan.

Ilya Chernomordik
sumber
2

Dalam kasus saya, JWT dibuat oleh API terpisah sehingga ASP.NET hanya perlu mendekode dan memvalidasinya. Berbeda dengan jawaban yang diterima, kami menggunakan RSA yang merupakan algoritma non-simetris, sehingga SymmetricSecurityKeykelas yang disebutkan di atas tidak akan berfungsi.

Inilah hasilnya.

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;
        }
    }
Ron Newcomb
sumber