วิธีรักษาความปลอดภัย ASP.NET Web API [ปิด]


397

ฉันต้องการสร้างบริการเว็บสงบโดยใช้ ASP.NET Web API ที่นักพัฒนาบุคคลที่สามจะใช้ในการเข้าถึงข้อมูลแอปพลิเคชันของฉัน

ฉันได้อ่านค่อนข้างมากเกี่ยวกับOAuthและดูเหมือนจะเป็นมาตรฐาน แต่การหาตัวอย่างที่ดีพร้อมเอกสารอธิบายว่ามันทำงานอย่างไร (และนั่นใช้งานได้จริง!) ดูเหมือนจะยากอย่างไม่น่าเชื่อ

มีตัวอย่างที่สร้างและใช้งานจริงและแสดงวิธีการใช้งานจริงหรือไม่?

ฉันได้ดาวน์โหลดตัวอย่างมากมาย:

  • DotNetOAuth - เอกสารเป็นสิ่งที่สิ้นหวังจากมุมมองของมือใหม่
  • Thinktecture - ไม่สามารถสร้างได้

ฉันยังได้ดูบล็อกที่แนะนำชุดรูปแบบโทเค็นที่เรียบง่าย (เช่นนี้ ) - นี่ดูเหมือนจะเป็นการประดิษฐ์วงล้อขึ้นใหม่ แต่มันมีข้อดีของการเป็นแนวคิดที่ค่อนข้างง่าย

ดูเหมือนว่ามีคำถามมากมายเช่นนี้ใน SO แต่ไม่มีคำตอบที่ดี

ทุกคนกำลังทำอะไรในพื้นที่นี้

คำตอบ:


292

ปรับปรุง:

ฉันได้เพิ่มลิงค์นี้ไปยังคำตอบอื่น ๆ ของฉันวิธีการใช้การพิสูจน์ตัวตน JWT สำหรับ ASP.NET Web APIที่นี่สำหรับทุกคนที่สนใจ JWT


เรามีการจัดการเพื่อใช้การรับรองความถูกต้อง HMAC เพื่อรักษาความปลอดภัยเว็บ API และมันก็ใช้ได้ การพิสูจน์ตัวตน HMAC ใช้คีย์ลับสำหรับผู้ใช้แต่ละรายซึ่งทั้งผู้บริโภคและเซิร์ฟเวอร์รู้ว่าจะ hmac แฮชข้อความควรใช้ HMAC256 ส่วนใหญ่แล้วรหัสผ่านที่ถูกแฮชของผู้ใช้บริการจะถูกใช้เป็นรหัสลับ

โดยปกติข้อความจะถูกสร้างขึ้นจากข้อมูลในคำขอ HTTP หรือแม้แต่ข้อมูลที่กำหนดเองซึ่งถูกเพิ่มไปยังส่วนหัว HTTP ข้อความอาจรวมถึง:

  1. Timestamp: เวลาที่ส่งคำขอ (UTC หรือ GMT)
  2. HTTP verb: GET, POST, PUT, DELETE
  3. โพสต์ข้อมูลและสตริงการสืบค้น
  4. URL

ภายใต้ประทุนการตรวจสอบ HMAC จะเป็น:

Consumer ส่งคำร้องขอ HTTP ไปยังเว็บเซิร์ฟเวอร์หลังจากสร้างลายเซ็น (เอาต์พุตของ hmac hash) เท็มเพลตของคำร้องขอ HTTP:

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

ตัวอย่างสำหรับคำขอ GET:

GET /webapi.hmac/api/values

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

ข้อความที่จะแฮชเพื่อรับลายเซ็น:

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

ตัวอย่างสำหรับคำขอ POST ที่มีสตริงข้อความค้นหา (ลายเซ็นด้านล่างไม่ถูกต้องเป็นเพียงตัวอย่าง)

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

ข้อความที่จะแฮชเพื่อรับลายเซ็น

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

โปรดทราบว่าข้อมูลฟอร์มและสตริงการสืบค้นควรเป็นไปตามลำดับดังนั้นโค้ดบนเซิร์ฟเวอร์จะได้รับสตริงการสืบค้นและข้อมูลฟอร์มเพื่อสร้างข้อความที่ถูกต้อง

เมื่อคำขอ HTTP มาถึงเซิร์ฟเวอร์ตัวกรองการดำเนินการตรวจสอบความถูกต้องจะดำเนินการเพื่อแยกวิเคราะห์คำขอเพื่อรับข้อมูล: กริยา HTTP, เวลาประทับ, uri ข้อมูลในฟอร์มและสตริงการสืบค้นจากนั้นยึดตามสิ่งเหล่านี้เพื่อสร้างลายเซ็น (ใช้ hmac hash) คีย์ (รหัสผ่านที่แฮช) บนเซิร์ฟเวอร์

