Bảo mật OWIN - Cách triển khai mã thông báo làm mới OAuth2


80

Tôi đang sử dụng mẫu Web Api 2 đi kèm với Visual Studio 2013 có một số phần mềm trung gian OWIN để thực hiện Xác thực Người dùng và những thứ tương tự.

Trong phần, OAuthAuthorizationServerOptionstôi nhận thấy rằng Máy chủ OAuth2 được thiết lập để phân phối các mã thông báo hết hạn sau 14 ngày

 OAuthOptions = new OAuthAuthorizationServerOptions
 {
      TokenEndpointPath = new PathString("/api/token"),
      Provider = new ApplicationOAuthProvider(PublicClientId,UserManagerFactory) ,
      AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
      AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
      AllowInsecureHttp = true
 };

Điều này không phù hợp với dự án mới nhất của tôi. Tôi muốn cung cấp các thẻ bearer_tokens tồn tại trong thời gian ngắn có thể được làm mới bằng cách sử dụngrefresh_token

Tôi đã thực hiện rất nhiều googling và không tìm thấy bất kỳ điều gì hữu ích.

Vì vậy, đây là cách tôi đã cố gắng để đạt được. Bây giờ tôi đã đạt đến điểm "WTF làm tôi bây giờ".

Tôi đã viết một RefreshTokenProvidertriển khai IAuthenticationTokenProvidertheo thuộc RefreshTokenProvidertính trên OAuthAuthorizationServerOptionslớp:

    public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
    {
       private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();

        public async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            var guid = Guid.NewGuid().ToString();


            _refreshTokens.TryAdd(guid, context.Ticket);

            // hash??
            context.SetToken(guid);
        }

        public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {
            AuthenticationTicket ticket;

            if (_refreshTokens.TryRemove(context.Token, out ticket))
            {
                context.SetTicket(ticket);
            }
        }

        public void Create(AuthenticationTokenCreateContext context)
        {
            throw new NotImplementedException();
        }

        public void Receive(AuthenticationTokenReceiveContext context)
        {
            throw new NotImplementedException();
        }
    }

    // Now in my Startup.Auth.cs
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/api/token"),
        Provider = new ApplicationOAuthProvider(PublicClientId,UserManagerFactory) ,
        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
        AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(2),
        AllowInsecureHttp = true,
        RefreshTokenProvider = new RefreshTokenProvider() // This is my test
    };

Vì vậy, bây giờ khi ai đó yêu cầu một, bearer_tokentôi đang gửi một refresh_token, điều đó thật tuyệt.

Vì vậy, bây giờ làm cách nào để sử dụng refresh_token này để có được một cái mới bearer_token, có lẽ tôi cần gửi yêu cầu đến điểm cuối mã thông báo của mình với một số tiêu đề HTTP cụ thể được đặt?

Tôi chỉ nghĩ to khi nhập ... Tôi có nên xử lý hết hạn refresh_token trong của mình SimpleRefreshTokenProviderkhông? Làm thế nào để một khách hàng có được một cái mới refresh_token?

Tôi thực sự có thể làm với một số tài liệu / tài liệu đọc vì tôi không muốn làm sai điều này và muốn tuân theo một số loại tiêu chuẩn.


7
Có một hướng dẫn tuyệt vời về cách triển khai mã thông báo làm mới bằng Owin và OAuth: bitoftech.net/2014/07/16/…
Philip Bergström

Câu trả lời:


76

Vừa triển khai Dịch vụ OWIN của tôi với Bearer (được gọi là access_token trong phần sau) và Làm mới Mã thông báo. Cái nhìn sâu sắc của tôi về điều này là bạn có thể sử dụng các luồng khác nhau. Vì vậy, nó phụ thuộc vào luồng bạn muốn sử dụng như thế nào bạn đặt thời gian hết hạn access_token và refresh_token.

Tôi sẽ mô tả hai luồng AB trong follwing (Tôi đề nghị những gì bạn muốn có là luồng B):

