Set Up Authentication Using JSON Web Tokens

You can use a JSON Web Token (JWT) to authenticate with GoodData as an alternative to OpenID Connect. JWTs provide a more modern alternative to simple API access tokens by enabling security features such as expiration time and tamper-proofing.

This article describes how to set up JWT authentication for GoodData and how to use it to secure your API calls:

  1. Create JWK
  2. Set New JWK
  3. Create JWT
  4. Make API Calls Using JWT

For an example on how to create JWK and JWT in JavaScript see the Create JWK and JWT in JavaScript article.

Create JWK

A JSON Web Key (JWK) is a key that is needed to verify JSON Web Tokens (JWT). To be able to create and use JWT for authentication you first need to set up a corresponding JWK.

You can store multiple JWKs within a GoodData organization.

Follow these steps:

  1. Generate a Key Pair
  2. Convert PEM to JWK

Generate a Key Pair

We need a key pair (private & public key) to be able to sign JWT and to verify it on the server side. Feel free to follow the instructions below or use your own prefered method.

Steps:

  1. Generate RSA private key:

    openssl genrsa -des3 -out private_key.pem 2048
    
  2. Generate public key:

    openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem
    
  3. Generate self-signed certificate:

    openssl req -new -x509 -key private_key.pem -subj /CN=client.example.com -days 1000 > certificate.crt
    

You should now have the following files:

  • private_key.pem
  • public_key.pem
  • self-signed certificate certificate.pem

Convert PEM to JWK

Public keys have to be uploaded to GoodData as JWKs, so you have to convert the PEM format to JWK. There are numerous libraries and tools that can be used to help you accomplish this. We will show you how to convert from PEM to JWK using the pem-jwk tool.

Steps:

  1. Install the pem-jwk tool:

    npm install -g pem-jwk
    
  2. Convert PEM to JWK using the following series of commands:

    kid=$(uuidgen)
    
    x5t=$(openssl x509 -fingerprint -noout -in certificate.crt -sha1 | cut -f2 -d'=' | sed 's/://g' | xxd -r -p | base64 | sed 's/+/-/g; s/\//_/g' | sed 's/=//g')
    
    x5c=$(sed /-/d certificate.crt | tr -d \\n)
    
    pem-jwk public_key.pem | jq --arg kid $kid --arg x5t $x5t --arg x5c $x5c -r '.+{kid: $kid, alg: "RS256", use: "sig",  x5t: $x5t, x5c: [$x5c]}'
    

    You should end up with a JWK, similar to this example:

    {
        "kty": "RSA",
        "n": "wAwTHQIRVkX4m6lI0ayO1b7FnR4hgH9KFQJPHO7i11zJ6exhs7nzS4WGTlOMzM_j17O3zcBEYfe1P65rhikRhRuYU3cBmqQGxTQEZcTqmOSZxjB7TPukp7R57IvbmYuHFZjxqSQQpazopvCCMHO5OECilT_Md_xuZtdZDehOYNwZM880kN0KKtGFDXDQzC110uk0R_mVatuPY1ZIe0lYnfkokKqfWma849zpcpJE5MiIIxTFsFANsRW3he72EodoDMEhYZnUOQ4dGk_t3OiY-NgtRKtI1vW5T-rsZ0Tl3oRqJmXPeE5TP8bC3n-nm_SJPtDyc2Q-8CO1EITIZR8Ikw",
        "e": "AQAB",
        "kid": "67C2BC3D-32E4-4C8C-93EF-9B03F0E65A3F",
        "alg": "RS256",
        "use": "sig",
        "x5t": "oLe3EKODu72OtVftIu8_WGaPWk8",
        "x5c": [
            "MIICtjCCAZ4CCQDH7kDyw9N1hDANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJjbGllbnQuZXhhbXBsZS5jb20wHhcNMjMwODAyMDY1NTMxWhcNMjYwNDI4MDY1NTMxWjAdMRswGQYDVQQDDBJjbGllbnQuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDADBMdAhFWRfibqUjRrI7VvsWdHiGAf0oVAk8c7uLXXMnp7GGzufNLhYZOU4zMz+PXs7fNwERh97U/rmuGKRGFG5hTdwGapAbFNARlxOqY5JnGMHtM+6SntHnsi9uZi4cVmPGpJBClrOim8IIwc7k4QKKVP8x3/G5m11kN6E5g3BkzzzSQ3Qoq0YUNcNDMLXXS6TRH+ZVq249jVkh7SVid+SiQqp9aZrzj3OlykkTkyIgjFMWwUA2xFbeF7vYSh2gMwSFhmdQ5Dh0aT+3c6Jj42C1Eq0jW9blP6uxnROXehGomZc94TlM/xsLef6eb9Ik+0PJzZD7wI7UQhMhlHwiTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACEdhG0PEWR8+cGfs1THIhYdrTyva9Zkb1+T/dqiEpRD3mE3+S0/5pEFcSdgqSwsyZ4CclDlTpjHtMC4ADpqXypB5LAxdyos6IQwDhczB0iDNcgB9FK+xlZjKuHo5Wscmy+B4Mi+7zzZw4t87V8D59ySlXQBbcoi6KSrM9+aY36op2VnA98bSkB4RSi4D7km8PKG1yBiRSGtbL1ihvRu8ZOSftdc3VKqYqgAQVfigKJtIeXgHF0ksN2Hfkwyoskq4nBUUukOKr6pC/OBBA3z74wUQeRUvggyZr0h6OnRHlcYKlt5zdo/JsAk9h23oJvRQ1yRWp7k0XOzF8/kLlwu6Jc="
        ]
    }
    

