从安全角度来看,自包含刷新令牌很容易受到攻击。
我们有刷新令牌的原因是为了延长访问令牌的有效期。换句话说,我们访问了一个授权服务器(或者有一个完全不同的授权流程),使用一些公钥验证它并从服务中获取访问令牌(使用私钥创建)。
刷新令牌必须存储在服务器端。我们不应该利用 JWT 的“自包含”属性来获取刷新令牌。这样做让我们除了更改我们的私钥之外,无法撤销刷新令牌。
示例:假设我从我的手机登录到您的应用程序并且它丢失了。
该应用程序包含刷新令牌权限,其中包含我的所有信息。如果 refresh_token 没有存储在后端,我们没有任何方法可以使该会话无效。现在需要更改创建令牌的私钥,这不好。
从 refresh_token 创建 access_token 的高级步骤可能是这样的:
让我们先打好基础:
数据库中的tokens表可以包含以下字段:
user_id<uuid>: id of the user
access_token<text>: access token
access_token_expiry<date>: access token expiring timestamp
refresh_token<text>: refresh token (sha)
refresh_token_expiry<date>: refresh token expiring timestamp
refresh_count: number of times the refresh token has been used to access access_token (just in case if you need this field)
请注意,我们应该将刷新令牌的哈希值存储在数据库中。
此外,一个用户可能有多个会话,比如他们从同一设备上的不同浏览器/不同设备登录到我们的应用程序。为了满足这个需求,我们需要创建一个 login_sessions 表
id<BIGINT>: primary key for the table
user_id<uuid>: id of the user who logged in
map_id<uuid>: This will be the key which maps one access token to its refresh token counterpart. Both tokens of the pair will contain this id in their body.
status<String>: could be ACTIVE|INACTIVE(logged out, INACTIVE user will not be allowed to get access_token from the refresh_token)
created_on<Date>: timestamp for record created on
modified_on<Date>: timestamp for record modified on
refresh_token 正文:
{
"iss": "ABC Service", (The Issuer of the token)
"sub": "4953fag3-ec5e-4ed3-b09e-d847f3f376c6", (The user_id, subject of the token)
"aud": "UI", (audience who will use the token)
"typ": "refresh", (type)
"iat": 1599326501,
"exp": 1601918501,
"jti": "749c77e5-bac0-43f1-aeea-1618ada0224f", (unique identifier for this token)
"mid": "e392692b-6d77-49a9-9928-ac3c3d5208a3" (unique identifier for access-refresh token pair - refers to map_id in login_sessions table)
}
access_token 正文:
{
"iss": "ABC Service", (The Issuer of the token)
"sub": "4953fag3-ec5e-4ed3-b09e-d847f3f376c6", (The user_id, subject of the token)
"aud": "UI", (audience who will use the token)
"typ": "access", (type)
"iat": 1599326501,
"exp": 1599412901,
"jti": "3d2985ef-e767-495e-af88-448fc0ecb167", (unique identifier for this token)
"mid": "e392692b-6d77-49a9-9928-ac3c3d5208a3", (unique identifier for access-refresh token pair - refers to map_id in login_sessions table)
}
"fname": "john",
"lname": "doe",
"roles": [99b18377-5b4c-4e68-8ff4-bac4aea93bd2]
"other_key": "other_value"
从刷新令牌生成访问令牌:
一旦访问令牌过期,我们点击授权服务器 api 来获取访问令牌,同时在正文中传递 refresh_token (jwt)。
授权服务器将公开此 API,该 API 将接受刷新令牌并执行以下步骤,然后返回访问令牌。
步骤:
1. Verify the refresh_token with public key (whose counterpart private formed the token initially)
2. Pick the mid(map_id) from the token body
3. Get login session record containing this mid from the login_sessions table. Go to next step if there exists some login_session record.
4. If the status of the login_session record is ACTIVE, create one access_token (using the private key) with relevant details in it's body
5. Send back the access token as a response.
如果上述步骤 1-4 中的任何一个失败,我们会发回带有相关消息的错误响应。