mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
[FEAT/BE] fix return backend without payload logout
This commit is contained in:
@@ -428,13 +428,6 @@ func (h *Controller) UserInfo(c *fiber.Ctx) error {
|
|||||||
return fiber.NewError(fiber.StatusBadGateway, "invalid user profile response")
|
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 != "" {
|
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||||
c.Set("Content-Type", ct)
|
c.Set("Content-Type", ct)
|
||||||
} else {
|
} else {
|
||||||
@@ -446,17 +439,9 @@ func (h *Controller) UserInfo(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// Logout clears SSO cookies and removes any leftover PKCE session state.
|
// Logout clears SSO cookies and removes any leftover PKCE session state.
|
||||||
func (h *Controller) Logout(c *fiber.Ctx) error {
|
func (h *Controller) Logout(c *fiber.Ctx) error {
|
||||||
requestedAlias := normalizeClientParam(c.Query("client"))
|
alias := ""
|
||||||
if requestedAlias == "" {
|
if singleAlias, _, ok := singleSSOClient(); ok {
|
||||||
requestedAlias = normalizeClientParam(c.Query("client_id"))
|
alias = singleAlias
|
||||||
}
|
|
||||||
var (
|
|
||||||
alias string
|
|
||||||
cfg config.SSOClientConfig
|
|
||||||
hasClientInfo bool
|
|
||||||
)
|
|
||||||
if requestedAlias != "" {
|
|
||||||
alias, cfg, hasClientInfo = findSSOClientConfig(requestedAlias)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
accessName := resolveSSOCookieName(config.SSOAccessCookieName, "access")
|
accessName := resolveSSOCookieName(config.SSOAccessCookieName, "access")
|
||||||
@@ -473,14 +458,7 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
|
|||||||
hadAccessCookie := accessToken != ""
|
hadAccessCookie := accessToken != ""
|
||||||
hadRefreshCookie := refreshToken != ""
|
hadRefreshCookie := refreshToken != ""
|
||||||
|
|
||||||
state := strings.TrimSpace(c.Query("state"))
|
if !hadAccessCookie && !hadRefreshCookie {
|
||||||
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 == "" {
|
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "not authenticated")
|
return fiber.NewError(fiber.StatusUnauthorized, "not authenticated")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,52 +483,20 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
|
|||||||
clearSSOCookie(c, refreshName)
|
clearSSOCookie(c, refreshName)
|
||||||
|
|
||||||
redirectTarget := ""
|
redirectTarget := ""
|
||||||
rawReturn := strings.TrimSpace(c.Query("return_to"))
|
if config.SSOPortalURL != "" {
|
||||||
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
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.Log.WithFields(logrus.Fields{
|
utils.Log.WithFields(logrus.Fields{
|
||||||
"client": alias,
|
"client": alias,
|
||||||
"state": state,
|
|
||||||
"redirect": redirectTarget,
|
"redirect": redirectTarget,
|
||||||
}).Info("sso logout completed")
|
}).Info("sso logout completed")
|
||||||
|
|
||||||
if redirectTarget != "" {
|
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"})
|
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
|
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 {
|
func defaultSSOClientAlias() string {
|
||||||
for alias := range config.SSOClients {
|
for alias := range config.SSOClients {
|
||||||
@@ -897,98 +704,6 @@ func normalizeClientParam(raw string) string {
|
|||||||
return strings.ToLower(value)
|
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) {
|
func findSSOClientConfig(requestedAlias string) (string, config.SSOClientConfig, bool) {
|
||||||
if requestedAlias == "" {
|
if requestedAlias == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user