Bagaimana cara mengamankan API Web ASP.NET [ditutup]

397

Saya ingin membangun layanan web yang tenang menggunakan ASP.NET Web API yang akan digunakan oleh pengembang pihak ketiga untuk mengakses data aplikasi saya.

Saya sudah membaca cukup banyak tentang OAuth dan tampaknya menjadi standar, tetapi menemukan sampel yang bagus dengan dokumentasi yang menjelaskan cara kerjanya (dan yang benar-benar berfungsi!) Tampaknya sangat sulit (terutama untuk pemula yang baru OAuth).

Apakah ada sampel yang benar-benar dibuat dan berfungsi serta menunjukkan cara mengimplementasikannya?

Saya telah mengunduh banyak sampel:

  • DotNetOAuth - dokumentasi tidak ada harapan dari perspektif pemula
  • Thinktecture - tidak dapat membangunnya

Saya juga telah melihat blog yang menyarankan skema berbasis token sederhana (seperti ini ) - ini sepertinya menciptakan kembali roda tetapi memang memiliki keuntungan karena secara konsep cukup sederhana.

Sepertinya ada banyak pertanyaan seperti ini di SO tetapi tidak ada jawaban yang bagus.

Apa yang semua orang lakukan di ruang ini?

Craig Shearer
sumber

Jawaban:

292

Memperbarui:

Saya telah menambahkan tautan ini ke jawaban saya yang lain bagaimana menggunakan otentikasi JWT untuk ASP.NET Web API di sini untuk siapa saja yang tertarik pada JWT.


Kami telah berhasil menerapkan otentikasi HMAC untuk mengamankan Web API, dan itu berfungsi dengan baik. Otentikasi HMAC menggunakan kunci rahasia untuk setiap konsumen yang konsumen dan server sama-sama kenal untuk pesan hash hmac, HMAC256 harus digunakan. Sebagian besar kasus, kata sandi hash dari konsumen digunakan sebagai kunci rahasia.

Pesan biasanya dibangun dari data dalam permintaan HTTP, atau bahkan data khusus yang ditambahkan ke header HTTP, pesan mungkin termasuk:

  1. Stempel waktu: waktu permintaan dikirim (UTC atau GMT)
  2. Kata kerja HTTP: GET, POST, PUT, DELETE.
  3. memposting data dan string kueri,
  4. URL

Di bawah tenda, otentikasi HMAC adalah:

Konsumen mengirimkan permintaan HTTP ke server web, setelah membangun tanda tangan (output dari hmac hash), templat permintaan HTTP:

User-Agent: {agent}   
Host: {host}   
Timestamp: {timestamp}
Authentication: {username}:{signature}

Contoh untuk permintaan GET:

GET /webapi.hmac/api/values

User-Agent: Fiddler    
Host: localhost    
Timestamp: Thursday, August 02, 2012 3:30:32 PM 
Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=

Pesan untuk hash untuk mendapatkan tanda tangan:

GET\n
Thursday, August 02, 2012 3:30:32 PM\n
/webapi.hmac/api/values\n

Contoh untuk permintaan POST dengan string kueri (tanda tangan di bawah ini tidak benar, hanya sebuah contoh)

POST /webapi.hmac/api/values?key2=value2

User-Agent: Fiddler    
Host: localhost    
Content-Type: application/x-www-form-urlencoded
Timestamp: Thursday, August 02, 2012 3:30:32 PM 
Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=

key1=value1&key3=value3

Pesan untuk hash untuk mendapatkan tanda tangan

GET\n
Thursday, August 02, 2012 3:30:32 PM\n
/webapi.hmac/api/values\n
key1=value1&key2=value2&key3=value3

Harap perhatikan bahwa data formulir dan string kueri harus berurutan, sehingga kode di server mendapatkan string kueri dan data formulir untuk membuat pesan yang benar.

Ketika permintaan HTTP datang ke server, filter tindakan otentikasi diimplementasikan untuk mem-parsing permintaan untuk mendapatkan informasi: HTTP verb, timestamp, uri, form data, dan string kueri, kemudian berdasarkan ini untuk membangun tanda tangan (gunakan hash hmac) dengan rahasia kunci (kata sandi hash) di server.

Kunci rahasia didapat dari database dengan nama pengguna sesuai permintaan.

Kemudian kode server membandingkan tanda tangan pada permintaan dengan tanda tangan yang dibangun; jika sama, otentikasi dilewatkan, jika tidak, gagal.

Kode untuk membuat tanda tangan:

