OpenID Connect: несоответствующее поведение токенов обновления между различными поставщиками удостоверений - PullRequest
0 голосов
/ 08 октября 2019

Я внедряю поставщика услуг и в настоящее время наблюдаю несогласованное поведение различных поставщиков удостоверений в отношении получения токенов обновления. Я собираюсь прикрепить свой код golang провайдера услуг внизу, на случай, если он может кому-то помочь или прояснить мой вопрос.

Я выполняю поток authorization_code, перенаправляя запрос на вход в систему к конечной точке */authnс параметром запроса access_type=offline. Затем второй шаг - получение кода авторизации в конечной точке обратного вызова, затем вызов конечной точки */token для обмена кодом для доступа и обновления токенов.

Я пробовал этот поток с 3 различными поставщиками удостоверений инашел следующие результаты:

  1. OneLogin (https://openid -connect.onelogin.com / oidc ): достаточно было добавить параметр запроса access_type=offline для получения токенов обновления.
  2. Okta (https://my -company.okta.com ): добавления access_type=offline было недостаточно. Мне нужно было добавить offline_access к параметру Scopes запроса на первом шаге (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 из Области и добавлял параметр запроса prompt со значением 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)
    })
}

Ответы [ 2 ]

1 голос
/ 08 октября 2019

Этот тип проблемы действительно очень распространен. Абстрагирование от аутентификации - я использую интерфейс «аутентификатора» или базовый класс, а затем специализируюсь там, где это необходимо. Пока сантехника отделена от вашей ценной логики, я считаю, что это хорошо работает.

1 голос
/ 08 октября 2019

К сожалению, я думаю, что различные провайдеры реализуют эту часть по-разному. Okta кажется наиболее совместимым из них (требуется offline_access, поскольку область действия - это то, что описывает спецификация OIDC.

Настройка значений области действия и, возможно, также возможность настройки пользовательскихпараметры (такие как параметр access_type) позволят избежать полностью пользовательских реализаций для каждого провайдера.

Параметр prompt является частью спецификации, поэтому его настройка в любом случае может быть хорошей идеей.

...