GoodData.UI: PGP SSO Example

  • 31 August 2021
  • 1 reply
  • 489 views

This article will guide you through a PGP SSO authentication example. We will use GoodData’s Accelerator Toolkit along with the OpenPGP library. PGP SSO is GoodData’s custom SSO implementation that is based on PGP key pair exchange.

 

This is a simple example to get you started. Much care, on the security side, needs to be taken when dealing with PGP keys. In this example we expose our private PGP key and passphrase in the Javascript code. This is done for the purpose of testing and should not be done in production.

 

A prerequisite that cannot be ignored is to configure an SSO provider. This requires working with GoodData support. The steps are explained here.

Once an SSO provider is set up, make sure that the user you will be using for testing has been provisioned with the SSO provider. This can be done by the domain administrator using this API.

 

Make sure ssoProvider is set to the SSO provider configured by GoodData support and make sure authenticationModes is set to an array with SSO like the image above. If authenticationModes is set to an empty array, the user will inherit the setting from the domain configuration.

A workspace with any Analytical Designer insight created will also be needed.

 


 

Once the SSO provider and the test user have been configured, we can spin up a React application using the Accelerator Toolkit. Run the following command in terminal:

 

npx @gooddata/create-gooddata-react-app

 

You will be prompted with some questions, be sure to set up your hostname correctly. My example here is using https://ramirez.internal.gooddata.com as the hostname:

? What is your application name? my-app

? What is your hostname? I have a custom hostname

? Insert your hostname: https://ramirez.internal.gooddata.com

? What is your application desired flavor? JavaScript

 

Next we will cd into the application directory and add the OpenPGP library:

cd my-app

yarn add openpgp

 

After adding the necessary libraries, we can start the application:

yarn start

 

You will be met by a Welcome page from the Accelerator Toolkit:

7vLvuQirafH1C4mxXcADlgGLpFW7OD7Ny3HZrWd-2hMKAk9GdSNaLq8_wEMNHU3l5-Y2iKng_QXkBSPu_Sp-uktLwBmKz0_ru5ljWsBpga57m9vQIRyo3qJB3J4uuk6RrKsgQD62

 

If a browser does not automatically pop up after starting the application, you can go to the application URL in the browser manually

https://localhost:3000/

 

This example will use PGP SSO to authenticate against a GoodData backend. In order to test the example, you will need a workspace along with any visualization built in Analytical Designer. We will use the InsightView component to render this visualization upon successful authentication.

 

Let’s make a few changes to the application before we get to the SSO code:

In /src/constants.js

Double check that the backend is set to the domain that you will be working with and set the workspace variable to the workspace identifier you will be working with.

 

In /src/routes/AppRouter.js

Find the line that says DELETE THIS LINE, and delete it.

This removes the redirect to the welcome page and sets up Home as the default landing page. We will be doing our work on the Home page.

 

Copy and paste the code below to /src/routes/Home.js

import React, { useState, useEffect } from "react";
import { useBackend } from "../contexts/Auth";

import Page from "../components/Page";
import { InsightView } from "@gooddata/sdk-ui-ext";
import * as openpgp from 'openpgp';

// GoodData pubic key used to encrypt request
const publicKeyArmored = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBExqdtABCAD4YZcUozx4vLtPWFbcpQt6/iBZQAs98d0JUGNy0szVQ2Ydm3zV\nvDqdxUxNkTK8/BRxTZ3i4C+D7nmQm2Zn3eByUNszxLLLKgpxtGQ2ntWNfKwpPyuk\nJkqjVrPvH3Naxn/jrm/hNZTq2DRWwqc33+XJbVGX7Br9ZeTTI3logp8PDN9jJrvE\nDKGO+hwfI+6tsu1O5/zzAUfMRuqOK+N2Dj+CBE6HTqhRLPkU0UMVcuBXDUBWvjFV\nLc8GW5kM0RzPTEx/iFk/o0mEM7Gzv6SxUDCx+kpAbOLuKl8Ke1ThFlm48mIPyXan\nhtVjkyjEi/TxdAR1Swj+c7MoSqmQW52CqMi1ABEBAAG0JEdvb2REYXRhIFNTTyA8\nc2VjdXJpdHlAZ29vZGRhdGEuY29tPokBVQQTAQgAPwIbAwYLCQgHAwIGFQgCCQoL\nBBYCAwECHgECF4AWIQTcrzX+SYX1KdvbqZ6HeyxHIENB9QUCXZWz3QUJGpE+jQAK\nCRCHeyxHIENB9bGRCADdRFspn40QebppVvH0AWaStkhHif3/F0MlHjRV/6sOW/5l\nGGFupn+kpTxQPRnMFf+Bd0zPTg0/ZbzwVatWItJA8mDkSgCGMaOqALgMNPJBfySG\nLvIlajdB6ThSqKUZVJ7UKWInYEILZj1j/WeD1IWfSWd/yLLpVAvRie2bcJMcZCJc\nD9rJAviFwOGElWflqi1uuro+Z+Iok0IoKNd7DP0TXXu0kFEuF/PkEr8EwJFqmFIh\n5UNLiTurHWkynWRs1jBamIhr5nzunMxZXGb77q31+kUd0TUd5oXzqVGYmn4+fYYW\n1T1QPFNwsUhjlRa+O3PNmfpBSmc9Cy0kYHfPTzxXuQENBExqdtABCADFPsHkjTFd\ndpJ7BQwKdwTWUOdPEO/+Qy/aiqB/jJxYqnZdGYlNLaU6FkAvn/+fgUnGbS98T74D\nWku0AnjWdN97oA59iZe46sq6JuMkP3dobeunZrzBBExNVco7/lAtlhLKVOUZMi5Q\nfHYwTZUtqrjwGrUGwBrxnxZQjkaFyzETlcN51hmSdRvU5GIKZbTsyzy1XwPPuyUO\nRWf6V+IvjAdMp26j/AufMNfxvcARiqgs6GDZZtP9ZoHhFlLLFde+h29rGCBiZZXZ\nMKT7sRnLe5m4+70fkdmUURPwcHfi1kE4W5gTHuvIq2nxdg94yXjQF+XLUQxtaWSS\nLFU4MypNIRDPABEBAAGJATwEGAEIACYCGwwWIQTcrzX+SYX1KdvbqZ6HeyxHIENB\n9QUCXZW0QQUJGpE+8QAKCRCHeyxHIENB9ailB/9FzGRyPI1Y1CWvn2AqfqAmAIFS\nhYtfv3j47k87QpI7qItyFJTyq5jAhE6kFFXv2SU7Di8qqFrs5t3Iq2DtLn9y1wrh\nnilJKldUDnz5Zl/9urXtQ7GX1oPKWFb0SucSuzTDIal/ZNvAzCmMicoy7sM6Ly1V\n1nCZqX/U+qkJ4hDjf9+lWzsBYHQNc+xuTW77z5fSD6S2HBASx3fPdDt8LMXT2aq/\nnN9zwEyvOF7rsz0b39wWKXlbF6YJXaMIEDrzMarHqLY1rgMEJHrFhdNWQ7MQSjj7\nkawghOlSp6ySbnu6p6kRH+nGj7Jnk3R1ZmR4kIDYnB1/oA4+xp2+Vw9vI2/2\n=yIXV\n-----END PGP PUBLIC KEY BLOCK-----\n";