private static string ComputeHash(string hashedPassword, string message)
{
    var key = Encoding.UTF8.GetBytes(hashedPassword.ToUpper());
    string hashString;

    using (var hmac = new HMACSHA256(key))
    {
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
        hashString = Convert.ToBase64String(hash);
    }

    return hashString;
}

Jadi, bagaimana cara mencegah serangan replay?

Tambahkan kendala untuk cap waktu, sesuatu seperti:

servertime - X minutes|seconds  <= timestamp <= servertime + X minutes|seconds 

(servertime: waktu permintaan datang ke server)

Dan, cache tanda tangan permintaan dalam memori (gunakan MemoryCache, harus tetap dalam batas waktu). Jika permintaan berikutnya datang dengan tanda tangan yang sama dengan permintaan sebelumnya, itu akan ditolak.

Kode demo diletakkan di sini: https://github.com/cuongle/Hmac.WebApi

cuongle
sumber
2
@ James: hanya stempel waktu yang tampaknya tidak cukup banyak, selama waktu singkat mereka dapat mensimulasikan permintaan dan dikirim ke server, saya baru saja mengedit posting saya, menggunakan keduanya akan menjadi yang terbaik.
cuongle
1
Apakah Anda yakin ini berfungsi sebagaimana mestinya? Anda hashing cap waktu dengan pesan dan caching pesan itu. Ini berarti setiap tanda tangan berbeda permintaan yang akan membuat tanda tangan cache Anda tidak berguna.
Filip Stas
1
@FilipStas: sepertinya saya tidak mengerti maksud Anda, alasan untuk menggunakan Cache di sini adalah untuk mencegah serangan estafet, tidak lebih
cuongle
1
@ ChrisO: Anda dapat merujuk [halaman ini] ( jokecamp.wordpress.com/2012/10/21/… ). Saya akan segera memperbarui sumber ini
cuongle
1
Solusinya disarankan berfungsi, tetapi Anda tidak dapat mencegah serangan Man-in-the-Middle, untuk itu Anda harus menerapkan HTTPS
refactor
34

Saya sarankan memulai dengan solusi paling mudah terlebih dahulu - mungkin HTTP Basic Authentication + HTTPS sederhana sudah cukup dalam skenario Anda.

Jika tidak (misalnya Anda tidak dapat menggunakan https, atau memerlukan manajemen kunci yang lebih rumit), Anda mungkin melihat solusi berbasis HMAC seperti yang disarankan oleh orang lain. Contoh yang baik dari API tersebut adalah Amazon S3 ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html )

Saya menulis posting blog tentang otentikasi berbasis HMAC di ASP.NET Web API. Ini membahas layanan Web API dan klien Web API dan kodenya tersedia di bitbucket. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Berikut adalah posting tentang Otentikasi Dasar di Web API: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

Ingat bahwa jika Anda akan memberikan API kepada pihak ke-3, Anda juga kemungkinan besar akan bertanggung jawab untuk mengirimkan perpustakaan klien. Otentikasi dasar memiliki keuntungan yang signifikan di sini karena didukung pada sebagian besar platform pemrograman di luar kotak. HMAC, di sisi lain, tidak terstandarisasi dan akan membutuhkan implementasi kustom. Ini harus relatif mudah tetapi masih membutuhkan pekerjaan.

PS. Ada juga opsi untuk menggunakan sertifikat HTTPS +. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/

Piotr Walat
sumber
23

Sudahkah Anda mencoba DevDefined. Ya?

Saya telah menggunakannya untuk mengamankan WebApi saya dengan OAuth 2-Legged. Saya juga berhasil mengujinya dengan klien PHP.

Sangat mudah untuk menambahkan dukungan untuk OAuth menggunakan perpustakaan ini. Inilah cara Anda dapat mengimplementasikan penyedia untuk ASP.NET MVC Web API:

1) Dapatkan kode sumber DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - versi terbaru memungkinkan OAuthContextBuilderekstensibilitas.

2) Bangun perpustakaan dan rujuk di proyek API Web Anda.

3) Buat pembangun konteks khusus untuk mendukung pembangunan konteks dari HttpRequestMessage:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Web;

using DevDefined.OAuth.Framework;

public class WebApiOAuthContextBuilder : OAuthContextBuilder
{
    public WebApiOAuthContextBuilder()
        : base(UriAdjuster)
    {
    }

