diff --git a/README.md b/README.md index 4a19bf5..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. @@ -1488,6 +1493,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 +1501,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..72707f3 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,14 @@ 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" + claimsIssKey = "iss" ) func NewJWT(authOpts map[string]string, logLevel log.Level, hasher hashing.HashComparer, version string) (*JWT, error) { @@ -69,9 +64,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 +120,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 +142,62 @@ 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 + } + + 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 claims.Subject + return map[string]interface{}(*claims), nil } -func getUsernameForToken(options tokenOptions, tokenStr string, skipExpiration bool) (string, error) { +func getIssForToken(options tokenOptions, tokenStr string, skipExpiration bool) (string, error) { claims, err := getJWTClaims(options.secret, tokenStr, skipExpiration) if err != nil { return "", err } - return getUsernameFromClaims(options, claims), nil + 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_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_remote.go b/backends/jwt_remote.go index f59aa69..f899033 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 { @@ -154,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) { @@ -181,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) { @@ -209,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 { @@ -229,13 +260,18 @@ 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) + 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 +359,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 774b710..2d5f8b8 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) @@ -717,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) @@ -1260,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() @@ -1272,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 } @@ -1385,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) { 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);