如何使用外部登录提供程序创建刷新令牌?

Posted

技术标签:

【中文标题】如何使用外部登录提供程序创建刷新令牌?【英文标题】:How to create Refresh Token with External Login Provider? 【发布时间】:2014-11-02 14:02:09 【问题描述】:

我在网上搜索过,但找不到解决问题的方法。我正在我的应用程序中实现 OAuth。我正在使用 ASP .NET Web API 2 和 Owin。场景是这样的,一旦用户请求令牌端点,他或她将收到一个访问令牌以及一个刷新令牌以生成新的访问令牌。我有一个类可以帮助我生成刷新令牌。就是这样:

   public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
    


       private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();



    public async Task CreateAsync(AuthenticationTokenCreateContext context)
        

            var refreshTokenId = Guid.NewGuid().ToString("n");
            using (AuthRepository _repo = new AuthRepository())
            
                var refreshTokenLifeTime = context.OwinContext.Get<string>                                    ("as:clientRefreshTokenLifeTime");
                var token = new RefreshToken() 
                 
                    Id = Helper.GetHash(refreshTokenId),
                    ClientId = clientid, 
                    Subject = context.Ticket.Identity.Name,
                    IssuedUtc = DateTime.UtcNow,
                    ExpiresUtc = DateTime.UtcNow.AddMinutes(15)
                ;
                context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
                context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;
                token.ProtectedTicket = context.SerializeTicket();
                var result = await _repo.AddRefreshToken(token);
                if (result)
                        
                    context.SetToken(refreshTokenId);
                
            
        

        // this method will be used to generate Access Token using the Refresh Token
        public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        

            string hashedTokenId = Helper.GetHash(context.Token);
            using (AuthRepository _repo = new AuthRepository())
            
                var refreshToken = await _repo.FindRefreshToken(hashedTokenId);
                if (refreshToken != null )
                
                    //Get protectedTicket from refreshToken class
                    context.DeserializeTicket(refreshToken.ProtectedTicket);
                    // one refresh token per user and client
                    var result = await _repo.RemoveRefreshToken(hashedTokenId);
                
            
        

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

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

现在我允许我的用户通过 facebook 注册。一旦用户在 facebook 上注册,我会生成一个访问令牌并将其提供给他。我也应该生成一个刷新令牌吗?我想到了一件事,就是像一天一样生成一个长访问令牌,然后这个用户必须再次使用 facebook 登录。但是如果我不想这样做,我可以给客户端一个刷新令牌,他可以使用它来刷新生成的访问令牌并获得一个新的。当有人使用 facebook 或外部注册或登录时,如何创建刷新令牌并将其附加到响应中?

这是我的外部注册 API

  public class AccountController : ApiController
    
      [AllowAnonymous]
      [Route("RegisterExternal")]
      public async Task<IHttpActionResult> RegisterExternal(RegisterExternalBindingModel model)
      

         if (!ModelState.IsValid)
         
            return BadRequest(ModelState);
         
         var accessTokenResponse = GenerateLocalAccessTokenResponse(model.UserName);
         return Ok(accessTokenResponse);
      


    

// 生成访问令牌的私有方法

private JObject GenerateLocalAccessTokenResponse(string userName)
        

            var tokenExpiration = TimeSpan.FromDays(1);
            ClaimsIdentity identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);
            identity.AddClaim(new Claim(ClaimTypes.Name, userName));
            identity.AddClaim(new Claim("role", "user"));
            var props = new AuthenticationProperties()
            
                IssuedUtc = DateTime.UtcNow,
                ExpiresUtc = DateTime.UtcNow.Add(tokenExpiration),
            ;
            var ticket = new AuthenticationTicket(identity, props);
            var accessToken = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket);
            JObject tokenResponse = new JObject(
                                        new JProperty("userName", userName),
                                        new JProperty("access_token", accessToken),
                                        // Here is what I need
                                        new JProperty("resfresh_token", GetRefreshToken()),
                                        new JProperty("token_type", "bearer"),
                                        new JProperty("refresh_token",refreshToken),
                                        new JProperty("expires_in", tokenExpiration.TotalSeconds.ToString()),
                                        new JProperty(".issued", ticket.Properties.IssuedUtc.ToString()),
                                        new JProperty(".expires", ticket.Properties.ExpiresUtc.ToString())
        );
            return tokenResponse;
        

