package middleware import ( "strings" // "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" ) const ( authContextLocalsKey = "auth.context" authUserLocalsKey = "auth.user" ) // AuthContext keeps authentication details captured by the middleware. type AuthContext struct { Token string Verification *sso.VerificationResult User *entity.User Roles []sso.Role Permissions map[string]struct{} } // Auth validates the incoming request against the central SSO access token and // loads the corresponding local user. Optional scopes can be provided to enforce // fine-grained authorization using the SSO access token scopes. func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler { return func(c *fiber.Ctx) error { // token := bearerToken(c) // if token == "" { // token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName)) // } // if token == "" { // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // } // verification, err := sso.VerifyAccessToken(token) // if err != nil { // utils.Log.WithError(err).Warn("auth: token verification failed") // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // } // if verification.UserID == 0 { // return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint") // } // if err := ensureNotRevoked(c, token, verification); err != nil { // return err // } // user, err := userService.GetBySSOUserID(c, verification.UserID) // if err != nil || user == nil { // utils.Log.WithError(err).Warn("auth: failed to resolve user from repository") // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // } // if len(requiredScopes) > 0 { // if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) { // return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") // } // } // var roles []sso.Role // permissions := make(map[string]struct{}) // if verification.UserID != 0 { // if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { // utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") // } else if profile != nil { // roles = profile.Roles // for _, perm := range profile.PermissionNames() { // if perm != "" { // permissions[perm] = struct{}{} // } // } // } // } // ctx := &AuthContext{ // Token: token, // Verification: verification, // User: user, // Roles: roles, // Permissions: permissions, // } // c.Locals(authContextLocalsKey, ctx) // c.Locals(authUserLocalsKey, user) return c.Next() } } // AuthenticatedUser returns the authenticated user populated by Auth. func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) { value := c.Locals(authUserLocalsKey) if user, ok := value.(*entity.User); ok && user != nil { return user, true } return nil, false } func ActorIDFromContext(c *fiber.Ctx) (uint, error) { // user, ok := AuthenticatedUser(c) // if !ok || user == nil || user.Id == 0 { // return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // } // return user.Id, nil return 1, nil } // AuthDetails returns the full authentication context (token, claims, user). func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) { value := c.Locals(authContextLocalsKey) if ctx, ok := value.(*AuthContext); ok && ctx != nil { return ctx, true } return nil, false } // ensureNotRevoked ensures the token is not revoked or superseded by a forced logout. func ensureNotRevoked(c *fiber.Ctx, token string, verification *sso.VerificationResult) error { revoker := session.GetRevocationStore() if revoker == nil { return nil } if fingerprint := session.TokenFingerprint(token); fingerprint != "" { revoked, err := revoker.IsRevoked(c.Context(), fingerprint) if err != nil { utils.Log.WithError(err).Warn("auth: token revocation check failed") return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } if revoked { return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } } if verification.UserID == 0 { return nil } logoutAt, err := revoker.UserLogoutTime(c.Context(), verification.UserID) if err != nil { utils.Log.WithError(err).Warn("auth: failed to load user logout marker") return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } if logoutAt.IsZero() { return nil } claims := verification.Claims if claims == nil || claims.IssuedAt == nil { return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } issuedAt := claims.IssuedAt.Time // Treat tokens issued at or before the forced logout timestamp as invalid. if !issuedAt.After(logoutAt) { return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } return nil } // bearerToken extracts a Bearer token from the Authorization header using // case-insensitive scheme matching and tolerant whitespace handling. func bearerToken(c *fiber.Ctx) string { parts := strings.Fields(c.Get("Authorization")) if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") { return strings.TrimSpace(parts[1]) } return "" } func hasAllScopes(have, required []string) bool { if len(required) == 0 { return true } set := make(map[string]struct{}, len(have)) for _, s := range have { s = strings.ToLower(strings.TrimSpace(s)) if s != "" { set[s] = struct{}{} } } for _, r := range required { r = strings.ToLower(strings.TrimSpace(r)) if r == "" { continue } if _, ok := set[r]; !ok { return false } } return true } // RequirePermissions ensures the authenticated user possesses all specified permissions. func RequirePermissions(perms ...string) fiber.Handler { required := canonicalPermissions(perms) return func(c *fiber.Ctx) error { if len(required) == 0 { return c.Next() } ctx, ok := AuthDetails(c) if !ok || ctx == nil { return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") } userPerms := ctx.permissionSet() if len(userPerms) == 0 { return fiber.NewError(fiber.StatusForbidden, "Insufficient permission") } for _, perm := range required { if _, has := userPerms[perm]; !has { return fiber.NewError(fiber.StatusForbidden, "Insufficient permission") } } return c.Next() } } // HasPermission reports whether the current request context includes the given permission. func HasPermission(c *fiber.Ctx, perm string) bool { ctx, ok := AuthDetails(c) if !ok || ctx == nil { return false } perm = canonicalPermission(perm) if perm == "" { return false } _, has := ctx.permissionSet()[perm] return has } func (a *AuthContext) permissionSet() map[string]struct{} { if a == nil || a.Permissions == nil { return nil } return a.Permissions } func canonicalPermissions(perms []string) []string { out := make([]string, 0, len(perms)) seen := make(map[string]struct{}, len(perms)) for _, perm := range perms { if canonical := canonicalPermission(perm); canonical != "" { if _, ok := seen[canonical]; ok { continue } seen[canonical] = struct{}{} out = append(out, canonical) } } return out } func canonicalPermission(perm string) string { return strings.ToLower(strings.TrimSpace(perm)) }