Merge branch 'feat/BE/sso-adjustment' into 'development'

Feat/be/sso adjustment

See merge request mbugroup/lti-api!273
This commit is contained in:
Adnan Zahir
2026-01-29 18:46:52 +07:00
4 changed files with 60 additions and 16 deletions
+5
View File
@@ -61,6 +61,7 @@ var (
SSOCookieDomain string
SSOCookieSecure bool
SSOCookieSameSite string
SSOAccessTokenMaxBytes int
SSOTokenBlacklistPrefix string
SSOPKCETTL time.Duration
SSOUserSyncDrift time.Duration
@@ -144,6 +145,10 @@ func init() {
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax")
SSOAccessTokenMaxBytes = viper.GetInt("SSO_ACCESS_TOKEN_MAX_BYTES")
if SSOAccessTokenMaxBytes <= 0 {
SSOAccessTokenMaxBytes = 4096
}
SSOTokenBlacklistPrefix = defaultString(viper.GetString("SSO_TOKEN_BLACKLIST_PREFIX"), "sso:blacklist")
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
SSOPKCETTL = time.Duration(ttl) * time.Second
@@ -200,7 +200,7 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusUnauthorized, "invalid access token")
}
issueCookies(c, struct {
if err := issueCookies(c, struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
@@ -218,7 +218,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
IDToken: tokenResp.IDToken,
Error: tokenResp.Error,
Description: tokenResp.Description,
}, verification)
}, verification); err != nil {
return err
}
utils.Log.WithFields(logrus.Fields{
"user_id": verification.UserID,
@@ -307,7 +309,9 @@ func (h *Controller) Callback(c *fiber.Ctx) error {
}
// prepare cookies
issueCookies(c, tokenResp, verification)
if err := issueCookies(c, tokenResp, verification); err != nil {
return err
}
redirectTarget := sessionData.ReturnTo
if redirectTarget == "" {
@@ -742,13 +746,21 @@ func issueCookies(c *fiber.Ctx, tokenResp struct {
IDToken string `json:"id_token"`
Error string `json:"error"`
Description string `json:"error_description"`
}, verification *sso.VerificationResult) {
}, verification *sso.VerificationResult) error {
if revoker := session.GetRevocationStore(); revoker != nil && verification != nil {
if err := revoker.ClearUserLogout(c.Context(), verification.UserID); err != nil {
utils.Log.WithError(err).Warn("failed to clear logout marker")
}
}
if max := config.SSOAccessTokenMaxBytes; max > 0 && len(tokenResp.AccessToken) > max {
utils.Log.WithFields(logrus.Fields{
"token_len": len(tokenResp.AccessToken),
"max_len": max,
}).Warn("sso access token exceeds cookie size limit")
return fiber.NewError(fiber.StatusRequestEntityTooLarge, "access token too large")
}
accessName := resolveSSOCookieName(config.SSOAccessCookieName, "access")
refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh")
maxAge := tokenResp.ExpiresIn
@@ -790,6 +802,7 @@ func issueCookies(c *fiber.Ctx, tokenResp struct {
// Optional: expose limited info via headers for FE debugging (avoid tokens)
c.Set("X-Auth-User", fmt.Sprintf("%d", verification.UserID))
return nil
}
func clearSSOCookie(c *fiber.Ctx, name string) {
@@ -291,6 +291,8 @@ func (h *UserSyncController) upsertUser(c *fiber.Ctx, alias string, req *userSyn
"user_id": req.User.ID,
}).Info("sso user synced")
sso.InvalidateProfileCache(c.Context(), uint(req.User.ID))
msg := fmt.Sprintf("User %s successfully", req.Action)
return c.Status(fiber.StatusOK).JSON(response.Success{
Code: fiber.StatusOK,
@@ -318,6 +320,8 @@ func (h *UserSyncController) logoutUser(c *fiber.Ctx, alias string, req *userSyn
"user_id": req.User.ID,
}).Info("sso user logout enforced")
sso.InvalidateProfileCache(c.Context(), uint(req.User.ID))
return c.Status(fiber.StatusOK).JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
@@ -341,6 +345,8 @@ func (h *UserSyncController) removeUser(c *fiber.Ctx, alias string, req *userSyn
"user_id": req.User.ID,
}).Info("sso user deleted")
sso.InvalidateProfileCache(c.Context(), uint(req.User.ID))
return c.Status(fiber.StatusOK).JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
+32 -12
View File
@@ -265,24 +265,44 @@ func profileCacheKey(userID uint) string {
return profileCachePrefix + strconv.FormatUint(uint64(userID), 10)
}
// InvalidateProfileCache clears cached profile data for the given user in both local and Redis caches.
func InvalidateProfileCache(ctx context.Context, userID uint) {
if userID == 0 {
return
}
key := profileCacheKey(userID)
profileLocalCache.Delete(key)
client := cache.Redis()
if client == nil {
return
}
if ctx == nil {
ctx = context.Background()
}
if err := client.Del(ctx, key).Err(); err != nil && !errors.Is(err, redis.Nil) {
utils.Log.WithError(err).Warn("sso profile redis delete failed")
}
}
func canonicalPermissionName(name string) string {
return strings.ToLower(strings.TrimSpace(name))
}
// userInfoEnvelope handles the varying shapes returned by the SSO userinfo endpoint.
type userInfoEnvelope struct {
Roles []userInfoRole `json:"roles"`
AreaIDs []uint `json:"area_ids"`
LocationIDs []uint `json:"location_ids"`
AllArea bool `json:"all_area"`
AllLocation bool `json:"all_location"`
Data *struct {
ID int64 `json:"id"`
Roles []userInfoRole `json:"roles"`
AreaIDs []uint `json:"area_ids"`
LocationIDs []uint `json:"location_ids"`
AllArea bool `json:"all_area"`
AllLocation bool `json:"all_location"`
Roles []userInfoRole `json:"roles"`
AreaIDs []uint `json:"area_ids"`
LocationIDs []uint `json:"location_ids"`
AllArea bool `json:"all_area"`
AllLocation bool `json:"all_location"`
Data *struct {
ID int64 `json:"id"`
Roles []userInfoRole `json:"roles"`
AreaIDs []uint `json:"area_ids"`
LocationIDs []uint `json:"location_ids"`
AllArea bool `json:"all_area"`
AllLocation bool `json:"all_location"`
} `json:"data"`
User *struct {
ID int64 `json:"id"`