Access token 可以以两种形式存在:self-contained 和 reference 。
Self-contained token 使用的是一个受保护,有时间限制的数据结构,其中包含了元数据 (metadata) 以及用于在线上传递用户或者客户端身份的 claim 。常用的格式就是 JSON Web Token (JWT) 。Self-contained token 的接收者可以在本地通过检查它的签名,issuer name ,audience 或者 scope 实现对令牌的验证。
Reference token (有时也称为 opaque token)恰恰相反,包含的仅是存储在 token service 中 token 的标识符。Token service 将 token 的内容存储在某些数据仓储中,并使用一个不可猜测的 id 与之关联,再将这个 id 传递给客户端。接收者需要开辟一个连接到 token service 的反向通道 (back-channel),然后将 token 发送给一个 validation endpoint ,如果是合法的,则将检索到的内容作为响应。
Reference token 有一个不错的特性,就是你对它的生命周期有较好的控制。Self-contained token 在它们过期之前你是很难撤销它们的,而 reference token 只要存在于 STS (Security Token Service 安全令牌服务)数据仓储中就是有效的,反之也就是无效。那么将它使用在以下的场景就非常不错:
- 在“紧急”情况下撤销 token (比如,丢失手机,遭受钓鱼攻击,等等)
- 在用户登出或者应用卸载的时候使令牌失效
Reference token 的缺点就是需要在资源服务器和 STS 之间建立一个反向通讯通道。
从网络的角度来看这可能是行不通的,而且有些人也会对额外的往返以及施加在 STS 上的负载有所顾虑。最后的两个问题其实可以很简单地通过缓存解决掉。
近几年我向许多客户展示这样的一个概念以及更加倾向于以下的一种架构:
如果 token 离开了公司的基础结构(比如,浏览器或者移动设备),就用 reference token ,因为你对它的生命周期可以有完整的控制。如果 token 只在内部使用,self-contained token 就很好了。
在 这个视频 36 分钟起的时候我也提到过 reference token (演示)。
IdentityServer3 自一开始就对 reference token 提供了支持。对于每个客户端,你可以将 access token 的类型设置为 JWT 或者 Reference , ITokenHandleStore 接口会关注 reference token 的持久化和撤销。
对于 reference token 的验证,我们提供了一个简单的 access token validation endpoint 。比如,这个 endpoint 可以供我们的 access token 验证中间件使用,它非常聪明,能够识别是 self-contained token 还是 reference token ,并且既可以在本地验证也可以使用 endpoint 来验证。所有的一切对于 API 来说都是透明的。
你只需要简单的指定 Authority (IdentityServer 的基 URL),中间件就会使用它来拉取配置(密钥,issuer name ,等等)并构造验证 endpoint 的 URL :
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44333/core",
RequiredScopes = new[] { "api1" }
});
这个中间件同样支持缓存和 scope 验证——访问 这里 可以查阅相关文档。
可以使用多种方式撤销 token ——比如,通过应用权限自服务 (self-service) 页面,token revocation endpoint ,结合 ITokenHandle 仓储接口来编写代码(比如,在登出的时候从你的用户服务中清除 token)或者从你的数据仓储中简单地删除 token 。
我们的验证 endpoint 不支持认证——只要你不考虑机密性而使用 reference token 都是没有问题的。
Token Introspection
许多 token 服务都有 reference token 这个特性,就像我们一样,他们都引入了各自的验证 endpoint 。几周之前 RFC 7662 ——“OAuth 2.0 Token Introspection”,已经定义并发布了标准的协议。
IdentityServer3 从 v2.3 开始也为此提供了 支持 。
最重要的不同就是现在访问 introspection endpoint 需要认证。由于这个 endpoint 对于客户端是不可访问的,但是对于资源服务器是可以,我们就将凭据(也称为 secret)放到 scope 定义中,比如:
var api1Scope = new Scope
{
Name = "api1",
Type = ScopeType.Resource,
ScopeSecrets = new List<Secret>
{
new Secret("secret".Sha256())
}
};
对于 secret 的解析和验证我们使用与客户端 secret 同样的扩展机制。这就意味着你可以使用共享的 secret ,客户端证书或者其它自定义的东西。
这也就意味着只有包含在 access token 中的 scope 可以自省令牌。对于其它的 scope ,token 将会简单地认为是 非法 的。
IdentityModel 针对 token introspection endpoint 提供了一个客户端类库:
var client = new IntrospectionClient(
"https://localhost:44333/core/connect/introspect",
"api1",
"secret");
var request = new IntrospectionRequest
{
Token = accessToken
};
var result = client.SendAsync(request).Result;
if (result.IsError)
{
Console.WriteLine(result.Error);
}
else
{
if (result.IsActive)
{
result.Claims.ToList().ForEach(c => Console.WriteLine("{0}: {1}",
c.Item1, c.Item2));
}
else
{
Console.WriteLine("token is not active");
}
}
这个客户端也用在验证中间件中。一旦我们看到额外的 secret 配置,中间件将会从旧的验证 endpoint 切换到新的 introspection endpoint :
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44333/core",
RequiredScopes = new[] { "api1" },
ClientId = "api1",
ClientSecret = "secret"
});
一旦切换到 introspection,你就可以在 IdentityServerOptions 中禁用旧的验证 endpoint :
var idsrvOptions = new IdentityServerOptions
{
Factory = factory,
SigningCertificate = Cert.Load(),
Endpoints = new EndpointOptions
{
EnableAccessTokenValidationEndpoint = false
}
};
Reference token 在许多情况下都是问题的真正解决者,并且 introspection 规范的引入以及认证的加入使得这个机制更加健壮,并且为将来 access token 的生命周期管理提供了良好的基础。