文章目录
提到OAuth2
,大家多少都有些了解。
不了解的话可以先看下之前的简单聊聊鉴权背后的那些技术[1]先回顾一下基本概念和流程。
简单来说,以google
授权为例,一般就是通过用户授权页面登录google
账号,再跳转用code
换取到相应权限的token
,就可以代表用户去发起一些google api
的请求。
直接代码实现这套授权逻辑并不复杂,不过如果还需要接入facebook
授权,instagram
授权呢,总不能挨个去实现一遍吧。
最好能有一套通用的解决方案来解放双手, 今天我们就聊聊如何用keycloak
实现一套通用的身份验证和授权管理方案。
提前说明,无法本地复刻的技术方案不利于理解,也不利于方案探讨。虽然本文章所用代码是使用了
rust
的axum
框架(为啥?因为rust
is future!)+keycloak
,但从服务启动到keycloak
服务及相关配置,都用docker-compose+terraform+shell
脚本化管理,可 100%本地复刻,欢迎本地尝试。(当然我说的是Mac
下)代码地址:https://github.com/NewbMiao/axum-koans[2]
OAuth
在引入keycloak
之前我们以google
为例先看下常规OAuth
怎么接入,方便后边和keycloak
接入对比。
前置工作:获取
google OAuth application
的clientId
和clientSecret
,不清楚的话,可以参考 Create a Google Application in How to setup Sign in with Google using Keycloak[3]
如下图,一般授权流程(standard flow
)中客户端和auth server
主要是两个阶段
auth url
跳转登录后请求换取授权令牌的code
auth callback
中用code
换取token
,得到能代表用户的credentials
,一般是accessToken
Authorization Code flow for OAuth
这个流程自己也可以实现,但一般都用oidc client
(其实现了OpenID connect
协议,是建立在OAuth2.0
上的身份验证协议,用来为应用提供用户身份信息)来实现。
编程语言实现上大同小异,下边代码以rust
的oauth2
库为例讲解
如果不熟悉rust
,可以重点看代码注释,也不影响理解
oidc client
// src/extensions/google_auth.rs@GoogleAuth::new
// 注册auth server 的授权登录地址,授权时会生成带有相应参数的 auth url
let auth_url =
AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string()).unwrap();
// 注册auth server 的授权登录成功后要跳转到的客户端地址(auth callback url),会携带code
let redirect_url = RedirectUrl::new(config.redirect_url).unwrap();
// 注册auth server 的code换取token的地址
let token_url =
TokenUrl::new("https://www.googleapis.com/oauth2/v3/token".to_string()).unwrap();
let client = BasicClient::new(
// 注册google application client credentials, 会有相应权限和客户端限制,如web application类型会有访问地址origin及callback地址的白名单限制
ClientId::new(config.client_id),
Some(ClientSecret::new(config.client_secret)),
auth_url,
Some(token_url),
)
.set_redirect_uri(redirect_url);
// src/extensions/google_auth.rs@GoogleAuth::auth_url
let (url, csrf_token) = client
// 参数是用于生成state的函数,这里用csrftoken,可以在auth callback中校验state参数是否合法
.authorize_url(CsrfToken::new_random)
// auth请求需要的权限(scope),一般获取用户信息的话,profile和email就好了
.add_scope(Scope::new(
"https://www.googleapis.com/auth/userinfo.profile".to_string(),
))
.add_scope(Scope::new(
"https://www.googleapis.com/auth/userinfo.email".to_string(),
))
// 需要显示OAuth需要授权的内容给用户来确认是否同意,就是我们常见的google授权确认页面
.add_extra_param("prompt", "consent")
// 允许应用程序获得长期有效的访问令牌(accessToken)和刷新令牌(refreshToken)
.add_extra_param("access_type", "offline")
.url();
这里参数access_type=offline
对于应用需要长期accessToken
是很关键的。一般accessToken
都有过期时间,如果没有有效的refreshToken
来刷新accessToken
,就会有accessToken
失效后还要用户再登录的尴尬局面-_-!
另外为安全考虑除了可以用state
做请求合法校验,还可以用`PKCE(Proof Key for Code Exchange)`[4]来加强, 实际用到的代码有实现,感兴趣可以看下
// src/extensions/google_auth.rs@GoogleAuth::get_tokens
// 校验请求,state及pkce, 这里省略展示
// code 换取token
let mut res = client.exchange_code(code);
// 请求发送,axum中不能使用block请求,防止阻塞框架的异步事件循环
let res = res.request_async(async_http_client).await?;
Ok(TokenInfo {
refresh_token: res.refresh_token().unwrap().secret().to_string(),
access_token: res.access_token().secret().to_string(),
})
这部分不复杂,按文档配好本地,可以访问http://localhost:8000/google/auth
来尝试上述flow
上边流程怎么让 keycloak 这个身份和访问管理系统接管呢,答案是使用keycloak IDP
(Identity provider
)
我们先看下需要如何配置相应配置,这里先用`terraform - keycloak provider`[5] 展示下配置。
等效的页面配置可以后边参考之前的链接 How to setup Sign in with Google using Keycloak[6]
# 这里使用默认的admin-cli配置keycloak
# 也可生成一个专门的client,用clientId+clientSecret的方式
provider "keycloak" {
client_id = "admin-cli"
url = "http://localhost:8080"
username = "***"
password = "***"
}
# 1. 创建一个realm(领域),并启用, 类似命名空间,代表一个安全的独立区域
resource "keycloak_realm" "realm_axum_koans" {
realm = "axum-koans"
enabled = true
}
# 2. 添加google idp, 这里需要google client credentials
resource "keycloak_oidc_google_identity_provider" "google" {
realm = keycloak_realm.realm_axum_koans.id
# client_id和secret通过环境变量获取
client_id = var.google_client_id
client_secret = var.google_client_secret
trust_email = true
# "*" 则不约束使用此idp的domain
hosted_domain = "*"
sync_mode = "IMPORT"
provider_id = "google"
default_scopes = "openid profile email"
}
# 3. 添加将要用来google auth打交道的client
resource "keycloak_openid_client" "client_axum_koans" {
realm_id = keycloak_realm.realm_axum_koans.id
name = "axum-koans"
enabled = true
client_id = "axum-koans"
client_secret = "***"
standard_flow_enabled = true
access_type = "CONFIDENTIAL"
# 配置auth callback url
valid_redirect_uris = [
"http://localhost:8000/keycloak/login-callback"
]
web_origins = ["*"]
use_refresh_tokens = true
}
别看代码版的配置稍微有点多,主要配置其实就只有注释里的三处,然后 google OAuth 的代理设置就完成了,不信我们继续往下看怎么代码接入
上边keycloak
配置了realm
,后边授权和token
获取都会和这个realm
下的issueUrl
打交道,这里issueUrl
就类似google
的auth server
地址。
keycloak oidc client
// src/extensions/keycloak_auth.rs@KeycloakAuth::new
// 我们配置生成的issue_url将会是:http://localhost:8080/realms/axum-koans
// 设置token url, auth url 和auth callback url(redirect url)
let token_url = TokenUrl::new(get_url_with_issuer(
&config.issuer_url,
"/protocol/openid-connect/token",
))
.unwrap();
let auth_url = AuthUrl::new(get_url_with_issuer(
&config.issuer_url,
"/protocol/openid-connect/auth",
))
.unwrap();
let redirect_url = RedirectUrl::new(config.redirect_url).unwrap();
let client = BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url))
.set_redirect_uri(redirect_url);
auth_url
方法基本和之前google
配置一模一样。
这里也能看出为啥需要oidc
协议,其实就是抽象化,提供了一种安全、标准化和可扩展的身份验证和授权协议。它简化了应用程序中的身份管理和访问控制,提供了一致的用户登录体验,并提高了应用程序的安全性。
这里auth url
默认跳转的是keycloak
登录页面,然后google idp
是作为一种登录选项让用户选择。但如果就打算让用户直接google
登录,可以跳过keycloak
登录页。
方法是使用客户端建议的idp(kc_idp_hint)
:`Client-suggested Identity Provider`[7]
这样就可以直接使用指定的idp
进行授权登录
代码如下
// src/extensions/keycloak_auth.rs@KeycloakAuth::auth_url
client.add_extra_param("kc_idp_hint", "google")
auth callback
换取token
方法也同 google auth callback
, 这里不赘述了。
不过这里拿到的是keycloak
的token
。要是需要google
的token
怎么办?
别急,有两种办法。
`token-exchange`[8] 是用于token
交换场景,我们这里是用keycloak token
换取外部google token
(external token
)
keycloak
配置
token-exchange
目前还是keycloak
预览(preview
)功能,需要至少在features
中启用admin-fine-grained-authz,token-exchange
才可使用(详见keycloak docker-composer
配置 )
// 启用idp获取refresh token
resource "keycloak_oidc_google_identity_provider" "google" {
...
# for token exchange to get google access token
request_refresh_token = true
}
// 启用 idp token exchange permission, 并用policy关联对应的client
resource "keycloak_identity_provider_token_exchange_scope_permission" "oidc_idp_permission" {
realm_id = keycloak_realm.realm_axum_koans.id
provider_alias = keycloak_oidc_google_identity_provider.google.alias
policy_type = "client"
clients = [
keycloak_openid_client.client_axum_koans.id
]
}
let token_url =
format!( "{}/protocol/openid-connect/token",&self.config.issuer_url);
let response = Client::new()
.post(token_url)
.form(&[
// token exchange type
(
"grant_type",
"urn:ietf:params:oauth:grant-type:token-exchange",
),
// 传入keycloak access token
("subject_token", &access_token),
("client_id", &self.config.client_id),
("client_secret", &self.config.client_secret),
// 请求换取google access token
(
"requested_token_type",
"urn:ietf:params:oauth:token-type:access_token",
),
// 要换取的external idp: google
("requested_issuer", "google"),
])
.send()
.await?;
// json deserialized as access token
Ok(from_str(&response.text().await?)?)
这样就获取到了可用的google access token
, 实际上内部是通过google refresh token
换取到的。
这样常规请求没问题了,只要你有keycloak access token
, 就能换取到google access token
来请求google api
。so easy?!
然而,要是需要google refresh token
怎么办?
有些场景是客户端需要自己通过google refresh token
换取access token
来发起请求的,难道这个时候客户端先去拿个keycloak access token
么。。。?
这就可以用Retrieving external IDP tokens[9]
底层实现是授权时存储了external token
,再配合添加broker read token
权限给生成的用户,就可以用keycloak access token
换取存储的external access token + refresh token
.
resource "keycloak_oidc_google_identity_provider" "google" {
...
# for retrieve idp token (with refresh token)
// 存储idp token
store_token = true
// 用户生成是添加broker read token 权限
add_read_token_role_on_create = true
}
题外话:这里
add_read_token_role_on_create
对应的配置在 21.1.1 版keycloak admin
页面没有,但admin api
确可以设置,也是很 tricky
就是直接换取refresh_token
, 请求地址指明对应的idp
即可
// src/extensions/keycloak_auth.rs@KeycloakAuth::get_idp_token
let token_url = format!( "{}/broker/google/token",&self.config.issuer_url);
let response = Client::new()
.get(token_url)
.bearer_auth(access_token)
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
.send()
.await?;
let res = response.text().await?;
Ok(from_str(&res)?)
题外话:当然直接给用户这么获取
refresh token
的能力并不安全,还需要考虑对broker read token
接口的访问约束等来更好的保证安全token
换取。
上边keycloak
授权方案可以本地配好环境后,用http://localhost:8000/keycloak/login 来尝试。
好了,keycloak
如何管理external auth
到这里就结束了。以上是我在使用keycloak
的一些摸索和思考,欢迎大家一起探讨。
再次附上本文的代码地址以供验证:https://github.com/NewbMiao/axum-koans[10]
[1]
简单聊聊鉴权背后的那些技术: http://blog.newbmiao.com/2021/09/19/tech-behind-authentication.html
[2]
https://github.com/NewbMiao/axum-koans: https://github.com/NewbMiao/axum-koans
[3]
How to setup Sign in with Google using Keycloak: https://keycloakthemes.com/blog/how-to-setup-sign-in-with-google-using-keycloak
[4]
PKCE(Proof Key for Code Exchange)
: https://blog.postman.com/pkce-oauth-how-to
[5]
terraform - keycloak provider
: https://registry.terraform.io/providers/mrparkers/keycloak/latest/docs
[6]
How to setup Sign in with Google using Keycloak: https://keycloakthemes.com/blog/how-to-setup-sign-in-with-google-using-keycloak
[7]
Client-suggested Identity Provider
: https://www.keycloak.org/docs/latest/server_admin/#_client_suggested_idp
[8]
token-exchange
: https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange
[9]
Retrieving external IDP tokens: https://www.keycloak.org/docs/latest/server_admin/#retrieving-external-idp-tokens
[10]
https://github.com/NewbMiao/axum-koans: https://github.com/NewbMiao/axum-koans
如果有用,点个 在看 ,让更多人看到
外链不能跳转,戳 阅读原文 查看参考资料