Authentication and Authorization

Systems establish authentication first. If implemented, authorization follows.

What is authentication?

Authentication is the act of validating that users are who they claim to be. Two methods of authentication are common:

  • Passwords: Usernames and passwords are the most common authentication factors. If a user enters the correct data, the system assumes the identity is valid and grants access.

  • Authentication apps: Generate security codes via an outside party that grants access.

What is authorization?

Authorization is the process of giving a user permission to access a specific resource or function. Other terms for this functionality are access control or client privilege. Users must first prove that their identities are genuine before a system grants them access to the requested resources. Authorization is generally controlled by an organization’s administrator.

Methods for accomplishing with Stargate

Stargate currently has two authentication and authorization methods, table-based and JSON web token (JWT)-based. The methods are mutually exclusive in open-source Stargate, so one method must be selected and implemented.

Both methods can be used simultaneously in Astra, DataStax’s Database-as-a-Service.

Table-based authentication/authorization

Table-based authentication and authorization uses the Stargate Auth API to generate an auth token based on a Cassandra username and password. The auth-table-based-service uses the generated auth token to allow Stargate API queries access to the Cassandra data.

By default, the token persists for 30 minutes with a sliding window. Each use of the token to authenticate resets the 30 minute window. A token created and used after 29 minutes will authenticate a request, but if 31 minutes passes before use, the token will no longer exist.

The length of time to persist the token is configurable using the system property stargate.auth_tokenttl. For example, set the token to persist for 100 seconds:

JAVA_OPTS='-Dstargate.auth_tokenttl=100' ./starctl \
--developer-mode --cluster-name test --cluster-version 3.11 --enable-auth

The step below uses cURL to access the REST interface to generate the needed token.

Generate an auth token

First generate an auth token that is required in each subsequent request in the X-Cassandra-Token header. Note the port for the auth service is 8081.

curl -L -X POST 'http://localhost:8081/v1/auth' \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "username": "cassandra",
    "password": "cassandra"
}'

You should receive a token in the response.

{"authToken":"{auth-token}"}

The authorization token returned must be either exported as an environment variable (cURL commands with REST or Document API) or entered into the HTTP Header (GraphQL mutations).

  • REST or Document API(/v2)

  • GraphQL

Store the auth token in an environment variable to make it easy to use with cURL.

export AUTH_TOKEN={auth-token}

Add the auth token to the HTTP Headers box in the lower lefthand corner of the GraphQL playground:

{
  "X-Cassandra-Token":"bff43799-4682-4375-99e8-23c8a9d0f304"
}

JWT-based authentication/authorization

Token-based authentication and authorization uses a third-party generator, such as Keycloak, to validate a JSON web token (JWT) and pull claims for user authorization. The auth-jwt-service uses generated claim to allow Stargate API queries access to the Cassandra data.

The steps required to enable JWT-based authentication and authorization involve three steps:

  • Setup a keycloak server using the stargate-realms.json configuration.

  • Add a new user to keycloak to enable a user with a specified role.

  • Start up stargate with authorization and authentication enabled.

  • A Cassandra cluster is assumed to also be running with roles and permissions granted.

Once these steps are completed, queries to Stargate can use the generated token as the X-Cassandra-Token for either cURL commands using the REST or Document API, or for GraphQL mutations.

If you are using Stargate as part of DataStax’s Astra DB cloud-native database, you can define roles and permissions using the Astra Portal.

You can use your own Keycloak server, but for the purposes of a quickstart, here are instructions for setting up a demonstration using docker-compose. The files are also found at https://github.com/stargate/docker-images/tree/master/examples/stargate-jwt-auth.

The startup script uses docker-compose to start 3 Cassandra nodes, one Stargate node, and Keycloak, and insert some data, create a role, and grant permissions. The docker-compose.yml contains the docker image information for starting the images. The stargate-realm.json is mounted on the Keycloak image to supply the stargate-specific realm used for the demo. A sixth node, docker-cqlsh, is started to complete the CQL commands required for the demo, and this image uses two mounted items, a cqlshrc file that specifies the Cassandra username and password, and a scripts directory that contains the CQL file of commands to execute.

Run the startup script

