mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat(BE-281): adjustment sso redirect,adjustment response closing,adjustment uniformity
This commit is contained in:
@@ -144,6 +144,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
|
||||
refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh")
|
||||
refreshToken := strings.TrimSpace(c.Cookies(refreshName))
|
||||
if refreshToken == "" {
|
||||
if target := buildStartRedirect(defaultSSOClientAlias()); target != "" {
|
||||
return c.Redirect(target, fiber.StatusFound)
|
||||
}
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
|
||||
}
|
||||
|
||||
@@ -174,6 +177,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
|
||||
if resp.StatusCode == fiber.StatusTooManyRequests {
|
||||
return fiber.NewError(fiber.StatusTooManyRequests, "Too many attempts, please slow down")
|
||||
}
|
||||
if target := buildStartRedirect(defaultSSOClientAlias()); target != "" {
|
||||
return c.Redirect(target, fiber.StatusFound)
|
||||
}
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
|
||||
}
|
||||
|
||||
@@ -425,6 +431,7 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
|
||||
refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh")
|
||||
|
||||
var accessToken, refreshToken string
|
||||
var verification *sso.VerificationResult
|
||||
if accessName != "" {
|
||||
accessToken = strings.TrimSpace(c.Cookies(accessName))
|
||||
}
|
||||
@@ -446,9 +453,10 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
if hadAccessCookie {
|
||||
if verification, err := sso.VerifyAccessToken(accessToken); err != nil {
|
||||
if v, err := sso.VerifyAccessToken(accessToken); err != nil {
|
||||
utils.Log.WithError(err).Warn("failed to verify access token during logout")
|
||||
} else {
|
||||
verification = v
|
||||
if revoker := session.GetRevocationStore(); revoker != nil {
|
||||
if err := revoker.MarkUserLogout(c.Context(), verification.UserID, time.Now().UTC()); err != nil {
|
||||
utils.Log.WithError(err).Warn("failed to mark user logout")
|
||||
@@ -475,6 +483,28 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
|
||||
} 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
|
||||
@@ -494,6 +524,177 @@ func (h *Controller) Logout(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "signed out"})
|
||||
}
|
||||
|
||||
func singleSSOClient() (string, config.SSOClientConfig, bool) {
|
||||
if len(config.SSOClients) != 1 {
|
||||
return "", config.SSOClientConfig{}, false
|
||||
}
|
||||
for alias, cfg := range config.SSOClients {
|
||||
if strings.TrimSpace(alias) == "" || strings.TrimSpace(cfg.PublicID) == "" {
|
||||
return "", config.SSOClientConfig{}, false
|
||||
}
|
||||
return alias, cfg, true
|
||||
}
|
||||
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 {
|
||||
if strings.TrimSpace(alias) == "" {
|
||||
continue
|
||||
}
|
||||
return alias
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildStartRedirect(alias string) string {
|
||||
alias = strings.TrimSpace(alias)
|
||||
if alias == "" {
|
||||
return ""
|
||||
}
|
||||
return "/api/sso/start?client=" + url.QueryEscape(alias)
|
||||
}
|
||||
|
||||
func (h *Controller) revokeToken(ctx context.Context, token string, verification *sso.VerificationResult) {
|
||||
if h.revoker == nil || verification == nil || verification.Claims == nil {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user