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);