mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 13:29:27 +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.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:
|
||||
# do not enable this unless the ircd is only accessible over internal networks
|
||||
allow-plaintext-resume: false
|
||||
@ -790,6 +779,24 @@ roleplay:
|
||||
# add the real nickname, in parentheses, to the end of every roleplay message?
|
||||
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,
|
||||
# various autoreplay features, and the resume extension
|
||||
history:
|
||||
|
28
default.yaml
28
default.yaml
@ -187,17 +187,6 @@ server:
|
||||
# - "192.168.1.1"
|
||||
# - "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:
|
||||
# do not enable this unless the ircd is only accessible over internal networks
|
||||
allow-plaintext-resume: false
|
||||
@ -816,6 +805,23 @@ roleplay:
|
||||
# add the real nickname, in parentheses, to the end of every roleplay message?
|
||||
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,
|
||||
# various autoreplay features, and the resume extension
|
||||
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()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
modes, present := channel.members[client]
|
||||
if present {
|
||||
for _, mode := range modes.AllModes() {
|
||||
result = append(result, mode.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
return present, modes.AllModes()
|
||||
}
|
||||
|
||||
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/email"
|
||||
"github.com/oragono/oragono/irc/isupport"
|
||||
"github.com/oragono/oragono/irc/jwt"
|
||||
"github.com/oragono/oragono/irc/languages"
|
||||
"github.com/oragono/oragono/irc/ldap"
|
||||
"github.com/oragono/oragono/irc/logger"
|
||||
@ -471,11 +472,6 @@ type TorListenersConfig struct {
|
||||
MaxConnectionsPerDuration int `yaml:"max-connections-per-duration"`
|
||||
}
|
||||
|
||||
type JwtServiceConfig struct {
|
||||
ExpiryInSeconds int64 `yaml:"expiry-in-seconds"`
|
||||
Secret string
|
||||
}
|
||||
|
||||
// Config defines the overall configuration.
|
||||
type Config struct {
|
||||
Network struct {
|
||||
@ -508,7 +504,6 @@ type Config struct {
|
||||
ProxyAllowedFrom []string `yaml:"proxy-allowed-from"`
|
||||
proxyAllowedFromNets []net.IPNet
|
||||
WebIRC []webircConfig `yaml:"webirc"`
|
||||
JwtServices map[string]JwtServiceConfig `yaml:"jwt-services"`
|
||||
MaxSendQString string `yaml:"max-sendq"`
|
||||
MaxSendQBytes int
|
||||
AllowPlaintextResume bool `yaml:"allow-plaintext-resume"`
|
||||
@ -537,6 +532,11 @@ type Config struct {
|
||||
addSuffix bool
|
||||
}
|
||||
|
||||
Extjwt struct {
|
||||
Default jwt.JwtServiceConfig `yaml:",inline"`
|
||||
Services map[string]jwt.JwtServiceConfig `yaml:"services"`
|
||||
}
|
||||
|
||||
Languages struct {
|
||||
Enabled bool
|
||||
Path string
|
||||
@ -811,6 +811,29 @@ func (conf *Config) prepareListeners() (err error) {
|
||||
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
|
||||
func LoadRawConfig(filename string) (config *Config, err error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
@ -927,13 +950,6 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
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:
|
||||
if config.Accounts.Bouncer != nil {
|
||||
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:
|
||||
err = config.generateISupport()
|
||||
if err != nil {
|
||||
@ -1190,7 +1211,9 @@ func (config *Config) generateISupport() (err error) {
|
||||
isupport.Add("CHANTYPES", chanTypes)
|
||||
isupport.Add("ELIST", "U")
|
||||
isupport.Add("EXCEPTS", "")
|
||||
if config.Extjwt.Default.Enabled() || len(config.Extjwt.Services) != 0 {
|
||||
isupport.Add("EXTJWT", "1")
|
||||
}
|
||||
isupport.Add("INVEX", "")
|
||||
isupport.Add("KICKLEN", strconv.Itoa(config.Limits.KickLen))
|
||||
isupport.Add("MAXLIST", fmt.Sprintf("beI:%s", strconv.Itoa(config.Limits.ChanListModes)))
|
||||
|
@ -20,12 +20,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/oragono/oragono/irc/caps"
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/history"
|
||||
"github.com/oragono/oragono/irc/jwt"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/sno"
|
||||
"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]
|
||||
func extjwtHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
expireInSeconds := int64(30)
|
||||
|
||||
accountName := client.AccountName()
|
||||
if accountName == "*" {
|
||||
accountName = ""
|
||||
@ -938,42 +936,42 @@ func extjwtHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
claims["channel"] = channel.Name()
|
||||
claims["joined"] = 0
|
||||
claims["cmodes"] = []string{}
|
||||
if channel.hasClient(client) {
|
||||
if present, cModes := channel.ClientStatus(client); present {
|
||||
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~
|
||||
service := "*"
|
||||
secret := "*"
|
||||
config := server.Config()
|
||||
var serviceName string
|
||||
var sConfig jwt.JwtServiceConfig
|
||||
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()
|
||||
info, exists := c.Server.JwtServices[service]
|
||||
if !exists {
|
||||
if !sConfig.Enabled() {
|
||||
rb.Add(nil, server.name, "FAIL", "EXTJWT", "NO_SUCH_SERVICE", client.t("No such service"))
|
||||
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 := token.SignedString([]byte(secret))
|
||||
tokenString, err := sConfig.Sign(claims)
|
||||
|
||||
if err == nil {
|
||||
maxTokenLength := 400
|
||||
|
||||
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:]
|
||||
}
|
||||
rb.Add(nil, server.name, "EXTJWT", msg.Params[0], service, tokenString)
|
||||
rb.Add(nil, server.name, "EXTJWT", msg.Params[0], serviceName, tokenString)
|
||||
} else {
|
||||
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