Add option for sending decrypted claims to JWT Javascript backend
This commit is contained in:
parent
d904546ac6
commit
788ee917e4
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue