github.com/safing/portbase@v0.19.5/api/authentication.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"net/url"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/tevino/abool"
    15  
    16  	"github.com/safing/portbase/config"
    17  	"github.com/safing/portbase/log"
    18  	"github.com/safing/portbase/modules"
    19  	"github.com/safing/portbase/rng"
    20  )
    21  
    22  const (
    23  	sessionCookieName = "Portmaster-API-Token"
    24  	sessionCookieTTL  = 5 * time.Minute
    25  )
    26  
    27  var (
    28  	apiKeys     = make(map[string]*AuthToken)
    29  	apiKeysLock sync.Mutex
    30  
    31  	authFnSet = abool.New()
    32  	authFn    AuthenticatorFunc
    33  
    34  	sessions     = make(map[string]*session)
    35  	sessionsLock sync.Mutex
    36  
    37  	// ErrAPIAccessDeniedMessage should be wrapped by errors returned by
    38  	// AuthenticatorFunc in order to signify a blocked request, including a error
    39  	// message for the user. This is an empty message on purpose, as to allow the
    40  	// function to define the full text of the error shown to the user.
    41  	ErrAPIAccessDeniedMessage = errors.New("")
    42  )
    43  
    44  // Permission defines an API requests permission.
    45  type Permission int8
    46  
    47  const (
    48  	// NotFound declares that the operation does not exist.
    49  	NotFound Permission = -2
    50  
    51  	// Dynamic declares that the operation requires permission to be processed,
    52  	// but anyone can execute the operation, as it reacts to permissions itself.
    53  	Dynamic Permission = -1
    54  
    55  	// NotSupported declares that the operation is not supported.
    56  	NotSupported Permission = 0
    57  
    58  	// PermitAnyone declares that anyone can execute the operation without any
    59  	// authentication.
    60  	PermitAnyone Permission = 1
    61  
    62  	// PermitUser declares that the operation may be executed by authenticated
    63  	// third party applications that are categorized as representing a simple
    64  	// user and is limited in access.
    65  	PermitUser Permission = 2
    66  
    67  	// PermitAdmin declares that the operation may be executed by authenticated
    68  	// third party applications that are categorized as representing an
    69  	// administrator and has broad in access.
    70  	PermitAdmin Permission = 3
    71  
    72  	// PermitSelf declares that the operation may only be executed by the
    73  	// software itself and its own (first party) components.
    74  	PermitSelf Permission = 4
    75  )
    76  
    77  // AuthenticatorFunc is a function that can be set as the authenticator for the
    78  // API endpoint. If none is set, all requests will have full access.
    79  // The returned AuthToken represents the permissions that the request has.
    80  type AuthenticatorFunc func(r *http.Request, s *http.Server) (*AuthToken, error)
    81  
    82  // AuthToken represents either a set of required or granted permissions.
    83  // All attributes must be set when the struct is built and must not be changed
    84  // later. Functions may be called at any time.
    85  // The Write permission implicitly also includes reading.
    86  type AuthToken struct {
    87  	Read       Permission
    88  	Write      Permission
    89  	ValidUntil *time.Time
    90  }
    91  
    92  type session struct {
    93  	sync.Mutex
    94  
    95  	token      *AuthToken
    96  	validUntil time.Time
    97  }
    98  
    99  // Expired returns whether the session has expired.
   100  func (sess *session) Expired() bool {
   101  	sess.Lock()
   102  	defer sess.Unlock()
   103  
   104  	return time.Now().After(sess.validUntil)
   105  }
   106  
   107  // Refresh refreshes the validity of the session with the given TTL.
   108  func (sess *session) Refresh(ttl time.Duration) {
   109  	sess.Lock()
   110  	defer sess.Unlock()
   111  
   112  	sess.validUntil = time.Now().Add(ttl)
   113  }
   114  
   115  // AuthenticatedHandler defines the handler interface to specify custom
   116  // permission for an API handler. The returned permission is the required
   117  // permission for the request to proceed.
   118  type AuthenticatedHandler interface {
   119  	ReadPermission(*http.Request) Permission
   120  	WritePermission(*http.Request) Permission
   121  }
   122  
   123  // SetAuthenticator sets an authenticator function for the API endpoint. If none is set, all requests will be permitted.
   124  func SetAuthenticator(fn AuthenticatorFunc) error {
   125  	if module.Online() {
   126  		return ErrAuthenticationImmutable
   127  	}
   128  
   129  	if !authFnSet.SetToIf(false, true) {
   130  		return ErrAuthenticationAlreadySet
   131  	}
   132  
   133  	authFn = fn
   134  	return nil
   135  }
   136  
   137  func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler, readMethod bool) *AuthToken {
   138  	tracer := log.Tracer(r.Context())
   139  
   140  	// Get required permission for target handler.
   141  	requiredPermission := PermitSelf
   142  	if authdHandler, ok := targetHandler.(AuthenticatedHandler); ok {
   143  		if readMethod {
   144  			requiredPermission = authdHandler.ReadPermission(r)
   145  		} else {
   146  			requiredPermission = authdHandler.WritePermission(r)
   147  		}
   148  	}
   149  
   150  	// Check if we need to do any authentication at all.
   151  	switch requiredPermission { //nolint:exhaustive
   152  	case NotFound:
   153  		// Not found.
   154  		tracer.Debug("api: no API endpoint registered for this path")
   155  		http.Error(w, "Not found.", http.StatusNotFound)
   156  		return nil
   157  	case NotSupported:
   158  		// A read or write permission can be marked as not supported.
   159  		tracer.Trace("api: authenticated handler reported: not supported")
   160  		http.Error(w, "Method not allowed.", http.StatusMethodNotAllowed)
   161  		return nil
   162  	case PermitAnyone:
   163  		// Don't process permissions, as we don't need them.
   164  		tracer.Tracef("api: granted %s access to public handler", r.RemoteAddr)
   165  		return &AuthToken{
   166  			Read:  PermitAnyone,
   167  			Write: PermitAnyone,
   168  		}
   169  	case Dynamic:
   170  		// Continue processing permissions, but treat as PermitAnyone.
   171  		requiredPermission = PermitAnyone
   172  	}
   173  
   174  	// The required permission must match the request permission values after
   175  	// handling the specials.
   176  	if requiredPermission < PermitAnyone || requiredPermission > PermitSelf {
   177  		tracer.Warningf(
   178  			"api: handler returned invalid permission: %s (%d)",
   179  			requiredPermission,
   180  			requiredPermission,
   181  		)
   182  		http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
   183  		return nil
   184  	}
   185  
   186  	// Authenticate request.
   187  	token, handled := checkAuth(w, r, requiredPermission > PermitAnyone)
   188  	switch {
   189  	case handled:
   190  		return nil
   191  	case token == nil:
   192  		// Use default permissions.
   193  		token = &AuthToken{
   194  			Read:  PermitAnyone,
   195  			Write: PermitAnyone,
   196  		}
   197  	}
   198  
   199  	// Get effective permission for request.
   200  	var requestPermission Permission
   201  	if readMethod {
   202  		requestPermission = token.Read
   203  	} else {
   204  		requestPermission = token.Write
   205  	}
   206  
   207  	// Check for valid request permission.
   208  	if requestPermission < PermitAnyone || requestPermission > PermitSelf {
   209  		tracer.Warningf(
   210  			"api: authenticator returned invalid permission: %s (%d)",
   211  			requestPermission,
   212  			requestPermission,
   213  		)
   214  		http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
   215  		return nil
   216  	}
   217  
   218  	// Check permission.
   219  	if requestPermission < requiredPermission {
   220  		// If the token is strictly public, return an authentication request.
   221  		if token.Read == PermitAnyone && token.Write == PermitAnyone {
   222  			w.Header().Set(
   223  				"WWW-Authenticate",
   224  				`Bearer realm="Portmaster API" domain="/"`,
   225  			)
   226  			http.Error(w, "Authorization required.", http.StatusUnauthorized)
   227  			return nil
   228  		}
   229  
   230  		// Otherwise just inform of insufficient permissions.
   231  		http.Error(w, "Insufficient permissions.", http.StatusForbidden)
   232  		return nil
   233  	}
   234  
   235  	tracer.Tracef("api: granted %s access to protected handler", r.RemoteAddr)
   236  
   237  	// Make a copy of the AuthToken in order mitigate the handler poisoning the
   238  	// token, as changes would apply to future requests.
   239  	return &AuthToken{
   240  		Read:  token.Read,
   241  		Write: token.Write,
   242  	}
   243  }
   244  
   245  func checkAuth(w http.ResponseWriter, r *http.Request, authRequired bool) (token *AuthToken, handled bool) {
   246  	// Return highest possible permissions in dev mode.
   247  	if devMode() {
   248  		return &AuthToken{
   249  			Read:  PermitSelf,
   250  			Write: PermitSelf,
   251  		}, false
   252  	}
   253  
   254  	// Database Bridge Access.
   255  	if r.RemoteAddr == endpointBridgeRemoteAddress {
   256  		return &AuthToken{
   257  			Read:  dbCompatibilityPermission,
   258  			Write: dbCompatibilityPermission,
   259  		}, false
   260  	}
   261  
   262  	// Check for valid API key.
   263  	token = checkAPIKey(r)
   264  	if token != nil {
   265  		return token, false
   266  	}
   267  
   268  	// Check for valid session cookie.
   269  	token = checkSessionCookie(r)
   270  	if token != nil {
   271  		return token, false
   272  	}
   273  
   274  	// Check if an external authentication method is available.
   275  	if !authFnSet.IsSet() {
   276  		return nil, false
   277  	}
   278  
   279  	// Authenticate externally.
   280  	token, err := authFn(r, server)
   281  	if err != nil {
   282  		// Check if the authentication process failed internally.
   283  		if !errors.Is(err, ErrAPIAccessDeniedMessage) {
   284  			log.Tracer(r.Context()).Errorf("api: authenticator failed: %s", err)
   285  			http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
   286  			return nil, true
   287  		}
   288  
   289  		// Return authentication failure message if authentication is required.
   290  		if authRequired {
   291  			log.Tracer(r.Context()).Warningf("api: denying api access from %s", r.RemoteAddr)
   292  			http.Error(w, err.Error(), http.StatusForbidden)
   293  			return nil, true
   294  		}
   295  
   296  		return nil, false
   297  	}
   298  
   299  	// Abort if no token is returned.
   300  	if token == nil {
   301  		return nil, false
   302  	}
   303  
   304  	// Create session cookie for authenticated request.
   305  	err = createSession(w, r, token)
   306  	if err != nil {
   307  		log.Tracer(r.Context()).Warningf("api: failed to create session: %s", err)
   308  	}
   309  	return token, false
   310  }
   311  
   312  func checkAPIKey(r *http.Request) *AuthToken {
   313  	// Get API key from request.
   314  	key := r.Header.Get("Authorization")
   315  	if key == "" {
   316  		return nil
   317  	}
   318  
   319  	// Parse API key.
   320  	switch {
   321  	case strings.HasPrefix(key, "Bearer "):
   322  		key = strings.TrimPrefix(key, "Bearer ")
   323  	case strings.HasPrefix(key, "Basic "):
   324  		user, pass, _ := r.BasicAuth()
   325  		key = user + pass
   326  	default:
   327  		log.Tracer(r.Context()).Tracef(
   328  			"api: provided api key type %s is unsupported", strings.Split(key, " ")[0],
   329  		)
   330  		return nil
   331  	}
   332  
   333  	apiKeysLock.Lock()
   334  	defer apiKeysLock.Unlock()
   335  
   336  	// Check if the provided API key exists.
   337  	token, ok := apiKeys[key]
   338  	if !ok {
   339  		log.Tracer(r.Context()).Tracef(
   340  			"api: provided api key %s... is unknown", key[:4],
   341  		)
   342  		return nil
   343  	}
   344  
   345  	// Abort if the token is expired.
   346  	if token.ValidUntil != nil && time.Now().After(*token.ValidUntil) {
   347  		log.Tracer(r.Context()).Warningf("api: denying api access from %s using expired token", r.RemoteAddr)
   348  		return nil
   349  	}
   350  
   351  	return token
   352  }
   353  
   354  func updateAPIKeys(_ context.Context, _ interface{}) error {
   355  	apiKeysLock.Lock()
   356  	defer apiKeysLock.Unlock()
   357  
   358  	log.Debug("api: importing possibly updated API keys from config")
   359  
   360  	// Delete current keys.
   361  	for k := range apiKeys {
   362  		delete(apiKeys, k)
   363  	}
   364  
   365  	// whether or not we found expired API keys that should be removed
   366  	// from the setting
   367  	hasExpiredKeys := false
   368  
   369  	// a list of valid API keys. Used when hasExpiredKeys is set to true.
   370  	// in that case we'll update the setting to only contain validAPIKeys
   371  	validAPIKeys := []string{}
   372  
   373  	// Parse new keys.
   374  	for _, key := range configuredAPIKeys() {
   375  		u, err := url.Parse(key)
   376  		if err != nil {
   377  			log.Errorf("api: failed to parse configured API key %s: %s", key, err)
   378  
   379  			continue
   380  		}
   381  
   382  		if u.Path == "" {
   383  			log.Errorf("api: malformed API key %s: missing path section", key)
   384  
   385  			continue
   386  		}
   387  
   388  		// Create token with default permissions.
   389  		token := &AuthToken{
   390  			Read:  PermitAnyone,
   391  			Write: PermitAnyone,
   392  		}
   393  
   394  		// Update with configured permissions.
   395  		q := u.Query()
   396  		// Parse read permission.
   397  		readPermission, err := parseAPIPermission(q.Get("read"))
   398  		if err != nil {
   399  			log.Errorf("api: invalid API key %s: %s", key, err)
   400  			continue
   401  		}
   402  		token.Read = readPermission
   403  		// Parse write permission.
   404  		writePermission, err := parseAPIPermission(q.Get("write"))
   405  		if err != nil {
   406  			log.Errorf("api: invalid API key %s: %s", key, err)
   407  			continue
   408  		}
   409  		token.Write = writePermission
   410  
   411  		expireStr := q.Get("expires")
   412  		if expireStr != "" {
   413  			validUntil, err := time.Parse(time.RFC3339, expireStr)
   414  			if err != nil {
   415  				log.Errorf("api: invalid API key %s: %s", key, err)
   416  				continue
   417  			}
   418  
   419  			// continue to the next token if this one is already invalid
   420  			if time.Now().After(validUntil) {
   421  				// mark the key as expired so we'll remove it from the setting afterwards
   422  				hasExpiredKeys = true
   423  
   424  				continue
   425  			}
   426  
   427  			token.ValidUntil = &validUntil
   428  		}
   429  
   430  		// Save token.
   431  		apiKeys[u.Path] = token
   432  		validAPIKeys = append(validAPIKeys, key)
   433  	}
   434  
   435  	if hasExpiredKeys {
   436  		module.StartLowPriorityMicroTask("api key cleanup", 0, func(ctx context.Context) error {
   437  			if err := config.SetConfigOption(CfgAPIKeys, validAPIKeys); err != nil {
   438  				log.Errorf("api: failed to remove expired API keys: %s", err)
   439  			} else {
   440  				log.Infof("api: removed expired API keys from %s", CfgAPIKeys)
   441  			}
   442  
   443  			return nil
   444  		})
   445  	}
   446  
   447  	return nil
   448  }
   449  
   450  func checkSessionCookie(r *http.Request) *AuthToken {
   451  	// Get session cookie from request.
   452  	c, err := r.Cookie(sessionCookieName)
   453  	if err != nil {
   454  		return nil
   455  	}
   456  
   457  	// Check if session cookie is registered.
   458  	sessionsLock.Lock()
   459  	sess, ok := sessions[c.Value]
   460  	sessionsLock.Unlock()
   461  	if !ok {
   462  		log.Tracer(r.Context()).Tracef("api: provided session cookie %s is unknown", c.Value)
   463  		return nil
   464  	}
   465  
   466  	// Check if session is still valid.
   467  	if sess.Expired() {
   468  		log.Tracer(r.Context()).Tracef("api: provided session cookie %s has expired", c.Value)
   469  		return nil
   470  	}
   471  
   472  	// Refresh session and return.
   473  	sess.Refresh(sessionCookieTTL)
   474  	log.Tracer(r.Context()).Tracef("api: session cookie %s is valid, refreshing", c.Value)
   475  	return sess.token
   476  }
   477  
   478  func createSession(w http.ResponseWriter, r *http.Request, token *AuthToken) error {
   479  	// Generate new session key.
   480  	secret, err := rng.Bytes(32) // 256 bit
   481  	if err != nil {
   482  		return err
   483  	}
   484  	sessionKey := base64.RawURLEncoding.EncodeToString(secret)
   485  
   486  	// Set token cookie in response.
   487  	http.SetCookie(w, &http.Cookie{
   488  		Name:     sessionCookieName,
   489  		Value:    sessionKey,
   490  		Path:     "/",
   491  		HttpOnly: true,
   492  		SameSite: http.SameSiteStrictMode,
   493  	})
   494  
   495  	// Create session.
   496  	sess := &session{
   497  		token: token,
   498  	}
   499  	sess.Refresh(sessionCookieTTL)
   500  
   501  	// Save session.
   502  	sessionsLock.Lock()
   503  	defer sessionsLock.Unlock()
   504  	sessions[sessionKey] = sess
   505  	log.Tracer(r.Context()).Debug("api: issued session cookie")
   506  
   507  	return nil
   508  }
   509  
   510  func cleanSessions(_ context.Context, _ *modules.Task) error {
   511  	sessionsLock.Lock()
   512  	defer sessionsLock.Unlock()
   513  
   514  	for sessionKey, sess := range sessions {
   515  		if sess.Expired() {
   516  			delete(sessions, sessionKey)
   517  		}
   518  	}
   519  
   520  	return nil
   521  }
   522  
   523  func deleteSession(sessionKey string) {
   524  	sessionsLock.Lock()
   525  	defer sessionsLock.Unlock()
   526  
   527  	delete(sessions, sessionKey)
   528  }
   529  
   530  func getEffectiveMethod(r *http.Request) (eMethod string, readMethod bool, ok bool) {
   531  	method := r.Method
   532  
   533  	// Get CORS request method if OPTIONS request.
   534  	if r.Method == http.MethodOptions {
   535  		method = r.Header.Get("Access-Control-Request-Method")
   536  		if method == "" {
   537  			return "", false, false
   538  		}
   539  	}
   540  
   541  	switch method {
   542  	case http.MethodGet, http.MethodHead:
   543  		return http.MethodGet, true, true
   544  	case http.MethodPost, http.MethodPut, http.MethodDelete:
   545  		return method, false, true
   546  	default:
   547  		return "", false, false
   548  	}
   549  }
   550  
   551  func parseAPIPermission(s string) (Permission, error) {
   552  	switch strings.ToLower(s) {
   553  	case "", "anyone":
   554  		return PermitAnyone, nil
   555  	case "user":
   556  		return PermitUser, nil
   557  	case "admin":
   558  		return PermitAdmin, nil
   559  	default:
   560  		return PermitAnyone, fmt.Errorf("invalid permission: %s", s)
   561  	}
   562  }
   563  
   564  func (p Permission) String() string {
   565  	switch p {
   566  	case NotSupported:
   567  		return "NotSupported"
   568  	case Dynamic:
   569  		return "Dynamic"
   570  	case PermitAnyone:
   571  		return "PermitAnyone"
   572  	case PermitUser:
   573  		return "PermitUser"
   574  	case PermitAdmin:
   575  		return "PermitAdmin"
   576  	case PermitSelf:
   577  		return "PermitSelf"
   578  	case NotFound:
   579  		return "NotFound"
   580  	default:
   581  		return "Unknown"
   582  	}
   583  }
   584  
   585  // Role returns a string representation of the permission role.
   586  func (p Permission) Role() string {
   587  	switch p {
   588  	case PermitAnyone:
   589  		return "Anyone"
   590  	case PermitUser:
   591  		return "User"
   592  	case PermitAdmin:
   593  		return "Admin"
   594  	case PermitSelf:
   595  		return "Self"
   596  	case Dynamic, NotFound, NotSupported:
   597  		return "Invalid"
   598  	default:
   599  		return "Invalid"
   600  	}
   601  }