From fb11295168040c25ad50031210c7d2f4d668d63d Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Wed, 19 Apr 2023 19:55:39 -0700 Subject: [PATCH] Re-sign JWT tokens for safer server deployments --- README.md | 13 ++++++++++++- pkg/config.go | 2 +- pkg/jwt/jwt.go | 16 ++++++++++++++++ pkg/websocket.go | 13 ++++++++++++- web/static/js/BareRTC.js | 9 +++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9bdc6a2..3dbdc3f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,11 @@ It is very much in the style of the old-school Flash based webcam chat rooms of * Specify multiple Public Channels that all users have access to. * Users can open direct message (one-on-one) conversations with each other. * No long-term server side state: messages are pushed out as they come in. +* Users may share pictures and GIFs from their computer, which are pushed out as `data:` URLs (images scaled and metadata stripped by server) directly to connected chatters with no storage required. * Users may broadcast their webcam which shows a camera icon by their name in the Who List. Users may click on those icons to open multiple camera feeds of other users they are interested in. + * Mutual webcam options: users may opt that anyone who views their cam must also be sharing their own camera first. + * Users may mark their own cameras as explicit/NSFW which marks the icon in red so other users can get a warning before clicking in (if NSFW is enabled in the settings.toml) + * Users may boot people off their camera, and to the booted person it appears the same as if the broadcaster had turned their camera off completely - the chat server lies about the camera status so the booted user can't easily tell they'd been booted. * Mobile friendly: works best on iPads and above but adapts to smaller screens well. * WebRTC means peer-to-peer video streaming so cheap on hosting costs! * Simple integration with your existing userbase via signed JWT tokens. @@ -44,12 +48,16 @@ Some important features still lacking: On first run it will create the default settings.toml file for you which you may then customize to your liking: ```toml +Version = 2 Title = "BareRTC" Branding = "BareRTC" WebsiteURL = "https://www.example.com" -UseXForwardedFor = true CORSHosts = ["https://www.example.com"] PermitNSFW = true +UseXForwardedFor = true +WebSocketReadLimit = 41943040 +MaxImageWidth = 1280 +PreviewImageWidth = 360 [JWT] Enabled = false @@ -79,6 +87,9 @@ A description of the config directives includes: * **UseXForwardedFor**: set it to true and (for logging) the user's remote IP will use the X-Real-IP header or the first address in X-Forwarded-For. Set this if you run the app behind a proxy like nginx if you want IPs not to be all localhost. * **CORSHosts**: your website's domain names that will be allowed to access [JSON APIs](#JSON APIs), like `/api/statistics`. * **PermitNSFW**: for user webcam streams, expressly permit "NSFW" content if the user opts in to mark their feed as such. Setting this will enable pop-up modals regarding NSFW video and give broadcasters an opt-in button, which will warn other users before they click in to watch. + * **WebSocketReadLimit**: sets a size limit for WebSocket messages - it essentially also caps the max upload size for shared images (add a buffer as images will be base64 encoded on upload). + * **MaxImageWidth**: for pictures shared in chat the server will resize them down to no larger than this width for the full size view. + * **PreviewImageWidth**: to not flood the chat, the image in chat is this wide and users can click it to see the MaxImageWidth in a lightbox modal. * **JWT**: settings for JWT [Authentication](#authentication). * Enabled (bool): activate the JWT token authentication feature. * Strict (bool): if true, **only** valid signed JWT tokens may log in. If false, users with no/invalid token can enter their own username without authentication. diff --git a/pkg/config.go b/pkg/config.go index 1f191b2..2000f3a 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -2,4 +2,4 @@ package barertc import "time" -const PingInterval = 15 * time.Second +const PingInterval = 30 * time.Second diff --git a/pkg/jwt/jwt.go b/pkg/jwt/jwt.go index bef805a..6e2b46e 100644 --- a/pkg/jwt/jwt.go +++ b/pkg/jwt/jwt.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "html/template" + "time" "git.kirsle.net/apps/barertc/pkg/config" "github.com/golang-jwt/jwt/v4" @@ -53,3 +54,18 @@ func ParseAndValidate(tokenStr string) (*Claims, bool, error) { return claims, authOK, nil } + +// ReSign will sign a new JWT token for existing claims. The chat server does this to send refreshed tokens +// to the front-end so the server can reboot gracefully, clients reconnect and not be told their auth had +// expired. New token expires after 5 minutes. +func (c Claims) ReSign() (string, error) { + // Refresh timestamps. + c.ExpiresAt = jwt.NewNumericDate(time.Now().Add(5 * time.Minute)) + c.IssuedAt = jwt.NewNumericDate(time.Now()) + c.NotBefore = jwt.NewNumericDate(time.Now()) + + // Generate the signed token and return it. + token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) + ss, err := token.SignedString([]byte(config.Current.JWT.SecretKey)) + return ss, err +} diff --git a/pkg/websocket.go b/pkg/websocket.go index 1893627..c6291a9 100644 --- a/pkg/websocket.go +++ b/pkg/websocket.go @@ -188,8 +188,19 @@ func (s *Server) WebSocket() http.HandlerFunc { return } case <-pinger.C: + // Send a ping, and a refreshed JWT token if the user sent one. + var token string + if sub.JWTClaims != nil { + if jwt, err := sub.JWTClaims.ReSign(); err != nil { + log.Error("ReSign JWT token for %s: %s", sub.Username, err) + } else { + token = jwt + } + } + sub.SendJSON(Message{ - Action: ActionPing, + Action: ActionPing, + JWTToken: token, }) case <-ctx.Done(): pinger.Stop() diff --git a/web/static/js/BareRTC.js b/web/static/js/BareRTC.js index db729de..7fa6d0f 100644 --- a/web/static/js/BareRTC.js +++ b/web/static/js/BareRTC.js @@ -608,6 +608,15 @@ const app = Vue.createApp({ this.disconnect = true; break; case "ping": + // New JWT token? + if (msg.jwt) { + this.jwt.token = msg.jwt; + } + + // Reset disconnect retry counter: if we were on long enough to get + // a ping, we're well connected and can reconnect no matter how many + // times the chat server is rebooted. + this.disconnectCount = 0; break; default: console.error("Unexpected action: %s", JSON.stringify(msg));