A) thời gian hết hạn của access_token và refresh_token giống nhau vì nó là 1200 giây hoặc 20 phút mặc định. Luồng này trước tiên cần ứng dụng khách của bạn gửi client_id và client_secret cùng với dữ liệu đăng nhập để nhận access_token, refresh_token và expiration_time. Với refresh_token, giờ đây bạn có thể lấy access_token mới trong 20 phút (hoặc bất cứ thứ gì bạn đặt AccessTokenExpireTimeSpan trong OAuthAuthorizationServerOptions thành). Vì lý do thời gian hết hạn của access_token và refresh_token giống nhau, khách hàng của bạn có trách nhiệm nhận một access_token mới trước thời gian hết hạn! Ví dụ: khách hàng của bạn có thể gửi lệnh gọi POST làm mới tới điểm cuối mã thông báo của bạn với phần thân (lưu ý: bạn nên sử dụng https trong sản xuất)

grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xxxxx

để nhận mã thông báo mới sau 19 phút, ví dụ: để ngăn các mã thông báo hết hạn.

B) trong luồng này, bạn muốn có thời hạn ngắn hạn cho access_token của mình và thời hạn dài hạn cho refresh_token của bạn. Hãy giả sử cho mục đích kiểm tra, bạn đặt access_token hết hạn sau 10 giây ( AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10)) và refresh_token thành 5 phút. Bây giờ đến phần thú vị đặt thời gian hết hạn của refresh_token: Bạn thực hiện việc này trong hàm createAsync của mình trong lớp SimpleRefreshTokenProvider như sau:

var guid = Guid.NewGuid().ToString();


        //copy properties and set the desired lifetime of refresh token
        var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary)
        {
            IssuedUtc = context.Ticket.Properties.IssuedUtc,
            ExpiresUtc = DateTime.UtcNow.AddMinutes(5) //SET DATETIME to 5 Minutes
            //ExpiresUtc = DateTime.UtcNow.AddMonths(3) 
        };
        /*CREATE A NEW TICKET WITH EXPIRATION TIME OF 5 MINUTES 
         *INCLUDING THE VALUES OF THE CONTEXT TICKET: SO ALL WE 
         *DO HERE IS TO ADD THE PROPERTIES IssuedUtc and 
         *ExpiredUtc to the TICKET*/
        var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);

        //saving the new refreshTokenTicket to a local var of Type ConcurrentDictionary<string,AuthenticationTicket>
        // consider storing only the hash of the handle
        RefreshTokens.TryAdd(guid, refreshTokenTicket);            
        context.SetToken(guid);

Giờ đây, khách hàng của bạn có thể gửi một cuộc gọi POST với refresh_token tới điểm cuối mã thông báo của bạn khi access_tokenhết hạn. Phần nội dung của cuộc gọi có thể trông như thế này:grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx

Một điều quan trọng là bạn có thể muốn sử dụng mã này không chỉ trong chức năng CreateAsync mà còn trong chức năng Tạo của bạn. Vì vậy, bạn nên cân nhắc sử dụng chức năng của riêng mình (ví dụ: CreateTokenInternal) cho đoạn mã trên. Tại đây, bạn có thể tìm thấy các triển khai của các luồng khác nhau bao gồm luồng refresh_token (nhưng không cần đặt thời gian hết hạn của refresh_token)

Đây là một mẫu triển khai của IAuthenticationTokenProvider trên github (với việc đặt thời gian hết hạn của refresh_token)

Tôi rất tiếc vì tôi không thể trợ giúp với các tài liệu khác ngoài Thông số OAuth và Tài liệu Microsoft API. Tôi sẽ đăng các liên kết ở đây nhưng danh tiếng của tôi không cho phép tôi đăng nhiều hơn 2 liên kết ....

Tôi hy vọng điều này có thể giúp một số người khác rảnh rỗi khi cố gắng triển khai OAuth2.0 với thời gian hết hạn refresh_token khác với thời gian hết hạn access_token. Tôi không thể tìm thấy một ví dụ triển khai trên web (ngoại trừ một trong số thinktecture được liên kết ở trên) và tôi đã mất một số giờ điều tra cho đến khi nó hoạt động với tôi.

