JWT(JSON Web Token)自动延长到期时间

时间:2014-11-04 15:41:23

标签: node.js api security authentication jwt

我想为我们的新REST API实现基于JWT的身份验证。但是由于在令牌中设置了到期,是否可以自动延长它?我不希望用户在每X分钟后需要登录,如果他们在那段时间内积极使用该应用程序。这将是一个巨大的用户体验失败。

但是延长到期时间会创建一个新令牌(旧令牌在到期之前仍然有效)。每次请求后生成一个新令牌听起来很傻。当多个令牌同时有效时,听起来像是一个安全问题。当然,我可以使用黑名单使旧的旧的无效,但我需要存储令牌。 JWT的一个好处就是没有存储空间。

我发现Auth0如何解决它。它们不仅使用JWT令牌,还使用刷新令牌: https://docs.auth0.com/refresh-token

但是,为了实现这一点(没有Auth0),我需要存储刷新令牌并保持其过期。那么真正的好处是什么?为什么不只有一个令牌(不是JWT)并在服务器上保持过期?

还有其他选择吗?使用JWT不适合这种情况吗?

13 个答案:

答案 0 :(得分:513)

我在Auth0工作,我参与了刷新令牌功能的设计。

这完全取决于应用程序的类型,这是我们推荐的方法。

Web应用程序

一个好的模式是在令牌到期之前刷新它。

将令牌过期时间设置为一周,并在每次用户打开Web应用程序时每隔一小时刷新一次令牌。如果用户未打开应用程序超过一周,则必须再次登录,这是可接受的Web应用程序UX。

要刷新令牌,您的API需要一个新端点,该端点接收有效的,未过期的JWT,并返回带有新过期字段的相同签名JWT。然后,Web应用程序会将令牌存储在某处。

移动/原生应用

大多数本机应用程序只登录一次。

这个想法是刷新令牌永不过期,并且它总是可以为有效的JWT进行交换。

永不过期的令牌问题是从不意味着永远不会。如果丢失手机怎么办?因此,它需要以某种方式由用户识别,并且应用程序需要提供撤销访问的方法。我们决定使用设备的名称,例如“maryo的iPad”。然后,用户可以转到该应用程序并撤消对“maryo的iPad”的访问权。

另一种方法是撤消特定事件的刷新令牌。一个有趣的事件是更改密码。

我们认为JWT对这些用例没用,所以我们使用随机生成的字符串,然后将它存储在我们这边。

答案 1 :(得分:61)

如果您自己处理身份验证(即不使用像Auth0这样的提供程序),可能会有以下情况:

  1. 发布JWT令牌,到期时间相对较短,比如15分钟。
  2. 应用程序在任何需要令牌的交易(令牌包含到期日期)之前检查令牌到期日期。如果令牌已过期,则首先要求API“刷新”#39;令牌(这对UX透明地完成)。
  3. API获取令牌刷新请求,但首先检查用户数据库以查看' reauth'已针对该用户配置文件设置了标志(令牌可以包含用户标识)。如果该标志存在,则拒绝令牌刷新,否则发出新令牌。
  4. 重复。
  5. ' reauth'例如,当用户重置密码时,将设置数据库后端中的标志。当用户下次登录时,该标志将被删除。

    此外,我们假设您有一项政策,即用户必须每72小时至少登录一次。在这种情况下,您的API令牌刷新逻辑还将检查用户从用户数据库的最后登录日期,并在此基础上拒绝/允许令牌刷新。

答案 2 :(得分:12)

在将应用程序移动到HTML5并在后端使用RESTful apis时,我正在修补。我想出的解决方案是:

  1. 成功登录后,会向客户发出一个会话时间为30分钟(或通常的服务器端会话时间)的令牌。
  2. 创建客户端计时器以调用服务以在令牌到期之前续订令牌。新令牌将取代现有的未来电话。
  3. 如您所见,这减少了频繁的刷新令牌请求。如果用户在触发续订令牌呼叫之前关闭浏览器/应用程序,则先前的令牌将及时到期,用户必须重新登录。

    可以实现更复杂的策略以满足用户不活动(例如忽略打开的浏览器选项卡)。在这种情况下,续订令牌调用应包括预期的到期时间,该时间不应超过定义的会话时间。应用程序必须相应地跟踪最后一次用户交互。

    我不喜欢设置长期到期的想法,因此这种方法可能不适用于需要较少频繁验证的本机应用程序。

答案 3 :(得分:12)

