0

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

我正在通过*/authn使用查询参数将登录请求重定向到端点来执行 authorization_code 流程access_type=offline。然后,第二步是在回调端点上接收授权码,然后调用*/token端点交换访问和刷新令牌的代码。

我用 3 个不同的身份提供者尝试了这个流程,发现了以下结果:

  1. OneLogin ( https://openid-connect.onelogin.com/oidcaccess_type=offline ):添加查询参数来接收刷新令牌就足够了。
  2. Okta ( https://my-company.okta.com ):添加access_type=offline还不够。我需要offline_access在第一步(authn)中添加到请求的范围参数。此配置也适用于 OneLogin!
  3. Google ( https://accounts.google.com ):但是,使用 Google,范围offline_access不受支持,并返回 400 BAD REQUEST:

    某些请求的范围无效。{valid=[openid, https://www.googleapis.com/auth/userinfo.profile , https://www.googleapis.com/auth/userinfo.email] , invalid=[offline_access]}

    与 Google 合作的唯一方法是offline_access从 Scopes 中删除并添加prompt带有 value的查询参数consent。但是,这不适用于 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)
    })
}
4

2 回答 2

1

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

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

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

于 2019-10-08T15:26:32.300 回答
1

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

于 2019-10-08T18:47:51.703 回答