Thông tin mới: Trong trường hợp của tôi, tôi có hai khả năng khác nhau để nhận mã thông báo. Một là nhận một access_token hợp lệ. Ở đó, tôi phải gửi cuộc gọi ĐĂNG với nội dung Chuỗi ở định dạng ứng dụng / x-www-form-urlencoded với dữ liệu sau

client_id=YOURCLIENTID&grant_type=password&username=YOURUSERNAME&password=YOURPASSWORD

Thứ hai là nếu access_token không còn hợp lệ nữa, chúng ta có thể thử refresh_token bằng cách gửi một lệnh gọi POST với phần thân String ở định dạng application/x-www-form-urlencodedvới dữ liệu saugrant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID


1
một trong những nhận xét của bạn nói rằng "hãy xem xét việc lưu trữ mã băm của xử lý", không phải nhận xét đó có nên áp dụng cho dòng trên không? Vé giữ nguyên hướng dẫn ban đầu, nhưng chúng tôi chỉ lưu trữ băm của hướng dẫn trong đó RefreshTokens, vì vậy nếu RefreshTokensbị lộ, kẻ tấn công không thể sử dụng thông tin đó!?
esskar


1
Như được mô tả trong luồng B, bạn có thể đặt thời gian hết hạn cho access_token bằng cách sử dụng AccessTokenExpireTimeSpan = TimeSpan.FromMinutes (60) trong một giờ hoặc FromWHATEVER cho thời gian bạn muốn access_token hết hạn. Nhưng hãy lưu ý rằng nếu bạn đang sử dụng refresh_token trong luồng của mình thì thời gian hết hạn của refresh_token phải cao hơn thời gian hết hạn của access_token. Vì vậy, ví dụ, chúng tôi 24 giờ cho access_token và 2 tháng cho refresh_token. Bạn có thể đặt thời gian hết hạn của access_token trong cấu hình OAuth.
Freddy

12
Không sử dụng Guids cho mã thông báo của bạn cũng như băm của chúng, nó không an toàn. Sử dụng không gian tên System.Cryptography để tạo một mảng byte ngẫu nhiên và chuyển đổi mảng đó thành một chuỗi. Nếu không, các mã thông báo làm mới của bạn có thể bị đoán bởi các cuộc tấn công bạo lực.
Bon

1
@Bon Bạn sẽ brute-force-đoán một Hướng dẫn? Bộ giới hạn tỷ lệ của bạn nên hoạt động trước khi kẻ tấn công có thể đăng ngay cả một số ít yêu cầu. Và nếu không, nó vẫn là một Hướng dẫn.
lonix

46

Bạn cần triển khai RefreshTokenProvider . Đầu tiên tạo lớp cho RefreshTokenProvider tức là.

public class ApplicationRefreshTokenProvider : AuthenticationTokenProvider
{
    public override void Create(AuthenticationTokenCreateContext context)
    {
        // Expiration time in seconds
        int expire = 5*60;
        context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddSeconds(expire));
        context.SetToken(context.SerializeTicket());
    }

    public override void Receive(AuthenticationTokenReceiveContext context)
    {
        context.DeserializeTicket(context.Token);
    }
}

Sau đó, thêm phiên bản vào OAuthOptions .

OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/authenticate"),
    Provider = new ApplicationOAuthProvider(),
    AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(expire),
    RefreshTokenProvider = new ApplicationRefreshTokenProvider()
};

Thao tác này sẽ tạo và trả lại mã thông báo làm mới mới mọi lúc, ngay cả khi bạn có thể chỉ cần trả lại mã thông báo truy cập mới chứ không phải mã thông báo làm mới mới. Ví dụ: wen đang gọi một mã thông báo truy cập nhưng với mã thông báo làm mới chứ không phải thông tin đăng nhập (tên người dùng / mật khẩu). Có cách nào để tránh điều này?
Mattias

Bạn có thể, nhưng nó không đẹp. Khóa context.OwinContext.Environmentchứa một Microsoft.Owin.Form#collectionkhóa cung cấp cho bạn FormCollectionnơi bạn có thể tìm thấy loại tài trợ và thêm mã thông báo cho phù hợp. Nó làm rò rỉ quá trình triển khai, nó có thể bị hỏng bất kỳ lúc nào với các bản cập nhật trong tương lai và tôi không chắc liệu nó có di động giữa các máy chủ OWIN hay không.
hvidgaard