Although most of the files will be explained here, for the demo, it is easiest to clone the github repository, so that all the required files are available.

To clone the repository:

git clone https://github.com/stargate/docker-images.git

Go to the stargate-jwt example:

cd <clone_location>/examples/stargate-jwt
Before starting the demo, be sure that Docker is running on your platform; for instance, install Docker Desktop for Mac or Windows.

To start the demo, run the startup script:

./start_jwt.sh

The script includes the following commands:

 #!/bin/sh

 # Make sure cass-1, the seed node, is up before bringing up other nodes and stargate
 docker-compose up -d cass-1 (1)
 # Wait until the seed node is up before bringing up more nodes
 (docker-compose logs -f cass-1 &) | grep -q "Created default superuser role" (2)

 # Bring up the 2nd C* node
 docker-compose up -d cass-2
 (docker-compose logs -f cass-1 &) | grep -q "is now part of the cluster"
 # Bring up the 3rd C* node
 docker-compose up -d cass-3
 (docker-compose logs -f cass-1 &) | grep -q "is now part of the cluster"

 # Bring up keycloak for handling JWTs
 docker-compose up -d keycloak (3)

 # Bring up the stargate
 docker-compose up -d stargate-jwt (4)
 # Wait until stargate is up before bringing up the metrics tools
 echo ""
 echo "Waiting for stargate to start up..." (5)
 while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' https://localhost:8082/health)" != "200" ]]; do
     printf '.'
     sleep 5
 done

 # this is where a cql script must be run to insert data and grant permissions
 docker run --rm -it -v /Users/lorina.poland/CLONES/stargate/docker-images/examples/stargate-jwt-auth/scripts:/scripts  \ (6)
 -v /Users/lorina.poland/CLONES/stargate/docker-images/examples/stargate-jwt-auth/cqlshrc:/.cassandra/cqlshrc  \
 --env CQLSH_HOST=host.docker.internal --env CQLSH_PORT=9045  nuvo/docker-cqlsh
1 Start first Cassandra node.
2 Ensure Cassandra is up and running before starting next node.
3 Start keycloak before stargate.
4 Start stargate node.
5 Some logic to monitor stargate progress.
6 Use docker run nuvo/docker-cqlsh to insert some data into Cassandra and create a role and grant permissions.

If you have limited memory on the platform where you run this script, you can comment out the second and third Cassandra nodes to reduce the payload.

The docker-compose.yml file identifies each node, run as a service, as well as the network that all nodes are attached to.

version: '2'

  services:
    cass-1:  (1)
      image: cassandra:3.11.8 (2)
      container_name: cass-1
      networks:
        - backend (3)
      ports:
        - 9044:9042
      mem_limit: 2G
      environment: (4)
        - HEAP_NEWSIZE=128M
        - MAX_HEAP_SIZE=1024M
        - CASSANDRA_SEEDS=cass-1
        - CASSANDRA_CLUSTER_NAME=backend-cluster
    cass-2:
      image: cassandra:3.11.8
      container_name: cass-2
      networks:
        - backend
      mem_limit: 2G
      depends_on:
        - cass-1
      environment:
        - HEAP_NEWSIZE=128M
        - MAX_HEAP_SIZE=1024M
        - CASSANDRA_SEEDS=cass-1
        - CASSANDRA_CLUSTER_NAME=backend-cluster
    cass-3:
      image: cassandra:3.11.8
      container_name: cass-3
      networks:
        - backend
      mem_limit: 2G
      depends_on:
        - cass-1
      environment:
        - HEAP_NEWSIZE=128M
        - MAX_HEAP_SIZE=1024M
        - CASSANDRA_SEEDS=cass-1
        - CASSANDRA_CLUSTER_NAME=backend-cluster
    stargate-jwt:
      image: jwt-sg:latest (5)
      container_name: stargate-jwt
      depends_on:
        - cass-1
      networks:
        - backend
      ports:
        - 8080:8080
        - 8081:8081
        - 8082:8082
        - 8085:8085
        - 9045:9042
      mem_limit: 2G
      environment: (6)

        - JAVA_OPTS=-XX:+CrashOnOutOfMemoryError -Xmx750M -Xms64M -Dstargate.auth_id=AuthJwtService -Dstargate.cql_use_auth_service=true -Dstargate.auth.jwt_provider_url=http://keycloak:4444/auth/realms/stargate/protocol/openid-connect/certs
        - CLUSTER_NAME=backend-cluster
        - CLUSTER_VERSION=3.11
        - SEED=cass-1
        - RACK_NAME=rack1
        - DATACENTER_NAME=dc1
        - ENABLE_AUTH=true
        - SIMPLE_SNITCH=true
    keycloak:
      image: quay.io/keycloak/keycloak:11.0.2 (7)
      container_name: keycloak
      networks:
        - backend
      ports: (8)
        - 4444:4444
        - 9990:9990
      environment: (9)
        - JAVA_OPTS=-Djboss.http.port=4444
        - KEYCLOAK_USER=admin
        - KEYCLOAK_PASSWORD=admin
        - KEYCLOAK_IMPORT=/tmp/stargate-realm.json
      volumes: (10)
        - ./stargate-realm.json:/tmp/stargate-realm.json
  networks:
    backend:
1 The name of the first Cassandra node
2 The docker image for Cassandra
3 The name of the backend used for all nodes
4 The environment variables identified using Cassandra docker image specifications
5 The docker image for the stargate node
6 The environment variables for Stargate to run with JWT-based authentication and authorization Note in particular that the URL to link Stargate and Keycloak uses keycloak:4444 which is different than the localhost:4444 that you can use to access the Keycloak server
7 A keycloak docker image
8 Mapped ports, so that server can be accessed locally
9 The environment variables for Keycloak
10 A local JSON file mounted on the keycloak docker service to supply specific realm settings

Copy the stargate-realm.json file to the same directory as the startup script, if you are not running the startup script from the git repository.

The CQL creates a role, two keyspaces and three tables, inserts some data, and grants the role permission to select or modify the data:

# create role for demos
CREATE ROLE IF NOT EXISTS 'web_user' WITH PASSWORD = 'web_user' AND LOGIN = TRUE;

# create REST API example
CREATE KEYSPACE IF NOT EXISTS store WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':'1'};

CREATE TABLE IF NOT EXISTS store.shopping_cart (userid text PRIMARY KEY, item_count int, last_update_timestamp timestamp);

INSERT INTO store.shopping_cart (userid, item_count, last_update_timestamp) VALUES ('9876', 2, toTimeStamp(toDate(now())));
INSERT INTO store.shopping_cart (userid, item_count, last_update_timestamp) VALUES ('1234', 5, toTimeStamp(toDate(now())));

GRANT MODIFY ON TABLE store.shopping_cart TO web_user;
GRANT SELECT ON TABLE store.shopping_cart TO web_user;

# create GraphQL example
CREATE KEYSPACE IF NOT EXISTS library WITH REPLICATION = {'class':'SimpleStrategy', 'replication_factor':'1'};

CREATE TABLE IF NOT EXISTS library.books (title text PRIMARY KEY, author text);
CREATE TABLE IF NOT EXISTS library.authors (name text PRIMARY KEY, title text);

GRANT MODIFY ON TABLE library.books TO web_user;
GRANT MODIFY ON TABLE library.authors TO web_user;
GRANT SELECT ON TABLE library.books TO web_user;
GRANT SELECT ON TABLE library.authors TO web_user;

Create a keycloak user

Create a keycloak user, so that the user’s JWT can be used to verify authentication and authorization later when a query is made. If you used the docker-compose startup script, this step has already completed.

TOKEN=$(curl -s --data "username=admin&password=admin&grant_type=password&client_id=admin-cli" https://localhost:4444/auth/realms/master/protocol/openid-connect/token | jq -r '.access_token') (1)

curl -L -X POST 'https://localhost:4444/auth/admin/realms/stargate/users' \
-H "Content-Type: application/json" \
-H "Authorization: bearer $TOKEN" \ (2)
--data-raw '{
    "username": "testuser1", (3)
    "enabled": true,
    "emailVerified": true,
    "attributes": {
        "userid": [
            "9876" (4)
        ],
        "role": [
            "web_user" (5)
        ]
    },
    "credentials": [
        {
            "type": "password",
            "value": "testuser1", (6)
            "temporary": "false"
        }
    ]
}'
1 Generate a token to access keycloak. The keycloak administrator username and password are used in this line.
2 The token generated in the first line is used to authorize the REST query access to keycloak to add a user.
3 The keycloak username is identified.
4 The userid attribute used in the Cassandra table column to verify accessibility is identified.
5 The Cassandra role is identified.
6 The keycloak password is identified.