The JWK attributes are as follows:

AttributesDescriptionPossible Values
ktyThe family of cryptographic algorithms used with the key.RSA
algThe specific cryptographic algorithm used with the key.RS256, RS384, RS512
useHow the key was meant to be used.sig - signature
x5c(Optional attribute) The x.509 certification chain. The first entry in the array is the certificate to use for token verification; the other certificates can be used to verify this first certificate.PKIX certificates
nThe modulus for the RSA public key.-
eThe exponent for the RSA public key.-
kidThe unique identifier for the key.string^(?!.)[.A-Za-z0-9_-]{1,255}$
x5t(Optional attribute) The thumbprint of the x.509 cert (SHA-1 thumbprint).base64url-encoded

Set New JWK

Once you have a JWK you must upload it to your GoodData server.

In GoodData you manage JWKs using the following API endpoints:

EndpointNotes
POST /api/v1/entities/jwksSet a new JWK
PUT /api/v1/entities/jwks/<JWK_ID>Update a JWK with ID
GET /api/v1/entities/jwksGet all JWKs
GET /api/v1/entities/jwks/<JWK_ID>Get a JWK for given ID
DELETE /api/v1/entities/jwks/<JWK_ID>Delete a JWK with ID

Note that to manage JWKs you need to have the Organization.MANAGE permission.

Set New JWK for a GoodData Organization

Make the following API call:

curl --request POST \
  --header "Authorization: Bearer $API_TOKEN" \
  --header 'Content-Type: application/vnd.gooddata.api+json' \
  --data '{
  "data": {
    "id": "jwk-1",
    "type": "jwk",
    "attributes": {
      "content": {
  "kty": "RSA",
  "n": "wAwTHQIRVkX4m6lI0ayO1b7FnR4hgH9KFQJPHO7i11zJ6exhs7nzS4WGTlOMzM_j17O3zcBEYfe1P65rhikRhRuYU3cBmqQGxTQEZcTqmOSZxjB7TPukp7R57IvbmYuHFZjxqSQQpazopvCCMHO5OECilT_Md_xuZtdZDehOYNwZM880kN0KKtGFDXDQzC110uk0R_mVatuPY1ZIe0lYnfkokKqfWma849zpcpJE5MiIIxTFsFANsRW3he72EodoDMEhYZnUOQ4dGk_t3OiY-NgtRKtI1vW5T-rsZ0Tl3oRqJmXPeE5TP8bC3n-nm_SJPtDyc2Q-8CO1EITIZR8Ikw",
  "e": "AQAB",
  "kid": "67C2BC3D-32E4-4C8C-93EF-9B03F0E65A3A",
  "alg": "RS256",
  "use": "sig",
  "x5t": "oLe3EKODu72OtVftIu8_WGaPWk8",
  "x5c": [
    "MIICtjCCAZ4CCQDH7kDyw9N1hDANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJjbGllbnQuZXhhbXBsZS5jb20wHhcNMjMwODAyMDY1NTMxWhcNMjYwNDI4MDY1NTMxWjAdMRswGQYDVQQDDBJjbGllbnQuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDADBMdAhFWRfibqUjRrI7VvsWdHiGAf0oVAk8c7uLXXMnp7GGzufNLhYZOU4zMz+PXs7fNwERh97U/rmuGKRGFG5hTdwGapAbFNARlxOqY5JnGMHtM+6SntHnsi9uZi4cVmPGpJBClrOim8IIwc7k4QKKVP8x3/G5m11kN6E5g3BkzzzSQ3Qoq0YUNcNDMLXXS6TRH+ZVq249jVkh7SVid+SiQqp9aZrzj3OlykkTkyIgjFMWwUA2xFbeF7vYSh2gMwSFhmdQ5Dh0aT+3c6Jj42C1Eq0jW9blP6uxnROXehGomZc94TlM/xsLef6eb9Ik+0PJzZD7wI7UQhMhlHwiTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACEdhG0PEWR8+cGfs1THIhYdrTyva9Zkb1+T/dqiEpRD3mE3+S0/5pEFcSdgqSwsyZ4CclDlTpjHtMC4ADpqXypB5LAxdyos6IQwDhczB0iDNcgB9FK+xlZjKuHo5Wscmy+B4Mi+7zzZw4t87V8D59ySlXQBbcoi6KSrM9+aY36op2VnA98bSkB4RSi4D7km8PKG1yBiRSGtbL1ihvRu8ZOSftdc3VKqYqgAQVfigKJtIeXgHF0ksN2Hfkwyoskq4nBUUukOKr6pC/OBBA3z74wUQeRUvggyZr0h6OnRHlcYKlt5zdo/JsAk9h23oJvRQ1yRWp7k0XOzF8/kLlwu6Jc="
  ]
      }
    }
  }
}'  $HOST_URL/api/v1/entities/jwks

You can confirm the JWK was uploaded correctly by making a GET call to the same API endpoint. Your JWK should be saved as:

{
    "data": {
        "attributes": { 
            "content": {
                "kty": "RSA",
                        "alg": "RS256",
                "use": "sig",
                "kid": "67C2BC3D-32E4-4C8C-93EF-9B03F0E65A3A",
                "x5t": "oLe3EKODu72OtVftIu8_WGaPWk8",        
                "x5c": [         "MIICujCCAaKgAwIBAgIECI8fsTANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDExR0ZXN0LWFsZXgucmVhY2g1Lm5ldDAeFw0yMDA3MjkwOTM0MjlaFw0yMjAyMTcxNDIwMzNaMB8xHTAbBgNVBAMTFHRlc3QtYWxleC5yZWFjaDUubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlzRszUeQ4WiSqvmYxMP10ngm8ALIoUwMH7Oa8vrZgD5pqalPjetPAxeVcAv2gTyDlOwtB0fGvlQo6n78pd9pTbgrzUjhmFuYN6OCfT6eN/2wu0LmwryFS2mbh7/1DTiKd2tZaRalskPECXTKkeks85HVqanB0860BYlGvQvfgrvhCWXXFJJeXvNwYNFYdDdrFQhoeOAEvRDKg9DdHZf6XzSR6Qk3w51FKn2b7imen/G52itD/kIen1hqqB2Jwt9SWyX5MSGySY2QwC18F6Dfs8L+t0mwCo6grGW9264Z5vlO0PWssEqGIX/ez6nk1ZdHXhoXwJ0W+6QzeQlUN8jNoQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAETbMWro4HI4ZuqtnjMZrgEOpx6WhAtxpMx5XFPVWbdp/DpPySotoWbbD6qCtYc34E+ec7mH7aHVap+Gl2IyeSHTht4FXfF9q/1Oj/fis/4DDi1iq00rJsU18D71mZ9FGWCWlO1nhW1KSTGbRJ3E0wSrNabcvaXcwEHokR3zm+xfRWjtbrq2hQ19R16xyOLVy4zrF95QxP4UN+Cvm8nmYur6bSqv+gCMvDsl+O/gtRHGgpUukHEJwnee1R3+1aIv+9zOF3HaaUC5neOLBFITGmeXgi8G2IhbG+JoXh/GUkb66TZUlUAM3qXYNL9Nf+2MQ7nAPTXcxlmImFUUrnv0c3"
                    ],
                "e": "AQAB",        
                "n": "wAwTHQIRVkX4m6lI0ayO1b7FnR4hgH9KFQJPHO7i11zJ6exhs7nzS4WGTlOMzM_j17O3zcBEYfe1P65rhikRhRuYU3cBmqQGxTQEZcTqmOSZxjB7TPukp7R57IvbmYuHFZjxqSQQpazopvCCMHO5OECilT_Md_xuZtdZDehOYNwZM880kN0KKtGFDXDQzC110uk0R_mVatuPY1ZIe0lYnfkokKqfWma849zpcpJE5MiIIxTFsFANsRW3he72EodoDMEhYZnUOQ4dGk_t3OiY-NgtRKtI1vW5T-rsZ0Tl3oRqJmXPeE5TP8bC3n-nm_SJPtDyc2Q-8CO1EITIZR8Ikw"
        }     
        },
        "id": "jwk-1",
        "type": "jwk"
    }
}

Create JWT

Once your JWK is created and stored in the GoodData JWKs store, you can generate a JWT for authentication by passing it in the Authorization Bearer header of your API calls.

Steps:

  1. Prepare a base64 URL encoded header:

    $ kid=<jwk_kid>
    $ jwt_header=$(echo -n "{\"alg\":\"RS256\",\"kid\":\"$kid\"}" | base64 | sed s/\+/-/ | sed -E s/=+$//)
    
    • kid is the JWK unique identifier.
  2. Prepare a base64 URL encoded payload:

    $ sub=<userId>
    $ iat=$(date +%s)
    $ exp=$(date -v+"1H" +%s)
    $ jti=$(uuidgen)
    $ payload=$(echo -n "{\"sub\":\"$sub\",\"name\":\"John Doe\",\"iat\":$iat,\"exp\":$exp,\"jti\":\"$jti\"}" | base64 | sed s/\+/-/ | sed -E s/=+$//)
    
    • sub is the userId of the user in the organization.
    • exp is the token expiration time.
  3. Create a signature:

    $ signature=$(echo -n "$jwt_header.$payload" | openssl dgst -sha256 -binary -sign private_key.pem | openssl enc -base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n')
    
  4. Create a signed JWT:

    $ signed_jwt="$jwt_header.$payload.$signature"
    

    You can use this signed JWT for authentication.

Make API Calls Using JWT

After uploading your JWK to the GoodData JWKs store, you can make API calls by replacing your API access token with the JWT token. For example:

curl --request GET \
  --header "Authorization: Bearer $signed_jwt" \
  --header 'Content-Type: application/vnd.gooddata.api+json' \
  $HOST_URL/api/v1/profile