รหัสลับนั้นได้มาจากฐานข้อมูลพร้อมชื่อผู้ใช้ในคำขอ

จากนั้นรหัสเซิร์ฟเวอร์จะเปรียบเทียบลายเซ็นต์ในคำขอกับลายเซ็นที่สร้างขึ้น หากเท่ากันจะผ่านการตรวจสอบความถูกต้องมิฉะนั้นจะล้มเหลว

รหัสเพื่อสร้างลายเซ็น:

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

ดังนั้นวิธีป้องกันการโจมตีซ้ำ?

เพิ่มข้อ จำกัด สำหรับการประทับเวลาบางอย่างเช่น:

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

(เวลา: เวลาของการร้องขอมาถึงเซิร์ฟเวอร์)

และแคชลายเซ็นของคำขอในหน่วยความจำ (ใช้ MemoryCache ควรเก็บในเวลาที่ จำกัด ) หากคำขอถัดไปมาพร้อมกับลายเซ็นเดียวกันกับคำขอก่อนหน้านี้คำขอนั้นจะถูกปฏิเสธ

รหัสตัวอย่างใส่ไว้ที่นี่: https://github.com/cuongle/Hmac.WebApi


2
@ James: เวลาประทับเท่านั้นที่ดูเหมือนจะไม่มากในช่วงเวลาสั้น ๆ พวกเขาอาจจำลองคำขอและส่งไปยังเซิร์ฟเวอร์ฉันเพิ่งแก้ไขโพสต์ของฉันการใช้ทั้งสองอย่างจะดีที่สุด
cuongle

1
คุณแน่ใจหรือว่าทำงานได้ตามที่ควร? คุณกำลังบันทึกเวลาด้วยข้อความและแคชข้อความนั้น นี่หมายถึงลายเซ็นที่แตกต่างกันแต่ละคำขอซึ่งจะทำให้ลายเซ็นแคชของคุณไร้ประโยชน์
Stas Filip

1
@FilipStas: ดูเหมือนว่าฉันไม่ได้รับจุดของคุณเหตุผลที่จะใช้แคชในที่นี่คือการป้องกันการโจมตีรีเลย์ไม่มีอะไรมาก
cuongle

1
@ChrisO: คุณสามารถอ้างอิง [หน้านี้] ( jokecamp.wordpress.com/2012/10/21/… ) ฉันจะอัปเดตแหล่งข้อมูลนี้ในไม่ช้า
cuongle

1
วิธีการแก้ปัญหาที่แนะนำนั้นใช้ได้ผล แต่คุณไม่สามารถป้องกันการจู่โจมแบบ Man-in-the-Middle เพื่อที่คุณจะต้องติดตั้ง HTTPS
refactor

34

ฉันขอแนะนำให้เริ่มต้นด้วยการแก้ปัญหาที่ตรงไปตรงมาที่สุดก่อน - การพิสูจน์ตัวตนพื้นฐาน HTTP แบบง่าย ๆ + HTTPS ก็เพียงพอแล้วในสถานการณ์ของคุณ

หากไม่ (ตัวอย่างเช่นคุณไม่สามารถใช้ https หรือต้องการการจัดการคีย์ที่ซับซ้อนมากขึ้น) คุณอาจดูโซลูชันที่ใช้ HMAC ตามที่ผู้อื่นแนะนำ ตัวอย่างที่ดีของ API ดังกล่าวคือ Amazon S3 ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html )

ฉันเขียนโพสต์บล็อกเกี่ยวกับการตรวจสอบตาม HMAC ใน ASP.NET Web API มันกล่าวถึงบริการ Web API และไคลเอนต์ Web API และรหัสที่มีอยู่ใน bitbucket http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

นี่คือโพสต์เกี่ยวกับการรับรองความถูกต้องพื้นฐานใน Web API: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

โปรดจำไว้ว่าหากคุณจะให้บริการ API แก่บุคคลที่สามคุณอาจต้องรับผิดชอบในการส่งมอบห้องสมุดลูกค้า การพิสูจน์ตัวตนพื้นฐานมีข้อได้เปรียบที่สำคัญที่นี่เนื่องจากรองรับแพลตฟอร์มการเขียนโปรแกรมส่วนใหญ่นอกกรอบ ในทางกลับกัน HMAC นั้นไม่ได้มาตรฐานและจะต้องมีการใช้งานที่กำหนดเอง สิ่งเหล่านี้ควรตรงไปตรงมา แต่ยังต้องการงาน

