mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
go.mod still declared `module github.com/hanzoai/deploy` after the repo was renamed. A Go module path is not cosmetic — it is the resolvable location. So `go get github.com/hanzoai/cd` could not work, and anything importing this module had to spell a name that no longer exists at that address. The build log of v3.6.0 is where it surfaced: the compiler was still emitting github.com/hanzoai/deploy/cmd/cd/commands. 3541 import paths across 690 .go, 19 .proto, 4 .yaml, 3 .sh, 2 .mod, Procfile and Makefile. Plus the 62 ghcr.io/hanzoai/deploy image references, moved to ghcr.io/hanzoai/cd to match hanzo.yml's build target — a distinct string from the module path, replaced separately so neither could clobber the other. go mod tidy clean, CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./... clean. Co-authored-by: Hanzo Dev <[email protected]>
141 lines
4.4 KiB
Go
141 lines
4.4 KiB
Go
package logout
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/hanzoai/cd/common"
|
|
httputil "github.com/hanzoai/cd/util/http"
|
|
jwtutil "github.com/hanzoai/cd/util/jwt"
|
|
"github.com/hanzoai/cd/util/session"
|
|
"github.com/hanzoai/cd/util/settings"
|
|
)
|
|
|
|
// NewHandler creates handler serving to do api/logout endpoint
|
|
func NewHandler(settingsMrg *settings.SettingsManager, sessionMgr *session.SessionManager, rootPath, baseHRef string) *Handler {
|
|
return &Handler{
|
|
settingsMgr: settingsMrg,
|
|
rootPath: rootPath,
|
|
baseHRef: baseHRef,
|
|
verifyToken: sessionMgr.VerifyToken,
|
|
revokeToken: sessionMgr.RevokeToken,
|
|
}
|
|
}
|
|
|
|
type Handler struct {
|
|
settingsMgr *settings.SettingsManager
|
|
rootPath string
|
|
verifyToken func(ctx context.Context, tokenString string) (jwt.Claims, string, error)
|
|
revokeToken func(ctx context.Context, id string, expiringAt time.Duration) error
|
|
baseHRef string
|
|
}
|
|
|
|
var (
|
|
tokenPattern = regexp.MustCompile(`{{token}}`)
|
|
logoutRedirectURLPattern = regexp.MustCompile(`{{logoutRedirectURL}}`)
|
|
)
|
|
|
|
func constructLogoutURL(logoutURL, token, logoutRedirectURL string) string {
|
|
constructedLogoutURL := tokenPattern.ReplaceAllString(logoutURL, token)
|
|
return logoutRedirectURLPattern.ReplaceAllString(constructedLogoutURL, logoutRedirectURL)
|
|
}
|
|
|
|
// ServeHTTP is the logout handler for Hanzo CD and constructs OIDC logout URL and redirects to it for OIDC issued sessions,
|
|
// and redirects user to '/login' for cd issued sessions
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
var tokenString string
|
|
var oidcConfig *settings.OIDCConfig
|
|
|
|
argoCDSettings, err := h.settingsMgr.GetSettings()
|
|
if err != nil {
|
|
http.Error(w, "Failed to retrieve argoCD settings: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
argoURL, err := argoCDSettings.ArgoURLForRequest(r)
|
|
if err != nil {
|
|
log.Warnf("unable to find Hanzo CD URL from config: %v", err)
|
|
}
|
|
if argoURL == "" {
|
|
// golang does not provide any easy way to determine scheme of current request
|
|
// so redirecting ot http which will auto-redirect too https if necessary
|
|
host := strings.TrimRight(r.Host, "/")
|
|
argoURL = "http://" + host + "/" + strings.TrimRight(strings.TrimLeft(h.rootPath, "/"), "/")
|
|
}
|
|
|
|
logoutRedirectURL := strings.TrimRight(strings.TrimLeft(argoURL, "/"), "/")
|
|
|
|
cookies := r.Cookies()
|
|
tokenString, err = httputil.JoinCookies(common.AuthCookieName, cookies)
|
|
// Build message safely: only include err when non-nil
|
|
if err != nil {
|
|
http.Error(w, "Failed to retrieve Hanzo CD auth token: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if tokenString == "" {
|
|
http.Error(w, "Failed to retrieve Hanzo CD auth token", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
for _, cookie := range cookies {
|
|
if !strings.HasPrefix(cookie.Name, common.AuthCookieName) {
|
|
continue
|
|
}
|
|
|
|
cdCookie := http.Cookie{
|
|
Name: cookie.Name,
|
|
Value: "",
|
|
}
|
|
|
|
cdCookie.Path = "/" + strings.TrimRight(strings.TrimLeft(h.baseHRef, "/"), "/")
|
|
w.Header().Add("Set-Cookie", cdCookie.String())
|
|
}
|
|
|
|
claims, _, err := h.verifyToken(r.Context(), tokenString)
|
|
if err != nil {
|
|
http.Redirect(w, r, logoutRedirectURL, http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
mapClaims, err := jwtutil.MapClaims(claims)
|
|
if err != nil {
|
|
http.Redirect(w, r, logoutRedirectURL, http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
issuer := jwtutil.StringField(mapClaims, "iss")
|
|
id := jwtutil.StringField(mapClaims, "jti")
|
|
// Workaround for Dex token, because does not have jti.
|
|
if id == "" {
|
|
id = jwtutil.StringField(mapClaims, "at_hash")
|
|
}
|
|
|
|
if exp, err := jwtutil.ExpirationTime(mapClaims); err == nil && id != "" {
|
|
ttl := time.Until(exp)
|
|
if ttl <= 0 {
|
|
// Token already expired; no need to persist revocation
|
|
log.Infof("token '%s' already expired, skipping revocation", id)
|
|
} else {
|
|
revokeCtx, cancel := context.WithTimeout(r.Context(), common.TokenRevocationTimeout)
|
|
defer cancel()
|
|
if err := h.revokeToken(revokeCtx, id, ttl); err != nil {
|
|
log.Warnf("failed to invalidate token '%s': %v", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if argoCDSettings.OIDCConfig() == nil || argoCDSettings.OIDCConfig().LogoutURL == "" || issuer == session.SessionManagerClaimsIssuer {
|
|
http.Redirect(w, r, logoutRedirectURL, http.StatusSeeOther)
|
|
} else {
|
|
oidcConfig = argoCDSettings.OIDCConfig()
|
|
logoutURL := constructLogoutURL(oidcConfig.LogoutURL, tokenString, logoutRedirectURL)
|
|
http.Redirect(w, r, logoutURL, http.StatusSeeOther)
|
|
}
|
|
}
|