GoodData.UI: PGP SSO Example
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 based on exchanging PGP key pairs.
This is a simple example to get you started. When dealing with PGP keys, much care is needed on the security side. In this example, we expose our private PGP key and passphrase in the JavaScript code. This is done for testing and should not be done in production.
A prerequisite that must be addressed is the configuration of an SSO provider. The steps are explained here.
Once an SSO provider is set up, ensure that the user you will be using for testing has been provisioned with the SSO provider. The domain administrator can do this using this [API](https://help.gooddata.com/doc/free/en/expand-your-gooddata-platform/api-reference/#section/Use-Cases/Update-a-user' s-information).
Make sure ssoProvider
is set to the SSO provider configured by GoodData support. 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 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’s 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:
If a browser does not automatically pop up after starting the application, you can go to the application URL in the browser manually:
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:
- Prepare a request for the PGP SSO API
- Sign the request using your private key
- 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 (example@gooddata.com) with your user:
gpg --export-secret-key -a "example@gooddata.com" > private.key
Like the public key, we must 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 unsigned, unencrypted request that holds the user information and the session length. Set the email parameter to the user you will be using for testing. Remember that the user will have to have the SSO Provider set in their profile. The validity or session length is set to 1 day in the example.
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:
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 at 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!