mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-17 03:59:56 +00:00
267 lines
8.6 KiB
Go
267 lines
8.6 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
log "github.com/sirupsen/logrus"
|
|
"go.opentelemetry.io/otel/metric"
|
|
|
|
serverauth "github.com/netbirdio/netbird/management/server/auth"
|
|
nbcontext "github.com/netbirdio/netbird/management/server/context"
|
|
"github.com/netbirdio/netbird/management/server/http/middleware/bypass"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/shared/auth"
|
|
"github.com/netbirdio/netbird/shared/management/http/util"
|
|
"github.com/netbirdio/netbird/shared/management/status"
|
|
)
|
|
|
|
type EnsureAccountFunc func(ctx context.Context, userAuth auth.UserAuth) (string, string, error)
|
|
type SyncUserJWTGroupsFunc func(ctx context.Context, userAuth auth.UserAuth) error
|
|
|
|
type GetUserFromUserAuthFunc func(ctx context.Context, userAuth auth.UserAuth) (*types.User, error)
|
|
|
|
// jwtTokenCtxKey carries the parsed JWT token.
|
|
type jwtTokenCtxKey struct{}
|
|
|
|
// AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens
|
|
type AuthMiddleware struct {
|
|
authManager serverauth.Manager
|
|
ensureAccount EnsureAccountFunc
|
|
getUserFromUserAuth GetUserFromUserAuthFunc
|
|
syncUserJWTGroups SyncUserJWTGroupsFunc
|
|
rateLimiter *APIRateLimiter
|
|
patUsageTracker *PATUsageTracker
|
|
}
|
|
|
|
// NewAuthMiddleware instance constructor
|
|
func NewAuthMiddleware(
|
|
authManager serverauth.Manager,
|
|
ensureAccount EnsureAccountFunc,
|
|
syncUserJWTGroups SyncUserJWTGroupsFunc,
|
|
getUserFromUserAuth GetUserFromUserAuthFunc,
|
|
rateLimiter *APIRateLimiter,
|
|
meter metric.Meter,
|
|
) *AuthMiddleware {
|
|
var patUsageTracker *PATUsageTracker
|
|
if meter != nil {
|
|
var err error
|
|
patUsageTracker, err = NewPATUsageTracker(context.Background(), meter)
|
|
if err != nil {
|
|
log.Errorf("Failed to create PAT usage tracker: %s", err)
|
|
}
|
|
}
|
|
|
|
return &AuthMiddleware{
|
|
authManager: authManager,
|
|
ensureAccount: ensureAccount,
|
|
syncUserJWTGroups: syncUserJWTGroups,
|
|
getUserFromUserAuth: getUserFromUserAuth,
|
|
rateLimiter: rateLimiter,
|
|
patUsageTracker: patUsageTracker,
|
|
}
|
|
}
|
|
|
|
// Handler composes the full authentication chain by wrapping the given
|
|
// handler with ValidationHandler followed by AccountAccessHandler.
|
|
func (m *AuthMiddleware) Handler(h http.Handler) http.Handler {
|
|
return m.ValidationHandler(m.AccountAccessHandler(h))
|
|
}
|
|
|
|
// ValidationHandler authenticates the caller via JWT or PAT and stores the
|
|
// resulting UserAuth in the request context. It performs no account-level work.
|
|
func (m *AuthMiddleware) ValidationHandler(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if bypass.ShouldBypass(r.URL.Path, h, w, r) {
|
|
return
|
|
}
|
|
|
|
authHeader := strings.Split(r.Header.Get("Authorization"), " ")
|
|
authType := strings.ToLower(authHeader[0])
|
|
|
|
// fallback to token when receive pat as bearer
|
|
if len(authHeader) >= 2 && authType == "bearer" && strings.HasPrefix(authHeader[1], "nbp_") {
|
|
authType = "token"
|
|
authHeader[0] = authType
|
|
}
|
|
|
|
switch authType {
|
|
case "bearer":
|
|
if err := m.validateJWT(r, authHeader); err != nil {
|
|
log.WithContext(r.Context()).Errorf("Error when validating JWT: %s", err.Error())
|
|
util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w)
|
|
return
|
|
}
|
|
h.ServeHTTP(w, r)
|
|
case "token":
|
|
if err := m.validatePAT(r, authHeader); err != nil {
|
|
log.WithContext(r.Context()).Debugf("Error when validating PAT: %s", err.Error())
|
|
// Check if it's a status error, otherwise default to Unauthorized
|
|
if _, ok := status.FromError(err); !ok {
|
|
err = status.Errorf(status.Unauthorized, "token invalid")
|
|
}
|
|
util.WriteError(r.Context(), err, w)
|
|
return
|
|
}
|
|
h.ServeHTTP(w, r)
|
|
default:
|
|
util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "no valid authentication provided"), w)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
|
|
// AccountAccessHandler runs post-validation access checks for JWT-authenticated
|
|
// requests. PAT requests pass through unchanged.
|
|
func (m *AuthMiddleware) AccountAccessHandler(h http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if bypass.ShouldBypass(r.URL.Path, h, w, r) {
|
|
return
|
|
}
|
|
|
|
userAuth, err := nbcontext.GetUserAuthFromRequest(r)
|
|
if err != nil {
|
|
util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "no valid authentication provided"), w)
|
|
return
|
|
}
|
|
|
|
if userAuth.IsPAT {
|
|
h.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
validatedToken, _ := r.Context().Value(jwtTokenCtxKey{}).(*jwt.Token)
|
|
|
|
if err := m.applyAccountAccess(r, userAuth, validatedToken); err != nil {
|
|
log.WithContext(r.Context()).Errorf("Error applying JWT account access: %s", err.Error())
|
|
util.WriteError(r.Context(), status.Errorf(status.Unauthorized, "token invalid"), w)
|
|
return
|
|
}
|
|
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (m *AuthMiddleware) validateJWT(r *http.Request, authHeaderParts []string) error {
|
|
token, err := getTokenFromJWTRequest(authHeaderParts)
|
|
if err != nil {
|
|
return fmt.Errorf("error extracting token: %w", err)
|
|
}
|
|
|
|
userAuth, validatedToken, err := m.authManager.ValidateAndParseToken(r.Context(), token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// propagates ctx change to upstream middleware
|
|
*r = *nbcontext.SetUserAuthInRequest(r, userAuth)
|
|
*r = *r.WithContext(context.WithValue(r.Context(), jwtTokenCtxKey{}, validatedToken))
|
|
return nil
|
|
}
|
|
|
|
func (m *AuthMiddleware) validatePAT(r *http.Request, authHeaderParts []string) error {
|
|
token, err := getTokenFromPATRequest(authHeaderParts)
|
|
if err != nil {
|
|
return fmt.Errorf("error extracting token: %w", err)
|
|
}
|
|
|
|
if m.patUsageTracker != nil {
|
|
m.patUsageTracker.IncrementUsage(token)
|
|
}
|
|
|
|
if !isTerraformRequest(r) && !m.rateLimiter.Allow(token) {
|
|
return status.Errorf(status.TooManyRequests, "too many requests")
|
|
}
|
|
|
|
ctx := r.Context()
|
|
user, pat, accDomain, accCategory, err := m.authManager.GetPATInfo(ctx, token)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid Token: %w", err)
|
|
}
|
|
if time.Now().After(pat.GetExpirationDate()) {
|
|
return fmt.Errorf("token expired")
|
|
}
|
|
|
|
if err := m.authManager.MarkPATUsed(ctx, pat.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
userAuth := auth.UserAuth{
|
|
UserId: user.Id,
|
|
AccountId: user.AccountID,
|
|
Domain: accDomain,
|
|
DomainCategory: accCategory,
|
|
IsPAT: true,
|
|
}
|
|
|
|
// propagates ctx change to upstream middleware
|
|
*r = *nbcontext.SetUserAuthInRequest(r, userAuth)
|
|
return nil
|
|
}
|
|
|
|
// applyAccountAccess executes account-level checks for an authenticated JWT
|
|
// user: ensures the account exists, verifies access via JWT groups, syncs
|
|
// groups, and fetches the user record.
|
|
func (m *AuthMiddleware) applyAccountAccess(r *http.Request, userAuth auth.UserAuth, validatedToken *jwt.Token) error {
|
|
ctx := r.Context()
|
|
|
|
// we need to call this method because if user is new, we will automatically add it to existing or create a new account
|
|
accountId, _, err := m.ensureAccount(ctx, userAuth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if userAuth.AccountId != accountId {
|
|
log.WithContext(ctx).Tracef("Auth middleware sets accountId from ensure, before %s, now %s", userAuth.AccountId, accountId)
|
|
userAuth.AccountId = accountId
|
|
}
|
|
|
|
userAuth, err = m.authManager.EnsureUserAccessByJWTGroups(ctx, userAuth, validatedToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := m.syncUserJWTGroups(ctx, userAuth); err != nil {
|
|
log.WithContext(ctx).Errorf("HTTP server failed to sync user JWT groups: %s", err)
|
|
}
|
|
|
|
if _, err := m.getUserFromUserAuth(ctx, userAuth); err != nil {
|
|
log.WithContext(ctx).Errorf("HTTP server failed to update user from user auth: %s", err)
|
|
return err
|
|
}
|
|
|
|
// propagates ctx change to upstream middleware
|
|
*r = *nbcontext.SetUserAuthInRequest(r, userAuth)
|
|
return nil
|
|
}
|
|
|
|
func isTerraformRequest(r *http.Request) bool {
|
|
ua := strings.ToLower(r.Header.Get("User-Agent"))
|
|
return strings.Contains(ua, "terraform")
|
|
}
|
|
|
|
// getTokenFromJWTRequest is a "TokenExtractor" that takes auth header parts and extracts
|
|
// the JWT token from the Authorization header.
|
|
func getTokenFromJWTRequest(authHeaderParts []string) (string, error) {
|
|
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
|
return "", errors.New("authorization header format must be Bearer {token}")
|
|
}
|
|
|
|
return authHeaderParts[1], nil
|
|
}
|
|
|
|
// getTokenFromPATRequest is a "TokenExtractor" that takes auth header parts and extracts
|
|
// the PAT token from the Authorization header.
|
|
func getTokenFromPATRequest(authHeaderParts []string) (string, error) {
|
|
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "token" {
|
|
return "", errors.New("authorization header format must be Token {token}")
|
|
}
|
|
|
|
return authHeaderParts[1], nil
|
|
}
|