【问题讨论】:

关于该主题的任何更新?我有完全相同的问题。 【参考方案1】:

我花了很多时间来寻找这个问题的答案。所以,我很乐意为您提供帮助。

1) 更改您的 ExternalLogin 方法。 它通常看起来像:

if (hasRegistered)

     Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);

     ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
                OAuthDefaults.AuthenticationType);
     ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
                CookieAuthenticationDefaults.AuthenticationType);

     AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);
     Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);

现在,实际上,需要添加 refresh_token。 方法如下所示:

if (hasRegistered)

     Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie);

     ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(UserManager,
                   OAuthDefaults.AuthenticationType);
     ClaimsIdentity cookieIdentity = await user.GenerateUserIdentityAsync(UserManager,
                    CookieAuthenticationDefaults.AuthenticationType);

     AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName);

     // ADD THIS PART
     var ticket = new AuthenticationTicket(oAuthIdentity, properties);
     var accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);

                Microsoft.Owin.Security.Infrastructure.AuthenticationTokenCreateContext context = 
                    new Microsoft.Owin.Security.Infrastructure.AuthenticationTokenCreateContext(
                        Request.GetOwinContext(), 
                        Startup.OAuthOptions.AccessTokenFormat, ticket);

     await Startup.OAuthOptions.RefreshTokenProvider.CreateAsync(context);
     properties.Dictionary.Add("refresh_token", context.Token);

     Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);

现在将生成刷新令牌。

2) 在 SimpleRefreshTokenProvider CreateAsync 方法中使用基本 context.SerializeTicket 存在问题。 来自Bit Of Technology的消息

好像在ReceiveAsync方法中,context.DeserializeTicket不是 在外部登录案例中完全返回身份验证票。 当我在调用之后查看 context.Ticket 属性时,它是空的。 与本地登录流程相比,DeserializeTicket 方法 将 context.Ticket 属性设置为 AuthenticationTicket。所以 现在的谜团是 DeserializeTicket 的行为如何不同 两个流动。创建数据库中受保护的票证字符串 在同一个 CreateAsync 方法中,不同之处仅在于我称之为 在 GenerateLocalAccessTokenResponse 中手动方法,与 Owin middlware 正常调用它...... SerializeTicket 或 DeserializeTicket 抛出错误……

因此,您需要使用 Microsoft.Owin.Security.DataHandler.Serializer.TicketSerializer 对票证进行序列化和反序列化。 它看起来像这样:

Microsoft.Owin.Security.DataHandler.Serializer.TicketSerializer serializer
                = new Microsoft.Owin.Security.DataHandler.Serializer.TicketSerializer();

token.ProtectedTicket = System.Text.Encoding.Default.GetString(serializer.Serialize(context.Ticket));

代替:

token.ProtectedTicket = context.SerializeTicket();

对于 ReceiveAsync 方法:

Microsoft.Owin.Security.DataHandler.Serializer.TicketSerializer serializer = new Microsoft.Owin.Security.DataHandler.Serializer.TicketSerializer();
context.SetTicket(serializer.Deserialize(System.Text.Encoding.Default.GetBytes(refreshToken.ProtectedTicket)));

代替:

context.DeserializeTicket(refreshToken.ProtectedTicket);

3) 现在您需要将 refresh_token 添加到 ExternalLogin 方法响应中。 覆盖 OAuthAuthorizationServerProvider 中的 AuthorizationEndpointResponse。像这样的:

public override Task AuthorizationEndpointResponse(OAuthAuthorizationEndpointResponseContext context)

     var refreshToken = context.OwinContext.Authentication.AuthenticationResponseGrant.Properties.Dictionary["refresh_token"];
     if (!string.IsNullOrEmpty(refreshToken))
     
          context.AdditionalResponseParameters.Add("refresh_token", refreshToken);
     
     return base.AuthorizationEndpointResponse(context);

所以.. 就是这样!现在,在调用 ExternalLogin 方法后,您将获得 url: https://localhost:44301/Account/ExternalLoginCallback?access_token=ACCESS_TOKEN&token_type=bearer&expires_in=300&state=STATE&refresh_token=TICKET&returnUrl=URL

希望对你有帮助)

【讨论】:

感谢您的努力。实际上,我正在将此代码用于我停止处理的项目。但是,我会尝试这段代码,一旦我再次开始处理它,我会让你知道我的反馈。但再次感谢您的努力,非常感谢您的帮助。 很高兴为您提供帮助)随时提问。 首先感谢您的解决方案。但由于某种原因,它对我不起作用。一旦我使用外部登录提供程序,我确实得到了 refresh_token,但是当我尝试使用它来获取新的访问令牌时,我得到一个 400 Bad Request 错误,正文:“error”:“invalid_grant”服务器上没有异常,我只是不知道如何调试它。 :-| (尝试了您的解决方案和@Wouter Crooy 提出的解决方案)您能否创建一个示例解决方案,或者给我任何提示? Alexander,你确定你收到的 refresh_token 是有效的吗?如果令牌无效/过期/错误序列化,则可以获得消息 "error":"invalid_grant"。尝试调试 RefreshTokenProvider 中的 ReceiveAsync/Receive 方法,确保令牌存储在数据库中并正确反序列化。 谢谢,正常登录成功实现了这个。这样做时不要忘记验证您的 client_id!【参考方案2】:

@giraffe 和其他人

几点说明。无需使用 custom 代码序列化器。

下面一行:

Microsoft.Owin.Security.Infrastructure.AuthenticationTokenCreateContext context = 
                new Microsoft.Owin.Security.Infrastructure.AuthenticationTokenCreateContext(
                    Request.GetOwinContext(), 
                    Startup.OAuthOptions.AccessTokenFormat, ticket);

使用令牌格式:Startup.OAuthOptions.AccessTokenFormat。由于我们要提供一个刷新令牌,因此需要将其更改为:Startup.OAuthOptions.RefreshTokenFormat

否则,如果您想获取新的访问令牌并刷新刷新令牌(grant_type=refresh_token&refresh_token=......),反序列化器/取消保护器将失败。因为它在解密阶段使用了错误的目的关键字。

【讨论】:

感谢您的评论!也许您是对的,但就我而言,该过程运行正常。下次我将尝试实现您的变体!【参考方案3】:

终于找到了解决我问题的方法。 首先,如果您在使用 OWIN 时遇到任何问题并且无法弄清楚出了什么问题,我建议您简单地启用符号调试并对其进行调试。可以在这里找到一个很好的解释: http://www.symbolsource.org/Public/Home/VisualStudio

我的错误只是,我在使用外部登录提供程序时计算了错误的 ExiresUtc。所以我的refreshtoken基本上总是马上过期....

如果您要实现刷新令牌,请查看这篇博客文章: http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/

要使其与外部提供商的刷新令牌一起使用,您必须在上下文中设置两个必需的参数(“as:clientAllowedOrigin”和“as:clientRefreshTokenLifeTime”) 所以而不是

var ticket = new AuthenticationTicket(oAuthIdentity, properties); var context = new Microsoft.Owin.Security.Infrastructure.AuthenticationTokenCreateContext( Request.GetOwinContext(), Startup.OAuthOptions.AccessTokenFormat,票证); 等待 Startup.OAuthOptions.RefreshTokenProvider.CreateAsync(context); properties.Dictionary.Add("refresh_token", context.Token);

需要先获取客户端并设置上下文参数

// 从数据库中检索客户端 var client = authRepository.FindClient(client_id); // 仅当客户端注册时才生成刷新令牌 如果(客户端!= null) var ticket = new AuthenticationTicket(oAuthIdentity, properties); var context = new AuthenticationTokenCreateContext(Request.GetOwinContext(), AuthConfig.OAuthOptions.RefreshTokenFormat, ticket); // 设置这两个上下文参数,否则不起作用!! context.OwinContext.Set("as:clientAllowedOrigin", client.AllowedOrigin); context.OwinContext.Set("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString()); 等待 AuthConfig.OAuthOptions.RefreshTokenProvider.CreateAsync(context); properties.Dictionary.Add("refresh_token", context.Token);

【讨论】:

以上是关于如何使用外部登录提供程序创建刷新令牌?的主要内容,如果未能解决你的问题,请参考以下文章

如何从外部应用程序安全地使用json web令牌,与wordpress rest api对话

页面刷新后如何保持用户登录

如何使用 Laravel 管理 OAuth 刷新令牌?

Android:如何通过 Google 登录 API 获取刷新令牌?

如何使用刷新令牌更新访问令牌?

如何在 React.js 应用程序中刷新 JWT 令牌?