blog

Integrating Kalix and Auth0 using JWTs

James Roper.
Cloud Architect, Lightbend.
  • 15 July 2022,
  • 12 minute read

This article is going to walk you through integrating Kalix and Auth0 using JSON Web Tokens (JWTs).

Prerequisites

  • An Auth0 account. The free plan is sufficient.
  • A Kalix account, with a trial project created.
  • A Docker repository that you can push to, such as Docker Hub.
  • The Kalix CLI installed and authenticated.
  • Java 11 or later installed.
  • Apache Maven installed.
  • A Docker build environment installed, such as Docker on Linux, or Docker Desktop on Windows or Mac OS, authenticated to push to your Docker repository.
  • Your favourite IDE to edit files.

Introduction

Auth0 is a popular user management service, it provides an API (and UI) for creating, managing and authenticating users, among other things. There are multiple ways to integrate an application with Auth0, one of them is using JWTs.

Kalix provides in built JWT support, compatible with the JWTs produced by Auth0. In this blog post, we will create the default Java SDK template project, which includes a counter value entity. We will deploy it to Kalix initially and test that it works. Then we will add JWT protection to its endpoints, and configure the service to trust JWTs issued by and for our Auth0 account. Finally, we will code in some authorization to validate that users can only access their own counters.

Create a Maven project

Let's create a Maven project.

mvn archetype:generate
  -DarchetypeGroupId=io.kalix
  -DarchetypeArtifactId=kalix-maven-archetype
  -DarchetypeVersion=1.0.4

For the groupId, select com.example.counter.jwt. For the artifactId, select jwt-counter. Leave all the other prompts as their defaults.

Now update the pom.xml file to point to your docker repository in the dockerImage property. My repository is the jamesroper Docker Hub repository:

<dockerImage>jamesroper/${project.artifactId}</dockerImage>

Now compile the project, to ensure that the Counter value entity is generated:

mvn compile

The generated counter value entity in src/main/java/com/example/counter/jwt/domain/Counter.java will initially have every command unimplemented. Implement it like so:

public class Counter extends AbstractCounter {
  @SuppressWarnings("unused")
  private final String entityId;

  public Counter(ValueEntityContext context) {
    this.entityId = context.entityId();
  }

  @Override
  public CounterDomain.CounterState emptyState() {
    return CounterDomain.CounterState.getDefaultInstance();
  }

  @Override
  public Effect<Empty> increase(CounterDomain.CounterState currentState, CounterApi.IncreaseValue increaseValue) {
    return effects().updateState(
        currentState.toBuilder()
            .setValue(currentState.getValue() + increaseValue.getValue())
            .build()
    ).thenReply(Empty.getDefaultInstance());
  }

  @Override
  public Effect<Empty> decrease(CounterDomain.CounterState currentState, CounterApi.DecreaseValue decreaseValue) {
    return effects().updateState(
        currentState.toBuilder()
            .setValue(currentState.getValue() - decreaseValue.getValue())
            .build()
    ).thenReply(Empty.getDefaultInstance());
  }

  @Override
  public Effect<Empty> reset(CounterDomain.CounterState currentState, CounterApi.ResetValue resetValue) {
    return effects().updateState(
        currentState.toBuilder()
            .setValue(0)
            .build()
    ).thenReply(Empty.getDefaultInstance());
  }

  @Override
  public Effect<CounterApi.CurrentCounter> getCurrentCounter(CounterDomain.CounterState currentState, CounterApi.GetCounter getCounter) {
    return effects().reply(
        CounterApi.CurrentCounter.newBuilder()
            .setValue(currentState.getValue())
            .build()
    );
  }
}

Build and deploy initial test

Now build and deploy the project, first to your Docker registry:

mvn deploy

And then deploy the service using the Kalix CLI:

kalix service deploy jwt-counter jamesroper/jwt-counter

Wait for the service to be deployed and ready:

$ kalix service list
NAME          AGE     REPLICAS   STATUS   DESCRIPTION   
jwt-counter   3m16s   1          Ready  

