Skip to content

Configuring JWT for Authentication

Pre Read

You should read how Privacera uses JWT for authentication before proceeding with this topic.

You need to enable and configure the JWT token User Identity feature in Privacera. This is a common configuration for

  • OLAC and FGAC connectors on Self Managed and Data Plane deployments
  • OLAC connectors on PrivaceraCloud

The configuration maps the token issuer value to a set of configurations that are used to validate the JWT token. You can configure multiple token issuers in Privacera.

Setup for JWT public key configuration

On the host where Privacera Manager is installed, do the following steps:

Bash
1
2
3
cd ~/privacera/privacera-manager
cp -n config/sample-vars/vars.jwt-auth.yaml config/custom-vars
vi config/custom-vars/vars.jwt-auth.yaml
Edit the file and modify the JWT_CONFIGURATION_LIST as given in next section.

Location of public key files for static public key configuration

For static public key configuration, the public key should be copied in a file in PEM format to the config/custom-properties directory.

After all the changes are done you need to update the helm chart, apply the changes and also run the post install steps

Bash
1
2
3
cd ~/privacera/privacera-manager
./privacera-manager.sh setup
./pm_with_helm.sh upgrade 

Run the following command to run the post install steps:

Bash
cd ~/privacera/privacera-manager
./privacera-manager.sh post-install
Fallback for Databricks OLAC and FGAC connectors

Copying the vars.jwt-auth.yaml to the config/custom-vars directory will enable JWT User Identity for all OLAC and FGAC connectors. If you want to continue using the logged-in user identity for Databricks OLAC and FGAC connectors, then you need to set this property by creating a new files in the config/custom-properties/privacera_spark_custom.properties directory.

Bash
vi ~/privacera/privacera-manager/config/custom-properties/privacera_spark_custom.properties
Add or edit this property in the file and set it to true.
Properties
privacera.jwt.dbx.login.user.fallback.enable=true

To enable JWT token for User Identity in PrivaceraCloud, you need to add below properties in s3 application.

Text Only
1
2
3
4
5
Navigate to `Goto Settings` >> `Applications` >> `s3` >> `Click on edit`

Now click on `Access Management` from pop-up and navigate to `Advanced properties` section

Add the properties given in the next section and click on save button.

Reference for JWT_CONFIGURATION_LIST

Here is the list of properties that you can configure in the JWT_CONFIGURATION_LIST property.

Description of properties in JWT_CONFIGURATION_LIST

These properties configure the payload of the JWT token:

  1. index

    • Description: Index of the JWT configuration. This is a unique identifier for the JWT configuration.
    • Required: Yes
    • Supported Values: 0, 1, 2, 3 etc.
  2. issuer

    • Description: Issuer of the JWT Payload. This is a string identifier. The JWT tokens that contain this value in the iss field will be validated using this configuration.
    • Required: Yes
  3. subject

    • Description: Subject of the JWT Payload. This is a string identifier. The JWT tokens that contain this value in the sub field will be used to enforce access control.
    • Required: Optional
    • Sample Value: infra_test_user
  4. secret

    • Description: Secret key to validate the JWT token. This is a string identifier. JWT tokens that include this value in their secret field will be validated using this configuration. This is specifically applicable when the JWT token is signed and encrypted using the HS256 algorithm.
    • Required: Optional
    • Sample Value: mysecret
  5. userKey

    • Description: JWT Payload key for the username.
    • Required: Optional
    • Default: client_id
  6. groupKey

    • Description: JWT Payload key for the group name.
    • Required: Optional
    • Default: scope
  7. parserType

    • Description: Specifies how the scope or group is formatted. Choose one of the following values:
      • PING_IDENTITY: Use when scope/group is an array. Example:
        JSON
        1
        2
        3
        {
            "scope": ["infra_test_group"]
        }
        
      • KEYCLOAK: Use when scope/group is space separated. Example:
        JSON
        1
        2
        3
        {
            "scope": "infra_test_group1 infra_test_group2 infra_test_group3"
        }
        
    • Required: Yes
  8. audience

    • Description: Audience for whom the JWT token has been issued. This is a string identifier. The JWT tokens that contain this value in the aud field will be validated using this configuration.
    • Required: Optional

