Skip to content

Commit

Permalink
Add option for sending decrypted claims to JWT Javascript backend
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel Tolstov committed Jun 11, 2022
1 parent d904546 commit 788ee91
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 52 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1488,13 +1488,16 @@ 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.

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):
Expand Down
60 changes: 33 additions & 27 deletions backends/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type tokenOptions struct {
skipUserExpiration bool
skipACLExpiration bool
secret string
userField string
userFieldKey string
}

type jwtChecker interface {
Expand All @@ -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) {
Expand Down Expand Up @@ -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"] {
Expand Down Expand Up @@ -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
})

Expand All @@ -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
}
51 changes: 33 additions & 18 deletions backends/jwt_javascript.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type jsJWTChecker struct {
superuserScript string
aclScript string

passClaims bool

options tokenOptions

runner *js.Runner
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
11 changes: 6 additions & 5 deletions backends/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 8 additions & 2 deletions test-files/jwt/parsed_user_script.js
Original file line number Diff line number Diff line change
@@ -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);

0 comments on commit 788ee91

Please sign in to comment.