Now, for a proper production application, you will want to expose it to the internet, which will also require updating the ACLs to allow internet communication. But, to keep things simple for this blog post, we'll just use the back office proxy to invoke the service without exposing it to the internet, using a web based UI. To do this, run:

kalix service proxy jwt-counter --grpcui

This will open a web browser, from where you can invoke commands on the counter API. From there you can test the service to ensure it works, try invoking the Increment method, with a particular counter_id and a non-zero value, and then invoke the GetCurrentCounter method, with the same counter_id, and verify that it returns the value you just incremented it to.

Adding bearer token validation

We now want to add bearer token validation to all the methods in the API. Modify src/main/proto/com/example/counter/jwt/counter_api.proto, to add the following JWT options to each method:

rpc Increase(IncreaseValue) returns (google.protobuf.Empty) {
  option (kalix.method).jwt.validate = BEARER_TOKEN;
};
rpc Decrease(DecreaseValue) returns (google.protobuf.Empty) {
  option (kalix.method).jwt.validate = BEARER_TOKEN;
};
rpc Reset(ResetValue) returns (google.protobuf.Empty) {
  option (kalix.method).jwt.validate = BEARER_TOKEN;
};
rpc GetCurrentCounter(GetCounter) returns (CurrentCounter) {
  option (kalix.method).jwt.validate = BEARER_TOKEN;
};

You can now build and push the image:

mvn deploy

Configuring Auth0 and the Kalix service

Now, on the Auth0 side, we will create a new application. The application type doesn't matter too much, I created a Native application, and called it my-application.

We now need to download the PEM keys for the application. On the application settings page, there is an application domain, which will look something like dev-9drqw0bx.us.auth0.com. Copy this domain, and then download https://<auth0-domain>/pem, and store the file somewhere. For example:

curl https://dev-9drqw0bx.us.auth0.com/pem > my-application.pem

Now we need to extract the public key from the certificate. This can be done using openssl:

openssl x509 -pubkey -noout -in my-certificate.pem  > pubkey.pem

Now we can configure that as a secret in our Kalix app:

kalix secret create asymmetric auth0 --public-key pubkey.pem

And finally, we can configure our jwt-counter service to use this public key for validating JWT keys. The --issuer is optional, but is useful to pin that key to only be used to validate JWTs issued by your Auth0 app. It must be your Auth0 app domain, wrapped in https://<auth0-domain>/:

kalix service jwts add jwt-counter
  --key-id auth0
  --algorithm RS256
  --issuer https://dev-9drqw0bx.us.auth0.com/
  --secret auth0

This will restart the service with the new JWT bearer token validation. Try to make another request using the web UI. You should get an error saying UNAUTHENTICATED: Bearer token expected.

Testing with a bearer token

Auth0 supports a number of different tokens. Generally, when using Auth0 tokens, you will want to obtain an access token for a user. This is usually done by first obtaining a refresh token for a logged in user, which can be stored by the local application, and then using that to get a short lived access token.

For this blog post, we're just going to use test access tokens that don't represent users, but instead represent the application itself. To create a test access token, we need to create an API. Go to APIs, and click "Create API". Call the API jwt-counter, and enter an identifier. Auth0 recommends that the identifier be a URL, but it can be any URL, it doesn't have to be valid and they will never invoke it. I selected https://jwt-counter.example.com. Leave the signing algorithm as RS256.

Now we can easily get a test token by going to the Test tab. In that, there's some sample code for how to get a test access token, you can execute this, or instead you can simply copy the already generated access token from the Response section in that page. The access token there is an access token that has the applications client_id as the subject, in the format <client-id>@clients. Now, in the web UI, add a metadata header named Authorization, and set the value to Bearer, followed by a space, followed by the token. The commands should now be accepted.

Applying authorization

Just authenticating a user is not usually enough, you want to ensure that that user can only access resources that they are authorized to access. To do this, we will write this reusable method for checking the that the current user matches the entity id:

