OpenID Connect:不同身份提供者之间的刷新令牌行为不一致
OpenID Connect: inconsistent refresh token behaviour between different Identity Providers
我正在实施一个服务提供者,目前观察到不同身份提供者在获取刷新令牌方面的行为不一致。我将在底部附上我的服务提供商 golang 代码,以防它可以帮助某人或澄清我的问题。
我正在执行 authorization_code 流程,方法是使用查询参数 access_type=offline
将登录请求重定向到 */authn
端点。然后,第二步是在回调端点上接收授权代码,然后调用 */token
端点以交换代码以获取访问和刷新令牌。
我已经用 3 个不同的身份提供者尝试了这个流程,发现了以下结果:
- OneLogin (https://openid-connect.onelogin.com/oidc): 添加查询参数
access_type=offline
即可接收刷新令牌。
- Okta (https://my-company.okta.com):添加
access_type=offline
是不够的。我需要在第一步 (authn) 中将 offline_access
添加到请求的 Scopes 参数中。此配置也适用于 OneLogin!
Google (https://accounts.google.com):然而,对于 Google,范围 offline_access
不是支持并返回 400 BAD REQUEST:
Some requested scopes were invalid. {valid=[openid, https://www.googleapis.com/auth/userinfo.profile, https://www.googleapis.com/auth/userinfo.email], invalid=[offline_access]}
唯一对 Google 起作用的是从 Scopes 中删除 offline_access
并添加值为 consent
的查询参数 prompt
。但是,这不适用于 Okta 或 OneLogin...
我是不是遗漏了什么,或者我应该为每个 IdP 提供自定义授权流程实现,以支持刷新令牌?
看起来很奇怪,考虑到协议是完全指定的。
package openidconnect
import (
"context"
"encoding/json"
"net/http"
"os"
oidc "github.com/coreos/go-oidc"
"golang.org/x/oauth2"
)
var oidcClientID = getEnv("****", "OIDC_CLIENT_ID")
var oidcClientSecret = getEnv("****", "OIDC_CLIENT_SECRET")
var oidcProvider = getEnv("****", "OIDC_PROVIDER")
var oidcLoginURI = "/v1/oidc_login"
var oidcCallbackURI = "/v1/oidc_callback"
var hostname = getEnv("http://localhost:8080", "HOSTNAME")
func getEnv(defaultValue, key string) string {
val := os.Getenv(key)
if val == "" {
return defaultValue
}
return val
}
//InitOpenIDConnect initiates open ID connect SSO
func InitOpenIDConnect() error {
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, oidcProvider)
if err != nil {
return err
}
// Configure an OpenID Connect aware OAuth2 client.
oidcConfig := oauth2.Config{
ClientID: oidcClientID,
ClientSecret: oidcClientSecret,
RedirectURL: hostname + oidcCallbackURI,
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
// TODO: For Okta and OneLogin, add oidc.ScopeOfflineAccess Scope for refresh token.
// Removed for now because Google API returns 400 when it is set.
}
handleOIDCLogin(&oidcConfig)
handleOIDCCallback(provider, &oidcConfig)
return nil
}
var approvalPromptOption = oauth2.SetAuthURLParam("prompt", "consent")
func handleOIDCLogin(config *oauth2.Config) {
state := "foobar" // Don't do this in production.
http.HandleFunc(oidcLoginURI, func(w http.ResponseWriter, r *http.Request) {
// approval prompt option is required for getting refresh token from Google API
redirectURL := config.AuthCodeURL(state, oauth2.AccessTypeOffline, approvalPromptOption)
http.Redirect(w, r, redirectURL, http.StatusFound)
})
}
func handleOIDCCallback(provider *oidc.Provider, config *oauth2.Config) {
state := "foobar" // Don't do this in production.
ctx := context.Background()
http.HandleFunc(oidcCallbackURI, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("state") != state {
http.Error(w, "state did not match", http.StatusBadRequest)
return
}
code := r.URL.Query().Get("code")
oauth2Token, err := config.Exchange(ctx, code)
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
tokenSource := config.TokenSource(ctx, oauth2Token)
refreshedToken, err := tokenSource.Token()
if err != nil {
http.Error(w, "Failed to get refresh token: "+err.Error(), http.StatusInternalServerError)
return
}
userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
if err != nil {
http.Error(w, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError)
return
}
resp := struct {
OAuth2Token *oauth2.Token
UserInfo *oidc.UserInfo
}{oauth2Token, userInfo}
data, err := json.MarshalIndent(resp, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
})
}
不幸的是,我认为不同的提供商对这部分的实现方式不同。 Okta 似乎是其中最合规的(需要 offline_access
,因为范围是 OIDC specification 所描述的。
使范围值可配置,并且可能还可以配置自定义参数(例如 access_type
参数)将是避免为每个提供程序完全自定义实现的一种方法。
prompt
参数是规范的一部分,因此无论如何使该参数可配置可能是个好主意。
这类问题确实很常见。抽象身份验证管道 - 我使用 'authenticator' 接口或基础 class,然后在需要的地方专门化。只要管道与您的宝贵逻辑分开,我发现它就可以正常工作。
我正在实施一个服务提供者,目前观察到不同身份提供者在获取刷新令牌方面的行为不一致。我将在底部附上我的服务提供商 golang 代码,以防它可以帮助某人或澄清我的问题。
我正在执行 authorization_code 流程,方法是使用查询参数 access_type=offline
将登录请求重定向到 */authn
端点。然后,第二步是在回调端点上接收授权代码,然后调用 */token
端点以交换代码以获取访问和刷新令牌。
我已经用 3 个不同的身份提供者尝试了这个流程,发现了以下结果:
- OneLogin (https://openid-connect.onelogin.com/oidc): 添加查询参数
access_type=offline
即可接收刷新令牌。 - Okta (https://my-company.okta.com):添加
access_type=offline
是不够的。我需要在第一步 (authn) 中将offline_access
添加到请求的 Scopes 参数中。此配置也适用于 OneLogin! Google (https://accounts.google.com):然而,对于 Google,范围
offline_access
不是支持并返回 400 BAD REQUEST:Some requested scopes were invalid. {valid=[openid, https://www.googleapis.com/auth/userinfo.profile, https://www.googleapis.com/auth/userinfo.email], invalid=[offline_access]}
唯一对 Google 起作用的是从 Scopes 中删除
offline_access
并添加值为consent
的查询参数prompt
。但是,这不适用于 Okta 或 OneLogin...
我是不是遗漏了什么,或者我应该为每个 IdP 提供自定义授权流程实现,以支持刷新令牌?
看起来很奇怪,考虑到协议是完全指定的。
package openidconnect
import (
"context"
"encoding/json"
"net/http"
"os"
oidc "github.com/coreos/go-oidc"
"golang.org/x/oauth2"
)
var oidcClientID = getEnv("****", "OIDC_CLIENT_ID")
var oidcClientSecret = getEnv("****", "OIDC_CLIENT_SECRET")
var oidcProvider = getEnv("****", "OIDC_PROVIDER")
var oidcLoginURI = "/v1/oidc_login"
var oidcCallbackURI = "/v1/oidc_callback"
var hostname = getEnv("http://localhost:8080", "HOSTNAME")
func getEnv(defaultValue, key string) string {
val := os.Getenv(key)
if val == "" {
return defaultValue
}
return val
}
//InitOpenIDConnect initiates open ID connect SSO
func InitOpenIDConnect() error {
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, oidcProvider)
if err != nil {
return err
}
// Configure an OpenID Connect aware OAuth2 client.
oidcConfig := oauth2.Config{
ClientID: oidcClientID,
ClientSecret: oidcClientSecret,
RedirectURL: hostname + oidcCallbackURI,
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
// TODO: For Okta and OneLogin, add oidc.ScopeOfflineAccess Scope for refresh token.
// Removed for now because Google API returns 400 when it is set.
}
handleOIDCLogin(&oidcConfig)
handleOIDCCallback(provider, &oidcConfig)
return nil
}
var approvalPromptOption = oauth2.SetAuthURLParam("prompt", "consent")
func handleOIDCLogin(config *oauth2.Config) {
state := "foobar" // Don't do this in production.
http.HandleFunc(oidcLoginURI, func(w http.ResponseWriter, r *http.Request) {
// approval prompt option is required for getting refresh token from Google API
redirectURL := config.AuthCodeURL(state, oauth2.AccessTypeOffline, approvalPromptOption)
http.Redirect(w, r, redirectURL, http.StatusFound)
})
}
func handleOIDCCallback(provider *oidc.Provider, config *oauth2.Config) {
state := "foobar" // Don't do this in production.
ctx := context.Background()
http.HandleFunc(oidcCallbackURI, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("state") != state {
http.Error(w, "state did not match", http.StatusBadRequest)
return
}
code := r.URL.Query().Get("code")
oauth2Token, err := config.Exchange(ctx, code)
if err != nil {
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
return
}
tokenSource := config.TokenSource(ctx, oauth2Token)
refreshedToken, err := tokenSource.Token()
if err != nil {
http.Error(w, "Failed to get refresh token: "+err.Error(), http.StatusInternalServerError)
return
}
userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
if err != nil {
http.Error(w, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError)
return
}
resp := struct {
OAuth2Token *oauth2.Token
UserInfo *oidc.UserInfo
}{oauth2Token, userInfo}
data, err := json.MarshalIndent(resp, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
})
}
不幸的是,我认为不同的提供商对这部分的实现方式不同。 Okta 似乎是其中最合规的(需要 offline_access
,因为范围是 OIDC specification 所描述的。
使范围值可配置,并且可能还可以配置自定义参数(例如 access_type
参数)将是避免为每个提供程序完全自定义实现的一种方法。
prompt
参数是规范的一部分,因此无论如何使该参数可配置可能是个好主意。
这类问题确实很常见。抽象身份验证管道 - 我使用 'authenticator' 接口或基础 class,然后在需要的地方专门化。只要管道与您的宝贵逻辑分开,我发现它就可以正常工作。