OpenID Connect:不同身份提供者之间的刷新令牌行为不一致

Posted

技术标签:

【中文标题】OpenID Connect:不同身份提供者之间的刷新令牌行为不一致【英文标题】:OpenID Connect: inconsistent refresh token behaviour between different Identity Providers 【发布时间】:2020-02-05 21:21:05 【问题描述】:

我正在实现一个服务提供者,目前观察到不同身份提供者在获取刷新令牌方面的行为不一致。我将在底部附上我的 Service Provider golang 代码,以防它可以帮助某人或澄清我的问题。

我正在通过使用查询参数access_type=offline 将登录请求重定向到*/authn 端点来执行authorization_code 流程。然后,第二步是在回调端点上接收授权码,然后调用*/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:

    某些请求的范围无效。 有效=[openid,https://www.googleapis.com/auth/userinfo.profile,https://www.googleapis.com/auth/userinfo.email],无效=[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: []stringoidc.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)
    )

【问题讨论】:

【参考方案1】:

不幸的是,我认为不同的提供商实现这部分的方式不同。 Okta 似乎是其中最符合要求的(要求 offline_access 作为范围是 OIDC specification 所描述的。

使范围值可配置,并且可能还可以配置自定义参数(例如access_type 参数),这将是避免每个提供者完全自定义实现的一种方法。

prompt 参数是规范的一部分,因此无论如何进行可配置可能是个好主意。

【讨论】:

【参考方案2】:

这类问题确实很常见。抽象身份验证管道 - 我使用“身份验证器”接口或基类,然后在需要的地方进行专门化。只要管道与您的宝贵逻辑分开,我发现它运作良好。

【讨论】:

以上是关于OpenID Connect:不同身份提供者之间的刷新令牌行为不一致的主要内容,如果未能解决你的问题,请参考以下文章

选择 OpenID Connect 提供程序

Asp.Net Core API OpenId-Connect 身份验证与 JWT 令牌使用 IdentityModel

使用 jumbojett/OpenID-Connect-PHP 库的 KeyCloak 身份验证流程

OpenID Connect webfinger 端点是用户帐户到 OpenID Connect 提供者的映射吗?

使用 OpenID Connect Gluu 身份验证提供程序来保护 Spring Boot Web App 客户端

.Net Core 2 OpenID Connect 身份验证和多个身份