github.com/argoproj/argo-cd/v3@v3.2.1/server/logout/logout.go (about)

     1  package logout
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"regexp"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/golang-jwt/jwt/v5"
    11  	log "github.com/sirupsen/logrus"
    12  
    13  	"github.com/argoproj/argo-cd/v3/common"
    14  	httputil "github.com/argoproj/argo-cd/v3/util/http"
    15  	jwtutil "github.com/argoproj/argo-cd/v3/util/jwt"
    16  	"github.com/argoproj/argo-cd/v3/util/session"
    17  	"github.com/argoproj/argo-cd/v3/util/settings"
    18  )
    19  
    20  // NewHandler creates handler serving to do api/logout endpoint
    21  func NewHandler(settingsMrg *settings.SettingsManager, sessionMgr *session.SessionManager, rootPath, baseHRef string) *Handler {
    22  	return &Handler{
    23  		settingsMgr: settingsMrg,
    24  		rootPath:    rootPath,
    25  		baseHRef:    baseHRef,
    26  		verifyToken: sessionMgr.VerifyToken,
    27  		revokeToken: sessionMgr.RevokeToken,
    28  	}
    29  }
    30  
    31  type Handler struct {
    32  	settingsMgr *settings.SettingsManager
    33  	rootPath    string
    34  	verifyToken func(tokenString string) (jwt.Claims, string, error)
    35  	revokeToken func(ctx context.Context, id string, expiringAt time.Duration) error
    36  	baseHRef    string
    37  }
    38  
    39  var (
    40  	tokenPattern             = regexp.MustCompile(`{{token}}`)
    41  	logoutRedirectURLPattern = regexp.MustCompile(`{{logoutRedirectURL}}`)
    42  )
    43  
    44  func constructLogoutURL(logoutURL, token, logoutRedirectURL string) string {
    45  	constructedLogoutURL := tokenPattern.ReplaceAllString(logoutURL, token)
    46  	return logoutRedirectURLPattern.ReplaceAllString(constructedLogoutURL, logoutRedirectURL)
    47  }
    48  
    49  // ServeHTTP is the logout handler for ArgoCD and constructs OIDC logout URL and redirects to it for OIDC issued sessions,
    50  // and redirects user to '/login' for argocd issued sessions
    51  func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    52  	var tokenString string
    53  	var oidcConfig *settings.OIDCConfig
    54  
    55  	argoCDSettings, err := h.settingsMgr.GetSettings()
    56  	if err != nil {
    57  		w.WriteHeader(http.StatusInternalServerError)
    58  		http.Error(w, "Failed to retrieve argoCD settings: "+err.Error(), http.StatusInternalServerError)
    59  		return
    60  	}
    61  
    62  	argoURL, err := argoCDSettings.ArgoURLForRequest(r)
    63  	if err != nil {
    64  		log.Warnf("unable to find ArgoCD URL from config: %v", err)
    65  	}
    66  	if argoURL == "" {
    67  		// golang does not provide any easy way to determine scheme of current request
    68  		// so redirecting ot http which will auto-redirect too https if necessary
    69  		host := strings.TrimRight(r.Host, "/")
    70  		argoURL = "http://" + host + "/" + strings.TrimRight(strings.TrimLeft(h.rootPath, "/"), "/")
    71  	}
    72  
    73  	logoutRedirectURL := strings.TrimRight(strings.TrimLeft(argoURL, "/"), "/")
    74  
    75  	cookies := r.Cookies()
    76  	tokenString, err = httputil.JoinCookies(common.AuthCookieName, cookies)
    77  	if tokenString == "" || err != nil {
    78  		w.WriteHeader(http.StatusBadRequest)
    79  		http.Error(w, "Failed to retrieve ArgoCD auth token: "+err.Error(), http.StatusBadRequest)
    80  		return
    81  	}
    82  
    83  	for _, cookie := range cookies {
    84  		if !strings.HasPrefix(cookie.Name, common.AuthCookieName) {
    85  			continue
    86  		}
    87  
    88  		argocdCookie := http.Cookie{
    89  			Name:  cookie.Name,
    90  			Value: "",
    91  		}
    92  
    93  		argocdCookie.Path = "/" + strings.TrimRight(strings.TrimLeft(h.baseHRef, "/"), "/")
    94  		w.Header().Add("Set-Cookie", argocdCookie.String())
    95  	}
    96  
    97  	claims, _, err := h.verifyToken(tokenString)
    98  	if err != nil {
    99  		http.Redirect(w, r, logoutRedirectURL, http.StatusSeeOther)
   100  		return
   101  	}
   102  
   103  	mapClaims, err := jwtutil.MapClaims(claims)
   104  	if err != nil {
   105  		http.Redirect(w, r, logoutRedirectURL, http.StatusSeeOther)
   106  		return
   107  	}
   108  
   109  	issuer := jwtutil.StringField(mapClaims, "iss")
   110  	id := jwtutil.StringField(mapClaims, "jti")
   111  	if exp, err := jwtutil.ExpirationTime(mapClaims); err == nil && id != "" {
   112  		if err := h.revokeToken(context.Background(), id, time.Until(exp)); err != nil {
   113  			log.Warnf("failed to invalidate token '%s': %v", id, err)
   114  		}
   115  	}
   116  
   117  	if argoCDSettings.OIDCConfig() == nil || argoCDSettings.OIDCConfig().LogoutURL == "" || issuer == session.SessionManagerClaimsIssuer {
   118  		http.Redirect(w, r, logoutRedirectURL, http.StatusSeeOther)
   119  	} else {
   120  		oidcConfig = argoCDSettings.OIDCConfig()
   121  		logoutURL := constructLogoutURL(oidcConfig.LogoutURL, tokenString, logoutRedirectURL)
   122  		http.Redirect(w, r, logoutURL, http.StatusSeeOther)
   123  	}
   124  }