mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-07 16:54:16 +01:00
refactor jwt token validation and add PAT to middleware auth
This commit is contained in:
parent
ecc4f8a10d
commit
db3a9f0aa2
@ -19,25 +19,28 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/netbirdio/netbird/management/server/activity/sqlite"
|
||||
httpapi "github.com/netbirdio/netbird/management/server/http"
|
||||
"github.com/netbirdio/netbird/management/server/metrics"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/activity/sqlite"
|
||||
httpapi "github.com/netbirdio/netbird/management/server/http"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/management/server/metrics"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/idp"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
|
||||
"github.com/netbirdio/netbird/encryption"
|
||||
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
|
||||
"github.com/netbirdio/netbird/encryption"
|
||||
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
||||
)
|
||||
|
||||
// ManagementLegacyPort is the port that was used before by the Management gRPC server.
|
||||
@ -179,13 +182,22 @@ var (
|
||||
tlsEnabled = true
|
||||
}
|
||||
|
||||
jwtValidator, err := jwtclaims.NewJWTValidator(
|
||||
config.HttpConfig.AuthIssuer,
|
||||
config.HttpConfig.AuthAudience,
|
||||
config.HttpConfig.AuthKeysLocation,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creating JWT validator: %v", err)
|
||||
}
|
||||
|
||||
httpAPIAuthCfg := httpapi.AuthCfg{
|
||||
Issuer: config.HttpConfig.AuthIssuer,
|
||||
Audience: config.HttpConfig.AuthAudience,
|
||||
UserIDClaim: config.HttpConfig.AuthUserIDClaim,
|
||||
KeysLocation: config.HttpConfig.AuthKeysLocation,
|
||||
}
|
||||
httpAPIHandler, err := httpapi.APIHandler(accountManager, appMetrics, httpAPIAuthCfg)
|
||||
httpAPIHandler, err := httpapi.APIHandler(accountManager, *jwtValidator, appMetrics, httpAPIAuthCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed creating HTTP API handler: %v", err)
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ type AccountManager interface {
|
||||
GetAccountByUserOrAccountID(userID, accountID, domain string) (*Account, error)
|
||||
GetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*Account, *User, error)
|
||||
GetAccountFromPAT(pat string) (*Account, *User, *PersonalAccessToken, error)
|
||||
MarkPATUsed(tokenID string) error
|
||||
IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error)
|
||||
AccountExists(accountId string) (*bool, error)
|
||||
GetPeerByKey(peerKey string) (*Peer, error)
|
||||
@ -1120,6 +1121,33 @@ func (am *DefaultAccountManager) redeemInvite(account *Account, userID string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *DefaultAccountManager) MarkPATUsed(tokenID string) error {
|
||||
unlock := am.Store.AcquireGlobalLock()
|
||||
defer unlock()
|
||||
|
||||
user, err := am.Store.GetUserByTokenID(tokenID)
|
||||
log.Debugf("User: %v", user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account, err := am.Store.GetAccountByUser(user.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pat, ok := account.Users[user.Id].PATs[tokenID]
|
||||
if !ok {
|
||||
return fmt.Errorf("token not found")
|
||||
}
|
||||
|
||||
pat.LastUsed = time.Now()
|
||||
|
||||
am.Store.SaveAccount(account)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAccountFromPAT returns Account and User associated with a personal access token
|
||||
func (am *DefaultAccountManager) GetAccountFromPAT(token string) (*Account, *User, *PersonalAccessToken, error) {
|
||||
if len(token) != PATLength {
|
||||
|
@ -3,24 +3,25 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
pb "github.com/golang/protobuf/proto" //nolint
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pb "github.com/golang/protobuf/proto" // nolint
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/timestamp"
|
||||
"github.com/netbirdio/netbird/encryption"
|
||||
"github.com/netbirdio/netbird/management/proto"
|
||||
internalStatus "github.com/netbirdio/netbird/management/server/status"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"google.golang.org/grpc/codes"
|
||||
gRPCPeer "google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/netbirdio/netbird/encryption"
|
||||
"github.com/netbirdio/netbird/management/proto"
|
||||
internalStatus "github.com/netbirdio/netbird/management/server/status"
|
||||
)
|
||||
|
||||
// GRPCServer an instance of a Management gRPC API server
|
||||
@ -31,7 +32,7 @@ type GRPCServer struct {
|
||||
peersUpdateManager *PeersUpdateManager
|
||||
config *Config
|
||||
turnCredentialsManager TURNCredentialsManager
|
||||
jwtMiddleware *middleware.JWTMiddleware
|
||||
jwtValidator *jwtclaims.JWTValidator
|
||||
jwtClaimsExtractor *jwtclaims.ClaimsExtractor
|
||||
appMetrics telemetry.AppMetrics
|
||||
}
|
||||
@ -45,10 +46,10 @@ func NewServer(config *Config, accountManager AccountManager, peersUpdateManager
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var jwtMiddleware *middleware.JWTMiddleware
|
||||
var jwtValidator *jwtclaims.JWTValidator
|
||||
|
||||
if config.HttpConfig != nil && config.HttpConfig.AuthIssuer != "" && config.HttpConfig.AuthAudience != "" && validateURL(config.HttpConfig.AuthKeysLocation) {
|
||||
jwtMiddleware, err = middleware.NewJwtMiddleware(
|
||||
jwtValidator, err = jwtclaims.NewJWTValidator(
|
||||
config.HttpConfig.AuthIssuer,
|
||||
config.HttpConfig.AuthAudience,
|
||||
config.HttpConfig.AuthKeysLocation)
|
||||
@ -86,7 +87,7 @@ func NewServer(config *Config, accountManager AccountManager, peersUpdateManager
|
||||
accountManager: accountManager,
|
||||
config: config,
|
||||
turnCredentialsManager: turnCredentialsManager,
|
||||
jwtMiddleware: jwtMiddleware,
|
||||
jwtValidator: jwtValidator,
|
||||
jwtClaimsExtractor: jwtClaimsExtractor,
|
||||
appMetrics: appMetrics,
|
||||
}, nil
|
||||
@ -187,11 +188,11 @@ func (s *GRPCServer) cancelPeerRoutines(peer *Peer) {
|
||||
}
|
||||
|
||||
func (s *GRPCServer) validateToken(jwtToken string) (string, error) {
|
||||
if s.jwtMiddleware == nil {
|
||||
return "", status.Error(codes.Internal, "no jwt middleware set")
|
||||
if s.jwtValidator == nil {
|
||||
return "", status.Error(codes.Internal, "no jwt validator set")
|
||||
}
|
||||
|
||||
token, err := s.jwtMiddleware.ValidateAndParse(jwtToken)
|
||||
token, err := s.jwtValidator.ValidateAndParse(jwtToken)
|
||||
if err != nil {
|
||||
return "", status.Errorf(codes.InvalidArgument, "invalid jwt token, err: %v", err)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
s "github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/middleware"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||
)
|
||||
|
||||
@ -25,18 +26,17 @@ type apiHandler struct {
|
||||
AuthCfg AuthCfg
|
||||
}
|
||||
|
||||
// EmptyObject is an empty struct used to return empty JSON object
|
||||
type emptyObject struct {
|
||||
}
|
||||
|
||||
// APIHandler creates the Management service HTTP API handler registering all the available endpoints.
|
||||
func APIHandler(accountManager s.AccountManager, appMetrics telemetry.AppMetrics, authCfg AuthCfg) (http.Handler, error) {
|
||||
jwtMiddleware, err := middleware.NewJwtMiddleware(
|
||||
authCfg.Issuer,
|
||||
authCfg.Audience,
|
||||
authCfg.KeysLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func APIHandler(accountManager s.AccountManager, jwtValidator jwtclaims.JWTValidator, appMetrics telemetry.AppMetrics, authCfg AuthCfg) (http.Handler, error) {
|
||||
authMiddleware := middleware.NewAuthMiddleware(
|
||||
accountManager.GetAccountFromPAT,
|
||||
jwtValidator.ValidateAndParse,
|
||||
accountManager.MarkPATUsed,
|
||||
authCfg.Audience)
|
||||
|
||||
corsMiddleware := cors.AllowAll()
|
||||
|
||||
@ -49,7 +49,7 @@ func APIHandler(accountManager s.AccountManager, appMetrics telemetry.AppMetrics
|
||||
metricsMiddleware := appMetrics.HTTPMiddleware()
|
||||
|
||||
router := rootRouter.PathPrefix("/api").Subrouter()
|
||||
router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, jwtMiddleware.Handler, acMiddleware.Handler)
|
||||
router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler, acMiddleware.Handler)
|
||||
|
||||
api := apiHandler{
|
||||
Router: router,
|
||||
@ -70,7 +70,7 @@ func APIHandler(accountManager s.AccountManager, appMetrics telemetry.AppMetrics
|
||||
api.addDNSSettingEndpoint()
|
||||
api.addEventsEndpoint()
|
||||
|
||||
err = api.Router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error {
|
||||
err := api.Router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error {
|
||||
methods, err := route.GetMethods()
|
||||
if err != nil {
|
||||
return err
|
||||
|
170
management/server/http/middleware/auth_middleware.go
Normal file
170
management/server/http/middleware/auth_middleware.go
Normal file
@ -0,0 +1,170 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/http/util"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
)
|
||||
|
||||
type GetAccountFromPATFunc func(token string) (*server.Account, *server.User, *server.PersonalAccessToken, error)
|
||||
type ValidateAndParseTokenFunc func(token string) (*jwt.Token, error)
|
||||
type MarkPATUsedFunc func(token string) error
|
||||
|
||||
// AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens
|
||||
type AuthMiddleware struct {
|
||||
getAccountFromPAT GetAccountFromPATFunc
|
||||
validateAndParseToken ValidateAndParseTokenFunc
|
||||
markPATUsed MarkPATUsedFunc
|
||||
audience string
|
||||
}
|
||||
|
||||
const (
|
||||
userProperty = "user"
|
||||
)
|
||||
|
||||
// NewAuthMiddleware instance constructor
|
||||
func NewAuthMiddleware(getAccountFromPAT GetAccountFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc, markPATUsed MarkPATUsedFunc, audience string) *AuthMiddleware {
|
||||
return &AuthMiddleware{
|
||||
getAccountFromPAT: getAccountFromPAT,
|
||||
validateAndParseToken: validateAndParseToken,
|
||||
markPATUsed: markPATUsed,
|
||||
audience: audience,
|
||||
}
|
||||
}
|
||||
|
||||
// Handler method of the middleware which authenticates a user either by JWT claims or by PAT
|
||||
func (a *AuthMiddleware) Handler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := strings.Split(r.Header.Get("Authorization"), " ")
|
||||
authType := auth[0]
|
||||
switch strings.ToLower(authType) {
|
||||
case "bearer":
|
||||
err := a.CheckJWTFromRequest(w, r)
|
||||
if err != nil {
|
||||
log.Debugf("Error when validating JWT claims: %s", err.Error())
|
||||
util.WriteError(status.Errorf(status.Unauthorized, "Token invalid"), w)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
case "token":
|
||||
err := a.CheckPATFromRequest(w, r)
|
||||
if err != nil {
|
||||
log.Debugf("Error when validating PAT claims: %s", err.Error())
|
||||
util.WriteError(status.Errorf(status.Unauthorized, "Token invalid"), w)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
default:
|
||||
util.WriteError(status.Errorf(status.Unauthorized, "No valid authentication provided"), w)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// CheckJWTFromRequest checks if the JWT is valid
|
||||
func (m *AuthMiddleware) CheckJWTFromRequest(w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
token, err := getTokenFromJWTRequest(r)
|
||||
|
||||
// If an error occurs, call the error handler and return an error
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error extracting token: %w", err)
|
||||
}
|
||||
|
||||
validatedToken, err := m.validateAndParseToken(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if validatedToken == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we get here, everything worked and we can set the
|
||||
// user property in context.
|
||||
newRequest := r.WithContext(context.WithValue(r.Context(), userProperty, validatedToken)) // nolint
|
||||
// Update the current request with the new context information.
|
||||
*r = *newRequest
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPATFromRequest checks if the PAT is valid
|
||||
func (m *AuthMiddleware) CheckPATFromRequest(w http.ResponseWriter, r *http.Request) error {
|
||||
token, err := getTokenFromPATRequest(r)
|
||||
|
||||
// If an error occurs, call the error handler and return an error
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error extracting token: %w", err)
|
||||
}
|
||||
|
||||
account, user, pat, err := m.getAccountFromPAT(token)
|
||||
if err != nil {
|
||||
util.WriteError(status.Errorf(status.Unauthorized, "Token invalid"), w)
|
||||
return fmt.Errorf("invalid Token: %w", err)
|
||||
}
|
||||
if time.Now().After(pat.ExpirationDate) {
|
||||
util.WriteError(status.Errorf(status.Unauthorized, "Token expired"), w)
|
||||
return fmt.Errorf("token expired")
|
||||
}
|
||||
|
||||
err = m.markPATUsed(pat.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
claimMaps := jwt.MapClaims{}
|
||||
claimMaps[jwtclaims.UserIDClaim] = user.Id
|
||||
claimMaps[m.audience+jwtclaims.AccountIDSuffix] = account.Id
|
||||
claimMaps[m.audience+jwtclaims.DomainIDSuffix] = account.Domain
|
||||
claimMaps[m.audience+jwtclaims.DomainCategorySuffix] = account.DomainCategory
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claimMaps)
|
||||
newRequest := r.WithContext(context.WithValue(r.Context(), jwtclaims.TokenUserProperty, jwtToken))
|
||||
// Update the current request with the new context information.
|
||||
*r = *newRequest
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTokenFromJWTRequest is a "TokenExtractor" that takes a give request and extracts
|
||||
// the JWT token from the Authorization header.
|
||||
func getTokenFromJWTRequest(r *http.Request) (string, error) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return "", nil // No error, just no token
|
||||
}
|
||||
|
||||
// TODO: Make this a bit more robust, parsing-wise
|
||||
authHeaderParts := strings.Fields(authHeader)
|
||||
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 a give request and extracts
|
||||
// the PAT token from the Authorization header.
|
||||
func getTokenFromPATRequest(r *http.Request) (string, error) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return "", nil // No error, just no token
|
||||
}
|
||||
|
||||
// TODO: Make this a bit more robust, parsing-wise
|
||||
authHeaderParts := strings.Fields(authHeader)
|
||||
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "token" {
|
||||
return "", errors.New("Authorization header format must be Token {token}")
|
||||
}
|
||||
|
||||
return authHeaderParts[1], nil
|
||||
}
|
1
management/server/http/middleware/auth_midleware_test.go
Normal file
1
management/server/http/middleware/auth_midleware_test.go
Normal file
@ -0,0 +1 @@
|
||||
package middleware
|
@ -1,249 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/netbirdio/netbird/management/server/http/util"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A function called whenever an error is encountered
|
||||
type errorHandler func(w http.ResponseWriter, r *http.Request, err string)
|
||||
|
||||
// TokenExtractor is a function that takes a request as input and returns
|
||||
// either a token or an error. An error should only be returned if an attempt
|
||||
// to specify a token was found, but the information was somehow incorrectly
|
||||
// formed. In the case where a token is simply not present, this should not
|
||||
// be treated as an error. An empty string should be returned in that case.
|
||||
type TokenExtractor func(r *http.Request) (string, error)
|
||||
|
||||
// Options is a struct for specifying configuration options for the middleware.
|
||||
type Options struct {
|
||||
// The function that will return the Key to validate the JWT.
|
||||
// It can be either a shared secret or a public key.
|
||||
// Default value: nil
|
||||
ValidationKeyGetter jwt.Keyfunc
|
||||
// The name of the property in the request where the user information
|
||||
// from the JWT will be stored.
|
||||
// Default value: "user"
|
||||
UserProperty string
|
||||
// The function that will be called when there's an error validating the token
|
||||
// Default value:
|
||||
ErrorHandler errorHandler
|
||||
// A boolean indicating if the credentials are required or not
|
||||
// Default value: false
|
||||
CredentialsOptional bool
|
||||
// A function that extracts the token from the request
|
||||
// Default: FromAuthHeader (i.e., from Authorization header as bearer token)
|
||||
Extractor TokenExtractor
|
||||
// Debug flag turns on debugging output
|
||||
// Default: false
|
||||
Debug bool
|
||||
// When set, all requests with the OPTIONS method will use authentication
|
||||
// Default: false
|
||||
EnableAuthOnOptions bool
|
||||
// When set, the middelware verifies that tokens are signed with the specific signing algorithm
|
||||
// If the signing method is not constant the ValidationKeyGetter callback can be used to implement additional checks
|
||||
// Important to avoid security issues described here: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
|
||||
// Default: nil
|
||||
SigningMethod jwt.SigningMethod
|
||||
}
|
||||
|
||||
type JWTMiddleware struct {
|
||||
Options Options
|
||||
}
|
||||
|
||||
func OnError(w http.ResponseWriter, r *http.Request, err string) {
|
||||
util.WriteError(status.Errorf(status.Unauthorized, ""), w)
|
||||
}
|
||||
|
||||
// New constructs a new Secure instance with supplied options.
|
||||
func New(options ...Options) *JWTMiddleware {
|
||||
|
||||
var opts Options
|
||||
if len(options) == 0 {
|
||||
opts = Options{}
|
||||
} else {
|
||||
opts = options[0]
|
||||
}
|
||||
|
||||
if opts.UserProperty == "" {
|
||||
opts.UserProperty = "user"
|
||||
}
|
||||
|
||||
if opts.ErrorHandler == nil {
|
||||
opts.ErrorHandler = OnError
|
||||
}
|
||||
|
||||
if opts.Extractor == nil {
|
||||
opts.Extractor = FromAuthHeader
|
||||
}
|
||||
|
||||
return &JWTMiddleware{
|
||||
Options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) logf(format string, args ...interface{}) {
|
||||
if m.Options.Debug {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// HandlerWithNext is a special implementation for Negroni, but could be used elsewhere.
|
||||
func (m *JWTMiddleware) HandlerWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
err := m.CheckJWTFromRequest(w, r)
|
||||
|
||||
// If there was an error, do not call next.
|
||||
if err == nil && next != nil {
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) Handler(h http.Handler) http.Handler {
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Let secure process the request. If it returns an error,
|
||||
// that indicates the request should not continue.
|
||||
err := m.CheckJWTFromRequest(w, r)
|
||||
|
||||
// If there was an error, do not continue.
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// FromAuthHeader is a "TokenExtractor" that takes a give request and extracts
|
||||
// the JWT token from the Authorization header.
|
||||
func FromAuthHeader(r *http.Request) (string, error) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return "", nil // No error, just no token
|
||||
}
|
||||
|
||||
// TODO: Make this a bit more robust, parsing-wise
|
||||
authHeaderParts := strings.Fields(authHeader)
|
||||
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
||||
return "", errors.New("Authorization header format must be Bearer {token}")
|
||||
}
|
||||
|
||||
return authHeaderParts[1], nil
|
||||
}
|
||||
|
||||
// FromParameter returns a function that extracts the token from the specified
|
||||
// query string parameter
|
||||
func FromParameter(param string) TokenExtractor {
|
||||
return func(r *http.Request) (string, error) {
|
||||
return r.URL.Query().Get(param), nil
|
||||
}
|
||||
}
|
||||
|
||||
// FromFirst returns a function that runs multiple token extractors and takes the
|
||||
// first token it finds
|
||||
func FromFirst(extractors ...TokenExtractor) TokenExtractor {
|
||||
return func(r *http.Request) (string, error) {
|
||||
for _, ex := range extractors {
|
||||
token, err := ex(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token != "" {
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) CheckJWTFromRequest(w http.ResponseWriter, r *http.Request) error {
|
||||
if !m.Options.EnableAuthOnOptions {
|
||||
if r.Method == "OPTIONS" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Use the specified token extractor to extract a token from the request
|
||||
token, err := m.Options.Extractor(r)
|
||||
|
||||
// If debugging is turned on, log the outcome
|
||||
if err != nil {
|
||||
m.logf("Error extracting JWT: %v", err)
|
||||
} else {
|
||||
m.logf("Token extracted: %s", token)
|
||||
}
|
||||
|
||||
// If an error occurs, call the error handler and return an error
|
||||
if err != nil {
|
||||
m.Options.ErrorHandler(w, r, err.Error())
|
||||
return fmt.Errorf("Error extracting token: %w", err)
|
||||
}
|
||||
|
||||
validatedToken, err := m.ValidateAndParse(token)
|
||||
if err != nil {
|
||||
m.Options.ErrorHandler(w, r, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if validatedToken == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we get here, everything worked and we can set the
|
||||
// user property in context.
|
||||
newRequest := r.WithContext(context.WithValue(r.Context(), m.Options.UserProperty, validatedToken)) //nolint
|
||||
// Update the current request with the new context information.
|
||||
*r = *newRequest
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAndParse validates and parses a given access token against jwt standards and signing methods
|
||||
func (m *JWTMiddleware) ValidateAndParse(token string) (*jwt.Token, error) {
|
||||
// If the token is empty...
|
||||
if token == "" {
|
||||
// Check if it was required
|
||||
if m.Options.CredentialsOptional {
|
||||
m.logf("no credentials found (CredentialsOptional=true)")
|
||||
// No error, just no token (and that is ok given that CredentialsOptional is true)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If we get here, the required token is missing
|
||||
errorMsg := "required authorization token not found"
|
||||
m.logf(" Error: No credentials found (CredentialsOptional=false)")
|
||||
return nil, fmt.Errorf(errorMsg)
|
||||
}
|
||||
|
||||
// Now parse the token
|
||||
parsedToken, err := jwt.Parse(token, m.Options.ValidationKeyGetter)
|
||||
|
||||
// Check if there was an error in parsing...
|
||||
if err != nil {
|
||||
m.logf("error parsing token: %v", err)
|
||||
return nil, fmt.Errorf("Error parsing token: %w", err)
|
||||
}
|
||||
|
||||
if m.Options.SigningMethod != nil && m.Options.SigningMethod.Alg() != parsedToken.Header["alg"] {
|
||||
errorMsg := fmt.Sprintf("Expected %s signing method but token specified %s",
|
||||
m.Options.SigningMethod.Alg(),
|
||||
parsedToken.Header["alg"])
|
||||
m.logf("error validating token algorithm: %s", errorMsg)
|
||||
return nil, fmt.Errorf("error validating token algorithm: %s", errorMsg)
|
||||
}
|
||||
|
||||
// Check if the parsed token is valid...
|
||||
if !parsedToken.Valid {
|
||||
errorMsg := "token is invalid"
|
||||
m.logf(errorMsg)
|
||||
return nil, errors.New(errorMsg)
|
||||
}
|
||||
|
||||
return parsedToken, nil
|
||||
}
|
@ -4,10 +4,12 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/status"
|
||||
)
|
||||
|
||||
// WriteJSONObject simply writes object to the HTTP reponse in JSON format
|
||||
@ -93,6 +95,8 @@ func WriteError(err error, w http.ResponseWriter) {
|
||||
httpStatus = http.StatusInternalServerError
|
||||
case status.InvalidArgument:
|
||||
httpStatus = http.StatusUnprocessableEntity
|
||||
case status.Unauthorized:
|
||||
httpStatus = http.StatusUnauthorized
|
||||
default:
|
||||
}
|
||||
msg = err.Error()
|
||||
|
@ -1,4 +1,4 @@
|
||||
package middleware
|
||||
package jwtclaims
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -17,6 +17,32 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Options is a struct for specifying configuration options for the middleware.
|
||||
type Options struct {
|
||||
// The function that will return the Key to validate the JWT.
|
||||
// It can be either a shared secret or a public key.
|
||||
// Default value: nil
|
||||
ValidationKeyGetter jwt.Keyfunc
|
||||
// The name of the property in the request where the user information
|
||||
// from the JWT will be stored.
|
||||
// Default value: "user"
|
||||
UserProperty string
|
||||
// The function that will be called when there's an error validating the token
|
||||
// Default value:
|
||||
CredentialsOptional bool
|
||||
// A function that extracts the token from the request
|
||||
// Default: FromAuthHeader (i.e., from Authorization header as bearer token)
|
||||
Debug bool
|
||||
// When set, all requests with the OPTIONS method will use authentication
|
||||
// Default: false
|
||||
EnableAuthOnOptions bool
|
||||
// When set, the middelware verifies that tokens are signed with the specific signing algorithm
|
||||
// If the signing method is not constant the ValidationKeyGetter callback can be used to implement additional checks
|
||||
// Important to avoid security issues described here: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
|
||||
// Default: nil
|
||||
SigningMethod jwt.SigningMethod
|
||||
}
|
||||
|
||||
// Jwks is a collection of JSONWebKey obtained from Config.HttpServerConfig.AuthKeysLocation
|
||||
type Jwks struct {
|
||||
Keys []JSONWebKey `json:"keys"`
|
||||
@ -32,14 +58,17 @@ type JSONWebKey struct {
|
||||
X5c []string `json:"x5c"`
|
||||
}
|
||||
|
||||
// NewJwtMiddleware creates new middleware to verify the JWT token sent via Authorization header
|
||||
func NewJwtMiddleware(issuer string, audience string, keysLocation string) (*JWTMiddleware, error) {
|
||||
type JWTValidator struct {
|
||||
options Options
|
||||
}
|
||||
|
||||
func NewJWTValidator(issuer string, audience string, keysLocation string) (*JWTValidator, error) {
|
||||
keys, err := getPemKeys(keysLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return New(Options{
|
||||
options := Options{
|
||||
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
|
||||
// Verify 'aud' claim
|
||||
checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
|
||||
@ -62,7 +91,58 @@ func NewJwtMiddleware(issuer string, audience string, keysLocation string) (*JWT
|
||||
},
|
||||
SigningMethod: jwt.SigningMethodRS256,
|
||||
EnableAuthOnOptions: false,
|
||||
}), nil
|
||||
}
|
||||
|
||||
if options.UserProperty == "" {
|
||||
options.UserProperty = "user"
|
||||
}
|
||||
|
||||
return &JWTValidator{
|
||||
options: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *JWTValidator) ValidateAndParse(token string) (*jwt.Token, error) {
|
||||
// If the token is empty...
|
||||
if token == "" {
|
||||
// Check if it was required
|
||||
if m.options.CredentialsOptional {
|
||||
log.Debugf("no credentials found (CredentialsOptional=true)")
|
||||
// No error, just no token (and that is ok given that CredentialsOptional is true)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If we get here, the required token is missing
|
||||
errorMsg := "required authorization token not found"
|
||||
log.Debugf(" Error: No credentials found (CredentialsOptional=false)")
|
||||
return nil, fmt.Errorf(errorMsg)
|
||||
}
|
||||
|
||||
// Now parse the token
|
||||
parsedToken, err := jwt.Parse(token, m.options.ValidationKeyGetter)
|
||||
|
||||
// Check if there was an error in parsing...
|
||||
if err != nil {
|
||||
log.Debugf("error parsing token: %v", err)
|
||||
return nil, fmt.Errorf("Error parsing token: %w", err)
|
||||
}
|
||||
|
||||
if m.options.SigningMethod != nil && m.options.SigningMethod.Alg() != parsedToken.Header["alg"] {
|
||||
errorMsg := fmt.Sprintf("Expected %s signing method but token specified %s",
|
||||
m.options.SigningMethod.Alg(),
|
||||
parsedToken.Header["alg"])
|
||||
log.Debugf("error validating token algorithm: %s", errorMsg)
|
||||
return nil, fmt.Errorf("error validating token algorithm: %s", errorMsg)
|
||||
}
|
||||
|
||||
// Check if the parsed token is valid...
|
||||
if !parsedToken.Valid {
|
||||
errorMsg := "token is invalid"
|
||||
log.Debugf(errorMsg)
|
||||
return nil, errors.New(errorMsg)
|
||||
}
|
||||
|
||||
return parsedToken, nil
|
||||
}
|
||||
|
||||
func getPemKeys(keysLocation string) (*Jwks, error) {
|
@ -48,6 +48,7 @@ type MockAccountManager struct {
|
||||
ListPoliciesFunc func(accountID, userID string) ([]*server.Policy, error)
|
||||
GetUsersFromAccountFunc func(accountID, userID string) ([]*server.UserInfo, error)
|
||||
GetAccountFromPATFunc func(pat string) (*server.Account, *server.User, *server.PersonalAccessToken, error)
|
||||
MarkPATUsedFunc func(pat string) error
|
||||
UpdatePeerMetaFunc func(peerID string, meta server.PeerSystemMeta) error
|
||||
UpdatePeerSSHKeyFunc func(peerID string, sshKey string) error
|
||||
UpdatePeerFunc func(accountID, userID string, peer *server.Peer) (*server.Peer, error)
|
||||
@ -186,6 +187,14 @@ func (am *MockAccountManager) GetAccountFromPAT(pat string) (*server.Account, *s
|
||||
return nil, nil, nil, status.Errorf(codes.Unimplemented, "method GetAccountFromPAT is not implemented")
|
||||
}
|
||||
|
||||
// MarkPATUsed mock implementation of MarkPATUsed from server.AccountManager interface
|
||||
func (am *MockAccountManager) MarkPATUsed(pat string) error {
|
||||
if am.MarkPATUsedFunc != nil {
|
||||
return am.MarkPATUsedFunc(pat)
|
||||
}
|
||||
return status.Errorf(codes.Unimplemented, "method MarkPATUsed is not implemented")
|
||||
}
|
||||
|
||||
// AddPATToUser mock implementation of AddPATToUser from server.AccountManager interface
|
||||
func (am *MockAccountManager) AddPATToUser(accountID string, userID string, pat *server.PersonalAccessToken) error {
|
||||
if am.AddPATToUserFunc != nil {
|
||||
|
Loading…
Reference in New Issue
Block a user