For Static public key configuration

  1. publickey
    • Description: JWT file name that you copied in previous steps. (in this case use Algorithm RS256)
    • Required: Required only for Static public Key

For Dynamic Public Key, here are the additional properties. Note that we are providing both the Privacera Manager property | PrivaceraCloud property.

  1. pubKeyProviderEndpoint | privacera.jwt.0.token.publickey.provider.url

    • Description: API URL by which we will return public key.
      • Format: https://my-sat-server/<api-to-get-public-key-by-kid>/ or https://my-sat-server/<api-to-get-public-key-by-kid>/?kid=
      • Privacera code will add <kid> at the end above URL and it will become like this https://my-sat-server/<api-to-get-public-key-by-kid>/<kid> or https://my-sat-server/<api-to-get-public-key-by-kid>/?kid=<kid>and that API should return public key of specific key id (kid) mentioned in JWT.
    • Required: Yes
  2. pubKeyProviderAuthType | privacera.jwt.0.token.publickey.provider.auth.type

    • Description: Authorization type as per API URL (BASIC/NONE)
    • Required: Optional
    • Default: NONE
  3. pubKeyProviderAuthUserName | privacera.jwt.0.token.publickey.provider.auth.username

    • Description: Username for JWKS Provider
    • Required: Required When pubKeyProviderAuthType=BASIC
  4. pubKeyProviderAuthTypePassword | privacera.jwt.0.token.publickey.provider.auth.password

    • Description: Password for JWKS Provider
    • Required: Required When pubKeyProviderAuthType=BASIC
  5. pubKeyProviderJsonResponseKey | privacera.jwt.0.token.provider.response.key

    • Description: JWKS Response JSON Key to get Public Key
    • Required: Yes
    • Default: x5c
  6. jwtTokenProviderKeyId | privacera.jwt.0.token.provider.key.id

    • Description: JWT Headers Key to get public key id to retrieve from JWKS Provider
    • Required: Yes

Static public key configuration

For static public key configuration, here are some sample configurations that you can put in the vars.jwt-auth.yaml file. Typically you will have only one configuration, but in some cases you may have multiple configurations. The meaning of these properties is explained in the Reference section .

YAML
JWT_CONFIGURATION_LIST:

  - index: 0
    issuer: "https://your-idp-domain.com/websec1"
    userKey: "sub"
    groupKey: "scope"
    parserType: "PING_IDENTITY"
    publickey: "jwttoken1.pub"
    audience: "https://dataserver.example.com"

  - index: 1
    issuer: "https://your-idp-domain.com/websec2"
    userKey: "client_id"
    groupKey: "scope"
    parserType: "KEYCLOAK"
    publickey: "jwttoken2.pub"

  - index: 2
    issuer: "https://your-idp-domain.com/websec2"
    userKey: "client_id"
    parserType: "KEYCLOAK"
    publickey: "jwttoken3.pub
Properties
privacera.jwt.oauth.enable=true

privacera.jwt.0.token.issuer=https://your-idp-domain.com/websec1
privacera.jwt.0.token.publickey=<public_key_in_string_format>
privacera.jwt.0.token.userKey=sub
privacera.jwt.0.token.groupKey=scope
privacera.jwt.0.token.parserType=PING_IDENTITY

privacera.jwt.1.token.issuer=https://your-idp-domain.com/websec2
privacera.jwt.1.token.publickey=<public_key_in_string_format>
privacera.jwt.1.token.userKey=client_id
privacera.jwt.1.token.groupKey=scope
privacera.jwt.1.token.parserType=KEYCLOAK

Dynamic public key configuration

Privacera also supports validating JWT tokens using a dynamic public key provider. This is useful when the public key is not static and can change. Here is the configuration for dynamic public key provider which uses Basic Authentication.

YAML
JWT_CONFIGURATION_LIST:
    -   index: 0
        issuer: "https://example.com/issuer"
        userKey: "sub"
        groupKey: "scope"
        parserType: "PING_IDENTITY"

        pubKeyProviderEndpoint: "https://<JWKS-provider>/get_public_key?kid="
        pubKeyProviderAuthType: "BASIC"
        pubKeyProviderAuthUserName: "<username>"
        pubKeyProviderAuthTypePassword: "<password>"
        pubKeyProviderJsonResponseKey: "x5c"
        jwtTokenProviderKeyId: "kid"