    public IOAuthContext FromHttpRequest(HttpRequestMessage request)
    {
        var context = new OAuthContext
            {
                RawUri = this.CleanUri(request.RequestUri), 
                Cookies = this.CollectCookies(request), 
                Headers = ExtractHeaders(request), 
                RequestMethod = request.Method.ToString(), 
                QueryParameters = request.GetQueryNameValuePairs()
                    .ToNameValueCollection(), 
            };

        if (request.Content != null)
        {
            var contentResult = request.Content.ReadAsByteArrayAsync();
            context.RawContent = contentResult.Result;

            try
            {
                // the following line can result in a NullReferenceException
                var contentType = 
                    request.Content.Headers.ContentType.MediaType;
                context.RawContentType = contentType;

                if (contentType.ToLower()
                    .Contains("application/x-www-form-urlencoded"))
                {
                    var stringContentResult = request.Content
                        .ReadAsStringAsync();
                    context.FormEncodedParameters = 
                        HttpUtility.ParseQueryString(stringContentResult.Result);
                }
            }
            catch (NullReferenceException)
            {
            }
        }

        this.ParseAuthorizationHeader(context.Headers, context);

        return context;
    }

    protected static NameValueCollection ExtractHeaders(
        HttpRequestMessage request)
    {
        var result = new NameValueCollection();

        foreach (var header in request.Headers)
        {
            var values = header.Value.ToArray();
            var value = string.Empty;

            if (values.Length > 0)
            {
                value = values[0];
            }

            result.Add(header.Key, value);
        }

        return result;
    }

    protected NameValueCollection CollectCookies(
        HttpRequestMessage request)
    {
        IEnumerable<string> values;

        if (!request.Headers.TryGetValues("Set-Cookie", out values))
        {
            return new NameValueCollection();
        }

        var header = values.FirstOrDefault();

        return this.CollectCookiesFromHeaderString(header);
    }

    /// <summary>
    /// Adjust the URI to match the RFC specification (no query string!!).
    /// </summary>
    /// <param name="uri">
    /// The original URI. 
    /// </param>
    /// <returns>
    /// The adjusted URI. 
    /// </returns>
    private static Uri UriAdjuster(Uri uri)
    {
        return
            new Uri(
                string.Format(
                    "{0}://{1}{2}{3}", 
                    uri.Scheme, 
                    uri.Host, 
                    uri.IsDefaultPort ?
                        string.Empty :
                        string.Format(":{0}", uri.Port), 
                    uri.AbsolutePath));
    }
}

4) Gunakan tutorial ini untuk membuat penyedia OAuth: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider . Pada langkah terakhir (Mengakses Contoh Sumber Daya Dilindungi) Anda dapat menggunakan kode ini di AuthorizationFilterAttributeatribut Anda :

public override void OnAuthorization(HttpActionContext actionContext)
{
    // the only change I made is use the custom context builder from step 3:
    OAuthContext context = 
        new WebApiOAuthContextBuilder().FromHttpRequest(actionContext.Request);

    try
    {
        provider.AccessProtectedResourceRequest(context);

        // do nothing here
    }
    catch (OAuthException authEx)
    {
        // the OAuthException's Report property is of the type "OAuthProblemReport", it's ToString()
        // implementation is overloaded to return a problem report string as per
        // the error reporting OAuth extension: http://wiki.oauth.net/ProblemReporting
        actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
            {
               RequestMessage = request, ReasonPhrase = authEx.Report.ToString()
            };
    }
}

Saya telah mengimplementasikan penyedia saya sendiri jadi saya belum menguji kode di atas (kecuali tentu saja WebApiOAuthContextBuilderyang saya gunakan di penyedia saya) tetapi harus berfungsi dengan baik.

Maksymilian Majer
sumber
Terima kasih - Saya akan melihat ini, meskipun untuk saat ini saya telah meluncurkan solusi berbasis HMAC saya sendiri.
Craig Shearer
1
@CraigShearer - hai, Anda mengatakan telah memutar sendiri .. hanya punya beberapa pertanyaan jika Anda tidak keberatan berbagi. Saya berada di posisi yang sama, di mana saya memiliki Web API MVC yang relatif kecil. Pengontrol API duduk berdampingan dengan pengontrol / tindakan lain yang berada di bawah formulir auth. Menerapkan OAuth tampaknya berlebihan ketika saya sudah memiliki penyedia keanggotaan yang bisa saya gunakan dan saya hanya perlu mengamankan beberapa operasi. Saya benar-benar menginginkan tindakan autentik yang mengembalikan token terenkripsi - lalu menggunakan token dalam panggilan berikutnya? selamat datang info sebelum saya berkomitmen untuk menerapkan solusi auth yang ada. Terima kasih!
sambomartin
@Maksymilian Majer - Mungkinkah Anda dapat membagikan bagaimana Anda menerapkan penyedia layanan ini secara lebih rinci? Saya mengalami beberapa masalah mengirim tanggapan kembali ke klien.
jlrolin
21