在后端没有任何额外安全存储的情况下,使JWT无效的替代解决方案是在users表上实现新的jwt_version整数列。如果用户希望注销或过期现有令牌,他们只需递增jwt_version字段。

生成新的JWT时,将jwt_version编码为JWT有效负载,如果新JWT应替换所有其他JWT,则可以选择性地增加该值。

在验证JWT时,jwt_version字段与user_id进行比较,只有在匹配时才授予授权。

答案 4 :(得分:8)

好问题 - 问题本身就有丰富的信息。

文章Refresh Tokens: When to Use Them and How They Interact with JWTs为这种情况提供了一个好主意。有些观点是: -

  • 刷新令牌包含获取新访问权限所需的信息 令牌。
  • 刷新令牌也可以过期,但相当长寿。
  • 刷新令牌通常受到严格的存储要求 确保它们不会泄露。
  • 他们也可以被授权服务器列入黑名单。

另请查看auth0/angular-jwt angularjs

适用于Web API。阅读Enable OAuth Refresh Tokens in AngularJS App using ASP .NET Web API 2, and Owin

答案 5 :(得分:6)

jwt-autorefresh

如果您使用的是节点(React / Redux / Universal JS),则可以安装npm i -S jwt-autorefresh

此库根据用户计算的访问令牌到期之前的秒数(基于令牌中编码的exp声明)计划刷新JWT令牌。它有一个广泛的测试套件,可以检查很多条件,以确保任何奇怪的活动都伴随着有关环境配置错误的描述性消息。

完整示例实施

import autorefresh from 'jwt-autorefresh'

/** Events in your app that are triggered when your user becomes authorized or deauthorized. */
import { onAuthorize, onDeauthorize } from './events'

/** Your refresh token mechanism, returning a promise that resolves to the new access tokenFunction (library does not care about your method of persisting tokens) */
const refresh = () => {
  const init =  { method: 'POST'
                , headers: { 'Content-Type': `application/x-www-form-urlencoded` }
                , body: `refresh_token=${localStorage.refresh_token}&grant_type=refresh_token`
                }
  return fetch('/oauth/token', init)
    .then(res => res.json())
    .then(({ token_type, access_token, expires_in, refresh_token }) => {
      localStorage.access_token = access_token
      localStorage.refresh_token = refresh_token
      return access_token
    })
}

/** You supply a leadSeconds number or function that generates a number of seconds that the refresh should occur prior to the access token expiring */
const leadSeconds = () => {
  /** Generate random additional seconds (up to 30 in this case) to append to the lead time to ensure multiple clients dont schedule simultaneous refresh */
  const jitter = Math.floor(Math.random() * 30)

  /** Schedule autorefresh to occur 60 to 90 seconds prior to token expiration */
  return 60 + jitter
}

let start = autorefresh({ refresh, leadSeconds })
let cancel = () => {}
onAuthorize(access_token => {
  cancel()
  cancel = start(access_token)
})

onDeauthorize(() => cancel())

免责声明:我是维护者

答案 6 :(得分:6)

我实际上是在PHP中使用Guzzle客户端为api创建了一个客户端库,但这个概念应该适用于其他平台。

基本上,我会发行两个令牌,一个短的(5分钟)和一个长的,一周后到期。如果客户端库收到对某个请求的401响应,则使用中间件尝试刷新短令牌。然后它将再次尝试原始请求,如果能够刷新,则对用户透明地获得正确的响应。如果失败,它只会将401发送给用户。

如果短令牌已过期但仍然可信并且长令牌有效且可信,则它将使用长令牌进行身份验证的服务上的特殊端点刷新短令牌(这是唯一可以使用的对于)。然后它将使用短令牌获取一个新的长令牌,从而每次刷新短令牌时将其延长一周。

这种方法还允许我们在最多5分钟内撤销访问权限,这对我们的使用是可以接受的,而无需存储令牌黑名单。

