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 (
|
||||
"encoding/json"
|
||||
"github.com/apex/log"
|
||||
"github.com/gbrlsnchs/jwt/v3"
|
||||
"strings"
|
||||
"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 {
|
||||
jwt.Payload
|
||||
sync.RWMutex
|
||||
|
@ -24,6 +53,7 @@ func (p *WebsocketPayload) GetPayload() *jwt.Payload {
|
|||
return &p.Payload
|
||||
}
|
||||
|
||||
// Returns the UUID of the server associated with this JWT.
|
||||
func (p *WebsocketPayload) GetServerUuid() string {
|
||||
p.RLock()
|
||||
defer p.RUnlock()
|
||||
|
@ -31,6 +61,33 @@ func (p *WebsocketPayload) GetServerUuid() string {
|
|||
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.
|
||||
func (p *WebsocketPayload) HasPermission(permission string) bool {
|
||||
p.RLock()
|
||||
|
@ -38,7 +95,7 @@ func (p *WebsocketPayload) HasPermission(permission string) bool {
|
|||
|
||||
for _, k := range p.Permissions {
|
||||
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/router/tokens"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/pterodactyl/wings/server/filesystem"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -45,12 +44,14 @@ var (
|
|||
ErrJwtNotPresent = errors.New("jwt: no jwt present")
|
||||
ErrJwtNoConnectPerm = errors.New("jwt: missing connect permission")
|
||||
ErrJwtUuidMismatch = errors.New("jwt: server uuid mismatch")
|
||||
ErrJwtOnDenylist = errors.New("jwt: created too far in past (denylist)")
|
||||
)
|
||||
|
||||
func IsJwtError(err error) bool {
|
||||
return errors.Is(err, ErrJwtNotPresent) ||
|
||||
errors.Is(err, ErrJwtNoConnectPerm) ||
|
||||
errors.Is(err, ErrJwtUuidMismatch) ||
|
||||
errors.Is(err, ErrJwtOnDenylist) ||
|
||||
errors.Is(err, jwt.ErrExpValidation)
|
||||
}
|
||||
|
||||
|
@ -62,8 +63,12 @@ func NewTokenPayload(token []byte) (*tokens.WebsocketPayload, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if payload.Denylisted() {
|
||||
return nil, ErrJwtOnDenylist
|
||||
}
|
||||
|
||||
if !payload.HasPermission(PermissionConnect) {
|
||||
return nil, errors.New("not authorized to connect to this socket")
|
||||
return nil, ErrJwtNoConnectPerm
|
||||
}
|
||||
|
||||
return &payload, nil
|
||||
|
@ -188,6 +193,10 @@ func (h *Handler) TokenValid() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if j.Denylisted() {
|
||||
return ErrJwtOnDenylist
|
||||
}
|
||||
|
||||
if !j.HasPermission(PermissionConnect) {
|
||||
return ErrJwtNoConnectPerm
|
||||
}
|
||||
|
@ -204,25 +213,23 @@ func (h *Handler) TokenValid() error {
|
|||
// error message, otherwise we just send back a standard error message.
|
||||
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
|
||||
j := h.GetJwt()
|
||||
expected := errors.Is(err, server.ErrSuspended) ||
|
||||
errors.Is(err, server.ErrIsRunning) ||
|
||||
errors.Is(err, filesystem.ErrNotEnoughDiskSpace)
|
||||
isJWTError := IsJwtError(err)
|
||||
|
||||
message := "an unexpected error was encountered while handling this request"
|
||||
if expected || (j != nil && j.HasPermission(PermissionReceiveErrors)) {
|
||||
message = err.Error()
|
||||
wsm := Message{
|
||||
Event: ErrorEvent,
|
||||
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)
|
||||
|
||||
wsm := Message{Event: ErrorEvent}
|
||||
m, u := h.GetErrorMessage(wsm.Args[0])
|
||||
wsm.Args = []string{m}
|
||||
|
||||
if 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}).
|
||||
Error("failed to handle websocket process; an error was encountered processing an event")
|
||||
}
|
||||
if !isJWTError && (len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true)) {
|
||||
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")
|
||||
}
|
||||
|
||||
return h.unsafeSendJson(wsm)
|
||||
|
|
Loading…
Reference in New Issue
Block a user