Amend JWT Endpoint to Update Logged-In User's Settings
* Add an admin /api/amend-jwt endpoint that allows your main website to post an updated JWT token for a user who may already be logged into chat. * It allows for updating a logged-in user's nickname, profile picture URL, username, chat moderation rules, etc. without them needing to exit and rejoin the chat room.
This commit is contained in:
parent
c5e3ffe09b
commit
dd951ba69d
14
Protocol.md
14
Protocol.md
|
@ -94,6 +94,20 @@ Just a keep-alive message to prevent the WebSocket connection from closing.
|
|||
}
|
||||
```
|
||||
|
||||
When the server sends a ping to a user who authenticated via JWT token, it will also provide a refreshed JWT token with an updated expiration date (in case the user is disconnected, so they can log back in gracefully), along with a mapping of the chat moderation rules in their token. Example:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"action": "ping",
|
||||
"jwt": "new.jwt.token",
|
||||
"jwtRules": {
|
||||
"IsRedCamRule": false,
|
||||
"IsNoVideoRule": false,
|
||||
//...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error
|
||||
|
||||
Sent by: Server.
|
||||
|
|
40
docs/API.md
40
docs/API.md
|
@ -50,6 +50,46 @@ The return schema looks like:
|
|||
}
|
||||
```
|
||||
|
||||
## POST /api/amend-jwt
|
||||
|
||||
This endpoint allows your main website to amend the JWT token for a user who may already be logged into the chat room.
|
||||
|
||||
For example, you can post a new JWT token for them that has a new nickname, avatar image URL, or provides new chat moderation rules that you want to take effect immediately for them in the chat room, rather than wait for them to log in again with a new JWT token.
|
||||
|
||||
The `Username` parameter should target their current username (which they may be currently logged into chat with), and the JWTToken is a new token to be delivered to them which they will update their page with (to apply new chat rules, etc.).
|
||||
|
||||
Notes:
|
||||
|
||||
* If the Username is not currently online, the endpoint will return a Not Found error saying such.
|
||||
* If the Username did not log in via a JWT token originally, the new JWT token will not be given to them.
|
||||
* The new JWT token must be correctly signed with your secret key, the same as a normal login JWT token.
|
||||
* It is possible to change the user's username with this endpoint: the "Username" parameter targets their current (old) username, and the JWT token may contain a new username.
|
||||
* **Important:** It is up to your main website to ensure the new username will not conflict with somebody else already in the chat room. It is undefined behavior if their username is set to one identical to another user on chat.
|
||||
* Changing their username may disrupt Direct Message threads other people have open with them: their web page will show the old username as offline, and they would need to find and start a new DM thread to continue chatting. So it is not recommended to update their username with this API although it is possible to.
|
||||
|
||||
When the username is found on chat (and was JWT authenticated originally), the chat server will:
|
||||
|
||||
1. Send them a 'ping' message with the new JWT token and rules struct.
|
||||
2. Send them a 'me' message with their (new?) username.
|
||||
3. Broadcast a Who List update so any change to their nickname, profile picture, VIP status, etc. will update for everybody.
|
||||
|
||||
```json
|
||||
{
|
||||
"APIKey": "from settings.toml",
|
||||
"Username": "alice",
|
||||
"JWTToken": "new.jwt.token"
|
||||
}
|
||||
```
|
||||
|
||||
The return schema looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"OK": true,
|
||||
"Error": "error string, omitted if none"
|
||||
}
|
||||
```
|
||||
|
||||
## POST /api/shutdown
|
||||
|
||||
Shut down (and hopefully, reboot) the chat server. It is equivalent to the `/shutdown` operator command issued in chat, but callable from your web application. It is also used as part of deadlock detection on the BareBot chatbot.
|
||||
|
|
122
pkg/api.go
122
pkg/api.go
|
@ -198,7 +198,6 @@ func (s *Server) Authenticate() http.HandlerFunc {
|
|||
func (s *Server) ShutdownAPI() http.HandlerFunc {
|
||||
type request struct {
|
||||
APIKey string
|
||||
Claims jwt.Claims
|
||||
}
|
||||
|
||||
type result struct {
|
||||
|
@ -273,6 +272,127 @@ func (s *Server) ShutdownAPI() http.HandlerFunc {
|
|||
})
|
||||
}
|
||||
|
||||
// Amend JWT (/api/amend-jwt) token for a user who may be logged into chat.
|
||||
//
|
||||
// This endpoint allows your website to send updates to a user's JWT token, such
|
||||
// as to change chat moderator rules or to swap their profile picture URL or
|
||||
// change their nickname/username.
|
||||
//
|
||||
// It is a POST request with a json body containing the following schema:
|
||||
//
|
||||
// {
|
||||
// "APIKey": "from settings.toml",
|
||||
// "Username": "alice",
|
||||
// "JWTToken": "new.jwt.token"
|
||||
// }
|
||||
//
|
||||
// The "Username" should match the username they currently would have in the
|
||||
// chat room, and the JWT token is a new auth token to replace their current
|
||||
// JWT with. You can change any properties in the new JWT token. For example,
|
||||
// if you include a new username in the JWT token, the chat server will rename
|
||||
// them (this will probably interrupt their DM threads people have open with
|
||||
// them, however).
|
||||
//
|
||||
// The return schema looks like:
|
||||
//
|
||||
// {
|
||||
// "OK": true,
|
||||
// "Error": "error string, omitted if none",
|
||||
// }
|
||||
func (s *Server) AmendJWT() http.HandlerFunc {
|
||||
type request struct {
|
||||
APIKey string
|
||||
Username string
|
||||
JWTToken string
|
||||
}
|
||||
|
||||
type result struct {
|
||||
OK bool
|
||||
Error string `json:",omitempty"`
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// JSON writer for the response.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
// Parse the request.
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
enc.Encode(result{
|
||||
Error: "Only POST methods allowed",
|
||||
})
|
||||
return
|
||||
} else if r.Header.Get("Content-Type") != "application/json" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
enc.Encode(result{
|
||||
Error: "Only application/json content-types allowed",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
// Parse the request payload.
|
||||
var (
|
||||
params request
|
||||
dec = json.NewDecoder(r.Body)
|
||||
)
|
||||
if err := dec.Decode(¶ms); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
enc.Encode(result{
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the API key.
|
||||
if params.APIKey != config.Current.AdminAPIKey {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
enc.Encode(result{
|
||||
Error: "Authentication denied.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the newly given JWT token.
|
||||
var claims *jwt.Claims
|
||||
if parsed, ok, err := jwt.ParseAndValidate(params.JWTToken); err != nil || !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
enc.Encode(result{
|
||||
Error: "Invalid JWT token.",
|
||||
})
|
||||
return
|
||||
} else {
|
||||
claims = parsed
|
||||
}
|
||||
|
||||
// Look for this username on chat.
|
||||
sub, err := s.GetSubscriber(params.Username)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
enc.Encode(result{
|
||||
Error: fmt.Sprintf("Username %s is not logged in", params.Username),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// If the subscriber is JWT authenticated, send them the new token.
|
||||
if sub.JWTClaims != nil {
|
||||
sub.JWTClaims = claims
|
||||
sub.SendPing()
|
||||
sub.SendMe()
|
||||
s.SendWhoList()
|
||||
}
|
||||
|
||||
// Send the response.
|
||||
enc.Encode(result{
|
||||
OK: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BlockList (/api/blocklist) allows your website to pre-sync mute lists between your
|
||||
// user accounts, so that when they see each other in chat they will pre-emptively mute
|
||||
// or boot one another.
|
||||
|
|
|
@ -64,6 +64,9 @@ type Message struct {
|
|||
// Sent on `echo` actions to condense multiple messages into one packet.
|
||||
Messages []Message `json:"messages,omitempty"`
|
||||
|
||||
// Sent on `ping` messages (along with updated JWTToken)
|
||||
JWTRules map[string]bool `json:"jwtRules,omitempty"`
|
||||
|
||||
// WebRTC negotiation messages: proxy their signaling messages
|
||||
// between the two users to negotiate peer connection.
|
||||
Candidate string `json:"candidate,omitempty"` // candidate
|
||||
|
|
25
pkg/ping.go
Normal file
25
pkg/ping.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package barertc
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||
)
|
||||
|
||||
// SendPing delivers the Ping message to connected subscribers.
|
||||
func (sub *Subscriber) SendPing() {
|
||||
// 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#%d: %s", sub.Username, sub.ID, err)
|
||||
} else {
|
||||
token = jwt
|
||||
}
|
||||
}
|
||||
|
||||
sub.SendJSON(messages.Message{
|
||||
Action: messages.ActionPing,
|
||||
JWTToken: token,
|
||||
JWTRules: sub.JWTClaims.Rules.ToDict(),
|
||||
})
|
||||
}
|
|
@ -188,17 +188,7 @@ func (s *Server) PollingAPI() http.HandlerFunc {
|
|||
// JWT once in a while. Equivalent to the WebSockets pinger channel.
|
||||
if time.Since(sub.lastPollJWT) > PingInterval {
|
||||
sub.lastPollJWT = time.Now()
|
||||
|
||||
if sub.JWTClaims != nil {
|
||||
if jwt, err := sub.JWTClaims.ReSign(); err != nil {
|
||||
log.Error("ReSign JWT token for %s#%d: %s", sub.Username, sub.ID, err)
|
||||
} else {
|
||||
sub.SendJSON(messages.Message{
|
||||
Action: messages.ActionPing,
|
||||
JWTToken: jwt,
|
||||
})
|
||||
}
|
||||
}
|
||||
sub.SendPing()
|
||||
}
|
||||
|
||||
enc.Encode(sub.FlushPollResponse())
|
||||
|
|
|
@ -59,6 +59,7 @@ func (s *Server) Setup() error {
|
|||
mux.Handle("/api/block/now", s.BlockNow())
|
||||
mux.Handle("/api/disconnect/now", s.DisconnectNow())
|
||||
mux.Handle("/api/authenticate", s.Authenticate())
|
||||
mux.Handle("/api/amend-jwt", s.AmendJWT())
|
||||
mux.Handle("/api/shutdown", s.ShutdownAPI())
|
||||
mux.Handle("/api/profile", s.UserProfile())
|
||||
mux.Handle("/api/message/history", s.MessageHistory())
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
|
||||
"git.kirsle.net/apps/barertc/pkg/config"
|
||||
"git.kirsle.net/apps/barertc/pkg/log"
|
||||
"git.kirsle.net/apps/barertc/pkg/messages"
|
||||
"git.kirsle.net/apps/barertc/pkg/util"
|
||||
"nhooyr.io/websocket"
|
||||
)
|
||||
|
@ -52,20 +51,7 @@ 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#%d: %s", sub.Username, sub.ID, err)
|
||||
} else {
|
||||
token = jwt
|
||||
}
|
||||
}
|
||||
|
||||
sub.SendJSON(messages.Message{
|
||||
Action: messages.ActionPing,
|
||||
JWTToken: token,
|
||||
})
|
||||
sub.SendPing()
|
||||
case <-ctx.Done():
|
||||
pinger.Stop()
|
||||
return
|
||||
|
|
20
src/App.vue
20
src/App.vue
|
@ -1418,6 +1418,7 @@ export default {
|
|||
onUnwatch: this.onUnwatch,
|
||||
onBlock: this.onBlock,
|
||||
onCut: this.onCut,
|
||||
onPing: this.onPing,
|
||||
|
||||
bulkMuteUsers: this.bulkMuteUsers,
|
||||
focusMessageBox: () => {
|
||||
|
@ -1460,6 +1461,25 @@ export default {
|
|||
// Send the server our current status and video setting.
|
||||
this.sendMe(true);
|
||||
},
|
||||
onPing(msg) {
|
||||
// Updated JWT chat moderation rules?
|
||||
if (msg.jwtRules) {
|
||||
this.jwt.rules = msg.jwtRules;
|
||||
|
||||
// Enforce them now.
|
||||
if (this.jwt.rules.IsNoBroadcastRule && this.webcam.active) {
|
||||
this.stopVideo();
|
||||
}
|
||||
|
||||
if (this.jwt.rules.IsNoVideoRule) {
|
||||
this.closeOpenVideos();
|
||||
}
|
||||
|
||||
if (this.jwt.rules.IsRedCamRule && this.webcam.active && !this.webcam.nsfw) {
|
||||
this.webcam.nsfw = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Front-end web app concerns.
|
||||
|
|
|
@ -30,6 +30,7 @@ class ChatClient {
|
|||
onUnwatch,
|
||||
onBlock,
|
||||
onCut,
|
||||
onPing,
|
||||
|
||||
// Misc function registrations for callback.
|
||||
onLoggedIn, // connection is fully established (first 'me' echo from server).
|
||||
|
@ -62,6 +63,7 @@ class ChatClient {
|
|||
this.onUnwatch = onUnwatch;
|
||||
this.onBlock = onBlock;
|
||||
this.onCut = onCut;
|
||||
this.onPing = onPing;
|
||||
|
||||
this.onLoggedIn = onLoggedIn;
|
||||
this.onNewJWT = onNewJWT;
|
||||
|
@ -234,6 +236,7 @@ class ChatClient {
|
|||
if (msg.jwt) {
|
||||
this.onNewJWT(msg.jwt);
|
||||
}
|
||||
this.onPing(msg);
|
||||
|
||||
// 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
|
||||
|
|
Loading…
Reference in New Issue
Block a user