Properties
privacera.jwt.oauth.enable=true

privacera.jwt.0.token.issuer=https://example.com/issuer
privacera.jwt.0.token.userKey=sub
privacera.jwt.0.token.groupKey=scope
privacera.jwt.0.token.parserType=PING_IDENTITY
privacera.jwt.0.token.publickey.provider.url=https://<JWKS-provider>/get_public_key?kid=
privacera.jwt.0.token.publickey.provider.auth.type=BASIC
privacera.jwt.0.token.publickey.provider.auth.username=<username>
privacera.jwt.0.token.publickey.provider.auth.password=<password>
privacera.jwt.0.token.provider.response.key=x5c
privacera.jwt.0.token.provider.key.id=kid

Dynamic public key configuration (Without Basic Authentication)

Here is the configuration for dynamic public key provider which does not use Basic Authentication.

YAML
1
2
3
4
5
6
7
8
9
JWT_CONFIGURATION_LIST:
    -   index: 0
        issuer: "https://example.com/issuer"
        userKey: "client_id"
        groupKey: "scope"
        parserType: "PING_IDENTITY"
        pubKeyProviderEndpoint: "https://my-sat-server/<api-to-get-public-key-by-kid>/"
        pubKeyProviderJsonResponseKey: "x5c"
        jwtTokenProviderKeyId: "kid
Properties
1
2
3
4
5
6
7
8
9
privacera.jwt.oauth.enable=true

privacera.jwt.0.token.issuer=https://example.com/issuer
privacera.jwt.0.token.userKey=client_id
privacera.jwt.0.token.groupKey=scope
privacera.jwt.0.token.parserType=PING_IDENTITY
privacera.jwt.0.token.publickey.provider.url=https://<JWKS-provider>/get_public_key?kid=
privacera.jwt.0.token.provider.response.key=x5c
privacera.jwt.0.token.provider.key.id=kid

Validating JWT token

You can validate the setup with your IDP. However, if you want to validate using your own temporary IDP, then follow the below steps:

Testing Static Key Validation

We will walk you through an end to end setup using Python script for generating JWT token and using a Python JWKS server. These utilities are for helping you do an end to end flow. These should not be used in a production environment.

1. Create Python virtual environment and install libraries
  1. Create a folder to store the script and Python virtual environment.

    Bash
    mkdir -p ~/privacera/privacera-jwt
    cd ~/privacera/privacera-jwt
    

  2. Create a requirements file to download libraries required by the script. These are open source libraries commonly used for signing and JWT creation

    Bash
    vi requirements.txt
    
    Add the following content to the file.
    Bash
    1
    2
    3
    4
    5
    6
    7
    cffi==1.15.1
    cryptography==40.0.2
    pycparser==2.21
    PyJWT==2.6.0
    Flask==2.1.0
    Flask-HTTPAuth==4.4.0
    Werkzeug==2.2.2
    

  3. Create a Python virtual environment. This is a one time step.

    Bash
    python3 -m venv venv
    

  4. Activate the virtual environment so that you can use the virtual environment. This step is required to be done everytime you start a new shell.

    Bash
    source venv/bin/activate
    

  5. Install the required libraries. This is a one time step.

    Bash
    pip3 install -r requirements.txt
    

  6. You can deactivate the Python virtual environment when are you done.

Bash
deactivate
And re-activate it when you resume working,
Bash
cd ~/privacera/privacera-jwt
source ./venv/bin/activate

2. Generate RSA 256 and EC 256 key-pairs for test purpose
  1. We are going to generate a RSA 256 key-pair and an EC 256 key-pair. These are for test purpose to show you how to use RSA and EC keys. Typically, you will be using only one type of signing algorithm in yours setup.

  2. Generate a RSA 256 key-pair. This will be used to sign the JWT token. The private key will be encrypted using a password. You need this if you want to sign the token using a RSA 256 key.

    Bash
    1
    2
    3
    4
    5
    6
    7
    8
    cd ~/privacera/privacera-jwt
    
    # encrypt the private key using known password
    openssl genrsa -des3 -out jwt-rs256-key-encrypted.pem -passout pass:welcome1 2048 
    
    # Extract the public key which will be configured into Privacera 
    openssl rsa -in jwt-rs256-key-encrypted.pem -outform PEM -pubout \
        -out jwt-rs256-public.pem -passin pass:welcome1
    

  3. Generate an EC 256 key pair. This will be used to sign the JWT token. The private key will be encrypted using a password. You need this if you want to sign the token using an EC 256 key.

    Bash
    cd ~/privacera/privacera-jwt
    
    # Generate ECDSA key pair
    openssl ecparam -name prime256v1 -genkey -noout -out private.ec.key
    
    # Convert the private key to PKCS8 encrypted format using password
    openssl pkcs8 -topk8 -in private.ec.key -out jwt-ec256-key-encrypted.pem \
        -passout pass:welcome1
    
    # Extract the public key
    openssl ec -in jwt-ec256-key-encrypted.pem -pubout -out jwt-ec256-public.pem \
        -passin pass:welcome1
    rm private.ec.key
    

3. Create the Python script for generating JWT token
  1. Create a Python script to generate the JWT token. This script will generate a JWT token and sign it using the private key from the keypairs that we have generated. It will take a command line argument to choose the keypair to use.

    Bash
    1
    2
    3
    4
    5
    6
    cd ~/privacera/privacera-jwt
    
    # If this is a new shell window, then activate the virtual environment
    source venv/bin/activate
    
    vi privacera_jwt.py
    
    Python
    from sys import argv
    
    import jwt
    import time
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.backends import default_backend
    
    if len(argv) > 1 and argv[1] == "rsa":
        key_pem_file = "./jwt-rs256-key-encrypted.pem"
        issuer = "https://idp.example.com/issuer1"
        kid = "kid_1"
    elif len(argv) > 1 and argv[1] == "ec":
        key_pem_file = "./jwt-ec256-key-encrypted.pem"
        issuer = "https://idp.example.com/issuer2"
        kid = "kid_2"
    else:
        print("Usage: python privacera_jwt.py rsa|ec")
        exit(1)
    
    # duration is 10 days
    duration_sec = 10 * 24 * 60 * 60
    expiry_epoch_sec = int(time.time()) + duration_sec
    
    # username in the token
    user_name = "infra_test_user"
    
    token = {
        "scope": [
            "infra_test_group_1",
            "infra_test_group_2",
        ],
        "iss": issuer,
        "aud": "privacera.dataserver",
        "sub": user_name,
        "iat": int(time.time()),
        "exp": expiry_epoch_sec
    }
    
    print(f"token={token}")
    
    # read the private key
    with open(key_pem_file, mode="rb") as private_file:
        pem_bytes = private_file.read()
    
    # passphrase for the private key
    passphrase = b"welcome1"
    
    private_key = serialization.load_pem_private_key(
        pem_bytes, password=passphrase, backend=default_backend()
    )
    if argv[1] == "rsa":
        encoded = jwt.encode(token, private_key, algorithm="RS256", headers={"kid": kid})
        print(f"encoded value using RSA 256 key:\n{encoded}")
    elif argv[1] == "ec":
        encoded = jwt.encode(token, private_key, algorithm="ES256", headers={"kid": kid})
        print(f"encoded value using EC 256 key:\n{encoded}")
    

  2. Before running the script, you can change the user_name variable to the user that you want to use. This user should be present in Privacera. You can also change the groups in the token variable. Run the script and copy the JWT token that is printed. You can generate the token using RSA or EC key by passing the argument to the script.

    Bash
    python3 privacera_jwt.py rsa
    
    Bash
    python3 privacera_jwt.py ec
    
    Use the encoded value from the output of the script as the JWT_TOKEN in the EMR, Databricks or Apache Spark cluster

  3. Static key configuration Copy the public key files to the Privacera Manager configuration directory.

    Bash
    1
    2
    3
    4
    5
    cp ~/privacera/privacera-jwt/jwt-rs256-public.pem \
        ~/privacera/privacera-manager/config/custom-properties
    
    cp ~/privacera/privacera-jwt/jwt-ec256-public.pem \
        ~/privacera/privacera-manager/config/custom-properties
    

4. Configure static key JWT configuration in Privacera Manager or PrivaceraCloud

Use the generated public keys to configure the static JWT configuration in Privacera Manager or PrivaceraCloud. Follow the steps from top of this document