PS นอกจากนี้ยังมีตัวเลือกในการใช้ใบรับรอง HTTPS + http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/


23

คุณลอง DevDefined.OAuth แล้วหรือยัง

ฉันใช้เพื่อรักษาความปลอดภัย WebApi ด้วย OAuth 2 ขา ฉันได้ทดสอบกับลูกค้า PHP สำเร็จแล้ว

มันค่อนข้างง่ายในการเพิ่มการสนับสนุนสำหรับ OAuth โดยใช้ห้องสมุดนี้ นี่คือวิธีที่คุณสามารถใช้ผู้ให้บริการสำหรับ ASP.NET MVC Web API:

1) รับซอร์สโค้ดของ DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - เวอร์ชันล่าสุดช่วยให้OAuthContextBuilderสามารถขยายได้

2) สร้างไลบรารีและอ้างอิงในโครงการ Web API ของคุณ

3) สร้างตัวสร้างบริบทที่กำหนดเองเพื่อสนับสนุนการสร้างบริบทจาก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) ใช้การกวดวิชานี้สำหรับการสร้างผู้ให้บริการ OAuth: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider ในขั้นตอนสุดท้าย (การเข้าถึงตัวอย่างทรัพยากรที่ได้รับการป้องกัน) คุณสามารถใช้รหัสนี้ในAuthorizationFilterAttributeแอตทริบิวต์ของคุณ:

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

ฉันใช้ผู้ให้บริการของตัวเองแล้วดังนั้นฉันจึงไม่ได้ทดสอบโค้ดด้านบน (ยกเว้นหลักสูตร WebApiOAuthContextBuilderที่ฉันใช้ในผู้ให้บริการ) แต่มันก็ใช้ได้ดี


ขอบคุณ - ฉันจะดูที่นี้ แต่ตอนนี้ฉันได้เปิดตัวโซลูชั่น HMAC ของฉันเอง
Craig Shearer

1
@CraigShearer - สวัสดีคุณบอกว่าคุณได้ทำเอง .. มีคำถามสองสามข้อถ้าคุณไม่รังเกียจที่จะแบ่งปัน ฉันอยู่ในตำแหน่งที่คล้ายกันซึ่งฉันมี MVC Web API ค่อนข้างน้อย ตัวควบคุม API อยู่ข้างๆตัวควบคุม / การทำงานอื่น ๆ ซึ่งอยู่ภายใต้การตรวจสอบความถูกต้องของแบบฟอร์ม การติดตั้ง OAuth ดูเหมือนจะเกินความจำเป็นเมื่อฉันมีผู้ให้บริการสมาชิกที่ฉันสามารถใช้ได้และฉันต้องการเพียงการรักษาความปลอดภัยจำนวนหนึ่ง ฉันต้องการการตรวจสอบสิทธิ์ที่ส่งคืนโทเค็นที่เข้ารหัสแล้วใช้โทเค็นในการโทรครั้งต่อไปหรือไม่ ยินดีต้อนรับข้อมูลใด ๆ ก่อนที่ฉันจะยอมรับการใช้วิธีแก้ไขปัญหาที่มีอยู่ ขอบคุณ!
sambomartin

@Maksymilian Majer - โอกาสใดที่คุณสามารถแบ่งปันวิธีการที่คุณใช้งานผู้ให้บริการในรายละเอียดเพิ่มเติม? ฉันมีปัญหาในการส่งคำตอบกลับไปยังลูกค้า
jlrolin

21

Web API แนะนำคุณสมบัติ[Authorize]เพื่อให้ความปลอดภัย สามารถตั้งค่าได้ทั่วโลก (global.asx)

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

หรือต่อตัวควบคุม:

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

แน่นอนว่าการรับรองความถูกต้องของคุณอาจแตกต่างกันไปและคุณอาจต้องการทำการพิสูจน์ตัวตนของคุณเองเมื่อสิ่งนี้เกิดขึ้นคุณอาจพบว่าการสืบทอดที่มีประโยชน์จาก Authorizate Attribute และขยายให้ตรงตามความต้องการของคุณ:

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

และในตัวควบคุมของคุณ:

