mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-29 07:29:31 +01:00
refactor/enhance jwt signing
This commit is contained in:
parent
bfeba1f2f3
commit
e61e0143bd
@ -161,17 +161,6 @@ server:
|
|||||||
# - "192.168.1.1"
|
# - "192.168.1.1"
|
||||||
# - "192.168.10.1/24"
|
# - "192.168.10.1/24"
|
||||||
|
|
||||||
# these services can integrate with the ircd using JSON Web Tokens (https://jwt.io)
|
|
||||||
# sometimes referred to with 'EXTJWT'
|
|
||||||
jwt-services:
|
|
||||||
# # service name
|
|
||||||
# call-host:
|
|
||||||
# # custom expiry length, default is 30s
|
|
||||||
# expiry-in-seconds: 45
|
|
||||||
|
|
||||||
# # secret string to verify the generated tokens
|
|
||||||
# secret: call-hosting-secret-token
|
|
||||||
|
|
||||||
# allow use of the RESUME extension over plaintext connections:
|
# allow use of the RESUME extension over plaintext connections:
|
||||||
# do not enable this unless the ircd is only accessible over internal networks
|
# do not enable this unless the ircd is only accessible over internal networks
|
||||||
allow-plaintext-resume: false
|
allow-plaintext-resume: false
|
||||||
@ -790,6 +779,24 @@ roleplay:
|
|||||||
# add the real nickname, in parentheses, to the end of every roleplay message?
|
# add the real nickname, in parentheses, to the end of every roleplay message?
|
||||||
add-suffix: true
|
add-suffix: true
|
||||||
|
|
||||||
|
# external services can integrate with the ircd using JSON Web Tokens (https://jwt.io).
|
||||||
|
# in effect, the server can sign a token attesting that the client is present on
|
||||||
|
# the server, is a member of a particular channel, etc.
|
||||||
|
extjwt:
|
||||||
|
# default service config (for `EXTJWT #channel`).
|
||||||
|
# expiration time for the token:
|
||||||
|
# expiration: 45s
|
||||||
|
# you can configure tokens to be signed either with HMAC and a symmetric secret:
|
||||||
|
# secret: "65PHvk0K1_sM-raTsCEhatVkER_QD8a0zVV8gG2EWcI"
|
||||||
|
# or with an RSA private key:
|
||||||
|
# #rsa-private-key-file: "extjwt.pem"
|
||||||
|
|
||||||
|
# named services:
|
||||||
|
# services:
|
||||||
|
# "jitsi":
|
||||||
|
# expiration: 30s
|
||||||
|
# secret: "qmamLKDuOzIzlO8XqsGGewei_At11lewh6jtKfSTbkg"
|
||||||
|
|
||||||
# history message storage: this is used by CHATHISTORY, HISTORY, znc.in/playback,
|
# history message storage: this is used by CHATHISTORY, HISTORY, znc.in/playback,
|
||||||
# various autoreplay features, and the resume extension
|
# various autoreplay features, and the resume extension
|
||||||
history:
|
history:
|
||||||
|
28
default.yaml
28
default.yaml
@ -187,17 +187,6 @@ server:
|
|||||||
# - "192.168.1.1"
|
# - "192.168.1.1"
|
||||||
# - "192.168.10.1/24"
|
# - "192.168.10.1/24"
|
||||||
|
|
||||||
# these services can integrate with the ircd using JSON Web Tokens (https://jwt.io)
|
|
||||||
# sometimes referred to with 'EXTJWT'
|
|
||||||
jwt-services:
|
|
||||||
# # service name
|
|
||||||
# call-host:
|
|
||||||
# # custom expiry length, default is 30s
|
|
||||||
# expiry-in-seconds: 45
|
|
||||||
|
|
||||||
# # secret string to verify the generated tokens
|
|
||||||
# secret: call-hosting-secret-token
|
|
||||||
|
|
||||||
# allow use of the RESUME extension over plaintext connections:
|
# allow use of the RESUME extension over plaintext connections:
|
||||||
# do not enable this unless the ircd is only accessible over internal networks
|
# do not enable this unless the ircd is only accessible over internal networks
|
||||||
allow-plaintext-resume: false
|
allow-plaintext-resume: false
|
||||||
@ -816,6 +805,23 @@ roleplay:
|
|||||||
# add the real nickname, in parentheses, to the end of every roleplay message?
|
# add the real nickname, in parentheses, to the end of every roleplay message?
|
||||||
add-suffix: true
|
add-suffix: true
|
||||||
|
|
||||||
|
# external services can integrate with the ircd using JSON Web Tokens (https://jwt.io).
|
||||||
|
# in effect, the server can sign a token attesting that the client is present on
|
||||||
|
# the server, is a member of a particular channel, etc.
|
||||||
|
extjwt:
|
||||||
|
# default service:
|
||||||
|
# expiration: 45s
|
||||||
|
# symmetric secret for HMAC signing:
|
||||||
|
# secret: "65PHvk0K1_sM-raTsCEhatVkER_QD8a0zVV8gG2EWcI"
|
||||||
|
# private key for RSA signing:
|
||||||
|
# rsa-private-key-file: "extjwt.pem"
|
||||||
|
|
||||||
|
# named services:
|
||||||
|
# services:
|
||||||
|
# "jitsi":
|
||||||
|
# expiration: 30s
|
||||||
|
# secret: "qmamLKDuOzIzlO8XqsGGewei_At11lewh6jtKfSTbkg"
|
||||||
|
|
||||||
# history message storage: this is used by CHATHISTORY, HISTORY, znc.in/playback,
|
# history message storage: this is used by CHATHISTORY, HISTORY, znc.in/playback,
|
||||||
# various autoreplay features, and the resume extension
|
# various autoreplay features, and the resume extension
|
||||||
history:
|
history:
|
||||||
|
@ -539,16 +539,11 @@ func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) ClientModeStrings(client *Client) (result []string) {
|
func (channel *Channel) ClientStatus(client *Client) (present bool, cModes modes.Modes) {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
modes, present := channel.members[client]
|
modes, present := channel.members[client]
|
||||||
if present {
|
return present, modes.AllModes()
|
||||||
for _, mode := range modes.AllModes() {
|
|
||||||
result = append(result, mode.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
|
func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/oragono/oragono/irc/custime"
|
"github.com/oragono/oragono/irc/custime"
|
||||||
"github.com/oragono/oragono/irc/email"
|
"github.com/oragono/oragono/irc/email"
|
||||||
"github.com/oragono/oragono/irc/isupport"
|
"github.com/oragono/oragono/irc/isupport"
|
||||||
|
"github.com/oragono/oragono/irc/jwt"
|
||||||
"github.com/oragono/oragono/irc/languages"
|
"github.com/oragono/oragono/irc/languages"
|
||||||
"github.com/oragono/oragono/irc/ldap"
|
"github.com/oragono/oragono/irc/ldap"
|
||||||
"github.com/oragono/oragono/irc/logger"
|
"github.com/oragono/oragono/irc/logger"
|
||||||
@ -471,11 +472,6 @@ type TorListenersConfig struct {
|
|||||||
MaxConnectionsPerDuration int `yaml:"max-connections-per-duration"`
|
MaxConnectionsPerDuration int `yaml:"max-connections-per-duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JwtServiceConfig struct {
|
|
||||||
ExpiryInSeconds int64 `yaml:"expiry-in-seconds"`
|
|
||||||
Secret string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config defines the overall configuration.
|
// Config defines the overall configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Network struct {
|
Network struct {
|
||||||
@ -508,7 +504,6 @@ type Config struct {
|
|||||||
ProxyAllowedFrom []string `yaml:"proxy-allowed-from"`
|
ProxyAllowedFrom []string `yaml:"proxy-allowed-from"`
|
||||||
proxyAllowedFromNets []net.IPNet
|
proxyAllowedFromNets []net.IPNet
|
||||||
WebIRC []webircConfig `yaml:"webirc"`
|
WebIRC []webircConfig `yaml:"webirc"`
|
||||||
JwtServices map[string]JwtServiceConfig `yaml:"jwt-services"`
|
|
||||||
MaxSendQString string `yaml:"max-sendq"`
|
MaxSendQString string `yaml:"max-sendq"`
|
||||||
MaxSendQBytes int
|
MaxSendQBytes int
|
||||||
AllowPlaintextResume bool `yaml:"allow-plaintext-resume"`
|
AllowPlaintextResume bool `yaml:"allow-plaintext-resume"`
|
||||||
@ -537,6 +532,11 @@ type Config struct {
|
|||||||
addSuffix bool
|
addSuffix bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Extjwt struct {
|
||||||
|
Default jwt.JwtServiceConfig `yaml:",inline"`
|
||||||
|
Services map[string]jwt.JwtServiceConfig `yaml:"services"`
|
||||||
|
}
|
||||||
|
|
||||||
Languages struct {
|
Languages struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Path string
|
Path string
|
||||||
@ -811,6 +811,29 @@ func (conf *Config) prepareListeners() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) processExtjwt() (err error) {
|
||||||
|
// first process the default service, which may be disabled
|
||||||
|
err = config.Extjwt.Default.Postprocess()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// now process the named services. it is an error if any is disabled
|
||||||
|
// also, normalize the service names to lowercase
|
||||||
|
services := make(map[string]jwt.JwtServiceConfig, len(config.Extjwt.Services))
|
||||||
|
for service, sConf := range config.Extjwt.Services {
|
||||||
|
err := sConf.Postprocess()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !sConf.Enabled() {
|
||||||
|
return fmt.Errorf("no keys enabled for extjwt service %s", service)
|
||||||
|
}
|
||||||
|
services[strings.ToLower(service)] = sConf
|
||||||
|
}
|
||||||
|
config.Extjwt.Services = services
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoadRawConfig loads the config without doing any consistency checks or postprocessing
|
// LoadRawConfig loads the config without doing any consistency checks or postprocessing
|
||||||
func LoadRawConfig(filename string) (config *Config, err error) {
|
func LoadRawConfig(filename string) (config *Config, err error) {
|
||||||
data, err := ioutil.ReadFile(filename)
|
data, err := ioutil.ReadFile(filename)
|
||||||
@ -927,13 +950,6 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
config.Server.capValues[caps.Multiline] = multilineCapValue
|
config.Server.capValues[caps.Multiline] = multilineCapValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// confirm jwt config
|
|
||||||
for name, info := range config.Server.JwtServices {
|
|
||||||
if info.Secret == "" {
|
|
||||||
return nil, fmt.Errorf("Could not parse jwt-services config, %s service has no secret set", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle legacy name 'bouncer' for 'multiclient' section:
|
// handle legacy name 'bouncer' for 'multiclient' section:
|
||||||
if config.Accounts.Bouncer != nil {
|
if config.Accounts.Bouncer != nil {
|
||||||
config.Accounts.Multiclient = *config.Accounts.Bouncer
|
config.Accounts.Multiclient = *config.Accounts.Bouncer
|
||||||
@ -1153,6 +1169,11 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = config.processExtjwt()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// now that all postprocessing is complete, regenerate ISUPPORT:
|
// now that all postprocessing is complete, regenerate ISUPPORT:
|
||||||
err = config.generateISupport()
|
err = config.generateISupport()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1190,7 +1211,9 @@ func (config *Config) generateISupport() (err error) {
|
|||||||
isupport.Add("CHANTYPES", chanTypes)
|
isupport.Add("CHANTYPES", chanTypes)
|
||||||
isupport.Add("ELIST", "U")
|
isupport.Add("ELIST", "U")
|
||||||
isupport.Add("EXCEPTS", "")
|
isupport.Add("EXCEPTS", "")
|
||||||
|
if config.Extjwt.Default.Enabled() || len(config.Extjwt.Services) != 0 {
|
||||||
isupport.Add("EXTJWT", "1")
|
isupport.Add("EXTJWT", "1")
|
||||||
|
}
|
||||||
isupport.Add("INVEX", "")
|
isupport.Add("INVEX", "")
|
||||||
isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen))
|
isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen))
|
||||||
isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes)))
|
isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes)))
|
||||||
|
@ -20,12 +20,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"github.com/goshuirc/irc-go/ircfmt"
|
"github.com/goshuirc/irc-go/ircfmt"
|
||||||
"github.com/goshuirc/irc-go/ircmsg"
|
"github.com/goshuirc/irc-go/ircmsg"
|
||||||
"github.com/oragono/oragono/irc/caps"
|
"github.com/oragono/oragono/irc/caps"
|
||||||
"github.com/oragono/oragono/irc/custime"
|
"github.com/oragono/oragono/irc/custime"
|
||||||
"github.com/oragono/oragono/irc/history"
|
"github.com/oragono/oragono/irc/history"
|
||||||
|
"github.com/oragono/oragono/irc/jwt"
|
||||||
"github.com/oragono/oragono/irc/modes"
|
"github.com/oragono/oragono/irc/modes"
|
||||||
"github.com/oragono/oragono/irc/sno"
|
"github.com/oragono/oragono/irc/sno"
|
||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
@ -914,8 +914,6 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
|||||||
|
|
||||||
// EXTJWT <target> [service_name]
|
// EXTJWT <target> [service_name]
|
||||||
func extjwtHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func extjwtHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
expireInSeconds := int64(30)
|
|
||||||
|
|
||||||
accountName := client.AccountName()
|
accountName := client.AccountName()
|
||||||
if accountName == "*" {
|
if accountName == "*" {
|
||||||
accountName = ""
|
accountName = ""
|
||||||
@ -938,42 +936,42 @@ func extjwtHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
claims["channel"] = channel.Name()
|
claims["channel"] = channel.Name()
|
||||||
claims["joined"] = 0
|
claims["joined"] = 0
|
||||||
claims["cmodes"] = []string{}
|
claims["cmodes"] = []string{}
|
||||||
if channel.hasClient(client) {
|
if present, cModes := channel.ClientStatus(client); present {
|
||||||
claims["joined"] = 1
|
claims["joined"] = 1
|
||||||
claims["cmodes"] = channel.ClientModeStrings(client)
|
var modeStrings []string
|
||||||
|
for _, cMode := range cModes {
|
||||||
|
modeStrings = append(modeStrings, string(cMode))
|
||||||
|
}
|
||||||
|
claims["cmodes"] = modeStrings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we default to a secret of `*`. if you want a real secret setup a service in the config~
|
config := server.Config()
|
||||||
service := "*"
|
var serviceName string
|
||||||
secret := "*"
|
var sConfig jwt.JwtServiceConfig
|
||||||
if 1 < len(msg.Params) {
|
if 1 < len(msg.Params) {
|
||||||
service = strings.ToLower(msg.Params[1])
|
serviceName = strings.ToLower(msg.Params[1])
|
||||||
|
sConfig = config.Extjwt.Services[serviceName]
|
||||||
|
} else {
|
||||||
|
serviceName = "*"
|
||||||
|
sConfig = config.Extjwt.Default
|
||||||
|
}
|
||||||
|
|
||||||
c := server.Config()
|
if !sConfig.Enabled() {
|
||||||
info, exists := c.Server.JwtServices[service]
|
|
||||||
if !exists {
|
|
||||||
rb.Add(nil, server.name, "FAIL", "EXTJWT", "NO_SUCH_SERVICE", client.t("No such service"))
|
rb.Add(nil, server.name, "FAIL", "EXTJWT", "NO_SUCH_SERVICE", client.t("No such service"))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
secret = info.Secret
|
|
||||||
if info.ExpiryInSeconds != 0 {
|
|
||||||
expireInSeconds = info.ExpiryInSeconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
claims["exp"] = time.Now().Unix() + expireInSeconds
|
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
tokenString, err := sConfig.Sign(claims)
|
||||||
tokenString, err := token.SignedString([]byte(secret))
|
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
maxTokenLength := 400
|
maxTokenLength := 400
|
||||||
|
|
||||||
for maxTokenLength < len(tokenString) {
|
for maxTokenLength < len(tokenString) {
|
||||||
rb.Add(nil, server.name, "EXTJWT", msg.Params[0], service, "*", tokenString[:maxTokenLength])
|
rb.Add(nil, server.name, "EXTJWT", msg.Params[0], serviceName, "*", tokenString[:maxTokenLength])
|
||||||
tokenString = tokenString[maxTokenLength:]
|
tokenString = tokenString[maxTokenLength:]
|
||||||
}
|
}
|
||||||
rb.Add(nil, server.name, "EXTJWT", msg.Params[0], service, tokenString)
|
rb.Add(nil, server.name, "EXTJWT", msg.Params[0], serviceName, tokenString)
|
||||||
} else {
|
} else {
|
||||||
rb.Add(nil, server.name, "FAIL", "EXTJWT", "UNKNOWN_ERROR", client.t("Could not generate EXTJWT token"))
|
rb.Add(nil, server.name, "FAIL", "EXTJWT", "UNKNOWN_ERROR", client.t("Could not generate EXTJWT token"))
|
||||||
}
|
}
|
||||||
|
77
irc/jwt/extjwt.go
Normal file
77
irc/jwt/extjwt.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (c) 2020 Daniel Oaks <daniel@danieloaks.net>
|
||||||
|
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoKeys = errors.New("No signing keys are enabled")
|
||||||
|
)
|
||||||
|
|
||||||
|
type MapClaims jwt.MapClaims
|
||||||
|
|
||||||
|
type JwtServiceConfig struct {
|
||||||
|
Expiration time.Duration
|
||||||
|
Secret string
|
||||||
|
secretBytes []byte
|
||||||
|
RSAPrivateKeyFile string `yaml:"rsa-private-key-file"`
|
||||||
|
rsaPrivateKey *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JwtServiceConfig) Postprocess() (err error) {
|
||||||
|
t.secretBytes = []byte(t.Secret)
|
||||||
|
t.Secret = ""
|
||||||
|
if t.RSAPrivateKeyFile != "" {
|
||||||
|
keyBytes, err := ioutil.ReadFile(t.RSAPrivateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d, _ := pem.Decode(keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.rsaPrivateKey, err = x509.ParsePKCS1PrivateKey(d.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
privateKey, err := x509.ParsePKCS8PrivateKey(d.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey); ok {
|
||||||
|
t.rsaPrivateKey = rsaPrivateKey
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Non-RSA key type for extjwt: %T", privateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JwtServiceConfig) Enabled() bool {
|
||||||
|
return t.Expiration != 0 && (len(t.secretBytes) != 0 || t.rsaPrivateKey != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *JwtServiceConfig) Sign(claims MapClaims) (result string, err error) {
|
||||||
|
claims["exp"] = time.Now().Unix() + int64(t.Expiration/time.Second)
|
||||||
|
|
||||||
|
if t.rsaPrivateKey != nil {
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(claims))
|
||||||
|
return token.SignedString(t.rsaPrivateKey)
|
||||||
|
} else if len(t.secretBytes) != 0 {
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(claims))
|
||||||
|
return token.SignedString(t.secretBytes)
|
||||||
|
} else {
|
||||||
|
return "", ErrNoKeys
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user