API Web memperkenalkan Atribut [Authorize]untuk memberikan keamanan. Ini dapat diatur secara global (global.asx)

public static void Register(HttpConfiguration config)
{
    config.Filters.Add(new AuthorizeAttribute());
}

Atau per pengontrol:

[Authorize]
public class ValuesController : ApiController{
...

Tentu saja jenis otentikasi Anda dapat bervariasi dan Anda mungkin ingin melakukan otentikasi Anda sendiri, ketika ini terjadi, Anda mungkin menemukan pewarisan yang bermanfaat dari Atribut Otorisasi dan memperluasnya untuk memenuhi persyaratan Anda:

public class DemoAuthorizeAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        if (Authorize(actionContext))
        {
            return;
        }
        HandleUnauthorizedRequest(actionContext);
    }

    protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        challengeMessage.Headers.Add("WWW-Authenticate", "Basic");
        throw new HttpResponseException(challengeMessage);
    }

    private bool Authorize(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        try
        {
            var someCode = (from h in actionContext.Request.Headers where h.Key == "demo" select h.Value.First()).FirstOrDefault();
            return someCode == "myCode";
        }
        catch (Exception)
        {
            return false;
        }
    }
}

Dan di controller Anda:

[DemoAuthorize]
public class ValuesController : ApiController{

Berikut ini tautan pada implementasi kustom lain untuk Otorisasi WebApi:

http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-membership-provider/

Dalorzo
sumber
Terima kasih atas contoh @Alorzo, tapi saya punya beberapa masalah. Saya melihat tautan terlampir, tetapi mengikuti instruksi tidak cukup bekerja. Saya juga menemukan info yang diperlukan hilang. Pertama, ketika saya membuat proyek baru, apakah benar untuk memilih Akun Pengguna Individual untuk otentikasi? Atau apakah saya membiarkannya tanpa otentikasi. Saya juga tidak mendapatkan kesalahan 302 yang disebutkan, tetapi saya mendapatkan kesalahan 401. Terakhir, bagaimana saya meneruskan info yang diperlukan dari pandangan saya ke controller? Seperti apakah panggilan ajax saya? Btw, saya menggunakan otentikasi bentuk untuk tampilan MVC saya. Apakah itu masalah?
Amanda
Ini bekerja dengan fantastis. Baik untuk belajar dan mulai bekerja pada token akses kita sendiri.
CodeName47
Satu komentar kecil - hati-hati dengan AuthorizeAttribute, karena ada dua kelas yang berbeda dengan nama yang sama, dalam ruang nama yang berbeda: 1. System.Web.Mvc.AuthorizeAttribute -> untuk pengontrol MVC 2. System.Web.Http.AuthorizeAttribute -> untuk WebApi.
Vitaliy Markitanov
5

Jika Anda ingin mengamankan API Anda dari server ke server (tidak ada pengalihan ke situs web untuk 2 otentikasi yang di-leged). Anda dapat melihat protokol Hibah Kredensial Klien OAuth2.

https://dev.twitter.com/docs/auth/application-only-auth

Saya telah mengembangkan perpustakaan yang dapat membantu Anda dengan mudah menambahkan dukungan semacam ini ke WebAPI Anda. Anda dapat menginstalnya sebagai paket NuGet:

https://nuget.org/packages/OAuth2ClientCredentialsGrant/1.0.0.0

Perpustakaan menargetkan .NET Framework 4.5.

Setelah Anda menambahkan paket ke proyek Anda, itu akan membuat file readme di root proyek Anda. Anda dapat melihat file readme itu untuk melihat cara mengkonfigurasi / menggunakan paket ini.

Bersulang!

Varun Chatterji
sumber
5
Apakah Anda membagikan / menyediakan kode sumber untuk kerangka kerja ini sebagai sumber terbuka?
barrypicker
JFR: First Link Rusak dan paket NuGet tidak pernah diperbarui
abdul qayyum
3

sebagai kelanjutan jawaban @ Cuong Le, pendekatan saya untuk mencegah serangan replay adalah

// Enkripsi Waktu Unix di sisi Klien menggunakan kunci pribadi bersama (atau kata sandi pengguna)

// Kirim sebagai bagian dari header permintaan ke server (API WEB)

// Dekripsi Waktu Unix di Server (API WEB) menggunakan kunci pribadi bersama (atau kata sandi pengguna)

// Periksa perbedaan waktu antara Waktu Unix Klien dan Waktu Unix Server, tidak boleh lebih dari x detik

// jika User ID / Hash Password sudah benar dan UnixTime yang didekripsi berada dalam x detik dari waktu server maka itu adalah permintaan yang valid

refactor
sumber