5. Configure and test OLAC or FGAC plugin using the generated JWT token

The JWT token with the username infra_test_user. You can create a temporary user called infra_test_user in Privacera and create Access policies for that user in privacera_s3 service repo. You can use this user to test the OLAC or FGAC plugin.

6. Test JWT configuration in Privacera Dataserver

Configure Privacera Dataserver to use the JWT configuration. [TOOD: Give Link here]

Follow this step to test the JWT configuration in Privacera Dataserver.

For now this will only work in Self Managed from Privacera release 9.3.0.1 onwards

Bash
1
2
3
4
curl -v -X POST \
-H 'Content-Type: application/json' \
-d '{"tokenStr": "<your-jwt>"}' \
https://DATASERVER_URL_IN_YOUR_ENV/services/jwt/validate
The response will be a JSON object with the status of the token validation.

Http Status code

Text Only
1
2
200 - if able to process request successfully
400 - if the payload is empty in request OR the token value is empty in the request

Content type: application/json

Response format for Valid Token

JSON
1
2
3
4
5
6
7
{
  "statusCode": 0,
  "isValid": true,
  "expiresOn": "<Date>",
  "algorithm": "<algorithm-type>",
  "message": "<message>"
}

Response format for Invalid Token

JSON
1
2
3
4
5
{
  "statusCode": 1,
  "isValid": false,
  "message": "<message>"
}

Response format if empty payload OR empty jwt token

JSON
1
2
3
4
5
{
  "statusCode": 2,
  "isValid": false,
  "message": "<message>"
}

Testing Dynamic Key Validation

For dynamic key validation, you can create a IDP server using Python and test your configuration.

