From 2dbf871ac442c01008e634c7ebd88e17c7702da9 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 6 Feb 2024 10:43:27 -0500 Subject: [PATCH] review fix: allow multiple token definitions --- default.yaml | 27 ++++++++++++++----------- irc/jwt/bearer.go | 46 +++++++++++++++++++++++++++++++++--------- irc/jwt/bearer_test.go | 14 ++++++++----- traditional.yaml | 27 ++++++++++++++----------- 4 files changed, 76 insertions(+), 38 deletions(-) diff --git a/default.yaml b/default.yaml index 38d8f834..aed49dae 100644 --- a/default.yaml +++ b/default.yaml @@ -604,18 +604,21 @@ accounts: enabled: false # should we automatically create users on presentation of a valid token? autocreate: true - algorithm: "hmac" # either 'hmac', 'rsa', or 'eddsa' (ed25519) - # hmac takes a symmetric key, rsa and eddsa take PEM-encoded public keys; - # either way, the key can be specified either as a YAML string: - key: "nANiZ1De4v6WnltCHN2H7Q" - # or as a path to the file containing the key: - #key-file: "jwt_pubkey.pem" - # list of JWT claim names to search for the user's account name (make sure the format - # is what you expect, especially if using "sub"): - account-claims: ["preferred_username"] - # if a claim is formatted as an email address, require it to have the following domain, - # and then strip off the domain and use the local-part as the account name: - #strip-domain: "example.com" + # any of these token definitions can be accepted, allowing for key rotation + tokens: + - + algorithm: "hmac" # either 'hmac', 'rsa', or 'eddsa' (ed25519) + # hmac takes a symmetric key, rsa and eddsa take PEM-encoded public keys; + # either way, the key can be specified either as a YAML string: + key: "nANiZ1De4v6WnltCHN2H7Q" + # or as a path to the file containing the key: + #key-file: "jwt_pubkey.pem" + # list of JWT claim names to search for the user's account name (make sure the format + # is what you expect, especially if using "sub"): + account-claims: ["preferred_username"] + # if a claim is formatted as an email address, require it to have the following domain, + # and then strip off the domain and use the local-part as the account name: + #strip-domain: "example.com" # channel options channels: diff --git a/irc/jwt/bearer.go b/irc/jwt/bearer.go index 09f9f74a..a9280b0b 100644 --- a/irc/jwt/bearer.go +++ b/irc/jwt/bearer.go @@ -19,8 +19,12 @@ var ( // JWTAuthConfig is the config for Ergo to accept JWTs via draft/bearer type JWTAuthConfig struct { - Enabled bool `yaml:"enabled"` - Autocreate bool `yaml:"autocreate"` + Enabled bool `yaml:"enabled"` + Autocreate bool `yaml:"autocreate"` + Tokens []JWTAuthTokenConfig `yaml:"tokens"` +} + +type JWTAuthTokenConfig struct { Algorithm string `yaml:"algorithm"` KeyString string `yaml:"key"` KeyFile string `yaml:"key-file"` @@ -35,6 +39,20 @@ func (j *JWTAuthConfig) Postprocess() error { return nil } + if len(j.Tokens) == 0 { + return fmt.Errorf("JWT authentication enabled, but no valid tokens defined") + } + + for i := range j.Tokens { + if err := j.Tokens[i].Postprocess(); err != nil { + return err + } + } + + return nil +} + +func (j *JWTAuthTokenConfig) Postprocess() error { keyBytes, err := j.keyBytes() if err != nil { return err @@ -74,7 +92,21 @@ func (j *JWTAuthConfig) Postprocess() error { return nil } -func (j *JWTAuthConfig) keyBytes() (result []byte, err error) { +func (j *JWTAuthConfig) Validate(t string) (accountName string, err error) { + if !j.Enabled || len(j.Tokens) == 0 { + return "", ErrAuthDisabled + } + + for i := range j.Tokens { + accountName, err = j.Tokens[i].Validate(t) + if err == nil { + return + } + } + return +} + +func (j *JWTAuthTokenConfig) keyBytes() (result []byte, err error) { if j.KeyFile != "" { o, err := os.Open(j.KeyFile) if err != nil { @@ -89,15 +121,11 @@ func (j *JWTAuthConfig) keyBytes() (result []byte, err error) { } // implements jwt.Keyfunc -func (j *JWTAuthConfig) keyFunc(_ *jwt.Token) (interface{}, error) { +func (j *JWTAuthTokenConfig) keyFunc(_ *jwt.Token) (interface{}, error) { return j.key, nil } -func (j *JWTAuthConfig) Validate(t string) (accountName string, err error) { - if !j.Enabled { - return "", ErrAuthDisabled - } - +func (j *JWTAuthTokenConfig) Validate(t string) (accountName string, err error) { token, err := j.parser.Parse(t, j.keyFunc) if err != nil { return "", err diff --git a/irc/jwt/bearer_test.go b/irc/jwt/bearer_test.go index 0d6b67f2..a87750b7 100644 --- a/irc/jwt/bearer_test.go +++ b/irc/jwt/bearer_test.go @@ -49,11 +49,15 @@ s/uzBKNwWf9UPTeIt+4JScg= func TestJWTBearerAuth(t *testing.T) { j := JWTAuthConfig{ - Enabled: true, - Algorithm: "rsa", - KeyString: rsaTestPubKey, - AccountClaims: []string{"preferred_username", "email"}, - StripDomain: "example.com", + Enabled: true, + Tokens: []JWTAuthTokenConfig{ + { + Algorithm: "rsa", + KeyString: rsaTestPubKey, + AccountClaims: []string{"preferred_username", "email"}, + StripDomain: "example.com", + }, + }, } if err := j.Postprocess(); err != nil { diff --git a/traditional.yaml b/traditional.yaml index 164f0d1e..31b2a47a 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -577,18 +577,21 @@ accounts: enabled: false # should we automatically create users on presentation of a valid token? autocreate: true - algorithm: "hmac" # either 'hmac', 'rsa', or 'eddsa' (ed25519) - # hmac takes a symmetric key, rsa and eddsa take PEM-encoded public keys; - # either way, the key can be specified either as a YAML string: - key: "nANiZ1De4v6WnltCHN2H7Q" - # or as a path to the file containing the key: - #key-file: "jwt_pubkey.pem" - # list of JWT claim names to search for the user's account name (make sure the format - # is what you expect, especially if using "sub"): - account-claims: ["preferred_username"] - # if a claim is formatted as an email address, require it to have the following domain, - # and then strip off the domain and use the local-part as the account name: - #strip-domain: "example.com" + # any of these token definitions can be accepted, allowing for key rotation + tokens: + - + algorithm: "hmac" # either 'hmac', 'rsa', or 'eddsa' (ed25519) + # hmac takes a symmetric key, rsa and eddsa take PEM-encoded public keys; + # either way, the key can be specified either as a YAML string: + key: "nANiZ1De4v6WnltCHN2H7Q" + # or as a path to the file containing the key: + #key-file: "jwt_pubkey.pem" + # list of JWT claim names to search for the user's account name (make sure the format + # is what you expect, especially if using "sub"): + account-claims: ["preferred_username"] + # if a claim is formatted as an email address, require it to have the following domain, + # and then strip off the domain and use the local-part as the account name: + #strip-domain: "example.com" # channel options channels: