oidc: use 'end_session_endpoint' from discovery, remove 'OIDC_LOGOUT_URL'

This commit is contained in:
yusing 2025-02-27 05:27:38 +08:00
parent 50262f2acc
commit f9b7e64d53
3 changed files with 66 additions and 31 deletions

View file

@ -4,31 +4,42 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"mime"
"net/http" "net/http"
"net/url" "net/url"
"slices" "slices"
"strings"
"time" "time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/net/gphttp" "github.com/yusing/go-proxy/internal/net/gphttp"
CE "github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
type OIDCProvider struct { type (
OIDCProvider struct {
oauthConfig *oauth2.Config oauthConfig *oauth2.Config
oidcProvider *oidc.Provider oidcProvider *oidc.Provider
oidcVerifier *oidc.IDTokenVerifier oidcVerifier *oidc.IDTokenVerifier
oidcLogoutURL *url.URL oidcEndSessionURL *url.URL
allowedUsers []string allowedUsers []string
allowedGroups []string allowedGroups []string
isMiddleware bool isMiddleware bool
} }
providerJSON struct {
oidc.ProviderConfig
EndSessionURL string `json:"end_session_endpoint"`
}
)
const CookieOauthState = "godoxy_oidc_state" const CookieOauthState = "godoxy_oidc_state"
const ( const (
@ -36,25 +47,50 @@ const (
OIDCLogoutPath = "/auth/logout" OIDCLogoutPath = "/auth/logout"
) )
func NewOIDCProvider(issuerURL, clientID, clientSecret, redirectURL, logoutURL string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) { func NewOIDCProvider(issuerURL, clientID, clientSecret, redirectURL string, allowedUsers, allowedGroups []string) (*OIDCProvider, error) {
if len(allowedUsers)+len(allowedGroups) == 0 { if len(allowedUsers)+len(allowedGroups) == 0 {
return nil, errors.New("OIDC users, groups, or both must not be empty") return nil, errors.New("OIDC users, groups, or both must not be empty")
} }
var logout *url.URL wellKnown := strings.TrimSuffix(issuerURL, "/") + "/.well-known/openid-configuration"
var err error resp, err := gphttp.Get(wellKnown)
if logoutURL != "" {
logout, err = url.Parse(logoutURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse logout URL: %w", err) return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("oidc: unable to read response body: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("oidc: %s: %s", resp.Status, body)
}
var p providerJSON
err = json.Unmarshal(body, &p)
if err != nil {
mimeType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err == nil && mimeType != "application/json" {
return nil, fmt.Errorf("oidc: unexpected content type: %q from OIDC provider discovery, have you configured the correct issuer URL?", mimeType)
}
return nil, fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
}
if p.IssuerURL != issuerURL {
return nil, fmt.Errorf("oidc: issuer did not match the issuer returned by provider, expected %q got %q", issuerURL, p.IssuerURL)
}
var endSessionURL *url.URL
if p.EndSessionURL != "" {
endSessionURL, err = url.Parse(p.EndSessionURL)
if err != nil {
return nil, fmt.Errorf("oidc: failed to parse end session URL: %w", err)
} }
} }
provider, err := oidc.NewProvider(context.Background(), issuerURL) provider := p.NewProvider(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to initialize OIDC provider: %w", err)
}
return &OIDCProvider{ return &OIDCProvider{
oauthConfig: &oauth2.Config{ oauthConfig: &oauth2.Config{
ClientID: clientID, ClientID: clientID,
@ -67,7 +103,7 @@ func NewOIDCProvider(issuerURL, clientID, clientSecret, redirectURL, logoutURL s
oidcVerifier: provider.Verifier(&oidc.Config{ oidcVerifier: provider.Verifier(&oidc.Config{
ClientID: clientID, ClientID: clientID,
}), }),
oidcLogoutURL: logout, oidcEndSessionURL: endSessionURL,
allowedUsers: allowedUsers, allowedUsers: allowedUsers,
allowedGroups: allowedGroups, allowedGroups: allowedGroups,
}, nil }, nil
@ -80,7 +116,6 @@ func NewOIDCProviderFromEnv() (*OIDCProvider, error) {
common.OIDCClientID, common.OIDCClientID,
common.OIDCClientSecret, common.OIDCClientSecret,
common.OIDCRedirectURL, common.OIDCRedirectURL,
common.OIDCLogoutURL,
common.OIDCAllowedUsers, common.OIDCAllowedUsers,
common.OIDCAllowedGroups, common.OIDCAllowedGroups,
) )
@ -130,7 +165,7 @@ func (auth *OIDCProvider) CheckToken(r *http.Request) error {
// Logical AND between allowed users and groups. // Logical AND between allowed users and groups.
allowedUser := slices.Contains(auth.allowedUsers, claims.Username) allowedUser := slices.Contains(auth.allowedUsers, claims.Username)
allowedGroup := len(CE.Intersect(claims.Groups, auth.allowedGroups)) > 0 allowedGroup := len(utils.Intersect(claims.Groups, auth.allowedGroups)) > 0
if !allowedUser && !allowedGroup { if !allowedUser && !allowedGroup {
return ErrUserNotAllowed return ErrUserNotAllowed
} }
@ -235,7 +270,7 @@ func (auth *OIDCProvider) LoginCallbackHandler(w http.ResponseWriter, r *http.Re
} }
func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) { func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.Request) {
if auth.oidcLogoutURL == nil { if auth.oidcEndSessionURL == nil {
DefaultLogoutCallbackHandler(auth, w, r) DefaultLogoutCallbackHandler(auth, w, r)
return return
} }
@ -247,7 +282,7 @@ func (auth *OIDCProvider) LogoutCallbackHandler(w http.ResponseWriter, r *http.R
} }
clearTokenCookie(w, r, auth.TokenCookieName()) clearTokenCookie(w, r, auth.TokenCookieName())
logoutURL := *auth.oidcLogoutURL logoutURL := *auth.oidcEndSessionURL
logoutURL.Query().Add("id_token_hint", token.Value) logoutURL.Query().Add("id_token_hint", token.Value)
http.Redirect(w, r, logoutURL.String(), http.StatusFound) http.Redirect(w, r, logoutURL.String(), http.StatusFound)

View file

@ -46,7 +46,6 @@ var (
// OIDC Configuration. // OIDC Configuration.
OIDCIssuerURL = GetEnvString("OIDC_ISSUER_URL", "") OIDCIssuerURL = GetEnvString("OIDC_ISSUER_URL", "")
OIDCLogoutURL = GetEnvString("OIDC_LOGOUT_URL", "")
OIDCClientID = GetEnvString("OIDC_CLIENT_ID", "") OIDCClientID = GetEnvString("OIDC_CLIENT_ID", "")
OIDCClientSecret = GetEnvString("OIDC_CLIENT_SECRET", "") OIDCClientSecret = GetEnvString("OIDC_CLIENT_SECRET", "")
OIDCRedirectURL = GetEnvString("OIDC_REDIRECT_URL", "") OIDCRedirectURL = GetEnvString("OIDC_REDIRECT_URL", "")

View file

@ -80,11 +80,12 @@ func (amw *oidcMiddleware) before(w http.ResponseWriter, r *http.Request) (proce
return false return false
} }
if r.URL.Path == auth.OIDCLogoutPath {
amw.auth.LogoutCallbackHandler(w, r)
}
if err := amw.auth.CheckToken(r); err != nil { if err := amw.auth.CheckToken(r); err != nil {
if errors.Is(err, auth.ErrMissingToken) { if errors.Is(err, auth.ErrMissingToken) {
amw.authMux.ServeHTTP(w, r) amw.authMux.ServeHTTP(w, r)
} else if r.URL.Path == auth.OIDCLogoutPath {
amw.auth.LogoutCallbackHandler(w, r)
} else { } else {
auth.WriteBlockPage(w, http.StatusForbidden, err.Error(), auth.OIDCLogoutPath) auth.WriteBlockPage(w, http.StatusForbidden, err.Error(), auth.OIDCLogoutPath)
} }