mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-27 02:24:30 +01:00
250 lines
7.4 KiB
Go
250 lines
7.4 KiB
Go
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
|
|
}
|