Add option for sending decrypted claims to JWT Javascript backend

This commit is contained in:
Pavel Tolstov 2022-05-03 20:18:20 +03:00
parent d904546ac6
commit 788ee917e4
5 changed files with 84 additions and 53 deletions

View File

@ -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):

View File

@ -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,23 +141,15 @@ 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
}
return claims.Subject
}
func getUsernameForToken(options tokenOptions, tokenStr string, skipExpiration bool) (string, error) {
claims, err := getJWTClaims(options.secret, tokenStr, skipExpiration)
@ -171,5 +157,25 @@ func getUsernameForToken(options tokenOptions, tokenStr string, skipExpiration b
return "", err
}
return getUsernameFromClaims(options, claims), nil
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 getClaimsForToken(options tokenOptions, tokenStr string, skipExpiration bool) (map[string]interface{}, error) {
claims, err := getJWTClaims(options.secret, tokenStr, skipExpiration)
if err != nil {
return make(map[string]interface{}), err
}
return map[string]interface{}(*claims), nil
}

View File

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

View File

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

View File

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