From 788ee917e4efddc18f6af1445169f0dce047340e Mon Sep 17 00:00:00 2001
From: Pavel Tolstov
Date: Tue, 3 May 2022 20:18:20 +0300
Subject: [PATCH 1/3] Add option for sending decrypted claims to JWT Javascript
backend
---
README.md | 3 ++
backends/jwt.go | 60 +++++++++++++++-------------
backends/jwt_javascript.go | 51 ++++++++++++++---------
backends/jwt_test.go | 11 ++---
test-files/jwt/parsed_user_script.js | 10 ++++-
5 files changed, 83 insertions(+), 52 deletions(-)
diff --git a/README.md b/README.md
index 4a19bf5..64b0f7f 100644
--- a/README.md
+++ b/README.md
@@ -1488,6 +1488,7 @@ The `javascript` backend allows to run a JavaScript interpreter VM to conduct ch
| js_user_script_path | | Y | Relative or absolute path to user check script |
| js_superuser_script_path | | Y | Relative or absolute path to superuser check script |
| js_acl_script_path | | Y | Relative or absolute path to ACL check script |
+| js_pass_claims | false | N | Pass all claims extracted from the token to check scripts |
This backend expects the user to define JS scripts that return a boolean result to the check in question.
@@ -1495,6 +1496,8 @@ The backend will pass `mosquitto` provided arguments along, that is:
- `username`, `password` and `clientid` for `user` checks.
- `username` for `superuser` checks.
- `username`, `topic`, `clientid` and `acc` for `ACL` checks.
+If `js_pass_claims` option is set, an additional argument `claims` containing the claims data extracted
+from the JWT token is passed to all checks.
This is a valid, albeit pretty useless, example script for ACL checks (see `test-files/jwt` dir for test scripts):
diff --git a/backends/jwt.go b/backends/jwt.go
index 06bb0e3..217a80e 100644
--- a/backends/jwt.go
+++ b/backends/jwt.go
@@ -17,7 +17,7 @@ type tokenOptions struct {
skipUserExpiration bool
skipACLExpiration bool
secret string
- userField string
+ userFieldKey string
}
type jwtChecker interface {
@@ -27,19 +27,13 @@ type jwtChecker interface {
Halt()
}
-// Claims defines the struct containing the token claims.
-// StandardClaim's Subject field should contain the username, unless an opt is set to support Username field.
-type Claims struct {
- jwtGo.StandardClaims
- // If set, Username defines the identity of the user.
- Username string `json:"username"`
-}
-
const (
- remoteMode = "remote"
- localMode = "local"
- jsMode = "js"
- filesMode = "files"
+ remoteMode = "remote"
+ localMode = "local"
+ jsMode = "js"
+ filesMode = "files"
+ claimsSubjectKey = "sub"
+ claimsUsernameKey = "username"
)
func NewJWT(authOpts map[string]string, logLevel log.Level, hasher hashing.HashComparer, version string) (*JWT, error) {
@@ -69,9 +63,9 @@ func NewJWT(authOpts map[string]string, logLevel log.Level, hasher hashing.HashC
}
if userField, ok := authOpts["jwt_userfield"]; ok && userField == "Username" {
- options.userField = userField
+ options.userFieldKey = claimsUsernameKey
} else {
- options.userField = "Subject"
+ options.userFieldKey = claimsSubjectKey
}
switch authOpts["jwt_mode"] {
@@ -125,9 +119,9 @@ func (o *JWT) Halt() {
o.checker.Halt()
}
-func getJWTClaims(secret string, tokenStr string, skipExpiration bool) (*Claims, error) {
+func getJWTClaims(secret string, tokenStr string, skipExpiration bool) (*jwtGo.MapClaims, error) {
- jwtToken, err := jwtGo.ParseWithClaims(tokenStr, &Claims{}, func(token *jwtGo.Token) (interface{}, error) {
+ jwtToken, err := jwtGo.ParseWithClaims(tokenStr, &jwtGo.MapClaims{}, func(token *jwtGo.Token) (interface{}, error) {
return []byte(secret), nil
})
@@ -147,29 +141,41 @@ func getJWTClaims(secret string, tokenStr string, skipExpiration bool) (*Claims,
return nil, errors.New("jwt invalid token")
}
- claims, ok := jwtToken.Claims.(*Claims)
+ claims, ok := jwtToken.Claims.(*jwtGo.MapClaims)
if !ok {
- log.Debugf("jwt error: expected *Claims, got %T", jwtToken.Claims)
+ log.Debugf("jwt error: expected *MapClaims, got %T", jwtToken.Claims)
return nil, errors.New("got strange claims")
}
return claims, nil
}
-func getUsernameFromClaims(options tokenOptions, claims *Claims) string {
- if options.userField == "Username" {
- return claims.Username
+func getUsernameForToken(options tokenOptions, tokenStr string, skipExpiration bool) (string, error) {
+ claims, err := getJWTClaims(options.secret, tokenStr, skipExpiration)
+
+ if err != nil {
+ return "", err
}
- return claims.Subject
+ username, found := (*claims)[options.userFieldKey]
+ if !found {
+ return "", nil
+ }
+
+ usernameString, ok := username.(string)
+ if !ok {
+ log.Debugf("jwt error: username expected to be string, got %T", username)
+ return "", errors.New("got strange username")
+ }
+
+ return usernameString, nil
}
-func getUsernameForToken(options tokenOptions, tokenStr string, skipExpiration bool) (string, error) {
+func getClaimsForToken(options tokenOptions, tokenStr string, skipExpiration bool) (map[string]interface{}, error) {
claims, err := getJWTClaims(options.secret, tokenStr, skipExpiration)
-
if err != nil {
- return "", err
+ return make(map[string]interface{}), err
}
- return getUsernameFromClaims(options, claims), nil
+ return map[string]interface{}(*claims), nil
}
diff --git a/backends/jwt_javascript.go b/backends/jwt_javascript.go
index ed7080c..628dfb3 100644
--- a/backends/jwt_javascript.go
+++ b/backends/jwt_javascript.go
@@ -16,6 +16,8 @@ type jsJWTChecker struct {
superuserScript string
aclScript string
+ passClaims bool
+
options tokenOptions
runner *js.Runner
@@ -79,6 +81,10 @@ func NewJsJWTChecker(authOpts map[string]string, options tokenOptions) (jwtCheck
return nil, errors.New("missing jwt_js_acl_script_path")
}
+ if passClaims, ok := authOpts["jwt_js_pass_claims"]; ok && passClaims == "true" {
+ checker.passClaims = true
+ }
+
checker.runner = js.NewRunner(checker.stackDepthLimit, checker.msMaxDuration)
return checker, nil
@@ -90,14 +96,10 @@ func (o *jsJWTChecker) GetUser(token string) (bool, error) {
}
if o.options.parseToken {
- username, err := getUsernameForToken(o.options, token, o.options.skipUserExpiration)
-
- if err != nil {
- log.Printf("jwt get user error: %s", err)
+ var err error
+ if params, err = o.addDataFromJWT(params, token, o.options.skipUserExpiration); err != nil {
return false, err
}
-
- params["username"] = username
}
granted, err := o.runner.RunScript(o.userScript, params)
@@ -108,20 +110,37 @@ func (o *jsJWTChecker) GetUser(token string) (bool, error) {
return granted, err
}
+func (o *jsJWTChecker) addDataFromJWT(params map[string]interface{}, token string, skipExpiration bool) (map[string]interface{}, error) {
+ claims, err := getClaimsForToken(o.options, token, skipExpiration)
+
+ if err != nil {
+ log.Printf("jwt get claims error: %s", err)
+ return nil, err
+ }
+
+ if o.passClaims {
+ params["claims"] = claims
+ }
+
+ if username, found := claims[o.options.userFieldKey]; found {
+ params["username"] = username.(string)
+ } else {
+ params["username"] = ""
+ }
+
+ return params, nil
+}
+
func (o *jsJWTChecker) GetSuperuser(token string) (bool, error) {
params := map[string]interface{}{
"token": token,
}
if o.options.parseToken {
- username, err := getUsernameForToken(o.options, token, o.options.skipUserExpiration)
-
- if err != nil {
- log.Printf("jwt get user error: %s", err)
+ var err error
+ if params, err = o.addDataFromJWT(params, token, o.options.skipUserExpiration); err != nil {
return false, err
}
-
- params["username"] = username
}
granted, err := o.runner.RunScript(o.superuserScript, params)
@@ -141,14 +160,10 @@ func (o *jsJWTChecker) CheckAcl(token, topic, clientid string, acc int32) (bool,
}
if o.options.parseToken {
- username, err := getUsernameForToken(o.options, token, o.options.skipACLExpiration)
-
- if err != nil {
- log.Printf("jwt get user error: %s", err)
+ var err error
+ if params, err = o.addDataFromJWT(params, token, o.options.skipACLExpiration); err != nil {
return false, err
}
-
- params["username"] = username
}
granted, err := o.runner.RunScript(o.aclScript, params)
diff --git a/backends/jwt_test.go b/backends/jwt_test.go
index 774b710..2a5ddcd 100644
--- a/backends/jwt_test.go
+++ b/backends/jwt_test.go
@@ -67,8 +67,8 @@ var notPresentJwtToken = jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims
})
var tkOptions = tokenOptions{
- secret: jwtSecret,
- userField: "Username",
+ secret: jwtSecret,
+ userFieldKey: "username",
}
func TestJWTClaims(t *testing.T) {
@@ -164,12 +164,13 @@ func TestJsJWTChecker(t *testing.T) {
Convey("Tokens may be pre-parsed and passed to the scripts", func() {
jsTokenOptions := tokenOptions{
- parseToken: true,
- secret: jwtSecret,
- userField: "Username",
+ parseToken: true,
+ secret: jwtSecret,
+ userFieldKey: "username",
}
authOpts["jwt_js_user_script_path"] = "../test-files/jwt/parsed_user_script.js"
+ authOpts["jwt_js_pass_claims"] = "true"
checker, err = NewJsJWTChecker(authOpts, jsTokenOptions)
So(err, ShouldBeNil)
diff --git a/test-files/jwt/parsed_user_script.js b/test-files/jwt/parsed_user_script.js
index 56a01bc..1ec8ce5 100644
--- a/test-files/jwt/parsed_user_script.js
+++ b/test-files/jwt/parsed_user_script.js
@@ -1,8 +1,14 @@
-function checkUser(token, username) {
+function checkUser(token, username, claims) {
+ if(claims.username != username) {
+ return false;
+ }
+ if(claims.iss != "jwt-test") {
+ return false;
+ }
if(username == "test") {
return true;
}
return false;
}
-checkUser(token, username);
+checkUser(token, username, claims);
From 4e1c35b4c93399abf01b104803b452de53f4ef24 Mon Sep 17 00:00:00 2001
From: Pavel Tolstov
Date: Sat, 21 May 2022 13:52:43 +0300
Subject: [PATCH 2/3] Add posibility to get hostname of authorizing backend
from "iss" claim of JWT token
---
README.md | 31 +++++-----
backends/jwt.go | 22 ++++++++
backends/jwt_remote.go | 99 ++++++++++++++++++++++++++++----
backends/jwt_test.go | 125 ++++++++++++++++++++++++++++++++++++++++-
4 files changed, 250 insertions(+), 27 deletions(-)
diff --git a/README.md b/README.md
index 64b0f7f..d495b96 100644
--- a/README.md
+++ b/README.md
@@ -880,22 +880,27 @@ The `jwt` backend is for auth with a JWT remote API, a local DB, a JavaScript VM
The following `auth_opt_` options are supported by the `jwt` backend when remote is set to true:
-| Option | default | Mandatory | Meaning |
-| ----------------- | --------- | :-------: | ---------------------------------- |
-| jwt_host | | Y | API server host name or ip |
-| jwt_port | | Y | TCP port number |
-| jwt_getuser_uri | | Y | URI for check username/password |
-| jwt_superuser_uri | | N | URI for check superuser |
-| jwt_aclcheck_uri | | Y | URI for check acl |
-| jwt_with_tls | false | N | Use TLS on connect |
-| jwt_verify_peer | false | N | Whether to verify peer for tls |
-| jwt_response_mode | status | N | Response type (status, json, text) |
-| jwt_params_mode | json | N | Data type (json, form) |
-| jwt_user_agent | mosquitto | N | User agent for requests |
-| jwt_http_method | POST | N | Http method used (POST, GET, PUT) |
+| Option | default | Mandatory | Meaning |
+| ------------------ | --------- | :-------: | ------------------------------------------------------------- |
+| jwt_host | | Y/N | API server host name or ip |
+| jwt_port | | Y | TCP port number |
+| jwt_getuser_uri | | Y | URI for check username/password |
+| jwt_superuser_uri | | N | URI for check superuser |
+| jwt_aclcheck_uri | | Y | URI for check acl |
+| jwt_with_tls | false | N | Use TLS on connect |
+| jwt_verify_peer | false | N | Whether to verify peer for tls |
+| jwt_response_mode | status | N | Response type (status, json, text) |
+| jwt_params_mode | json | N | Data type (json, form) |
+| jwt_user_agent | mosquitto | N | User agent for requests |
+| jwt_http_method | POST | N | Http method used (POST, GET, PUT) |
+| jwt_host_whitelist | | Y/N | List of hosts that are eligible to be an authoritative server |
URIs (like jwt_getuser_uri) are expected to be in the form `/path`. For example, if jwt_with_tls is `false`, jwt_host is `localhost`, jwt_port `3000` and jwt_getuser_uri is `/user`, mosquitto will send a http request to `http://localhost:3000/user` to get a response to check against. How data is sent (either json encoded or as form values) and received (as a simple http status code, a json encoded response or plain text), is given by options jwt_response_mode and jwt_params_mode.
+if the option `jwt_parse_token` is set to `true`, `jwt_host` can be omitted and the host will be taken from the `Issuer` (`iss` field) claim of the JWT token. In this case the option `jwt_host_whitelist` is mandatory and must contain
+either a comma-separated list of the valid hostnames/ip addresses (with or without `:` part) or the `*` (asterisk) symbol. If the `Issuer` claim is not contained in this list of valid hosts, the authorization will fail. Special
+value `*` means "any host" and is intended for testing/development purposes only - NEVER use this in production!
+
If the option `jwt_superuser_uri` is not set then `superuser` checks are disabled for this mode.
For all URIs, the backend will send a request with the `Authorization` header set to `Bearer token`, where token should be a correct JWT token and corresponds to the `username` received from Mosquitto.
diff --git a/backends/jwt.go b/backends/jwt.go
index 217a80e..72707f3 100644
--- a/backends/jwt.go
+++ b/backends/jwt.go
@@ -34,6 +34,7 @@ const (
filesMode = "files"
claimsSubjectKey = "sub"
claimsUsernameKey = "username"
+ claimsIssKey = "iss"
)
func NewJWT(authOpts map[string]string, logLevel log.Level, hasher hashing.HashComparer, version string) (*JWT, error) {
@@ -179,3 +180,24 @@ func getClaimsForToken(options tokenOptions, tokenStr string, skipExpiration boo
return map[string]interface{}(*claims), nil
}
+
+func getIssForToken(options tokenOptions, tokenStr string, skipExpiration bool) (string, error) {
+ claims, err := getJWTClaims(options.secret, tokenStr, skipExpiration)
+
+ if err != nil {
+ return "", err
+ }
+
+ iss, found := (*claims)[claimsIssKey]
+ if !found {
+ return "", nil
+ }
+
+ issString, ok := iss.(string)
+ if !ok {
+ log.Debugf("jwt error: iss expected to be string, got %T", iss)
+ return "", errors.New("got strange iss")
+ }
+
+ return issString, nil
+}
diff --git a/backends/jwt_remote.go b/backends/jwt_remote.go
index f59aa69..d48b506 100644
--- a/backends/jwt_remote.go
+++ b/backends/jwt_remote.go
@@ -8,6 +8,7 @@ import (
"io/ioutil"
h "net/http"
"net/url"
+ "regexp"
"strconv"
"strings"
"time"
@@ -17,14 +18,15 @@ import (
)
type remoteJWTChecker struct {
- userUri string
- superuserUri string
- aclUri string
- userAgent string
- host string
- port string
- withTLS bool
- verifyPeer bool
+ userUri string
+ superuserUri string
+ aclUri string
+ userAgent string
+ host string
+ port string
+ hostWhitelist []string
+ withTLS bool
+ verifyPeer bool
paramsMode string
httpMethod string
@@ -40,6 +42,10 @@ type Response struct {
Error string `json:"error"`
}
+const (
+ whitelistMagicForAnyHost = "*"
+)
+
func NewRemoteJWTChecker(authOpts map[string]string, options tokenOptions, version string) (jwtChecker, error) {
var checker = &remoteJWTChecker{
withTLS: false,
@@ -97,11 +103,36 @@ func NewRemoteJWTChecker(authOpts map[string]string, options tokenOptions, versi
if hostname, ok := authOpts["jwt_host"]; ok {
checker.host = hostname
+ } else if options.parseToken {
+ checker.host = ""
} else {
remoteOk = false
missingOpts += " jwt_host"
}
+ if hostWhitelist, ok := authOpts["jwt_host_whitelist"]; ok {
+ if hostWhitelist == whitelistMagicForAnyHost {
+ log.Warning(
+ "Backend host whitelisting is turned off. This is not secure and should not be used in " +
+ "the production environment")
+ checker.hostWhitelist = append(checker.hostWhitelist, whitelistMagicForAnyHost)
+ } else {
+ for _, host := range strings.Split(hostWhitelist, ",") {
+ strippedHost := strings.TrimSpace(host)
+ /* Not-so-strict check if we have a valid value (domain name or ip address with optional
+ port) as a part of the host whitelist. TODO: Consider using more robust check, i.e.
+ using "govalidator" or similar package instead. */
+ if matched, _ := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9](?:\:[0-9]+)?$`, strippedHost); !matched {
+ return nil, errors.Errorf("JWT backend error: bad host %s in jwt_host_whitelist", strippedHost)
+ }
+ checker.hostWhitelist = append(checker.hostWhitelist, strippedHost)
+ }
+ }
+ } else if checker.host == "" {
+ remoteOk = false
+ missingOpts += " jwt_host_whitelist"
+ }
+
if port, ok := authOpts["jwt_port"]; ok {
checker.port = port
} else {
@@ -229,13 +260,19 @@ func (o *remoteJWTChecker) jwtRequest(host, uri, token string, dataMap map[strin
tlsStr = "https://"
}
- fullURI := fmt.Sprintf("%s%s%s", tlsStr, o.host, uri)
- if o.port != "" {
- fullURI = fmt.Sprintf("%s%s:%s%s", tlsStr, o.host, o.port, uri)
+ var err error
+ host, err = o.getHost(token)
+ if err != nil {
+ return false, err
+ }
+
+ fullURI := fmt.Sprintf("%s%s%s", tlsStr, host, uri)
+ // If "host" variable already has port set, do not use the value of jwt_port option from config.
+ if !strings.Contains(host, ":") && o.port != "" {
+ fullURI = fmt.Sprintf("%s%s:%s%s", tlsStr, host, o.port, uri)
}
var resp *h.Response
- var err error
var req *h.Request
switch o.paramsMode {
@@ -323,3 +360,41 @@ func (o *remoteJWTChecker) jwtRequest(host, uri, token string, dataMap map[strin
log.Debugf("jwt request approved for %s", token)
return true, nil
}
+
+func (o *remoteJWTChecker) getHost(token string) (string, error) {
+ if o.host != "" {
+ return o.host, nil
+ }
+
+ // Actually this should never happen because of configuration sanity check. TODO: consider removing this condition.
+ if !o.options.parseToken {
+ errorString := fmt.Sprintf("impossible to obtain host for the authorization request - token parsing is turned off")
+ return "", errors.New(errorString)
+ }
+
+ iss, err := getIssForToken(o.options, token, o.options.skipUserExpiration)
+ if err != nil {
+ errorString := fmt.Sprintf("cannot obtain host for the authorization request from token %s: %s", token, err)
+ return "", errors.New(errorString)
+ }
+
+ if !o.isHostWhitelisted(iss) {
+ errorString := fmt.Sprintf("host %s obtained from host is not whitelisted; rejecting", iss)
+ return "", errors.New(errorString)
+ }
+
+ return iss, nil
+}
+
+func (o *remoteJWTChecker) isHostWhitelisted(host string) bool {
+ if len(o.hostWhitelist) == 1 && o.hostWhitelist[0] == whitelistMagicForAnyHost {
+ return true
+ }
+
+ for _, whitelistedHost := range o.hostWhitelist {
+ if whitelistedHost == host {
+ return true
+ }
+ }
+ return false
+}
diff --git a/backends/jwt_test.go b/backends/jwt_test.go
index 2a5ddcd..2d5f8b8 100644
--- a/backends/jwt_test.go
+++ b/backends/jwt_test.go
@@ -718,12 +718,49 @@ func TestJWTAllJsonServer(t *testing.T) {
authOpts["jwt_mode"] = "remote"
authOpts["jwt_params_mode"] = "json"
authOpts["jwt_response_mode"] = "json"
- authOpts["jwt_host"] = strings.Replace(mockServer.URL, "http://", "", -1)
authOpts["jwt_port"] = ""
authOpts["jwt_getuser_uri"] = "/user"
authOpts["jwt_superuser_uri"] = "/superuser"
authOpts["jwt_aclcheck_uri"] = "/acl"
+ parseTkOptions := tkOptions
+ parseTkOptions.parseToken = true
+
+ Convey("Given inconsistent auth options, NewRemoteJWTChecker should fail", t, func() {
+
+ Convey("Given jwt_host is not set, jwt_host_whitelist should be set and valid", func() {
+
+ authOpts["jwt_host_whitelist"] = ""
+
+ _, err := NewRemoteJWTChecker(authOpts, parseTkOptions, version)
+ So(err, ShouldNotBeNil)
+
+ authOpts["jwt_host_whitelist"] = "good-host:8000, bad_host"
+
+ _, err = NewRemoteJWTChecker(authOpts, parseTkOptions, version)
+ So(err, ShouldNotBeNil)
+
+ })
+
+ authOpts["jwt_host_whitelist"] = "*"
+
+ Convey("Given jwt_host is not set, jwt_parse_token should be true", func() {
+
+ _, err := NewRemoteJWTChecker(authOpts, tkOptions, version)
+ So(err, ShouldNotBeNil)
+
+ })
+ })
+
+ Convey("Given consistent auth options, NewRemoteJWTChecker should be created", t, func() {
+
+ authOpts["jwt_host_whitelist"] = "good-host:8000, 10.0.0.1:10, some.good.host, 10.0.0.2"
+ _, err := NewRemoteJWTChecker(authOpts, parseTkOptions, version)
+ So(err, ShouldBeNil)
+ })
+
+ authOpts["jwt_host"] = strings.Replace(mockServer.URL, "http://", "", -1)
+
Convey("Given correct options an http backend instance should be returned", t, func() {
hb, err := NewJWT(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, ""), version)
So(err, ShouldBeNil)
@@ -1261,6 +1298,7 @@ func TestJWTFormStatusOnlyServer(t *testing.T) {
version := "2.0.0"
+ rightToken := token
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
@@ -1273,7 +1311,7 @@ func TestJWTFormStatusOnlyServer(t *testing.T) {
gToken := r.Header.Get("Authorization")
gToken = strings.TrimPrefix(gToken, "Bearer ")
- if token != gToken {
+ if rightToken != gToken {
w.WriteHeader(http.StatusNotFound)
return
}
@@ -1386,6 +1424,89 @@ func TestJWTFormStatusOnlyServer(t *testing.T) {
})
+ serverHostAddr := strings.Replace(mockServer.URL, "http://", "", -1)
+
+ authOpts["jwt_host"] = ""
+ authOpts["jwt_parse_token"] = "true"
+ authOpts["jwt_secret"] = jwtSecret
+
+ tokenWithIss, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "iss": serverHostAddr,
+ "aud": "jwt-test",
+ "nbf": nowSecondsSinceEpoch,
+ "exp": expSecondsSinceEpoch,
+ "sub": "user",
+ "username": username,
+ }).SignedString([]byte(jwtSecret))
+
+ wrongIssToken, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "iss": serverHostAddr,
+ "aud": "jwt-test",
+ "nbf": nowSecondsSinceEpoch,
+ "exp": expSecondsSinceEpoch,
+ "sub": "user",
+ "username": "wrong_user",
+ }).SignedString([]byte(jwtSecret))
+
+ rightToken = tokenWithIss
+ Convey("Given empty jwt_host field and correct iss claim authorization should work", t, func() {
+
+ authOpts["jwt_host_whitelist"] = serverHostAddr + ", sometherhost"
+ hbWhitelistedHost, err := NewJWT(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, ""), version)
+ So(err, ShouldBeNil)
+
+ Convey("Given correct password/username and iss host is whitelisted, get user should return true", func() {
+
+ authenticated, err := hbWhitelistedHost.GetUser(tokenWithIss, "", "")
+ So(err, ShouldBeNil)
+ So(authenticated, ShouldBeTrue)
+
+ })
+
+ Convey("Given incorrect password/username, get user should return false", func() {
+
+ authenticated, err := hbWhitelistedHost.GetUser(wrongIssToken, "", "")
+ So(err, ShouldBeNil)
+ So(authenticated, ShouldBeFalse)
+
+ })
+
+ authOpts["jwt_port"] = "12345"
+ hbWhitelistedHostBadConfigPort, err := NewJWT(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, ""), version)
+ So(err, ShouldBeNil)
+
+ Convey("Given jwt_port is present in config, port from iss field should be used anyway", func() {
+
+ authenticated, err := hbWhitelistedHostBadConfigPort.GetUser(tokenWithIss, "", "")
+ So(err, ShouldBeNil)
+ So(authenticated, ShouldBeTrue)
+
+ })
+
+ authOpts["jwt_host_whitelist"] = "*"
+ hbAnyHost, err := NewJWT(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, ""), version)
+ So(err, ShouldBeNil)
+
+ Convey("Given correct password/username and all hosts are allowed, get user should return true", func() {
+
+ authenticated, err := hbAnyHost.GetUser(tokenWithIss, "", "")
+ So(err, ShouldBeNil)
+ So(authenticated, ShouldBeTrue)
+
+ })
+
+ authOpts["jwt_host_whitelist"] = "otherhost1, otherhost2"
+ hbBadHost, err := NewJWT(authOpts, log.DebugLevel, hashing.NewHasher(authOpts, ""), version)
+ So(err, ShouldBeNil)
+
+ Convey("Given host from iss is not whitelisted, get user should fail even if the credentials are correct", func() {
+
+ authenticated, err := hbBadHost.GetUser(tokenWithIss, "", "")
+ So(err, ShouldNotBeNil)
+ So(authenticated, ShouldBeFalse)
+
+ })
+ })
}
func TestJWTFormTextResponseServer(t *testing.T) {
From 55321df94e00e203a58444efce8e2fbdc8224806 Mon Sep 17 00:00:00 2001
From: Pavel Tolstov
Date: Wed, 15 Jun 2022 11:28:28 +0300
Subject: [PATCH 3/3] Remove unused parameter from jwtRequest()
---
backends/jwt_remote.go | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/backends/jwt_remote.go b/backends/jwt_remote.go
index d48b506..f899033 100644
--- a/backends/jwt_remote.go
+++ b/backends/jwt_remote.go
@@ -185,7 +185,7 @@ func (o *remoteJWTChecker) GetUser(token string) (bool, error) {
}
}
- return o.jwtRequest(o.host, o.userUri, token, dataMap, urlValues)
+ return o.jwtRequest(o.userUri, token, dataMap, urlValues)
}
func (o *remoteJWTChecker) GetSuperuser(token string) (bool, error) {
@@ -212,7 +212,7 @@ func (o *remoteJWTChecker) GetSuperuser(token string) (bool, error) {
}
}
- return o.jwtRequest(o.host, o.superuserUri, token, dataMap, urlValues)
+ return o.jwtRequest(o.superuserUri, token, dataMap, urlValues)
}
func (o *remoteJWTChecker) CheckAcl(token, topic, clientid string, acc int32) (bool, error) {
@@ -240,14 +240,14 @@ func (o *remoteJWTChecker) CheckAcl(token, topic, clientid string, acc int32) (b
urlValues.Add("username", username)
}
- return o.jwtRequest(o.host, o.aclUri, token, dataMap, urlValues)
+ return o.jwtRequest(o.aclUri, token, dataMap, urlValues)
}
func (o *remoteJWTChecker) Halt() {
// NO-OP
}
-func (o *remoteJWTChecker) jwtRequest(host, uri, token string, dataMap map[string]interface{}, urlValues url.Values) (bool, error) {
+func (o *remoteJWTChecker) jwtRequest(uri, token string, dataMap map[string]interface{}, urlValues url.Values) (bool, error) {
// Don't do the request if the client is nil.
if o.client == nil {
@@ -260,8 +260,7 @@ func (o *remoteJWTChecker) jwtRequest(host, uri, token string, dataMap map[strin
tlsStr = "https://"
}
- var err error
- host, err = o.getHost(token)
+ host, err := o.getHost(token)
if err != nil {
return false, err
}