1. Create Python script to run as JWKS server
  1. For testing the dynamic public key configuration, you can use the following script to serve the public key using JWKS endpoint.

    Bash
    1
    2
    3
    4
    5
    6
    cd ~/privacera/privacera-jwt
    
    # If this is a new shell window, then activate the virtual environment
    source venv/bin/activate
    
    vi jwks_server.py
    
    Paste the following code in the file.
    Python
    import base64
    import json
    
    from datetime import datetime, timedelta
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives import serialization, hashes
    import cryptography.hazmat.primitives.asymmetric.rsa as rsa
    import cryptography.hazmat.primitives.asymmetric.ec as ec
    
    from flask import Flask, request, jsonify
    from flask_httpauth import HTTPBasicAuth
    
    
    def compute_sha256_thumbprint(public_key_pem):
        # Load the PEM encoded public key
        public_key = serialization.load_pem_public_key(
            public_key_pem,
            backend=default_backend()
        )
    
        # Compute SHA-256 hash of the DER encoding of the public key
        der_encoding = public_key.public_bytes(
            encoding=serialization.Encoding.DER,
            format=serialization.PublicFormat.SubjectPublicKeyInfo
        )
        sha256_hash = hashes.Hash(hashes.SHA256(), backend=default_backend())
        sha256_hash.update(der_encoding)
        thumbprint = sha256_hash.finalize()
    
        return thumbprint
    
    
    def int_to_base64(n):
        # Determine the number of bytes required to represent the integer
        num_bytes = (n.bit_length() + 7) // 8
    
        # Convert the integer to bytes
        int_bytes = n.to_bytes(num_bytes, byteorder='big')
    
        # Encode the bytes using base64
        base64_bytes = base64.b64encode(int_bytes)
    
        # Convert the base64 bytes to a string
        base64_string = base64_bytes.decode('utf-8')
    
        return base64_string
    
    
    def get_response(kid, pem_file_path, expiry_time_delta):
        # Read the public key from a PEM file
        with open(pem_file_path, "rb") as pem_file:
            public_key_pem = pem_file.read()
    
        # Remove BEGIN and END headers and footers
        pem_lines = public_key_pem.decode('utf-8').split('\n')
        pem_contents = (''.join(pem_lines)
                        .replace('-----BEGIN PUBLIC KEY-----', '')
                        .replace('-----END PUBLIC KEY-----', ''))
    
        # Load the PEM encoded public key
        public_key = serialization.load_pem_public_key(
            public_key_pem,
            backend=default_backend()
        )
    
        print(f"type={type(public_key)}")
    
        # Extract SHA-256 thumbprint
        thumbprint = compute_sha256_thumbprint(public_key_pem)
        thumbprint_hex = thumbprint.hex()
    
        print("PEM File (Single String without Headers and Footers):")
        print(pem_contents)
    
        # Check the type of the public key
        if isinstance(public_key, rsa.RSAPublicKey):
    
            # Extract modulus and exponent from the public key
            modulus = public_key.public_numbers().n
            exponent = public_key.public_numbers().e
    
            response = {
                "kty": "RSA",
                "use": "sig",
                "e": int_to_base64(modulus),
                "n": int_to_base64(exponent),
                "x5t#S256": thumbprint_hex,
                "x5c": [pem_contents],
                "kid": kid,
                "exp": (datetime.utcnow() + expiry_time_delta).timestamp(),
            }
    
            print(json.dumps(response, indent=4))
            return response
    
        elif isinstance(public_key, ec.EllipticCurvePublicKey):
            ec_numbers = public_key.public_numbers()
    
            # Extract x and y coordinates
            x = ec_numbers.x
            y = ec_numbers.y
    
            response = {
                "kty": "EC",
                "use": "sig",
                "x": int_to_base64(x),
                "y": int_to_base64(y),
                "x5t#S256": thumbprint_hex,
                "x5c": [pem_contents],
                "kid": kid,
                "exp": (datetime.utcnow() + expiry_time_delta).timestamp(),
            }
            return response
        else:
            return "Unknown"
    
    
    def main():
        USERNAME = 'admin'
        PASSWORD = 'Welcome@123'
    
        kid_dict = {
            'kid_1': 'jwt-rs256-public.pem',
            'kid_2': 'jwt-ec256-public.pem'
        }
    
        app = Flask(__name__)
        auth = HTTPBasicAuth()
    
        # Verify username and password for basic authentication
        @auth.verify_password
        def verify_password(username, password):
            return username == USERNAME and password == PASSWORD
    
        # GET API to retrieve public key by kid
        @app.route('/get_public_key', methods=['GET'])
        @auth.login_required
        def get_public_key():
            kid = request.args.get('kid')
            public_key = kid_dict.get(kid)
    
            if public_key is None:
                return jsonify({'error': 'Public key not found'}), 404
            else:
                return get_response(kid, public_key, timedelta(days=30))
    
        app.run(host="0.0.0.0", port=9090, debug=True)
    
    
    if __name__ == "__main__":
        print("starting main")
        main()
        print("ending main")
    
    Start the server and keep it running.
    Bash
    python3 jwks_server.py
    

  2. You can test this endpoint using a curl command as follows, from another shell,

    Bash
    curl -u 'admin:Welcome@123' 'http://localhost:9090/get_public_key?kid=kid_1'
    
    JSON
    {
      "keys": [
        {
          "e": "rTfaKZ...AgkWaJ0lbKSGfQ==",
          "exp": 1724306625,
          "kid": "kid_1",
          "kty": "RSA",
          "n": "AQAB",
          "use": "sig",
          "x5c": [
            "MIIBIjANBgkq...AQAB"
          ],
          "x5t#S256": "ebdef...21d279f863"
        }
      ]
    }   
    
    Bash
    curl -u 'admin:Welcome@123' 'http://localhost:9090/get_public_key?kid=kid_2'
    
    JSON
    {
      "keys": [
        {
          "exp": 1724306623,
          "kid": "kid_2",
          "kty": "EC",
          "use": "sig",
          "x": "QQWKgcN...CEahadqHgsc=",
          "x5c": [
            "MFkwEwYHKo...uknLaYRtXQw=="
          ],
          "x5t#S256": "b7d582f...91d88a07e0",
          "y": "mQL/B2CBV...iLpJy2mEbV0M="
        }
      ]
    }
    

2. Configure Dynamic public key using the Python JWKS server endpoint

Follow the instruction from the top of this document to configure the dynamic public key configuration in Privacera Manager or PrivaceraCloud.

After Privacera Manager has been run so that it restarts the Privacera Dataserver, you can use the dataserver test endpoint as given above. You can then test the OLAC plugin. Similarly, you will have to upload the newly generated FGAC plugin configuration and us it to test the FGAC plugin.

Comments