From daca97f113d630ebcf2916abf0717d6df9d921ac Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 26 Feb 2026 11:17:13 +0700 Subject: [PATCH] [FEAT/BE] fix return backend without payload logout --- .../modules/sso/controllers/sso.controller.go | 305 +----------------- 1 file changed, 10 insertions(+), 295 deletions(-) diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index 41ece390..d35bf78e 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -428,13 +428,6 @@ func (h *Controller) UserInfo(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadGateway, "invalid user profile response") } - // if sanitized, perms, ok := sanitizeUserInfoPayload(body); ok { - // if caps := capabilities.FromPermissions(perms); len(caps) > 0 { - // injectCapabilities(sanitized, caps) - // } - // return c.Status(resp.StatusCode).JSON(sanitized) - // } - if ct := resp.Header.Get("Content-Type"); ct != "" { c.Set("Content-Type", ct) } else { @@ -446,17 +439,9 @@ func (h *Controller) UserInfo(c *fiber.Ctx) error { // Logout clears SSO cookies and removes any leftover PKCE session state. func (h *Controller) Logout(c *fiber.Ctx) error { - requestedAlias := normalizeClientParam(c.Query("client")) - if requestedAlias == "" { - requestedAlias = normalizeClientParam(c.Query("client_id")) - } - var ( - alias string - cfg config.SSOClientConfig - hasClientInfo bool - ) - if requestedAlias != "" { - alias, cfg, hasClientInfo = findSSOClientConfig(requestedAlias) + alias := "" + if singleAlias, _, ok := singleSSOClient(); ok { + alias = singleAlias } accessName := resolveSSOCookieName(config.SSOAccessCookieName, "access") @@ -473,14 +458,7 @@ func (h *Controller) Logout(c *fiber.Ctx) error { hadAccessCookie := accessToken != "" hadRefreshCookie := refreshToken != "" - state := strings.TrimSpace(c.Query("state")) - if state != "" { - if err := h.store.Delete(c.Context(), state); err != nil { - utils.Log.Warnf("failed to delete pkce session during logout: %v", err) - } - } - - if !hadAccessCookie && !hadRefreshCookie && state == "" { + if !hadAccessCookie && !hadRefreshCookie { return fiber.NewError(fiber.StatusUnauthorized, "not authenticated") } @@ -505,52 +483,20 @@ func (h *Controller) Logout(c *fiber.Ctx) error { clearSSOCookie(c, refreshName) redirectTarget := "" - rawReturn := strings.TrimSpace(c.Query("return_to")) - if hasClientInfo { - if rawReturn == "" { - rawReturn = cfg.DefaultReturnURI - } - if normalized, err := normalizeReturnTarget(rawReturn, cfg); err == nil { - redirectTarget = normalized - } else if rawReturn != "" { - utils.Log.WithError(err).Warn("invalid return_to during logout") - } - } else if rawReturn == "" && config.SSOPortalURL != "" { - if alias, singleCfg, ok := singleClientFromToken(verification); ok { - if normalized, err := normalizeReturnTarget(singleCfg.DefaultReturnURI, singleCfg); err == nil && normalized != "" { - redirectTarget = normalized - alias, cfg, hasClientInfo = alias, singleCfg, true - } else { - redirectTarget = config.SSOPortalURL - } - } else if accessToken != "" { - if alias, singleCfg, ok := h.singleClientFromSSO(c.Context(), accessToken); ok { - if normalized, err := normalizeReturnTarget(singleCfg.DefaultReturnURI, singleCfg); err == nil && normalized != "" { - redirectTarget = normalized - alias, cfg, hasClientInfo = alias, singleCfg, true - } else { - redirectTarget = config.SSOPortalURL - } - } else { - redirectTarget = config.SSOPortalURL - } - } else { - redirectTarget = config.SSOPortalURL - } - } else if rawReturn != "" { - if strings.HasPrefix(rawReturn, "/") && !strings.HasPrefix(rawReturn, "//") { - redirectTarget = rawReturn - } + if config.SSOPortalURL != "" { + redirectTarget = config.SSOPortalURL } utils.Log.WithFields(logrus.Fields{ "client": alias, - "state": state, "redirect": redirectTarget, }).Info("sso logout completed") if redirectTarget != "" { - return c.Redirect(redirectTarget, fiber.StatusFound) + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "status": "signed out", + "redirect": redirectTarget, + }) } return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "signed out"}) @@ -569,145 +515,6 @@ func singleSSOClient() (string, config.SSOClientConfig, bool) { return "", config.SSOClientConfig{}, false } -func singleClientFromToken(verification *sso.VerificationResult) (string, config.SSOClientConfig, bool) { - if verification == nil || verification.Claims == nil { - return "", config.SSOClientConfig{}, false - } - return singleClientFromScopes(verification.Claims.Scopes()) -} - -func (h *Controller) singleClientFromSSO(ctx context.Context, accessToken string) (string, config.SSOClientConfig, bool) { - accessToken = strings.TrimSpace(accessToken) - if accessToken == "" { - return "", config.SSOClientConfig{}, false - } - meURL := strings.TrimSpace(config.SSOGetMeURL) - if meURL == "" { - return "", config.SSOClientConfig{}, false - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil) - if err != nil { - utils.Log.WithError(err).Warn("failed to build SSO getme request") - return "", config.SSOClientConfig{}, false - } - req.Header.Set("Authorization", "Bearer "+accessToken) - - resp, err := h.httpClient.Do(req) - if err != nil { - utils.Log.WithError(err).Warn("SSO getme request failed") - return "", config.SSOClientConfig{}, false - } - defer resp.Body.Close() - - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { - utils.Log.WithField("status", resp.StatusCode).Warn("SSO getme responded with error") - return "", config.SSOClientConfig{}, false - } - - var payload struct { - Data struct { - Roles []struct { - Client *struct { - Alias string `json:"alias"` - } `json:"client"` - } `json:"roles"` - } `json:"data"` - } - if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { - utils.Log.WithError(err).Warn("failed to decode SSO getme response") - return "", config.SSOClientConfig{}, false - } - - aliases := make(map[string]struct{}) - for _, role := range payload.Data.Roles { - if role.Client == nil { - continue - } - alias := strings.ToLower(strings.TrimSpace(role.Client.Alias)) - if alias != "" { - aliases[alias] = struct{}{} - } - } - if len(aliases) != 1 { - return "", config.SSOClientConfig{}, false - } - for alias := range aliases { - if normalized, cfg, ok := findClientAlias(alias); ok { - return normalized, cfg, true - } - return "", config.SSOClientConfig{}, false - } - return "", config.SSOClientConfig{}, false -} - -func singleClientFromScopes(scopes []string) (string, config.SSOClientConfig, bool) { - if len(scopes) == 0 { - return "", config.SSOClientConfig{}, false - } - seen := make(map[string]struct{}) - for _, scope := range scopes { - if alias, ok := matchClientAliasFromScope(scope); ok { - seen[alias] = struct{}{} - } - if len(seen) > 1 { - return "", config.SSOClientConfig{}, false - } - } - if len(seen) != 1 { - return "", config.SSOClientConfig{}, false - } - for alias := range seen { - if normalized, cfg, ok := findClientAlias(alias); ok { - return normalized, cfg, true - } - } - return "", config.SSOClientConfig{}, false -} - -func matchClientAliasFromScope(scope string) (string, bool) { - scope = strings.ToLower(strings.TrimSpace(scope)) - if scope == "" { - return "", false - } - prefix := scope - if idx := strings.IndexAny(prefix, ".:"); idx > 0 { - prefix = prefix[:idx] - } - if prefix == "" { - return "", false - } - if alias, _, ok := findClientAlias(prefix); ok { - return alias, true - } - if prefix == "user-management" { - if alias, _, ok := findClientAlias("umgmt"); ok { - return alias, true - } - } - if prefix == "umgmt" { - if alias, _, ok := findClientAlias("user-management"); ok { - return alias, true - } - } - return "", false -} - -func findClientAlias(alias string) (string, config.SSOClientConfig, bool) { - alias = strings.TrimSpace(alias) - if alias == "" { - return "", config.SSOClientConfig{}, false - } - if cfg, ok := config.SSOClients[alias]; ok && strings.TrimSpace(cfg.PublicID) != "" { - return alias, cfg, true - } - for key, cfg := range config.SSOClients { - if strings.EqualFold(key, alias) && strings.TrimSpace(cfg.PublicID) != "" { - return key, cfg, true - } - } - return "", config.SSOClientConfig{}, false -} func defaultSSOClientAlias() string { for alias := range config.SSOClients { @@ -897,98 +704,6 @@ func normalizeClientParam(raw string) string { return strings.ToLower(value) } -func sanitizeUserInfoPayload(body []byte) (map[string]any, []string, bool) { - if len(body) == 0 { - return map[string]any{}, nil, true - } - - var payload any - if err := json.Unmarshal(body, &payload); err != nil { - return nil, nil, false - } - - perms := collectPermissionNames(payload) - - sensitive := map[string]struct{}{ - "roles": {}, - "permissions": {}, - } - payload = scrubSensitiveKeys(payload, sensitive) - - sanitized, ok := payload.(map[string]any) - if !ok { - sanitized = map[string]any{"data": payload} - } - - return sanitized, perms, true -} - -func scrubSensitiveKeys(value any, sensitive map[string]struct{}) any { - switch v := value.(type) { - case map[string]any: - for key, val := range v { - if _, ok := sensitive[strings.ToLower(key)]; ok { - delete(v, key) - continue - } - v[key] = scrubSensitiveKeys(val, sensitive) - } - return v - case []any: - for i, item := range v { - v[i] = scrubSensitiveKeys(item, sensitive) - } - return v - default: - return value - } -} - -func collectPermissionNames(value any) []string { - names := make(map[string]struct{}) - collectPermissionRec(value, names) - out := make([]string, 0, len(names)) - for name := range names { - out = append(out, name) - } - return out -} - -func collectPermissionRec(value any, acc map[string]struct{}) { - switch v := value.(type) { - case map[string]any: - for key, val := range v { - if strings.EqualFold(key, "permissions") { - if arr, ok := val.([]any); ok { - for _, item := range arr { - if perm, ok := item.(map[string]any); ok { - if name, ok := perm["name"].(string); ok && strings.TrimSpace(name) != "" { - acc[strings.ToLower(strings.TrimSpace(name))] = struct{}{} - } - } - } - } - } else { - collectPermissionRec(val, acc) - } - } - case []any: - for _, item := range v { - collectPermissionRec(item, acc) - } - } -} - -func injectCapabilities(payload map[string]any, caps map[string]bool) { - if len(caps) == 0 { - return - } - if data, ok := payload["data"].(map[string]any); ok { - data["capabilities"] = caps - return - } - payload["capabilities"] = caps -} func findSSOClientConfig(requestedAlias string) (string, config.SSOClientConfig, bool) { if requestedAlias == "" {