Smarthome: Support JSON Web Tokens as bearer tokens for access control

Created on 5 Mar 2017  路  5Comments  路  Source: eclipse-archived/smarthome

Hi,
I experimented yesterday with the everlasting issue of access control in ESH based on my experience with JWT tokens and I ended implementing some basic working support as an exercise, therefore I wanted to spark discussion here first instead of submitting an incomplete PR out of the blue.

It's curerntly sitting here:
https://github.com/eclipse/smarthome/compare/master...ghys:jwt-auth?expand=1

I'll quote the commit message first:

This introduces another auth mechanism for authentication to the REST
endpoints: using self-contained JSON Web Tokens (RFC 7519) containing
pre-agreed claims related to the identity and the access granted by an
external trusted identity/authorization provider.

With this approach, user and role management as well as the authentication
mechanism is handled by the external IdP and the information in the token
is simply used as-is because of the trust relationship between ESH and the
IdP service. Of course, this currently breaks existing UIs and other clients
of the API because they don't know how to request a token from the IdP.
Ultimately, an OpenID Connect or OAuth2 service delivering this type of
tokens should be embedded within ESH itself.

JWTs may also contain scopes limiting the access of the client application
(relying party) to certain operations - for instance, some apps should
not be allowed access to endpoints managing things or bindings, even if the
user (subject) himself has the admin role, because they have no business
doing so. The RequiredScopesDynamicFeature ensures the scopes required by
the API operation (for now, described in the Swagger specification) are
found in the provided token. In practice, the IdP service will require the
consent of the user the first time with a dedicated screen ("This
application will be able to: manage your things, update your items, etc."),
but this could be bypassed for "trusted" clients like the standard UIs.

The tokens are supposed to be signed using JWS HS256 (the only one
supported for now) and their signature will be verified against a
Base64-encoded pre-shared secret defined in the service configuration - the
issuer claim ('iss') is also expected to match the value specified in the
configuration.

Signed-off-by: Yannick Schaus eclipse@schaus.net

I also found this article to be a good high-level introduction to the general concept: https://jwt.io/introduction/

Here's how to try it out in practice:

  1. The pre-shared secret and trusted issuer name are currently set to silly values in smarthome.cfg (asymmetric keys are desirable but not implemented yet)
# Configuration of JWT token verification
org.eclipse.smarthome.auth.jwt:trustedIssuer=smarthome
org.eclipse.smarthome.auth.jwt:preSharedKey=bXkgdmVyeSBzZWNyZXQga2V5
  1. You need jose4j (download 0.5.5 here) in targetplatform/third-party

  2. Generate a token. You can generate tokens using https://jwt.io (some identity management products like Keycloak, Thinktecture IdentityServer can generate them as well)
    Put the pre-shared key in the 'secret' textbox and check 'secret base64 encoded' and paste the following payload (some standard claims missing):

{
  "sub": "1234567890",
  "iss": "smarthome",
  "name": "John Doe",
  "role": "administrator",
  "scope": "items things things.change"
}
  1. Then call the API with the resulting token as a Bearer token in an Authorization header:

curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoic21hcnRob21lIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluaXN0cmF0b3IiLCJzY29wZSI6Iml0ZW1zIHRoaW5ncyB0aGluZ3MuY2hhbmdlIn0.3O8QPT9ucYSoVIx7sZ6ET5potQedmBQSNYt0kAWt61Y' http://localhost:8080/rest/items

will return the expected 200 OK response.

Note if you alter the token in any way, the signature doesn't validate anymore:

{
  "error": {
    "message": "JWS signature is invalid: JsonWebSignature{\"alg\":\"HS256\",\"typ\":\"JWT\"}-\u003eeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoic21hcnRob21lIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluaXN0cmF0b3IiLCJzY29wZSI6Iml0ZW1zIHRoaW5ncyB0aGluZ3MuY2hhbmdlIn0.3O8QPT9ucYSoVIx7sZ6ET5potQedmBQSNYt0kAWt61",
    "http-code": 500,
    "exception": {
      "class": "org.eclipse.smarthome.core.auth.AuthenticationException",
      "message": "JWS signature is invalid: JsonWebSignature{\"alg\":\"HS256\",\"typ\":\"JWT\"}-\u003eeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoic21hcnRob21lIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluaXN0cmF0b3IiLCJzY29wZSI6Iml0ZW1zIHRoaW5ncyB0aGluZ3MuY2hhbmdlIn0.3O8QPT9ucYSoVIx7sZ6ET5potQedmBQSNYt0kAWt61",
      "localized-message": "JWS signature is invalid: JsonWebSignature{\"alg\":\"HS256\",\"typ\":\"JWT\"}-\u003eeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoic21hcnRob21lIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluaXN0cmF0b3IiLCJzY29wZSI6Iml0ZW1zIHRoaW5ncyB0aGluZ3MuY2hhbmdlIn0.3O8QPT9ucYSoVIx7sZ6ET5potQedmBQSNYt0kAWt61",
      "cause": "org.jose4j.jwt.consumer.InvalidJwtSignatureException"
    }
  }
}

The claims are also used for access control, so the 'role' claims are checked against @RolesAllowed. I also tried adding Swagger annotations to a few operations so the RequiredScopesDynamicFeature will pick them up (probably not the best idea to use the Swagger annotations but since they were there...):

    @ApiOperation(value = "Get all available items.", response = EnrichedItemDTO.class, responseContainer = "List", authorizations = {
            @Authorization(value = "oauth2", scopes = {
                    @AuthorizationScope(scope = "items", description = "List items and read their states") }) })

Therefore if you generate another token without the items scope, the request will fail:

curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoic21hcnRob21lIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluaXN0cmF0b3IiLCJzY29wZSI6InRoaW5ncyB0aGluZ3MuY2hhbmdlIn0.9E6v-oOHVNDhofZdn14qxbbn-tvEUeXjjGpL98wCufM' http://localhost:8080/rest/items

{
  "error": {
    "message": "Missing scope in token : items",
    "http-code": 403,
    "exception": {
      "class": "javax.ws.rs.ForbiddenException",
      "message": "Missing scope in token : items",
      "localized-message": "Missing scope in token : items"
    }
  }
}

This is useful when implementing OAuth2 flows with apps requesting (and be allowed or not) scopes according to their needs.

Again, this is still quite early, my implementation is probably far from perfect, the IdP/STS service is not done, the UIs have to be adapted to request tokens, but I believe it's going in the right direction. I really think this could pave the way to a modern, robust, industry-standard authorization layer for ESH.

Also note it's completely optional and basic auth can still be enabled instead for simpler scenarios.

Eager to read what you think!

Most helpful comment

I think all in all what is being discussed here is something really important for ESH.

trust heavily depends on the HTTPS protocol with valid certiicates and stable callback URLs which remain (I believe) the exception rather the norm for ESH installations;

I think this could be addressed e.g. in the following way: Implementing an easy way to add a domain and TLS certificate to ESH. I'm not sure if a service can do that all but possibly it can. It could let the user enter its domain name and offer registering a sub domain in case no domain is available. It could further receive a letsencrypt certificate and renew it on regular basis. In the middle it could tell the user - for home users - how to setup port forwarding. The dyndns could be either handled by the service as long as the users chooses to get a subdomain or by the users router. In case the user still wants to use a subdomain it could offer to set a CNAME record (e.g. AVM for its FritzBox offers a random subdomain for DynDns which works very well but is hard to remember).
In combination with the Apps one could even use a self signed certificate and pin it which could avoid regular renewal and would even be more secure as the connection would immediately fail when any other certificate is being used instead.

I agree with the second point, but also think it could be a major advantage if this would be tackled as early as possible as it avoids having more and more stuff not supporting it (which often is an argument for the user to better not protect anything as it currently works, but if Authentication and Authorization works early enough, users are not going to use extensions not supporting it for the same reason).

All 5 comments

It sounds very interesting. I hopefully have some time to have a look at your changes and read dig into it.

I have used JWT in a custom REST interface myself to limit read / write access to things, items etc.
For me this involved some more changes, as I need an access control manager that not only protects an endpoint for a special user, but also the respond differs for user / role. For example the getAll methods of the item REST endpoint must not return items the caller does not have read access and so on.

Do you still use your JWT changes? So, are you still interested in this issue or is it obsolete?

You're right about the implications this type of auth mechanism would require.
Notably:
1) trust heavily depends on the HTTPS protocol with valid certiicates and stable callback URLs which remain (I believe) the exception rather the norm for ESH installations;
2) all UIs would need to implement some flow (OAuth2 or OpenID Connect) for requesting and eventually renewing tokens (note: as a benefit though, certain UIs could be disabled for certain users unless they are authorized - for example, Paper UI would request admin scopes with an implicit flow which would fail at the IdP level if the user doesn't have the necessary role(s) to request this scope, effectively preventing the user from entering the UI);
3) an integrated identity provider (as an OSGi bundle), responsible for managing both applications/scopes and user/roles, as well as performing authentication and issuing tokens, makes much more sense than requiring an external one - I don't know of any viable option except writing one, with JAAS realms as backends, and writing an IdP from scratch is hard;
4) lastly, the REST APIs could indeed use the claims in the principal to filter operations with more granularity than the endpoint level - for example, interacting with ESH items in a certain group could be allowed to certain roles/scopes only, or arrays in responses could be filtered as you mention. This sounds like substantial work, but e.g. some clever conventions in group names (like access_<rolename>) could perhaps simplify it.

