使用 OpenID Connect 对用户进行身份验证
在本章中,您将更深入地了解 Keycloak 如何通过利用 OpenID Connect 标准对应用程序中的用户进行身份验证。通过使用为本书编写的示例应用程序,我们将看到应用程序和 Keycloak 之间的第一手交互,包括请求和响应的内容。
在本章结束时,您将对 OpenID Connect 有很好的了解,包括如何对用户进行身份验证、了解 ID 令牌以及处理用户注销。
在本章中,我们将介绍以下主要主题:
-
运行 OpenID Connect Playground
-
了解 Discovery 端点
-
验证用户身份
-
了解 ID 令牌
-
调用 UserInfo 端点
-
处理用户注销
技术要求
要运行本章中包含的示例应用程序,您需要在工作站上安装 Node. js(https://nodejs.org/)。
您还需要拥有与本书关联的 GitHub 存储库的本地克隆。GitHub 存储库可在https://github.com/PacktPublishing/Keycloak—Identity-andAccess-Management-for-Modern-Applications-2nd-Edition获得。
查看以下链接以查看 Code in Action 视频:https://packt.link/xZ3iN
运行 OpenID Connect 操作练习
OpenID Connect(OIDC)playground应用程序是专门为本书开发的,目的是让您尽可能轻松地以实用的方式理解和试验 OIDC。
playground 应用程序不使用任何 OIDC 库,而是所有 OIDC请求都是由应用程序自己制作的。这里需要注意的一点是,这个应用程序没有以安全的方式实现 OIDC,并且忽略了对生产应用程序很重要的请求中的可选参数。这有两个原因。首先,这是为了让您可以专注于理解 OIDC 的一般概念。其次,如果您决定为 OIDC 实现自己的应用程序库,您应该对规范有很好的理解,在本书的范围内详细介绍 OIDC 超出了本书的范围。
在继续阅读本章之前,您应该启动 OIDC Playground应用程序,因为它将在本章的其余部分中使用。
要运行 OIDC playground 应用程序,请打开终端并运行以下命令:
cd Keycloak---Identity-and-Access-Management-for-Modern-Applications-2nd-Edition/ch4/
$ npm install
$ npm start
要验证应用程序是否正在运行,请在浏览器中打开http://localhost:8000/。以下屏幕截图显示了 OIDC playground 应用程序页面:
图 4.1:OpenID Connect Playground应用程序
为了能够使用 Playground 应用程序,您需要运行 Keycloak、一个包含您可以登录的用户的领域以及一个具有以下配置的客户端:
-
Client ID:oidc-playground
-
Client authentication: Off
-
Authentication flow: Standard flow
-
Valid Redirect URIs:
-
Web Origins:
如果您不确定如何做到这一点,您应该参考第 1 章,Keycloak 入门,和第 2 章,保护您的第一个应用程序。
了解Discovery端点
OIDC 发现规范是 OIDC 依赖方库互操作性和可用性的一个重要方面。如果没有此规范,您将需要在应用程序中进行大量手动配置才能使用 OpenID 提供程序进行身份验证(有关 OpenID 提供程序的更多信息,请参阅第 3 章,标准简介)。
它是一个可选规范,OpenID 提供程序可以决定是否要实现。幸运的是,大多数 OpenID 提供程序,包括 Keycloak,都实现了这个规范。
通过简单地知道 OpenID 提供者的基本 URL(通常称为颁发者 URL),依赖方可以发现有关提供者的许多有用信息。它通过从标准端点(即/. well-known/openid-configuration 加载所谓的 OpenID 提供者元数据来做到这一点。
要更好地理解 OpenID 提供者元数据,请在浏览器中打开 OIDC playground。您可以看到已经为issuer输入填写了一个值。
issuer URL 为http://localhost:8080/realms/myrealm。让我们将此 URL 分解,看看issuser URL 的各个部分:
-
http://localhost:8080:这是 Keycloak 的根 URL。在生产系统中,这显然是一个真实的域名,并且将使用 HTTPS(例如,https://auth.mycompany.com/)。
-
/realms/myrealm:由于 Keycloak 支持多租户,这用于分隔 Keycloak 实例中的每个领域。
如果您的 Keycloak 在不同的主机名或端口上运行,或者有不同的领域,您应该更改颁发者字段。否则,您可以保持原样。
现在单击加载 OpenID 提供者配置。当您单击此按钮时,Playground应用程序向http://localhost:8080/realm/myrealm/.well-known/openid-configuration发送请求(假设您未触及颁发者 URL),并收到此 Keycloak 实例的 OpenID 提供者元数据形式的响应。返回的元数据显示在playground应用程序的 OpenID 提供者配置部分。
playground应用程序的以下屏幕截图显示了加载的 OpenID 提供程序元数据的示例:
图 4.2:Keycloak 的 OpenID 提供程序元数据
在以下列表中,我们将了解其中一些值的含义:
-
authorization_endpoint:用于身份验证请求的 URL
-
token_endpoint:用于令牌请求的 URL
-
introspection_endpoint:用于内省请求的 URL
-
userinfo_endpoint:用于 UserInfo 请求的 URL
-
grant_types_supported:支持的授权类型列表
-
response_types_supported:支持的响应类型列表
使用所有这些元数据,依赖方可以就如何使用 OpenID 提供程序做出明智的决策,包括向哪些端点发送请求以及可以使用哪些授权类型和响应类型。
如果您仔细查看了元数据,您可能已经注意到 Keycloak 支持 authorization_code 授权类型以及代码和令牌响应类型。这是个好消息,因为我们将在下一节中使用此授权类型和这些响应类型来验证playground应用程序中的用户。
验证用户身份
使用 Keycloak 对用户进行身份验证的最常见方法是通过 OpenID Connect authorization code flow.。
总之,要使用此流对用户进行身份验证,应用程序会重定向到 Keycloak,该 Keycloak 会显示一个登录页面来对用户进行身份验证。用户通过身份验证后,应用程序会收到一个 ID 令牌,其中包含有关用户的信息。
在下图中,更详细地显示了授权码流程:
图 4.3:授权码流程
图中的步骤更详细地解释如下:
-
用户点击应用程序中的登录按钮。
-
应用程序生成身份验证请求。
-
认证请求以 302 重定向的形式发送给用户,指示用户代理重定向到 Keycloak 提供的授权端点。
-
用户代理通过认证请求使用应用程序指定的查询参数打开授权端点。
-
Keycloak 向用户显示登录页面。用户输入他们的用户名和凭据并提交表单。
-
Keycloak 验证用户凭据后,创建授权码,返回给应用程序。
-
应用程序现在可以交换 ID 令牌的授权码,以及刷新令牌。
-
让我们回到 OIDC playground 应用程序。由于您已经在上一节中加载了 OpenID Provider 元数据,因此 playground 应用程序已经知道在哪里发送身份验证请求。要发送身份验证请求,请单击标记为 2 - Authentication。
显示的表单具有您应该填写的以下值:
-
client_id:这是使用 Keycloak 注册的应用程序的客户端 ID。如果您在创建客户端时使用了与 oidc-playground 不同的值,则应更改此值。
-
scope:默认值为 openid,这意味着我们将执行 OpenID 请求。暂时保持原样。
-
prompt:这可以用于几个不同的目的。例如,如果您在此字段中输入none,Keycloak 将不会向用户显示登录屏幕,而是仅在用户已经使用 Keycloak 登录时对用户进行身份验证。您还可以使用值 login 要求用户再次登录,即使他们已经使用 Keycloak 登录。
-
max_age:这是自用户上次使用 Keycloak 进行身份验证以来的最大秒数。例如,如果您将此字段设置为 60,则表示如果自用户上次进行身份验证以来超过 60 秒,Keycloak 将重新对用户进行身份验证。
-
login_hint:如果应用程序碰巧知道它想要验证的用户的用户名,它可以使用此参数在登录页面上自动填写用户名。
现在,我们通过单击标记为 Generate Authentication Request 的按钮来看看身份验证请求的外观。您现在将看到应用程序将 user-agent 重定向到以启动身份验证的实际请求。
来自 Playground 应用程序的以下屏幕截图显示了一个示例身份验证请求:
图 4.4:身份验证请求
这包括将 response_type 参数设置为 code,这意味着应用程序希望从 Keycloak 接收授权代码。
接下来,单击标有发送身份验证请求的按钮。您现在将被重定向到 Keycloak 登录页面。填写用户的用户名和密码,然后单击登录。
如果您想尝试一下,您可以尝试以下步骤:
-
将prompt 设置为login:使用此值,Keycloak 应始终要求您重新验证。
-
将 max_age 设置为 60:使用此值,如果您自上次身份验证以来等待至少 60 秒,Keycloak 将重新对您进行身份验证。
-
将 login_hint 设置为您的用户名:这应该在 Keycloak 登录页面中预先填写用户名。
如果您尝试上述任何步骤,请不要忘记再次生成并发送身份验证请求以查看 Keycloak 的行为方式。
Keycloak 重定向回Playground应用程序后,您将在身份验证响应部分看到身份验证响应。该代码称为授权代码,应用程序使用它来获取 ID 令牌和刷新令牌。
现在应用程序已经有了授权码,您可以继续将其兑换成一些令牌。
单击标记为 3-Token 的按钮。您将看到授权码已在表单上填写,因此您可以继续单击标记为发送令牌请求的按钮。
在令牌请求下,可以看到应用程序向 Keycloak 提供的令牌端点发送的请求,它包含授权码,并将 grant_type 设置为 authorization_code,这意味着应用程序想要为令牌交换一个授权码。
Playground 应用程序的以下屏幕截图中显示了一个示例 Token Request:
图 4.5:令牌请求
在 Token Response 下,可以看到 Keycloak 发送给应用程序的响应,如果得到值 invalid_grant 的错误,很可能是以下两个原因之一:
-
您的步骤有点太慢了:默认情况下,授权码仅在一分钟内有效,因此如果从 Keycloak 接收身份验证响应到发送令牌请求之间超过一分钟,请求将失败。
-
您多次发送令牌请求:授权码仅有效一次,因此如果它包含在多个令牌请求中,请求将失败。
以下屏幕截图显示了来自 Playground 应用程序的成功令牌响应示例:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJyWXQ0UWg2R1VqcVh6dER3QXExdUdTb3VtZ0YtMXdZVzB4UTUwam1xMm40In0.eyJleHAiOjE3NDAzNjg3MDMsImlhdCI6MTc0MDM2ODQwMywiYXV0aF90aW1lIjoxNzQwMzY4MzYxLCJqdGkiOiJmMzE4NWJkNi1mN2Q4LTQzM2EtYWMzOS00N2NmYTBmZGNlNDIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjM4MDgwL3JlYWxtcy9teXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjRiZjMyN2Y0LWQ3ZmEtNDViZS1iMmUxLTY3MjZjNmVlYzE5YyIsInR5cCI6IkJlYXJlciIsImF6cCI6Im9pZGMtcGxheWdyb3VuZCIsInNpZCI6IjM0MTY3N2VhLWUyOTUtNDA5Ni05Mjk3LWRmNzYxNGRhN2E3NyIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovL2xvY2FsaG9zdDo4MDAwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLW15cmVhbG0iLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkRhdnkgU3VuIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiZGF2eSIsImdpdmVuX25hbWUiOiJEYXZ5IiwiZmFtaWx5X25hbWUiOiJTdW4iLCJlbWFpbCI6InN1bmRhd2VpeHlAZ21haWwuY29tIn0.RQCOBmkMV59r1yCsPChmKruzf5-OGug5eaiSMmiOBPwsNvV9OabUa90xjkKiSczCplKEViZUuY2VZXoAcSdT3r2WeUks8VLBwSc6KFepEYCj8mRYohlZ2cO2wwIt-MqqS6OxnPmCNSVsIaQjFN73kiXp-isRiEgCzET6VoZIK5WUK1Vlby0mD7lTRM-P789y0LjVb8mwQLi21EbfUdven1qahOA1tuvc0FPWbrkgTFEKZSe7Gwd6OXyOCSqU2cV4R9gnS2685QCGDuYRYTiNtPs4EKktWVpOUC2N4TALEmkGfNS9oYOuOKmRg01u5gng_Xm6-S-vZjhW2nMQC2C4wg",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjYzA1NTRhYy0wZGU0LTQ1NDItODM0ZC1jY2MyYzNjYzU4ZjIifQ.eyJleHAiOjE3NDAzNzAyMDMsImlhdCI6MTc0MDM2ODQwMywianRpIjoiMGVlMjA4ZTItMjdjYy00ZmZkLWI3Y2YtZGNhZGM0YjNjZjg0IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDozODA4MC9yZWFsbXMvbXlyZWFsbSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzgwODAvcmVhbG1zL215cmVhbG0iLCJzdWIiOiI0YmYzMjdmNC1kN2ZhLTQ1YmUtYjJlMS02NzI2YzZlZWMxOWMiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoib2lkYy1wbGF5Z3JvdW5kIiwic2lkIjoiMzQxNjc3ZWEtZTI5NS00MDk2LTkyOTctZGY3NjE0ZGE3YTc3Iiwic2NvcGUiOiJvcGVuaWQgd2ViLW9yaWdpbnMgYmFzaWMgcm9sZXMgcHJvZmlsZSBlbWFpbCBhY3IifQ.Au7yQlWVfvSxa_D-ZG4zs6eiSAli-Kt_XrYJc1vBMjxM4YCZtuPIa48z3XlQ8U0ecmPh8oCATmEy2JpRu2I2fw",
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJyWXQ0UWg2R1VqcVh6dER3QXExdUdTb3VtZ0YtMXdZVzB4UTUwam1xMm40In0.eyJleHAiOjE3NDAzNjg3MDMsImlhdCI6MTc0MDM2ODQwMywiYXV0aF90aW1lIjoxNzQwMzY4MzYxLCJqdGkiOiIzYjQ0YTY2Mi1jZjZlLTRiN2UtYWNkYS1iMTk4MDU1N2U0ZTkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjM4MDgwL3JlYWxtcy9teXJlYWxtIiwiYXVkIjoib2lkYy1wbGF5Z3JvdW5kIiwic3ViIjoiNGJmMzI3ZjQtZDdmYS00NWJlLWIyZTEtNjcyNmM2ZWVjMTljIiwidHlwIjoiSUQiLCJhenAiOiJvaWRjLXBsYXlncm91bmQiLCJzaWQiOiIzNDE2NzdlYS1lMjk1LTQwOTYtOTI5Ny1kZjc2MTRkYTdhNzciLCJhdF9oYXNoIjoiOWFCWW80QndjUnJ0WjZrZGdyaGFmZyIsImFjciI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJEYXZ5IFN1biIsInByZWZlcnJlZF91c2VybmFtZSI6ImRhdnkiLCJnaXZlbl9uYW1lIjoiRGF2eSIsImZhbWlseV9uYW1lIjoiU3VuIiwiZW1haWwiOiJzdW5kYXdlaXh5QGdtYWlsLmNvbSJ9.OpNV5gA1GRe-xfkShgGf-p-qxJMsFI7yCKyGSXrwBZj8dvqRFLuk4XeD5DgsvxhaBPttceuDqlpEx1xM8zoYY0p6ue_MC0hztvDg2wpwdhdDrEykUEQr8os7svXGUOWRGnJZXMbKlQMI4kOjLuNq8IqKr46VG40Mcy-qLvkXFKurFs7KUoyj0LPak53Ay_3_-60f7Sq4n7OWmB1_AMPLyWVDJ-TF3lu9s1_Y_KF1mwOAkKD53HMSsakvg4pAiWtxiL_gPT7XYj7JH1xMS_TcESa5YJixmalE4X9jHEmL1yBv0tUPsUyN2UUuXCH9ZiqF-5s5pqPH9IVR_5O9s0jD-w",
"not-before-policy": 0,
"session_state": "341677ea-e295-4096-9297-df7614da7a77",
"scope": "openid profile email"
}
图 4.6:令牌响应
让我们看一下这个响应中的值:
-
access_token:这是访问令牌,在 Keycloak 中是一个签名的 JWT。我们将在下一章更详细地介绍 OAuth 2.0 时对此进行详细介绍。
-
expires_in:由于访问令牌有时是不透明的,这将为应用程序提供令牌何时过期的提示。
-
refresh_token:这是刷新令牌,我们将在下一节中详细介绍。
-
refresh_token_expires_in:刷新令牌也是不透明的,这为应用程序提供了刷新令牌何时过期的提示。
-
token_type:这是访问令牌的类型,在 Keycloak 中始终是不记名的。
-
id_token:这是 ID 令牌,我们将在下一节中更详细地介绍。
-
session_state:这是用户使用 Keycloak 的会话 ID。
-
scope:应用程序在身份验证请求中向 Keycloak 请求范围,但实际返回的令牌范围可能与请求的范围不匹配。
在下一节中,我们将更深入地了解Playground应用程序刚刚从 Keycloak 接收到的 ID 令牌。
了解 ID 令牌
在上一节中,您收到了一个令牌响应,包括来自 Keycloak 的 ID 令牌,但我们没有仔细查看 ID 令牌内部的内容。
ID 令牌默认为签名 JSON Web Token(JWT),其格式如下:
<Header>.<Payload>.<Signature>
标头和有效负载是 Base64 URL 编码的 JSON 文档。
如果您查看 Playground 应用程序中的 Token Response,您可以看到其编码格式的 ID 令牌。编码 ID 令牌的示例也显示在 Playground 应用程序的以下屏幕截图中:
"token_type": "Bearer",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJyWXQ0UWg2R1VqcVh6dER3QXExdUdTb3VtZ0YtMXdZVzB4UTUwam1xMm40In0.eyJleHAiOjE3NDAzNjg3MDMsImlhdCI6MTc0MDM2ODQwMywiYXV0aF90aW1lIjoxNzQwMzY4MzYxLCJqdGkiOiIzYjQ0YTY2Mi1jZjZlLTRiN2UtYWNkYS1iMTk4MDU1N2U0ZTkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjM4MDgwL3JlYWxtcy9teXJlYWxtIiwiYXVkIjoib2lkYy1wbGF5Z3JvdW5kIiwic3ViIjoiNGJmMzI3ZjQtZDdmYS00NWJlLWIyZTEtNjcyNmM2ZWVjMTljIiwidHlwIjoiSUQiLCJhenAiOiJvaWRjLXBsYXlncm91bmQiLCJzaWQiOiIzNDE2NzdlYS1lMjk1LTQwOTYtOTI5Ny1kZjc2MTRkYTdhNzciLCJhdF9oYXNoIjoiOWFCWW80QndjUnJ0WjZrZGdyaGFmZyIsImFjciI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJEYXZ5IFN1biIsInByZWZlcnJlZF91c2VybmFtZSI6ImRhdnkiLCJnaXZlbl9uYW1lIjoiRGF2eSIsImZhbWlseV9uYW1lIjoiU3VuIiwiZW1haWwiOiJzdW5kYXdlaXh5QGdtYWlsLmNvbSJ9.OpNV5gA1GRe-xfkShgGf-p-qxJMsFI7yCKyGSXrwBZj8dvqRFLuk4XeD5DgsvxhaBPttceuDqlpEx1xM8zoYY0p6ue_MC0hztvDg2wpwdhdDrEykUEQr8os7svXGUOWRGnJZXMbKlQMI4kOjLuNq8IqKr46VG40Mcy-qLvkXFKurFs7KUoyj0LPak53Ay_3_-60f7Sq4n7OWmB1_AMPLyWVDJ-TF3lu9s1_Y_KF1mwOAkKD53HMSsakvg4pAiWtxiL_gPT7XYj7JH1xMS_TcESa5YJixmalE4X9jHEmL1yBv0tUPsUyN2UUuXCH9ZiqF-5s5pqPH9IVR_5O9s0jD-w",
"not-before-policy": 0,
图 4.7:已编码 ID 令牌
在 ID Token 部分下,您将看到解码的令牌分为三个部分。标头告诉您使用了什么算法、有效负载的类型以及用于签署令牌的密钥的密钥 ID。
Playground 应用程序的以下屏幕截图显示了解码 ID Token 的示例:
图 4.8:解码的 ID 令牌
让我们看一下 ID 令牌中的一些声明(值):
-
exp:当令牌过期时。
-
iat:代币发行时。
-
auth_time:用户上次进行身份验证的时间。
-
jti:此令牌的唯一标识符。
-
aud:令牌的受众,必须包含对用户进行身份验证的依赖方。
-
azp:令牌颁发给的一方。
-
sub:经过身份验证的用户的唯一标识符。在引用用户时,建议使用它而不是用户名或电子邮件,因为它们可能会随着时间的推移而变化。
JWT 中的所有时间都以 Unix 纪元时间(自 1970 年 1 月 1 日以来的秒)表示。它并不完全对人类可读,但对计算机来说很棒,与其他格式相比占用的空间很小。您可以找到一个方便的工具,将纪元时间转换为人类可读的日期,
除了前面列出的声明之外,还有关于用户的信息,例如给定的姓名、姓氏和首选用户名。
如果您使用https://www.epochconverter.com/查看 ID 令牌的 exp 值,您会注意到令牌仅在几分钟内过期。
通常,ID 令牌的持续时间很短,以降低令牌被泄露的风险。这并不意味着应用程序必须重新验证用户身份;相反,有一个单独的刷新令牌可用于获取更新的 ID 令牌。刷新令牌的有效期要长得多,只能直接与 Keycloak 一起使用,这意味着 Keycloak 可以验证令牌是否仍然有效。
接下来,让我们尝试刷新 ID 令牌。单击 4 - Refresh,然后单击标有发送刷新请求的按钮。
在刷新请求窗口中,您将看到Playground向 Keycloak 令牌端点发送的请求。它使用授权类型 refresh_token,并包括刷新令牌和客户端 ID。
以下来自 Playground 应用程序的屏幕截图显示了一个示例刷新请求:
图 4.9:刷新请求
在刷新响应下,您将看到发送到Playground的响应 Keycloak。它与原始令牌请求的响应几乎相同。
以下来自 Playground 应用程序的屏幕截图显示了一个示例刷新响应:
图 4.10:刷新响应
这里需要注意的一点是,刷新响应还包括一个刷新令牌。应用程序在下次想要刷新 ID 令牌时使用此更新的刷新令牌非常重要。这很重要,原因如下:
-
密钥轮换(Key rotation):Keycloak 为了保证安全性,可能会定期更换其签名密钥。新的签名密钥生成后,Keycloak 依赖客户端获取用新密钥签名的刷新令牌。如果应用继续使用旧的刷新令牌,在签名验证时会失败,导致无法正常刷新 ID 令牌,进而影响用户的认证和访问。
-
会话空闲(Session idle):客户端或会话有一个 “会话空闲” 的特性,这意味着刷新令牌的过期时间可能比与之关联的会话更短。如果应用不及时更新刷新令牌,当旧的刷新令牌过期后,即使会话本身还未结束,应用也无法再通过它来获取新的 ID 令牌,用户可能会被要求重新登录。
-
刷新令牌泄漏检测**(Refresh token leak detection)**:为了检测刷新令牌是否泄露,Keycloak 默认情况下禁止重复使用刷新令牌(虽然该功能目前默认是禁用状态)。如果应用不使用更新后的刷新令牌,而是重复使用旧的刷新令牌,一旦发生令牌泄露,恶意攻击者就有可能利用旧令牌获取新的 ID 令牌,从而访问用户资源。
最后,刷新 ID 令牌后,除了令牌的过期时间(exp)、颁发时间(iat)和令牌 ID(jti)会改变外,应用还可以在不重新进行用户认证的情况下,从 Keycloak 更新用户信息。这为用户体验和应用的数据一致性提供了便利。
在接下来的几个部分中,您应该保持Playground应用程序打开。在新的浏览器窗口中,打开 Keycloak 管理控制台,单击用户,然后找到您在对Playground应用程序进行身份验证时使用的用户。
首先,让我们尝试更新用户配置文件。
更新User profile
更改用户的电子邮件、名字和姓氏。然后返回Playground应用程序并单击发送刷新请求按钮。您现在会注意到用户配置文件已更新。
现在您已经尝试更新用户配置文件,让我们尝试向用户添加自定义属性。
添加自定义属性
让我们看一下添加自定义属性的步骤:
-
回到 Keycloak Admin Console 窗口,打开Real settings,单击User profile。
-
新增一个自定义属性,将Attribute [Name] 设置为 myattribute,然后单击Create。您现在已经为用户添加了自定义属性,值为myvalue,但这仍然不适用于应用程序。
-
我们现在将创建client scope。客户端范围允许创建可重用的声明组,这些声明组被添加到发给客户端的令牌中。在左侧的菜单中,单击Client scope,然后单击Create client scope。对于表单中的名称,输入 myscope。保持其他所有内容不变,然后单击保存。
-
现在我们将通过创建映射器将自定义属性添加到客户端范围。单击Mapper,然后单击 Configure a new mapper。然后选择 User Attribute。
-
填写表单:
Name: myattribute
User Attribute: myattribute
Token Claim Name: myattribute
Claim JSON Type: String
-
-
确保 Add to ID Token 已打开,然后单Save。接下来,我们将您新创建的客户端范围添加到客户端。
-
在左侧菜单中,单击Clients并找到 oidc-playground 应用程序。选择Client Scopes;然后选择Add client scope窗口,选择 myscope 并单击Add,然后选择Optional。
-
当我们将此范围添加到客户端的可选客户端范围时,这意味着客户端必须显式请求此范围。如果您将其添加到默认客户端范围,它将始终为客户端添加。
-
我们这样做是因为我们想展示客户端如何使用范围参数从 Keycloak 请求不同的信息。这允许客户端在任何给定时间只请求它需要的信息,这在要求用户同意使用他们的数据以及避免令牌中不必要的声明时特别有用。
您将在下一章中了解更多信息。
-
-
现在回到Playground应用程序,再次单击 Send Refresh Request。您会注意到您的自定义属性尚未添加到 ID 令牌中。
如果刷新令牌时出现错误,这可能是因为您与 Keycloak 的单点登录(SSO)会话已过期。默认情况下,如果 10 分钟内没有活动,SSO
会话将过期。在本书的后面,我们将看看如何更改它。
现在让我们发送一个新的身份验证请求,但这次我们将包含 myscope 范围。在Playground应用程序中,单击 2 – Authentication。在范围字段中,将值设置为 openid myscope。确保您将 openid 留在其中,否则,Keycloak 将不会向您发送 ID 令牌。现在再次执行以下步骤以获取新令牌:
-
点击Generate Authentication Request。
-
点击Send Authentication Request。
-
点击 3 – Token。
-
点击Send Token Request。
在 ID 令牌的有效负载中,您现在将注意到刚刚添加到客户端的自定义声明。
现在您已经添加了自定义属性,让我们将角色添加到 ID 令牌中。
向 ID 令牌添加角色
默认情况下,角色不会添加到 ID 令牌中。您可以通过转到Client Scopes,然后选择roles这个client scope来更改此行为。单击Mappers,然后选择realm roles。打开Add to ID Token,然后单击保存。
假设您正在使用的用户是您在第 1 章 Keycloak 入门中创建的用户,该用户应该有一个与之关联的领域角色。如果它是不同的用户,请确保它确实有一个与之关联的领域角色。
回到Playground应用程序并再次刷新令牌。您现在将在 ID 令牌中看到 realm_access。
默认情况下,所有角色都添加到所有客户端。这并不理想,因为您希望限制每个单独客户端的访问权限。
这对 ID 令牌的影响较小,因为它仅用于向特定客户端验证用户身份,而对用于访问其他服务的访问令牌的影响较大。
到目前为止,您应该对应用程序如何使用 ID 令牌来验证用户以及发现有关用户的信息有了相当好的理解。如果您想对客户端范围进行更多试验,现在将是一个好时机,因为Playground应用程序将允许您使用范围并在 ID 令牌中查看结果。
在下一节中,我们将了解应用程序发现有关经过身份验证的用户的信息的不同方式。
调用 UserInfo 端点
除了能够从 ID 令牌中查找有关经过身份验证的用户的信息外,还可以使用通过 OIDC 流获得的访问令牌调用 UserInfo 端点。
让我们通过打开 Playground 应用程序来尝试一下。此时您可能必须发送新的身份验证和令牌请求,因为您的 SSO 会话可能已过期。
如果您是快速读者(或者您获得了新令牌),请单击 5-UserInfo。在 UserInfo 请求下,您将看到Playground应用程序正在向 Keycloak UserInfo 端点发送请求,包括授权标头中的访问令牌。
来自 playground 应用程序的以下屏幕截图显示了一个示例 UserInfo Request:
图 4.11:用户信息请求
在 UserInfo Response 下,您将看到发送的响应 Keycloak。您可能会注意到,这并没有 ID 令牌中的所有附加字段,而只是一个简单的 JSON 响应,仅包括用户属性。
来自 Playground 应用程序的以下屏幕截图显示了一个示例 UserInfo Response:
图 4.12:UserInfo 响应
正如您可以通过客户端范围和协议映射器配置 Keycloak 在 ID 令牌中返回的信息一样,您也可以配置在 UserInfo 端点中返回的信息。此外,您可以控制返回给调用 UserInfo 端点的客户端的信息,而不是获得访问令牌的客户端。这意味着如果将单个访问令牌发送到两个单独的资源服务器,它们可能会在 UserInfo 端点中看到相同访问令牌的不同信息。
让我们尝试向 UserInfo 端点添加一些自定义信息。这一次,我们将不使用客户端范围,而是直接向客户端添加协议映射器。打开 Keycloak 管理控制台,然后在 Clients 下,找到 oidc-playground 客户端。单击 Client scopes,然后选择 oidc-playground-dedicated 客户端范围。单击Configure a new mapper,然后选择 Hardcoded claim。最后,用以下值填写表单:
-
Name: myotherclaim
-
Token Claim Name: myotherclaim
-
Claim value: My Other Claim
-
Claim JSON Type: String
确保Add to userinfo 已打开,然后单击保存。返回Playground应用程序并使用发送 UserInfo 请求按钮发送新的 UserInfo 请求。您现在将在响应中看到myotherclaim。
关于 UserInfo 端点需要记住的一点是,它只能使用通过 OIDC 流程获得的访问令牌进行调用。我们可以通过转到 playground 应用程序,然后点击 “2 – Authentication” 按钮来尝试这一点。
在scope字段中,删除 openid。然后Generate Authentication Request和Send Authentication Request。
现在点击 3 – Token,然后点击Send Token Request。您现在会注意到在令牌响应中没有 id_token 值,这就是为什么在 ID 令牌部分没有显示 ID 令牌的原因。 现在,如果您转到 5-UserInfo 并单击Send UserInfo Reques,您还会注意到 UserInfo 请求失败。
在下一节中,我们将了解如何处理用户注销。
处理用户注销
在 SSO 体验中处理注销实际上可能是一项相当困难的任务,尤其是如果您希望立即注销用户正在使用的所有应用程序。
启动注销
例如,用户可以通过单击应用程序中的注销按钮来启动注销。单击注销按钮时,应用程序将向 OpenID Connect RP 发起的注销发送请求。
应用程序将用户重定向到 Keycloak End Session 端点,该端点在 OpenID Provider Metadata 中注册为 end_session_endpoint。端点采用以下参数:
-
id_token_hint:以前颁发的 ID 令牌。Keycloak 使用此令牌来识别正在注销的客户端、用户以及客户端想要注销的会话。
-
post_logout_redirect_uri:如果客户端希望 Keycloak 在注销后重定向回它,它可以将 URL 传递给 Keycloak。客户端必须事先使用 Keycloak 注册注销 URL。
-
state:这允许客户端在注销请求和重定向之间保持状态。Keycloak 在重定向到客户端时简单地传递此参数。
-
ui_locales:客户端可以使用此参数向 Keycloak 提示登录屏幕应该使用什么语言环境。
当 Keycloak 收到注销请求时,它会通知同一会话中的其他客户端有关注销的信息。然后,它将使会话失效,这实际上使所有令牌无效。
利用 ID令牌 和访问令牌过期时间
应用程序发现用户是否已注销的最简单且可能最可靠的机制是利用 ID令牌和访问令牌通常具有较短有效期这一事实。由于 Keycloak 在用户注销时使会话无效,刷新令牌不能再用于获取新令牌。
此策略有一个缺点,即从用户注销到所有应用程序都有效注销可能需要几分钟,但在许多情况下,这已经绰绰有余。
这对于公共客户端来说也是一个很好的策略。因为它们通常自己并不直接提供服务,而是利用访问令牌来调用其他服务,所以它们会很快意识到会话不再有效。
在令牌有效期较长的情况下,调用 Token Introspection 终端节点来定期检查令牌有效性仍然是一种很好的做法,我们将在下一章中介绍。
利用 OIDC 会话管理
通过 OIDC 会话管理,应用程序可以发现会话是否已注销,而无需向 Keycloak 发出任何请求,也不需要 Keycloak 向其发送任何请求。
这是通过监控 Keycloak 管理的特殊会话 cookie 的状态来实现的。由于应用程序通常托管在与 Keycloak 不同的域上,因此它无法直接读取此 cookie。相反,一个隐藏的 HTML iframe 标签加载一个带有 Keycloak 的特殊页面,该页面监控 cookie 值,并在观察到会话状态发生变化时向应用程序发送一个事件。
这是一种有效的策略,尤其是在应用程序当前处于打开状态时。如果应用程序未打开,则意味着该应用程序在下次打开之前不会观察到注销操作。例如,如果工作站遭到入侵,恶意方也有可能阻止会话 iframe 完成其工作,从而使应用程序会话仍然处于打开状态。但是,这可以相对容易地缓解。一种选择是仅在应用程序打开时保持应用程序会话处于打开状态。Keycloak JavaScript 适配器正是通过仅在窗口状态中存储令牌来实现这一点。当然,通过为令牌设置较短的过期时间也可以缓解这种情况。遗憾的是,如今 OIDC 会话管理方法的相关性越来越低,因为许多浏览器已开始阻止对第三方内容的访问。这实际上意味着在某些浏览器中,隐藏的会话 iframe 不再能够访问会话 cookie。
因此,在新的应用程序中利用这种方法不是一个好主意,而且在已经使用这种方法的应用程序中,你很可能想要迁移出这种方法。
OIDC 会话管理是一种应用程序用于发现会话是否已注销的机制,其原理是通过监控 Keycloak 管理的特殊会话 cookie
状态来实现,具体如下:
工作原理:Keycloak 管理一个特殊的会话 cookie,应用程序通常与 Keycloak 托管在不同域,无法直接读取该
cookie。应用程序通过在页面中嵌入一个隐藏的 HTML iframe 标签,加载带有 Keycloak
的特殊页面。这个特殊页面负责监控会话 cookie 的值,一旦检测到会话状态发生变化(如用户注销导致会话 cookie
失效),便会向应用程序发送一个事件通知,告知应用程序会话已注销。优势与局限:在应用程序当前处于打开状态时,这种方式较为有效,能及时发现会话注销情况。然而,它存在一些局限性。如果应用程序未打开,那么在下次打开之前,应用程序无法感知到注销操作。并且,若用户的工作站遭到入侵,恶意方有可能阻止会话
iframe 正常工作,使得应用程序会话在用户注销后仍保持打开状态。缓解措施:为了缓解上述问题,一种有效的方式是仅在应用程序打开时保持应用程序会话处于打开状态,比如 Keycloak JavaScript
适配器通过仅在窗口状态中存储令牌来实现这一点。另外,为令牌设置较短的过期时间也能在一定程度上降低风险。当前应用情况:如今,许多浏览器开始阻止对第三方内容的访问,这导致隐藏的会话 iframe 在一些浏览器中无法访问会话 cookie,使得 OIDC
会话管理方法的相关性逐渐降低。因此,在新应用开发中,不建议使用这种方式;对于已经使用该方式的应用程序,也最好迁移到其他方式。
OIDC 反向通道注销
通过 OIDC 反向通道注销,应用程序可以注册终端节点以接收注销事件。
当使用 Keycloak 启动注销时,它将向会话中注册了反向通道注销端点的所有应用程序发送注销令牌。
注销令牌类似于 ID 令牌,因此它是一个签名的 JWT。收到注销令牌后,应用程序会验证签名,现在可以注销与 Keycloak 会话 ID 关联的应用程序会话。
对于服务器端应用程序,使用反向通道注销相当有效。但是,对于具有会话粘性的集群应用程序,处理起来确实有点复杂。扩展有状态应用程序的一种常见方法是在应用程序的实例之间分配应用程序会话,并且不能保证来自 Keycloak 的注销请求被发送到实际持有应用程序会话的同一应用程序实例。配置负载均衡器以将注销请求路由到正确的会话并非易事,因此这通常必须在应用程序级别处理。
对于无状态的服务器端应用程序,注销请求也很难处理,因为在这种情况下,会话通常存储在 cookie 中。在这种情况下,应用程序必须记住注销请求,直到下次向应用程序发出给定会话的请求,或者应用程序会话过期。
OIDC 反向通道注销是一种应用程序处理用户注销的机制,具体介绍如下:
- 基本原理:应用程序通过注册一个端点来接收注销事件。当在
Keycloak 中启动注销操作时,Keycloak 会向同一会话中所有注册了反向通道注销端点的应用程序发送注销令牌。这个注销令牌和 ID
令牌类似,是一个经过签名的 JWT。应用程序在收到注销令牌后,会对其签名进行验证,验证通过后,就可以注销与 Keycloak 会话 ID
相关联的应用程序会话,从而实现用户从应用程序中注销的功能。- 应用场景及问题
- 服务器端应用程序:对于服务器端应用程序来说,使用反向通道注销比较有效。但如果是具有会话粘性的集群应用程序,处理起来会较为复杂。因为在扩展有状态应用程序时,通常会将应用程序会话分布在多个应用实例中,Keycloak发出的注销请求不一定会发送到实际持有应用程序会话的那个实例上。配置负载均衡器来确保注销请求能正确路由到对应的会话并不简单,一般需要在应用程序层面进行处理。
- 无状态服务器端应用程序:无状态的服务器端应用程序处理注销请求也有困难。因为这类应用的会话通常存储在 cookie中,应用程序需要记住注销请求,直到下一次针对该会话有新的请求到来,或者会话过期。
- 与其他注销方式对比:相较于利用 ID
和访问令牌过期时间的方式,OIDC 反向通道注销可以实现更即时的注销,不受令牌有效期的延迟影响;与 OIDC
会话管理相比,它不受浏览器阻止第三方内容访问的限制,不过在处理集群应用和无状态服务器端应用时存在一定复杂性。而与 OIDC
前端通道注销相比,虽然反向通道注销在处理某些应用类型时也有困难,但不存在 OpenID Provider 无法确认应用是否成功注销的问题。- 适用场景总结:在需要实现即时注销功能,尤其是服务器端应用程序场景下,OIDC
反向通道注销是一种可行的选择。但在实际应用中,需要根据应用程序的架构(如是否为集群应用、有无状态等)来综合考虑其适用性,并解决可能出现的问题 。
OIDC 前端通道注销
OpenID Connect 前端通道注销在 OpenID 提供程序的注销页面上为每个在前端通道注销端点注册了的应用程序呈现一个隐藏的 iframe。从理论上讲,这是注销无状态服务器端应用程序以及客户端应用程序的好方法。然而,在实践中,它可能不可靠。OpenID 提供程序没有有效的方法来发现应用程序已成功注销,因此使用这种方法有点碰运气。
此外,OIDC Front-Channel 注销方法还受到浏览器阻止第三方内容的影响,这意味着当 OpenID Provider 在 iframe 中打开注销端点时,无法访问任何应用程序级 Cookie,从而使应用程序无法访问当前的身份验证会话。
OIDC 前端通道注销是 OpenID Connect(OIDC)中用于实现用户注销的一种方式,其原理是在 OpenID Provider的注销页面上,为每个注册了前端通道注销端点的应用程序渲染一个隐藏的iframe,理论上可用于注销无状态服务器端应用程序以及客户端应用程序。但在实际使用中,存在一些问题:
- 可靠性问题:OpenID Provider 没有有效的方法来确定应用程序是否已成功注销,使用这种方法存在不确定性,可能会出现注销失败却未被察觉的情况。
- 浏览器限制问题:如今许多浏览器阻止第三方内容,当 OpenID Provider 在 iframe 中打开注销端点时,应用程序无法访问任何应用程序级的 cookie。这就导致应用程序无法获取当前的认证会话信息,进而影响注销操作的正常进行。
由于上述问题,OIDC 前端通道注销在实际应用中不太可靠,相比之下,在需要即时注销的场景中,OIDC
反向通道注销或依赖较短应用会话和令牌过期的方式可能更为合适 。
OIDC 前端通道注销是 OpenID Connect(OIDC)中用于实现用户注销的一种方式,其原理是在 OpenID Provider 的注销页面上,为每个注册了前端通道注销端点的应用程序渲染一个隐藏的 iframe,理论上可用于注销无状态服务器端应用程序以及客户端应用程序。但在实际使用中,存在一些问题:
可靠性问题:OpenID Provider 没有有效的方法来确定应用程序是否已成功注销,使用这种方法存在不确定性,可能会出现注销失败却未被察觉的情况。
浏览器限制问题:如今许多浏览器阻止第三方内容,当 OpenID Provider 在 iframe 中打开注销端点时,应用程序无法访问任何应用程序级的 cookie。这就导致应用程序无法获取当前的认证会话信息,进而影响注销操作的正常进行。
由于上述问题,OIDC 前端通道注销在实际应用中不太可靠,相比之下,在需要即时注销的场景中,OIDC 反向通道注销或依赖较短应用会话和令牌过期的方式可能更为合适 。
您应该如何处理注销?
总之,最简单的方法是简单地依赖相对较短的应用程序会话和令牌过期。由于 Keycloak 将使用户保持登录状态,因此可以有效地使用较短的应用程序会话,而无需用户频繁地重新进行身份验证。
在其他情况下,或者必须立即注销时,您应该利用 OIDC Back-Channel Logout。
总结
在本章中,您亲身体验了 OIDC 身份验证流程中的交互。您了解了应用程序如何准备身份验证请求,然后将用户代理重定向到 Keycloak 授权端点进行身份验证。然后,您了解了应用程序如何获取授权码,并将其交换为 ID 令牌。通过检查 ID 令牌,您随后了解了应用程序如何查找有关经过身份验证的用户的信息。您还学习了如何利用 Keycloak 中的客户端范围和协议映射器来添加有关用户的其他信息。最后,您学习了如何处理单点登录,以及如何处理单点注销。
您现在应该对 OpenID Connect 以及如何使用它来保护您自己的应用程序有了基本的了解。我们将在本书后面以这些知识为基础,让您准备好开始使用 Keycloak 保护您的所有应用程序。
在下一章中,您将对 OAuth 2.0 有更深入的了解,并提供有关如何使用 Keycloak 在应用程序中使用此标准的实用指南。
问题
-
OpenID Connect Discovery 规范如何让您更轻松地在不同的 OpenID 提供商之间切换?
-
应用程序如何发现有关经过身份验证的用户的信息?
-
如何添加有关经过身份验证的用户的其他信息?
延伸阅读
有关本章中涵盖的主题的更多信息,请参阅以下链接:
- OpenID Connect Core 规范:https://openid.net/specs/openid-connectcore-1_0.html
- OpenID Connect 发现规范:https://openid.net/specs/openid-connectdiscovery-1_0.html
- OpenID Connect 会话管理规范:https://openid.net/specs/openid-connect-session-1_0.html
- OpenID Connect 反向通道注销规范:https://openid.net/specs/openid-connect-backchannel-1_0.html