延迟编辑:重新阅读这几个月后,我应该指出你可以在刷新短令牌时撤销访问权限,因为它提供了更昂贵的调用的机会(例如,调用数据库看到如果用户已被禁止,而不是在每次拨打您的服务时付费。

答案 7 :(得分:5)

今天,许多人选择与JWT进行会话管理,却没有意识到出于感知简单性而放弃了什么。我的回答详细说明了问题的第二部分:

那么真正的好处是什么?为什么不只有一个令牌(而不是JWT)并在服务器上保留到期时间?

还有其他选择吗?使用JWT是否不适用于这种情况?

JWT能够支持基本的会话管理,但有一些限制。作为自描述令牌,它们在服务器端不需要任何状态。这使它们具有吸引力。例如,如果该服务没有持久层,则不需要仅将其引入会话管理。

但是,无国籍也是其缺点的主要原因。由于它们仅以固定的内容和到期时间发布一次,因此您无法使用典型的会话管理设置来完成您想做的事情。

也就是说,您不能按需使它们失效。这意味着您无法实施安全登出,因为无法使已发布的令牌失效。同样,您无法实现空闲超时。一种解决方案是保留黑名单,但这会引入状态。

我更详细地写了post explaining these drawbacks。需要明确的是,您可以通过增加更多的复杂性(滑动会话,刷新令牌等)来解决这些问题

对于其他选项,如果您的客户仅通过浏览器与您的服务进行交互,我强烈建议您使用基于cookie的会话管理解决方案。我还compiled a list authentication methods目前在网络上广泛使用。

答案 8 :(得分:2)

这种方法怎么样:

  • 对于每个客户端请求,服务器将令牌的expirationTime与(currentTime - lastAccessTime)
  • 进行比较
  • 如果 expirationTime< (currentTime - lastAccessedTime),它将最后一个lastccessedTime更改为currentTime。
  • 如果浏览器上的活动时间超过expirationTime,或者浏览器窗口关闭且 expirationTime> (currentTime - lastAccessedTime),然后服务器可以使令牌过期并要求用户再次登录。

在这种情况下,我们不需要额外的终点来刷新令牌。 非常感谢任何背包。

答案 9 :(得分:2)

我通过在令牌数据中添加变量解决了这个问题:

softexp - I set this to 5 mins (300 seconds)

我将expiresIn选项设置为我想要的时间,然后再强制用户重新登录。我的设定为30分钟。这必须大于softexp的值。

当我的客户端应用程序向服务器API(需要令牌,例如客户列表页面)发送请求时,服务器会根据其原始到期时间检查提交的令牌是否仍然有效(expiresIn )价值。如果它无效,服务器将以特定于此错误的状态进行响应,例如。 INVALID_TOKEN

如果令牌仍然基于expiredIn值有效,但已超过softexp值,则服务器将以此错误的单独状态作出响应,例如。 EXPIRED_TOKEN

(Math.floor(Date.now() / 1000) > decoded.softexp)

在客户端,如果收到EXPIRED_TOKEN响应,它应该通过向服务器发送续订请求来自动续订令牌。这对用户是透明的,并自动处理客户端应用程序。

服务器中的续订方法必须检查令牌是否仍然有效:

jwt.verify(token, secret, (err, decoded) => {})

如果上述方法失败,服务器将拒绝续订令牌。

答案 10 :(得分:1)

以下是撤销JWT访问令牌的步骤:

1)登录后,发送2个令牌(访问令牌,刷新令牌)以响应客户端。
2)访问令牌的有效期将更少,而刷新将具有较长的到期时间。
3)客户端(前端)将刷新令牌存储在其本地存储中,并将访问令牌存储在cookie中。
4)客户端将使用访问令牌来调用api。但是,当它过期时,请从本地存储中选择刷新令牌,然后调用auth服务器api以获取新令牌。
5)您的身份验证服务器将公开一个api,它将接受刷新令牌并检查其有效性并返回新的访问令牌。
6)刷新令牌过期后,用户将注销。

如果需要更多详细信息,请告诉我,我也可以共享代码(Java + Spring引导)。

答案 11 :(得分:1)

参考-Refresh Expired JWT Example

另一种选择是,一旦JWT过期,用户/系统将调用 另一个网址应该是/ refreshtoken。与此请求一起,还应传递过期的JWT。然后,服务器将返回一个可由用户/系统使用的新JWT。

enter image description here

答案 12 :(得分:-1)

services.Configure(Configuration.GetSection("ApplicationSettings"));

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 

        services.AddDbContext<AuthenticationContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));

        services.AddDefaultIdentity<ApplicationUser>()
            .AddEntityFrameworkStores<AuthenticationContext>();

        services.Configure<IdentityOptions>(options =>
        {
            options.Password.RequireDigit = false;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireLowercase = false;
            options.Password.RequireUppercase = false;
            options.Password.RequiredLength = 4;
        }
        );

        services.AddCors();

        //Jwt Authentication

        var key = Encoding.UTF8.GetBytes(Configuration["ApplicationSettings:JWT_Secret"].ToString());

        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(x=> {
            x.RequireHttpsMetadata = false;
            x.SaveToken = false;
            x.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                ClockSkew = TimeSpan.Zero
            };
        });
    }