private <T> Effect<T> authorize(Supplier<Effect<T>> block) {
  var maybeSubject = commandContext().metadata().jwtClaims().subject();
  if (maybeSubject.isEmpty()) {
    return effects().error("No subject", Status.Code.UNAUTHENTICATED);
  } else {
    var subject = maybeSubject.get();
    if (!subject.equals(entityId)) {
      return effects().error("User " + subject + " is not authorized to access this counter", Status.Code.PERMISSION_DENIED);
    } else {
      return block.get();
    }
  }
}

Now we can modify each method to use this to authorize the handler:

@Override
public Effect<Empty> increase(CounterDomain.CounterState currentState, CounterApi.IncreaseValue increaseValue) {
  return authorize(() -> effects().updateState(
      currentState.toBuilder()
          .setValue(currentState.getValue() + increaseValue.getValue())
          .build()
  ).thenReply(Empty.getDefaultInstance()));
}

@Override
public Effect<Empty> decrease(CounterDomain.CounterState currentState, CounterApi.DecreaseValue decreaseValue) {
  return authorize(() -> effects().updateState(
      currentState.toBuilder()
          .setValue(currentState.getValue() - decreaseValue.getValue())
          .build()
  ).thenReply(Empty.getDefaultInstance()));
}

@Override
public Effect<Empty> reset(CounterDomain.CounterState currentState, CounterApi.ResetValue resetValue) {
  return authorize(() -> effects().updateState(
      currentState.toBuilder()
          .setValue(0)
          .build()
  ).thenReply(Empty.getDefaultInstance()));
}

@Override
public Effect<CounterApi.CurrentCounter> getCurrentCounter(CounterDomain.CounterState currentState, CounterApi.GetCounter getCounter) {
  return authorize(() -> effects().reply(
      CounterApi.CurrentCounter.newBuilder()
          .setValue(currentState.getValue())
          .build()
  ));
}

Now deploy and restart:

mvn deploy
kalix service restart jwt-counter

And now, if you use the token generated above with any arbitrary counter_id, you'll get the following message:

User 033DlUR8azrQK83kChGr9YiiBdFhjo3D@clients is not authorized to access this counter

Where the user id is the client id for your application. Now if you set the counter_id to be the user id that was returned in that error message, your calls should succeed.

Only accepting Auth0 tokens

For additional security, especially if you have multiple JWT keys from different providers configured, you can configure the bearer token validation to only accept the Auth0 keys. To do this, add the bearer_token_issuer annotation to your gRPC methods, to match the Auth0 issuer for your tokens:

rpc Increase(IncreaseValue) returns (google.protobuf.Empty) {
  option (kalix.method).jwt = {
    validate: BEARER_TOKEN
    bearer_token_issuer: "https://dev-9drqw0bx.us.auth0.com/"
  };
};
rpc Decrease(DecreaseValue) returns (google.protobuf.Empty) {
  option (kalix.method).jwt = {
    validate: BEARER_TOKEN
    bearer_token_issuer: "https://dev-9drqw0bx.us.auth0.com/"
  };
};
rpc Reset(ResetValue) returns (google.protobuf.Empty) {
  option (kalix.method).jwt = {
    validate: BEARER_TOKEN
    bearer_token_issuer: "https://dev-9drqw0bx.us.auth0.com/"
  };
};
rpc GetCurrentCounter(GetCounter) returns (CurrentCounter) {
  option (kalix.method).jwt = {
    validate: BEARER_TOKEN
    bearer_token_issuer: "https://dev-9drqw0bx.us.auth0.com/"
  };
};

Conclusion

In this blog post, we have seen how Kalix can easily integrate with Auth0, using JWT access tokens. This provides a straight forward way to integrate user management into your Kalix app without having to write everything from scratch. Kalix's JWT support doesn't end there though, Kalix has built in support for validating message fields against JWT claims. For more information on generating and validating tokens for authorization purposes, read Using JWTs in the Kalix documentation.

Author Section will go here