// Private key connected to your GoodData SSO provider used to sign request
// Should not be exposed in production codebase, only here as an example
const privateKeyArmored = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlQWGBGDt ... \n-----END PGP PRIVATE KEY BLOCK-----\n";

// Passphrase for the private key
// Should not be exposed in production codebase, only here as an example
const passphrase = "passphrase";

const Home = () => {
const [authorized, setAuthorized] = useState(false);
const backend = useBackend();

useEffect(() => {
(async () => {
const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored });

const privateKey = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
passphrase
});

// Prepare the message with the login and session validity info
const date = new Date();
const pgpRequest = {
email: "david.ramirez+sso@gooddata.com",
// set session duration to 1 day from now
validity: date.setUTCDate(date.getUTCDate() + 1) / 1000
};
const message = await openpgp.createMessage({ text: JSON.stringify(pgpRequest) });

// Sign and encrypt the message
const encrypted = await openpgp.encrypt({
message: message,
encryptionKeys: publicKey,
signingKeys: privateKey
});

// We need to replace newlines with text value '\n'
// in order for the request to be POSTed successfully
const claims = encrypted.replace(/(\r\n|\n|\r)/gm, '\n');

// Send the SSO POST request to GoodData
backend.sdk.user.loginSso(claims,"pgp-ramirez-internal","/").then((response) => {
setAuthorized(true);
})
.catch(error => {
console.log(error.responseBody);
setAuthorized(false);
});
})()
}, [backend]);

return (
<Page>
<div style={{ height: 400 }}>
{ authorized && <InsightView insight="insight-id" /> }
</div>
</Page>
);
};

export default Home;

Set the insight-id for the InsightView component to a valid insight identifier in the workspace you are using.

 

Let’s look at the example a little closer. The process involves creating an encrypted claims request. The steps are as follows:

  1. Prepare a request for the PGP SSO API
  2. Sign the request using your private key
  3. Encrypt the request using the GoodData public key

 

We need some information to complete these steps:

publicKeyArmored

This is the GoodData public PGP key which you can download here. I have replaced the newlines with text value ‘\n’ in order for it to work in the code. You do not have to change anything here.

 

privateKeyArmored

This is the private part of the key used when SSO was configured with GoodData support. As stated previously, we include the private key here just for testing. To get the private key, you can run the following PGP command, just replace my user (david.ramirez@gooddata.com) with your user:

gpg --export-secret-key -a "david.ramirez@gooddata.com" > private.key

Similar to the public key, we need to escape newlines with the text ‘\n’. Once newlines have been escaped you can update the code with the new value.

 

passphrase

This is the passphrase for the private key. Similar to the private key, it is only here for testing and should not be exposed.

 

pgpRequest

This is the un-signed un-encrypted request which holds the user information and the session length. Set the email parameter to the user you will be using for testing. Remember the user will have to have the SSO Provider set in their profile. In the example, the validity or session length is set to 1 day.

 

After setting the privateKeyArmored, passphrase, and pgpRequest variables the code will generate the encrypted claims.

 

claims

This is the final form of the request which will act as the payload for the POST request sent to the GoodData PGP SSO API. Again we have to escape the newlines with the text ‘\n’.

 



 

As a summary, the following input should have been filled in to the example code:

 

privateKeyArmored

passphrase

pgpRequest

Insight-id

 

If everything is good to go, you will see the insight rendered on the Home tab:

aFQ_B81iMyQuADEOqnW0xIfbvU5oZMeDkLL7bEsegV5Y7IULfpEXidPsI-NfQUXIisl--jjuFo87wI9TCXWToBWxmRaL82e8CoPytGNrXDQQuwx-d9Ysd40PowcUYVm-IFOiLqJ8=s0

 

 If you do not see the insight, there likely was an error. Open the developer console and you should see an error message:

 

 

This example is a great starting point for implementing PGP SSO into your GD.UI application. As mentioned in the beginning of the article, use this as only an example. Exposing private keys and passphrases is never a good idea and should only be done for testing. Thanks for reading!


1 reply

You no longer have to contact Support to configure the SSO Provider. It is now possible to configure the SSO provider by using the self-service API which is documented here.

Reply