safing-portbase/api/authentication.go
2021-01-06 13:34:25 +01:00

363 lines
9.7 KiB
Go

package api
import (
"context"
"encoding/base64"
"errors"
"net/http"
"sync"
"time"
"github.com/tevino/abool"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/log"
"github.com/safing/portbase/rng"
)
// Permission defines an API requests permission.
type Permission int8
const (
// NotFound declares that the operation does not exist.
NotFound Permission = -2
// Require declares that the operation requires permission to be processed,
// but anyone can execute the operation.
Require Permission = -1
// NotSupported declares that the operation is not supported.
NotSupported Permission = 0
// PermitAnyone declares that anyone can execute the operation without any
// authentication.
PermitAnyone Permission = 1
// PermitUser declares that the operation may be executed by authenticated
// third party applications that are categorized as representing a simple
// user and is limited in access.
PermitUser Permission = 2
// PermitAdmin declares that the operation may be executed by authenticated
// third party applications that are categorized as representing an
// administrator and has broad in access.
PermitAdmin Permission = 3
// PermitSelf declares that the operation may only be executed by the
// software itself and its own (first party) components.
PermitSelf Permission = 4
)
// AuthenticatorFunc is a function that can be set as the authenticator for the
// API endpoint. If none is set, all requests will have full access.
// The returned AuthToken represents the permissions that the request has.
type AuthenticatorFunc func(r *http.Request, s *http.Server) (*AuthToken, error)
// AuthToken represents either a set of required or granted permissions.
// All attributes must be set when the struct is built and must not be changed
// later. Functions may be called at any time.
// The Write permission implicitly also includes reading.
type AuthToken struct {
Read Permission
Write Permission
validUntil time.Time
validLock sync.Mutex
}
// Expired returns whether the token has expired.
func (token *AuthToken) Expired() bool {
token.validLock.Lock()
defer token.validLock.Unlock()
return time.Now().After(token.validUntil)
}
// Refresh refreshes the validity of the token with the given TTL.
func (token *AuthToken) Refresh(ttl time.Duration) {
token.validLock.Lock()
defer token.validLock.Unlock()
token.validUntil = time.Now().Add(ttl)
}
// AuthenticatedHandler defines the handler interface to specify custom
// permission for an API handler. The returned permission is the required
// permission for the request to proceed.
type AuthenticatedHandler interface {
ReadPermission(*http.Request) Permission
WritePermission(*http.Request) Permission
}
const (
cookieName = "Portmaster-API-Token"
cookieTTL = 5 * time.Minute
)
var (
authFnSet = abool.New()
authFn AuthenticatorFunc
authTokens = make(map[string]*AuthToken)
authTokensLock sync.Mutex
// ErrAPIAccessDeniedMessage should be wrapped by errors returned by
// AuthenticatorFunc in order to signify a blocked request, including a error
// message for the user. This is an empty message on purpose, as to allow the
// function to define the full text of the error shown to the user.
ErrAPIAccessDeniedMessage = errors.New("")
)
// SetAuthenticator sets an authenticator function for the API endpoint. If none is set, all requests will be permitted.
func SetAuthenticator(fn AuthenticatorFunc) error {
if module.Online() {
return ErrAuthenticationImmutable
}
if !authFnSet.SetToIf(false, true) {
return ErrAuthenticationAlreadySet
}
authFn = fn
return nil
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := authenticateRequest(w, r, next)
if token != nil {
if _, apiRequest := getAPIContext(r); apiRequest != nil {
apiRequest.AuthToken = token
}
next.ServeHTTP(w, r)
}
})
}
func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler) *AuthToken {
tracer := log.Tracer(r.Context())
// Check if authenticator is set.
if !authFnSet.IsSet() {
// Return highest available permissions for the request.
return &AuthToken{
Read: PermitSelf,
Write: PermitSelf,
}
}
// Check if request is read only.
readRequest := isReadMethod(r.Method)
// Get required permission for target handler.
requiredPermission := PermitSelf
if authdHandler, ok := targetHandler.(AuthenticatedHandler); ok {
if readRequest {
requiredPermission = authdHandler.ReadPermission(r)
} else {
requiredPermission = authdHandler.WritePermission(r)
}
}
// Check if we need to do any authentication at all.
switch requiredPermission {
case NotFound:
// Not found.
tracer.Trace("api: authenticated handler reported: not found")
http.Error(w, "Not found.", http.StatusNotFound)
return nil
case NotSupported:
// A read or write permission can be marked as not supported.
tracer.Trace("api: authenticated handler reported: not supported")
http.Error(w, "Method not allowed.", http.StatusMethodNotAllowed)
return nil
case PermitAnyone:
// Don't process permissions, as we don't need them.
tracer.Tracef("api: granted %s access to public handler", r.RemoteAddr)
return &AuthToken{
Read: PermitAnyone,
Write: PermitAnyone,
}
case Require:
// Continue processing permissions, but treat as PermitAnyone.
requiredPermission = PermitAnyone
}
// The required permission must match the request permission values after
// handling the specials.
if requiredPermission < PermitAnyone || requiredPermission > PermitSelf {
tracer.Warningf(
"api: handler returned invalid permission: %s (%d)",
requiredPermission,
requiredPermission,
)
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
return nil
}
// Check for an existing auth token.
token := checkAuthToken(r)
// Get auth token from authenticator if none was in the request.
if token == nil {
var err error
token, err = authFn(r, server)
if err != nil {
// Check for internal error.
if !errors.Is(err, ErrAPIAccessDeniedMessage) {
tracer.Errorf("api: authenticator failed: %s", err)
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
return nil
}
// If authentication failed and we require authentication, return an
// authentication error.
if requiredPermission != PermitAnyone {
// Return authentication error.
tracer.Warningf("api: denying api access to %s", r.RemoteAddr)
http.Error(w, err.Error(), http.StatusForbidden)
return nil
}
token = &AuthToken{
Read: PermitAnyone,
Write: PermitAnyone,
}
}
// Apply auth token to request.
err = applyAuthToken(w, token)
if err != nil {
tracer.Warningf("api: failed to create auth token: %s", err)
}
}
// Get effective permission for request.
var requestPermission Permission
if readRequest {
requestPermission = token.Read
} else {
requestPermission = token.Write
}
// Check for valid request permission.
if requestPermission < PermitAnyone || requestPermission > PermitSelf {
tracer.Warningf(
"api: authenticator returned invalid permission: %s (%d)",
requestPermission,
requestPermission,
)
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
return nil
}
// Check permission.
if requestPermission < requiredPermission {
http.Error(w, "Insufficient permissions.", http.StatusForbidden)
return nil
}
tracer.Tracef("api: granted %s access to authenticated handler", r.RemoteAddr)
// Make a copy of the AuthToken in order mitigate the handler poisoning the
// token, as changes would apply to future requests.
return &AuthToken{
Read: token.Read,
Write: token.Write,
}
}
func checkAuthToken(r *http.Request) *AuthToken {
// Get auth token from request.
c, err := r.Cookie(cookieName)
if err != nil {
return nil
}
// Check if auth token is registered.
authTokensLock.Lock()
token, ok := authTokens[c.Value]
authTokensLock.Unlock()
if !ok {
log.Tracer(r.Context()).Tracef("api: provided auth token %s is unknown", c.Value)
return nil
}
// Check if token is still valid.
if token.Expired() {
log.Tracer(r.Context()).Tracef("api: provided auth token %s has expired", c.Value)
return nil
}
// Refresh token and return.
token.Refresh(cookieTTL)
log.Tracer(r.Context()).Tracef("api: auth token %s is valid, refreshing", c.Value)
return token
}
func applyAuthToken(w http.ResponseWriter, token *AuthToken) error {
// Generate new token secret.
secret, err := rng.Bytes(32) // 256 bit
if err != nil {
return err
}
secretHex := base64.RawURLEncoding.EncodeToString(secret)
// Set token cookie in response.
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: secretHex,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
// Set token TTL.
token.Refresh(cookieTTL)
// Save token.
authTokensLock.Lock()
defer authTokensLock.Unlock()
authTokens[secretHex] = token
return nil
}
func cleanAuthTokens(_ context.Context, _ *modules.Task) error {
authTokensLock.Lock()
defer authTokensLock.Unlock()
for secret, token := range authTokens {
if token.Expired() {
delete(authTokens, secret)
}
}
return nil
}
func isReadMethod(method string) bool {
return method == http.MethodGet || method == http.MethodHead
}
func (p Permission) String() string {
switch p {
case NotSupported:
return "NotSupported"
case Require:
return "Require"
case PermitAnyone:
return "PermitAnyone"
case PermitUser:
return "PermitUser"
case PermitAdmin:
return "PermitAdmin"
case PermitSelf:
return "PermitSelf"
case NotFound:
return "NotFound"
default:
return "Unknown"
}
}