天天看點

.Net Identity OAuth 2.0 SecurityStamp 使用

起源:

近期幫别人做項目,涉及到OAuth認證,服務端主動使token失效,要使對應使用者不能再繼續通路,隻能重新登陸,或者重新授權。

場景:

這種場景OAuth2.0是支援的,比如使用者修改了密碼,那所有之前保留在各個用戶端的token均失效,要求使用者重新提供憑證。

原因:

在之前的項目中,一旦授權服務AuthorizationServer發放了token,所有的資源服務隻會通過統一的加密解密算法,解析出token中的資訊包括使用者身份等,就直接使用了。這樣因為不和資料庫或外部存儲媒體互動,是以效率會比較高。以下是在所有資源服務、包括認證服務Web.config中的配置,machineKey必須相同。

<system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
    <machineKey decryptionKey="B7E121C5839A624E" validation="SHA1" validationKey="C2B8DF31AB96asd428066DFDA1A479542825F3B48865C4E47AF6A026F2" />
  </system.web>      

這樣效率是提高了,但是每次驗證不與後端(尤指存儲)互動,是以token一旦發出,除非用戶端主動清除或着超出有效時間,否則都有效,有一定的失竊風險。

分析:

.Net在IdentityServer3.0在使用者表中使用SecurityStamp字段,作為安全辨別,去識别是否失效。預設情況是沒有開啟驗證的,否則就像之前說的每次都會和資料庫互動,降低性能。關于SecurityStamp的說明,大家可以看一下這篇文章:

What is ASP.NET Identity's IUserSecurityStampStore<TUser> interface?

    This is meant to represent the current snapshot of your user's credentials. So if nothing changes, the stamp will stay the same. But if the user's password is changed, or a login is removed (unlink your google/fb account), the stamp will change. This is needed for things like automatically signing users/rejecting old cookies when this occurs, which is a feature that's coming in 2.0. 

    Edit: Updated for 2.0.0. So the primary purpose of the 

SecurityStamp

 is to enable sign out everywhere. The basic idea is that whenever something security related is changed on the user, like a password, it is a good idea to automatically invalidate any existing sign in cookies, so if your password/account was previously compromised, the attacker no longer has access. 

    In 2.0.0 we added the following configuration to hook the 

OnValidateIdentity

 method in the 

CookieMiddleware

 to look at the 

SecurityStamp

 and reject cookies when it has changed. It also automatically refreshes the user's claims from the database every 

refreshInterval

 if the stamp is unchanged (which takes care of things like changing roles etc)

文章中給出的代碼如下:

app.UseCookieAuthentication(new CookieAuthenticationOptions {
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider {
        // Enables the application to validate the security stamp when the user logs in.
        // This is a security feature which is used when you change a password or add an external login to your account.  
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(30),
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    }
});      

雖然這篇文章大緻講明白了,這是個什麼東西和怎麼用,但是仔細想想,還是有問題,因為我們是使用Bearer方式,這裡隻是Cookie,是以還差點什麼。在資源服務中的配置如下:

namespace ResourceServer
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions());
        }
    }
}      

是以我們應該更改的是OAuthBearerAuthenticationOptions這個對象。有找到文章參考如下:

Using bearer tokens (ASP.NET Identity 2.0) with WCF Data Services

總結:

在資源服務中,引入Microsoft.AspNet.Identity.Owin包,在Startup中,更改如下代碼:

public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
            {
                Provider = new OAuthBearerAuthenticationProvider()
                {
                    OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<UserManager, User>(
                        validateInterval: TimeSpan.FromMinutes(30),
                        regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
                }
            });
        }
    }      

 檢視SecurityStampValidator類的源碼:

public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser, TKey>(
            TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentityCallback,
            Func<ClaimsIdentity, TKey> getUserIdCallback)
            where TManager : UserManager<TUser, TKey>
            where TUser : class, IUser<TKey>
            where TKey : IEquatable<TKey>
        {
            .....
               var user = await manager.FindByIdAsync(userId).WithCurrentCulture();
                        var reject = true;
                        // Refresh the identity if the stamp matches, otherwise reject
                        if (user != null && manager.SupportsUserSecurityStamp)
                        {
                            var securityStamp =
                                context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType);
                            if (securityStamp == await manager.GetSecurityStampAsync(userId).WithCurrentCulture())
                            {
                                reject = false;
                                // Regenerate fresh claims if possible and resign in
                                if (regenerateIdentityCallback != null)
                                {
                                    var identity = await regenerateIdentityCallback.Invoke(manager, user).WithCurrentCulture();
                                    if (identity != null)
                                    {
                                        // Fix for regression where this value is not updated
                                        // Setting it to null so that it is refreshed by the cookie middleware
                                        context.Properties.IssuedUtc = null;
                                        context.Properties.ExpiresUtc = null;
                                        context.OwinContext.Authentication.SignIn(context.Properties, identity);
                                    }
                                }
                            }
                        }
            .....
            };
        }      

它會自己判斷token中解析出來的securityStamp是否與資料庫之前儲存的一緻,至此,完全解決。

問題:

  • 我們之前說過的,每次請求認證都會請求資料庫,降低性能。
  • 資源服務不能獨立,必須與使用者庫綁定。