Add support for denying JWT JTI's that are generated before a specific time
This commit is contained in:
parent
912d95de24
commit
65664b63e7
|
@ -2,11 +2,40 @@ package tokens
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/apex/log"
|
||||||
"github.com/gbrlsnchs/jwt/v3"
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The time at which Wings was booted. No JWT's created before this time are allowed to
|
||||||
|
// connect to the socket since they may have been marked as denied already and therefore
|
||||||
|
// could be invalid at this point.
|
||||||
|
//
|
||||||
|
// By doing this we make it so that a user who gets disconnected from Wings due to a Wings
|
||||||
|
// reboot just needs to request a new token as if their old token had expired naturally.
|
||||||
|
var wingsBootTime = time.Now()
|
||||||
|
|
||||||
|
// A map that contains any JTI's that have been denied by the Panel and the time at which
|
||||||
|
// they were marked as denied. Therefore any JWT with the same JTI and an IssuedTime that
|
||||||
|
// is the same as or before this time should be considered invalid.
|
||||||
|
//
|
||||||
|
// This is used to allow the Panel to revoke tokens en-masse for a given user & server
|
||||||
|
// combination since the JTI for tokens is just MD5(user.id + server.uuid). When a server
|
||||||
|
// is booted this listing is fetched from the panel and the Websocket is dynamically updated.
|
||||||
|
var denylist sync.Map
|
||||||
|
|
||||||
|
// Adds a JTI to the denylist by marking any JWTs generated before the current time as
|
||||||
|
// being invalid if they use the same JTI.
|
||||||
|
func DenyJTI(jti string) {
|
||||||
|
log.WithField("jti", jti).Debugf("adding all JWTs with JTI of \"%s\" created before current time to denylist", jti)
|
||||||
|
|
||||||
|
denylist.Store(jti, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// A JWT payload for Websocket connections. This JWT is passed along to the Websocket after
|
||||||
|
// it has been connected to by sending an "auth" event.
|
||||||
type WebsocketPayload struct {
|
type WebsocketPayload struct {
|
||||||
jwt.Payload
|
jwt.Payload
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
@ -24,6 +53,7 @@ func (p *WebsocketPayload) GetPayload() *jwt.Payload {
|
||||||
return &p.Payload
|
return &p.Payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the UUID of the server associated with this JWT.
|
||||||
func (p *WebsocketPayload) GetServerUuid() string {
|
func (p *WebsocketPayload) GetServerUuid() string {
|
||||||
p.RLock()
|
p.RLock()
|
||||||
defer p.RUnlock()
|
defer p.RUnlock()
|
||||||
|
@ -31,6 +61,33 @@ func (p *WebsocketPayload) GetServerUuid() string {
|
||||||
return p.ServerUUID
|
return p.ServerUUID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the JWT has been marked as denied by the instance due to either being issued
|
||||||
|
// before Wings was booted, or because we have denied all tokens with the same JTI
|
||||||
|
// occurring before a set time.
|
||||||
|
func (p *WebsocketPayload) Denylisted() bool {
|
||||||
|
// If there is no IssuedAt present for the token, we cannot validate the token so
|
||||||
|
// just immediately mark it as not valid.
|
||||||
|
if p.IssuedAt == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the time that the token was issued is before the time at which Wings was booted
|
||||||
|
// then the token is invalid for our purposes, even if the token "has permission".
|
||||||
|
if p.IssuedAt.Time.Before(wingsBootTime) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, if the token was issued before a time that is currently denied for this
|
||||||
|
// token instance, ignore the permissions response.
|
||||||
|
if t, ok := denylist.Load(p.JWTID); ok {
|
||||||
|
if p.IssuedAt.Time.Before(t.(time.Time)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Checks if the given token payload has a permission string.
|
// Checks if the given token payload has a permission string.
|
||||||
func (p *WebsocketPayload) HasPermission(permission string) bool {
|
func (p *WebsocketPayload) HasPermission(permission string) bool {
|
||||||
p.RLock()
|
p.RLock()
|
||||||
|
@ -38,7 +95,7 @@ func (p *WebsocketPayload) HasPermission(permission string) bool {
|
||||||
|
|
||||||
for _, k := range p.Permissions {
|
for _, k := range p.Permissions {
|
||||||
if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") {
|
if k == permission || (!strings.HasPrefix(permission, "admin") && k == "*") {
|
||||||
return true
|
return !p.Denylisted()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ import (
|
||||||
"github.com/pterodactyl/wings/environment/docker"
|
"github.com/pterodactyl/wings/environment/docker"
|
||||||
"github.com/pterodactyl/wings/router/tokens"
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"github.com/pterodactyl/wings/server/filesystem"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -45,12 +44,14 @@ var (
|
||||||
ErrJwtNotPresent = errors.New("jwt: no jwt present")
|
ErrJwtNotPresent = errors.New("jwt: no jwt present")
|
||||||
ErrJwtNoConnectPerm = errors.New("jwt: missing connect permission")
|
ErrJwtNoConnectPerm = errors.New("jwt: missing connect permission")
|
||||||
ErrJwtUuidMismatch = errors.New("jwt: server uuid mismatch")
|
ErrJwtUuidMismatch = errors.New("jwt: server uuid mismatch")
|
||||||
|
ErrJwtOnDenylist = errors.New("jwt: created too far in past (denylist)")
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsJwtError(err error) bool {
|
func IsJwtError(err error) bool {
|
||||||
return errors.Is(err, ErrJwtNotPresent) ||
|
return errors.Is(err, ErrJwtNotPresent) ||
|
||||||
errors.Is(err, ErrJwtNoConnectPerm) ||
|
errors.Is(err, ErrJwtNoConnectPerm) ||
|
||||||
errors.Is(err, ErrJwtUuidMismatch) ||
|
errors.Is(err, ErrJwtUuidMismatch) ||
|
||||||
|
errors.Is(err, ErrJwtOnDenylist) ||
|
||||||
errors.Is(err, jwt.ErrExpValidation)
|
errors.Is(err, jwt.ErrExpValidation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,8 +63,12 @@ func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.Denylisted() {
|
||||||
|
return nil, ErrJwtOnDenylist
|
||||||
|
}
|
||||||
|
|
||||||
if !payload.HasPermission(PermissionConnect) {
|
if !payload.HasPermission(PermissionConnect) {
|
||||||
return nil, errors.New("not authorized to connect to this socket")
|
return nil, ErrJwtNoConnectPerm
|
||||||
}
|
}
|
||||||
|
|
||||||
return &payload, nil
|
return &payload, nil
|
||||||
|
@ -188,6 +193,10 @@ func (h *Handler) TokenValid() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if j.Denylisted() {
|
||||||
|
return ErrJwtOnDenylist
|
||||||
|
}
|
||||||
|
|
||||||
if !j.HasPermission(PermissionConnect) {
|
if !j.HasPermission(PermissionConnect) {
|
||||||
return ErrJwtNoConnectPerm
|
return ErrJwtNoConnectPerm
|
||||||
}
|
}
|
||||||
|
@ -204,25 +213,23 @@ func (h *Handler) TokenValid() error {
|
||||||
// error message, otherwise we just send back a standard error message.
|
// error message, otherwise we just send back a standard error message.
|
||||||
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
|
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
|
||||||
j := h.GetJwt()
|
j := h.GetJwt()
|
||||||
expected := errors.Is(err, server.ErrSuspended) ||
|
isJWTError := IsJwtError(err)
|
||||||
errors.Is(err, server.ErrIsRunning) ||
|
|
||||||
errors.Is(err, filesystem.ErrNotEnoughDiskSpace)
|
|
||||||
|
|
||||||
message := "an unexpected error was encountered while handling this request"
|
wsm := Message{
|
||||||
if expected || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
|
Event: ErrorEvent,
|
||||||
message = err.Error()
|
Args: []string{"an unexpected error was encountered while handling this request"},
|
||||||
|
}
|
||||||
|
if isJWTError || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
|
||||||
|
wsm.Event = JwtErrorEvent
|
||||||
|
wsm.Args = []string{err.Error()}
|
||||||
}
|
}
|
||||||
|
|
||||||
m, u := h.GetErrorMessage(message)
|
m, u := h.GetErrorMessage(wsm.Args[0])
|
||||||
|
|
||||||
wsm := Message{Event: ErrorEvent}
|
|
||||||
wsm.Args = []string{m}
|
wsm.Args = []string{m}
|
||||||
|
|
||||||
if len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true) {
|
if !isJWTError && (len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true)) {
|
||||||
if !expected && !IsJwtError(err) {
|
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
|
||||||
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
|
Error("failed to handle websocket process; an error was encountered processing an event")
|
||||||
Error("failed to handle websocket process; an error was encountered processing an event")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.unsafeSendJson(wsm)
|
return h.unsafeSendJson(wsm)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user