mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh
This commit is contained in:
@@ -61,6 +61,7 @@ var (
|
|||||||
SSOCookieDomain string
|
SSOCookieDomain string
|
||||||
SSOCookieSecure bool
|
SSOCookieSecure bool
|
||||||
SSOCookieSameSite string
|
SSOCookieSameSite string
|
||||||
|
SSOAccessTokenMaxBytes int
|
||||||
SSOTokenBlacklistPrefix string
|
SSOTokenBlacklistPrefix string
|
||||||
SSOPKCETTL time.Duration
|
SSOPKCETTL time.Duration
|
||||||
SSOUserSyncDrift time.Duration
|
SSOUserSyncDrift time.Duration
|
||||||
@@ -144,6 +145,10 @@ func init() {
|
|||||||
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
|
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
|
||||||
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
|
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
|
||||||
SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax")
|
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")
|
SSOTokenBlacklistPrefix = defaultString(viper.GetString("SSO_TOKEN_BLACKLIST_PREFIX"), "sso:blacklist")
|
||||||
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
|
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
|
||||||
SSOPKCETTL = time.Duration(ttl) * time.Second
|
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")
|
return fiber.NewError(fiber.StatusUnauthorized, "invalid access token")
|
||||||
}
|
}
|
||||||
|
|
||||||
issueCookies(c, struct {
|
if err := issueCookies(c, struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
@@ -218,7 +218,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
|
|||||||
IDToken: tokenResp.IDToken,
|
IDToken: tokenResp.IDToken,
|
||||||
Error: tokenResp.Error,
|
Error: tokenResp.Error,
|
||||||
Description: tokenResp.Description,
|
Description: tokenResp.Description,
|
||||||
}, verification)
|
}, verification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
utils.Log.WithFields(logrus.Fields{
|
utils.Log.WithFields(logrus.Fields{
|
||||||
"user_id": verification.UserID,
|
"user_id": verification.UserID,
|
||||||
@@ -307,7 +309,9 @@ func (h *Controller) Callback(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prepare cookies
|
// prepare cookies
|
||||||
issueCookies(c, tokenResp, verification)
|
if err := issueCookies(c, tokenResp, verification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
redirectTarget := sessionData.ReturnTo
|
redirectTarget := sessionData.ReturnTo
|
||||||
if redirectTarget == "" {
|
if redirectTarget == "" {
|
||||||
@@ -742,13 +746,21 @@ func issueCookies(c *fiber.Ctx, tokenResp struct {
|
|||||||
IDToken string `json:"id_token"`
|
IDToken string `json:"id_token"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
Description string `json:"error_description"`
|
Description string `json:"error_description"`
|
||||||
}, verification *sso.VerificationResult) {
|
}, verification *sso.VerificationResult) error {
|
||||||
if revoker := session.GetRevocationStore(); revoker != nil && verification != nil {
|
if revoker := session.GetRevocationStore(); revoker != nil && verification != nil {
|
||||||
if err := revoker.ClearUserLogout(c.Context(), verification.UserID); err != nil {
|
if err := revoker.ClearUserLogout(c.Context(), verification.UserID); err != nil {
|
||||||
utils.Log.WithError(err).Warn("failed to clear logout marker")
|
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")
|
accessName := resolveSSOCookieName(config.SSOAccessCookieName, "access")
|
||||||
refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh")
|
refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh")
|
||||||
maxAge := tokenResp.ExpiresIn
|
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)
|
// Optional: expose limited info via headers for FE debugging (avoid tokens)
|
||||||
c.Set("X-Auth-User", fmt.Sprintf("%d", verification.UserID))
|
c.Set("X-Auth-User", fmt.Sprintf("%d", verification.UserID))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearSSOCookie(c *fiber.Ctx, name string) {
|
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,
|
"user_id": req.User.ID,
|
||||||
}).Info("sso user synced")
|
}).Info("sso user synced")
|
||||||
|
|
||||||
|
sso.InvalidateProfileCache(c.Context(), uint(req.User.ID))
|
||||||
|
|
||||||
msg := fmt.Sprintf("User %s successfully", req.Action)
|
msg := fmt.Sprintf("User %s successfully", req.Action)
|
||||||
return c.Status(fiber.StatusOK).JSON(response.Success{
|
return c.Status(fiber.StatusOK).JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
@@ -318,6 +320,8 @@ func (h *UserSyncController) logoutUser(c *fiber.Ctx, alias string, req *userSyn
|
|||||||
"user_id": req.User.ID,
|
"user_id": req.User.ID,
|
||||||
}).Info("sso user logout enforced")
|
}).Info("sso user logout enforced")
|
||||||
|
|
||||||
|
sso.InvalidateProfileCache(c.Context(), uint(req.User.ID))
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(response.Common{
|
return c.Status(fiber.StatusOK).JSON(response.Common{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
@@ -341,6 +345,8 @@ func (h *UserSyncController) removeUser(c *fiber.Ctx, alias string, req *userSyn
|
|||||||
"user_id": req.User.ID,
|
"user_id": req.User.ID,
|
||||||
}).Info("sso user deleted")
|
}).Info("sso user deleted")
|
||||||
|
|
||||||
|
sso.InvalidateProfileCache(c.Context(), uint(req.User.ID))
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(response.Common{
|
return c.Status(fiber.StatusOK).JSON(response.Common{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
|
|||||||
@@ -265,24 +265,44 @@ func profileCacheKey(userID uint) string {
|
|||||||
return profileCachePrefix + strconv.FormatUint(uint64(userID), 10)
|
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 {
|
func canonicalPermissionName(name string) string {
|
||||||
return strings.ToLower(strings.TrimSpace(name))
|
return strings.ToLower(strings.TrimSpace(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// userInfoEnvelope handles the varying shapes returned by the SSO userinfo endpoint.
|
// userInfoEnvelope handles the varying shapes returned by the SSO userinfo endpoint.
|
||||||
type userInfoEnvelope struct {
|
type userInfoEnvelope struct {
|
||||||
Roles []userInfoRole `json:"roles"`
|
Roles []userInfoRole `json:"roles"`
|
||||||
AreaIDs []uint `json:"area_ids"`
|
AreaIDs []uint `json:"area_ids"`
|
||||||
LocationIDs []uint `json:"location_ids"`
|
LocationIDs []uint `json:"location_ids"`
|
||||||
AllArea bool `json:"all_area"`
|
AllArea bool `json:"all_area"`
|
||||||
AllLocation bool `json:"all_location"`
|
AllLocation bool `json:"all_location"`
|
||||||
Data *struct {
|
Data *struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Roles []userInfoRole `json:"roles"`
|
Roles []userInfoRole `json:"roles"`
|
||||||
AreaIDs []uint `json:"area_ids"`
|
AreaIDs []uint `json:"area_ids"`
|
||||||
LocationIDs []uint `json:"location_ids"`
|
LocationIDs []uint `json:"location_ids"`
|
||||||
AllArea bool `json:"all_area"`
|
AllArea bool `json:"all_area"`
|
||||||
AllLocation bool `json:"all_location"`
|
AllLocation bool `json:"all_location"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
User *struct {
|
User *struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
|
|||||||
Reference in New Issue
Block a user