To be honest about the status, this issue was more a proposal to spark discussion before talking about a complete implementation - my changes in the core auth layer are practically unusable as they are now without 2. and 3., and I don't know if/when I'll find the time to address those to really change the situation. So if you're doing some housekeeping and wish to close this issue until there are actual advancements, by all means feel free to do so ;)

I think all in all what is being discussed here is something really important for ESH.

trust heavily depends on the HTTPS protocol with valid certiicates and stable callback URLs which remain (I believe) the exception rather the norm for ESH installations;

I think this could be addressed e.g. in the following way: Implementing an easy way to add a domain and TLS certificate to ESH. I'm not sure if a service can do that all but possibly it can. It could let the user enter its domain name and offer registering a sub domain in case no domain is available. It could further receive a letsencrypt certificate and renew it on regular basis. In the middle it could tell the user - for home users - how to setup port forwarding. The dyndns could be either handled by the service as long as the users chooses to get a subdomain or by the users router. In case the user still wants to use a subdomain it could offer to set a CNAME record (e.g. AVM for its FritzBox offers a random subdomain for DynDns which works very well but is hard to remember).
In combination with the Apps one could even use a self signed certificate and pin it which could avoid regular renewal and would even be more secure as the connection would immediately fail when any other certificate is being used instead.

I agree with the second point, but also think it could be a major advantage if this would be tackled as early as possible as it avoids having more and more stuff not supporting it (which often is an argument for the user to better not protect anything as it currently works, but if Authentication and Authorization works early enough, users are not going to use extensions not supporting it for the same reason).

This is essential for ESH. Lets keep this going, we have the ongoing discussion here https://github.com/eclipse/smarthome/issues/579#issuecomment-356952399

Was this page helpful?
0 / 5 - 0 ratings

Related issues

binderth picture binderth  路  4Comments

doandzhi picture doandzhi  路  6Comments

individualist picture individualist  路  3Comments

remogloor picture remogloor  路  10Comments

sjsf picture sjsf  路  7Comments