This sequence of commands access Keycloak as an administrator, to create the information required to validate a user and grant access.

Test the authentication and authorization

Now let’s try some queries to see if the authentication and authorization is working.

This command should work, because the userid of the testuser1 user has the role web_user which is granted permission to select data from the table, based on the appropriate claim x-stargate-userid.

REST v1

  • Access is granted

  • Result

USER_TOKEN=$(curl -s --data "username=testuser1&password=testuser1&grant_type=password&client_id=user-service" https://localhost:4444/auth/realms/stargate/protocol/openid-connect/token | jq -r '.access_token') (1)

curl -sL 'localhost:8082/v1/keyspaces/store/tables/shopping_cart/rows/9876' \
-H "X-Cassandra-Token: $USER_TOKEN" | jq . (2)
1 Get a token that is passed to keycloak with the keycloak username and password.
2 Fetch the row that has userid=9876. jq makes the returned JSON pretty.
{
  "count": 1,
  "rows": [
    {
      "item_count": 2,
      "userid": "9876",
      "last_update_timestamp": "2020-11-06T00:00:00Z"
    }
  ]
}

Note that a USER_TOKEN is generated from the Keycloak service and used as the X-Cassandra-Token in the following REST command.

This next query should fail, because the userid=1234 that the user testuser1 should not have access to.

  • Access is not granted

  • Result

USER_TOKEN=$(curl -s --data "username=testuser1&password=testuser1&grant_type=password&client_id=user-service" https://localhost:4444/auth/realms/stargate/protocol/openid-connect/token | jq -r '.access_token')

curl -sL 'localhost:8082/v1/keyspaces/store/tables/shopping_cart/rows/1234' \
-H "X-Cassandra-Token: $USER_TOKEN" | jq .
{
  "description": "Role unauthorized for operation: Not allowed to access this resource",
  "code": 401
}

REST v2

Note that either REST v1 or REST v2 can be used with the JWT:

  • Access is granted

  • Result

export USER_TOKEN=$(curl -s --data "username=testuser1&password=testuser1&grant_type=password&client_id=user-service" https://localhost:4444/auth/realms/stargate/protocol/openid-connect/token | jq -r '.access_token')

curl -sL -X GET 'localhost:8082/v2/keyspaces/store/shopping_cart/9876' \
-H "Content-Type: application/json" \
-H "X-Cassandra-Token: $USER_TOKEN"
{"count":1,"data":[{"item_count":2,"userid":"9876","last_update_timestamp":{"nano":0,"epochSecond":1607299200}}]}

Document API

The Document API can also use JWTs to access data:

  • Access is granted

  • Result

export USER_TOKEN=$(curl -s --data "username=testuser1&password=testuser1&grant_type=password&client_id=user-service" https://localhost:4444/auth/realms/stargate/protocol/openid-connect/token | jq -r '.access_token')

curl --location --request GET 'localhost:8082/v2/schemas/namespaces' \
 --header "X-Cassandra-Token: $USER_TOKEN" \
 --header 'Content-Type: application/json'
 {
   {"data":[{"name":"system_distributed"},{"name":"system"},{"name":"system_schema"},{"name":"stargate_system"},{"name":"library"},{"name":"system_auth"},{"name":"system_traces"}]}
 }

GraphQL

GraphQL can also use the JWTs for authentication and authorization, if the USER_TOKEN is copied to the header of GraphQL queries.

export USER_TOKEN=$(curl -s --data "username=testuser1&password=testuser1&grant_type=password&client_id=user-service" https://localhost:4444/auth/realms/stargate/protocol/openid-connect/token | jq -r '.access_token')
printenv | grep USER_TOKEN

and copy the USER_TOKEN displayed to X-Cassandra-Token as shown in graphql-using.adoc[Using the GraphQL API].