[DemoAuthorize]
public class ValuesController : ApiController{

นี่คือลิงค์ของการดำเนินการที่กำหนดเองอื่น ๆ สำหรับการอนุญาต WebApi:

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


ขอบคุณสำหรับตัวอย่าง @Dalorzo แต่ฉันมีปัญหาบางอย่าง ฉันดูลิงค์ที่แนบมา แต่การทำตามคำแนะนำนั้นไม่ได้ผล ฉันยังพบข้อมูลที่จำเป็นขาดหายไป ประการแรกเมื่อฉันสร้างโครงการใหม่มันถูกต้องที่จะเลือกบัญชีผู้ใช้ส่วนบุคคลสำหรับการตรวจสอบ? หรือฉันจะทิ้งไว้โดยไม่มีการรับรองความถูกต้อง ฉันยังไม่ได้รับข้อผิดพลาด 302 ที่กล่าวถึง แต่ฉันได้รับข้อผิดพลาด 401 ท้ายสุดฉันจะส่งข้อมูลที่จำเป็นจากมุมมองของฉันไปยังตัวควบคุมได้อย่างไร การโทรอาแจ็กซ์ของฉันต้องเป็นอย่างไร? Btw ฉันใช้การตรวจสอบแบบฟอร์มสำหรับมุมมอง MVC ของฉัน นั่นเป็นปัญหาหรือไม่?
อแมนดา

มันใช้งานได้ดีมาก ยินดีที่ได้เรียนรู้และเริ่มทำงานกับโทเค็นการเข้าถึงของเราเอง
CodeName47

หนึ่งความคิดเห็นเล็ก ๆ - ระวังด้วยAuthorizeAttributeเนื่องจากมีสองคลาสที่ต่างกันด้วยชื่อเดียวกันในเนมสเปซที่ต่างกัน: 1. System.Web.Mvc.AuthorizeAttribute -> สำหรับตัวควบคุม MVC 2. System.Web.Http.AuthorizeAttribute -> สำหรับ WebApi
Vitaliy Markitanov

5

หากคุณต้องการรักษาความปลอดภัย API ของคุณในรูปแบบเซิร์ฟเวอร์ไปยังเซิร์ฟเวอร์ (ไม่มีการเปลี่ยนเส้นทางไปยังเว็บไซต์สำหรับการรับรองความถูกต้องแบบ 2 ขา) คุณสามารถดูโปรโตคอลให้สิทธิ์ลูกค้า OAuth2

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

ฉันได้พัฒนาห้องสมุดที่สามารถช่วยคุณเพิ่มการสนับสนุนประเภทนี้ลงใน WebAPI ของคุณได้อย่างง่ายดาย คุณสามารถติดตั้งเป็นแพ็คเกจ NuGet:

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

ไลบรารีเป้าหมาย. NET Framework 4.5

เมื่อคุณเพิ่มแพ็คเกจลงในโครงการของคุณแล้วมันจะสร้างไฟล์ readme ในรูทของโครงการของคุณ คุณสามารถดูไฟล์ readme นั้นเพื่อดูวิธีกำหนดค่า / ใช้แพ็คเกจนี้

ไชโย!


5
คุณแชร์ / ระบุซอร์สโค้ดสำหรับเฟรมเวิร์กนี้เป็นโอเพ่นซอร์สหรือไม่
barrypicker

JFR: การเชื่อมโยงแรกที่ใช้งานไม่ได้และแพ็คเกจ NuGet ไม่เคยอัปเดต
Abdul qayyum

3

ในการตอบสนองต่อ @ Cuong Le วิธีการของฉันเพื่อป้องกันการโจมตีซ้ำจะเป็น

// เข้ารหัสเวลา Unix ที่ฝั่งไคลเอ็นต์โดยใช้ไพรเวตคีย์ (หรือรหัสผ่านของผู้ใช้)

// ส่งเป็นส่วนหนึ่งของส่วนหัวคำขอไปยังเซิร์ฟเวอร์ (WEB API)

// ถอดรหัสเวลา Unix ที่เซิร์ฟเวอร์ (WEB API) โดยใช้ไพรเวตคีย์ (หรือรหัสผ่านของผู้ใช้)

// ตรวจสอบความแตกต่างของเวลาระหว่างเวลา Unix ของไคลเอ็นต์และเวลา Unix ของเซิร์ฟเวอร์ไม่ควรมากกว่า x วินาที

// ถ้า ID ผู้ใช้ / รหัสแฮชถูกต้องและ UnixTime ที่ถอดรหัสแล้วนั้นอยู่ในระยะเวลา x วินาทีของเวลาเซิร์ฟเวอร์มันเป็นคำขอที่ถูกต้อง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.