3
bạn có thể tránh việc ban hành một cập nhật mới thẻ mỗi lần bằng cách đọc "grant_type" giá trị từ đối tượng OwinRequest, như vậy: var form = await context.Request.ReadFormAsync(); var grantType = form.GetValue("grant_type"); sau đó phát hành làm mới thẻ nếu loại tài trợ không phải là "refresh_token"
Duy

1
@mattias Bạn vẫn muốn trả lại mã thông báo làm mới mới trong trường hợp đó. Nếu không, khách hàng sẽ bị chao đảo sau khi làm mới lần đầu tiên, vì mã thông báo truy cập thứ hai hết hạn và họ không có cách nào để làm mới mà không cần nhắc lại thông tin đăng nhập.
Eric Eskildsen

9

Tôi không nghĩ rằng bạn nên sử dụng một mảng để duy trì mã thông báo. Bạn cũng không cần một hướng dẫn làm mã thông báo.

Bạn có thể dễ dàng sử dụng context.SerializeTicket ().

Xem mã dưới đây của tôi.

public class RefreshTokenProvider : IAuthenticationTokenProvider
{
    public async Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        Create(context);
    }

    public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    {
        Receive(context);
    }

    public void Create(AuthenticationTokenCreateContext context)
    {
        object inputs;
        context.OwinContext.Environment.TryGetValue("Microsoft.Owin.Form#collection", out inputs);

        var grantType = ((FormCollection)inputs)?.GetValues("grant_type");

        var grant = grantType.FirstOrDefault();

        if (grant == null || grant.Equals("refresh_token")) return;

        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);

        context.SetToken(context.SerializeTicket());
    }

    public void Receive(AuthenticationTokenReceiveContext context)
    {
        context.DeserializeTicket(context.Token);

        if (context.Ticket == null)
        {
            context.Response.StatusCode = 400;
            context.Response.ContentType = "application/json";
            context.Response.ReasonPhrase = "invalid token";
            return;
        }

        if (context.Ticket.Properties.ExpiresUtc <= DateTime.UtcNow)
        {
            context.Response.StatusCode = 401;
            context.Response.ContentType = "application/json";
            context.Response.ReasonPhrase = "unauthorized";
            return;
        }

        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);
        context.SetTicket(context.Ticket);
    }
}

2

Câu trả lời của Freddy đã giúp tôi rất nhiều để làm được điều này. Vì lợi ích của sự hoàn chỉnh, đây là cách bạn có thể triển khai băm mã thông báo:

private string ComputeHash(Guid input)
{
    byte[] source = input.ToByteArray();

    var encoder = new SHA256Managed();
    byte[] encoded = encoder.ComputeHash(source);

    return Convert.ToBase64String(encoded);
}

Trong CreateAsync:

var guid = Guid.NewGuid();
...
_refreshTokens.TryAdd(ComputeHash(guid), refreshTokenTicket);
context.SetToken(guid.ToString());

ReceiveAsync:

public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
    Guid token;

    if (Guid.TryParse(context.Token, out token))
    {
        AuthenticationTicket ticket;

        if (_refreshTokens.TryRemove(ComputeHash(token), out ticket))
        {
            context.SetTicket(ticket);
        }
    }
}

Làm thế nào để băm giúp ích trong trường hợp này?
Ajaxe

3
@Ajaxe: Giải pháp gốc đã lưu trữ Hướng dẫn. Với băm, chúng tôi không giữ mã thông báo văn bản thuần túy mà giữ mã băm của nó. Ví dụ: nếu bạn lưu trữ mã thông báo trong cơ sở dữ liệu, thì tốt hơn nên lưu trữ mã băm. Nếu cơ sở dữ liệu bị xâm phạm, các mã thông báo sẽ không thể sử dụng được miễn là chúng được mã hóa.
Knelis

Không chỉ để bảo vệ khỏi các mối đe dọa từ bên ngoài mà còn để ngăn chặn nhân viên (những người có quyền truy cập vào cơ sở dữ liệu) đánh cắp mã thông báo.
lonix
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.