Skip to main content

Enhanced Guide: Embedding Qlik Cloud Content with JWT Authentication

cancel
Showing results for 
Search instead for 
Did you mean: 
glennvancauter
Partner - Contributor II
Partner - Contributor II

Enhanced Guide: Embedding Qlik Cloud Content with JWT Authentication

Last Update:

Aug 29, 2023 4:57:04 AM

Updated By:

glennvancauter

Created date:

Aug 29, 2023 4:57:04 AM

Hello there!

Solving the Qlik Cloud Content Embedding Puzzle with JWT Iframe Magic

Welcome to a tutorial designed for those navigating the waters of embedding Qlik Cloud content while ensuring a seamless, invisible authentication experience. Say goodbye to complexities and embrace a dynamic way to present Qlik Cloud app/sheet using iframes.

The default behavior of embedding an iframe with tenant URL, app ID, and sheet ID usually leads to unwanted redirections, demanding logins before content becomes visible.

Enter JWT authentication, the solution that sends a payload to Qlik Cloud, signs the response, and generates a JWT token for seamless authentication.

Let's dive in!

Step 1: Enabling JWT for Your Qlik Cloud Tenant Begin by enabling JWT for your Qlik Cloud tenant. This involves raising a support case with Qlik to activate JWT.

Step 2: Crafting the Key Pair for JWT Configuration As JWT gets activated, generate a public/private key pair to configure JWT authentication for Qlik Cloud. Follow this guide.

Step 3: Configuring JWT IDP Once JWT is live in your Qlik Cloud tenant, configure the JWT Identity Provider (IDP) using details obtained from the public/private key pair. Follow this guide.

Step 4: Preparing the Iframe Presentation

Gather essentials:

  1. Open the desired APP and copy its app ID from the URL.
  2. Open the SHEET you want to showcase and copy its sheet ID from the URL.

Take action:

  1. Within the Management Console, navigate to "Web" and set up a Web Integration. Origin should match the host of your PHP/HTML file for iframe presentation (e.g., https://www.example.com).
  2. In the Management Console, visit "Content Security Policy" and create a new entry with the "frame-ancestors" directive. Use the same origin as your Web Integration.

Step 5: Interacting with the JWT Endpoint

This thrilling step involves:

  1. Sending a payload.
  2. Signing the payload.
  3. Outputting the JWT Token.

This process allows us to authenticate (POST) against https://yourcloudtenant/login/jwt-session. This session creation facilitates content presentation through iframes.

For implementation, I've used node.JS, requiring modules such as fs, jsonwebtoken, and uid-safe. Follow the provided links for installation instructions.

  1. Install node.JS: Follow the installation guide.

  2. Install module uid-safe: Check out the installation details here.

  3. Install module jsonwebtoken: Install it via this link.

  4. Install module fs: Find installation instructions here.

Create the .js file, e.g., generate.js, to handle payload/signing and JWT token output. Replace placeholders with your specifics.

Here's a peek at the code:

 

const fs = require('fs');
const jwt = require('jsonwebtoken');
const uid = require('uid-safe');

const privateKey = fs.readFileSync('<the path to your privatekey.pem, for example: certificates/privatekey.pem>', 'utf8'); // Adjust the path and encoding

const signingOptions = {
  keyid: '<the keyid. You can copy this when you create the JWT IDP on your cloud tenant>',
  algorithm: 'RS256',
  issuer: '<the issuer. You can copy this when you create the JWT IDP on your cloud tenant>',
  expiresIn: '30s',
  notBefore: '-30s',
  audience: 'qlik.api/login/jwt-session',
};

const payload = {
  jti: uid.sync(32),
  sub: '<the subject of the user>',
  subType: 'user',
  name: '<full name of the user>',
  email: '<e-mail address of the user>',
  email_verified: true,
  groups: []
};

const myToken = jwt.sign(payload, privateKey, signingOptions);

// Output the token
process.stdout.write(myToken);

 

To proceed, some aspects within the JWT payload need customization. Let's address these requirements:

  1. sub: The "sub" field within the payload represents the user ID used to transmit the payload. Obtain this ID by accessing the URL https://yourcloudtenant/api/v1/users/me after logging into your Qlik Cloud tenant. The URL will display relevant information on-screen; focus on the URL itself. You'll find a segment like "****-ugW-**-nGH8sRxq97ksBpwx." Remove the asterisks and utilize this value for the "sub" entry.

  2. name and email: These fields are straightforward. Retrieve the user's full name and email from the same endpoint mentioned earlier. Alternatively, you can click the "i" icon in your user list within the Management Console to access this information.

  3. keyid and issuer: These values are furnished during JWT IDP creation. When you set up the JWT Identity Provider, these details will be visible. Finally, the const privateKey should point to the location of your .pem file on the server.

Make sure to make these adjustments to ensure the JWT payload accurately reflects the user and adheres to the requirements of your Qlik Cloud integration.

This .js file interacts with the JWT endpoint and prepares the authentication token.

Testing JWT Token Generation

Now comes the exciting part – verifying the successful generation of the JWT token. To do this, execute the following command in your terminal:

 

node <filename.js>

 

Upon running this command, your terminal will display the resulting JWT token, which appears as a lengthy encrypted string. This token is crucial for establishing secure authentication within your Qlik Cloud integration.

Here's a glimpse of the token (partially redacted for security reasons):Every character within this token holds encrypted information necessary for the secure operation of your integration.

 

eyJhbGciOi....7sNtjlad6CJA1AV2552GGMclPXBPdfXUyrxGzNvzN0L9Rr8jhB4AyrVVXUsBjgCo6VbF6WogxPbk7P07OPN-P9awEG_7oCVaBK9gJUXT4GSI8moy7VOMFHO6tzLennruCiHIMUhKy9L3ncXIzwe9vEvllZ1bvs

 

Remember, your Qlik Cloud experience is about to get smoother as this token plays a pivotal role in seamless and secure authentication. Now that you've got the token, it's time to move on to the next steps.

Step 11: Validating the JWT Token

You can verify the validity of the generated JWT token by using Postman, a versatile API testing tool. Follow these steps:

  1. Download Postman if you haven't already.

  2. Create a new POST request in Postman, directing it to https://yourcloudtenant/login/jwt-session.

  3. Set the necessary headers:

    • Content-Type: application/json
    • qlik-web-integration-id: <Your web integration ID>
    • Authorization: Bearer <Your JWT Token>

If all goes well, you'll receive a 200 OK status, indicating the successful authentication of the JWT token.

Step 12: Crafting the JWT Token Endpoint

To provide a convenient endpoint for code reference, you can create a simple .php file that executes the node generate.js command on your server and presents the JWT token in encoded JSON format.

Here's an example of the PHP code:

 

<?php
header('Content-Type: application/json');
echo json_encode(['body' => `node generate.js`]);

 

This endpoint URL will be something like https://www.example.com/qlikcloud/jwt_token.php.

Step 13: Building the Iframe Display Page

Finally, let's build the index.php/index.html file that utilizes the JWT Endpoint to acquire and utilize the token for the iframe display. You'll need to replace placeholders with your actual details:

 

<html>

<head>
    <script src="https://unpkg.com/enigma.js/enigma.min.js"></script>
</head>

<body>

    <div id="main">
        <div id="message"></div>
        <iframe id='qlik_frame' style='border:none;width:100%;height:900px;'></iframe>
    </div>

    <script>

        //    CONFIGURATION

        const TENANT = '<yourcloudtenant>';
        const JWTENDPOINT = 'https://www.example.com/qlikcloud/jwt_token.php';
        const WEBINTEGRATIONID = '<web integration id>';
        const APPID = '<appid>;
        const SHEETID = '<sheetid>';
        const IDENTITY = '<not mandatory, fill in whatever you want>';

        //    MAIN

        (async function main() {
            const isLoggedIn = await qlikLogin();
            const qcsHeaders = await getQCSHeaders();
            const [session, enigmaApp] = await connectEnigma(qcsHeaders, APPID, IDENTITY);
            handleDisconnect(session);
            const theme = await getTheme(enigmaApp);
            renderSingleIframe('qlik_frame', APPID, SHEETID, theme, IDENTITY);
        })();

        //    LOGIN

        async function qlikLogin() {
            const loggedIn = await checkLoggedIn();
            if (loggedIn.status !== 200) {
                const tokenRes = await (await getJWTToken(JWTENDPOINT)).json();
                const loginRes = await jwtLogin(tokenRes.body);
                if (loginRes.status != 200) {
                    const message = 'Something went wrong while logging in.';
                    alert(message);
                    throw new Error(message);
                }
                const recheckLoggedIn = await checkLoggedIn();
                if (recheckLoggedIn.status !== 200) {
                    const message = 'Third-party cookies are not enabled in your browser settings and/or browser mode.';
                    alert(message);
                    throw new Error(message);
                }
            }
            console.log('Logged in!');
            return true;
        }

        async function checkLoggedIn() {
            return await fetch(`https://${TENANT}/api/v1/users/me`, {
                mode: 'cors',
                credentials: 'include',
                headers: {
                    'qlik-web-integration-id': WEBINTEGRATIONID
                },
            })
        }

        //    Get the JWT and use it to obtain Qlik Cloud session cookie.

        async function getJWTToken(jwtEndpoint) {
            return await fetch(jwtEndpoint, {
                mode: 'cors',
                method: 'GET'
            })
        }

        async function jwtLogin(token) {
            const authHeader = `Bearer ${token}`;
            return await fetch(`https://${TENANT}/login/jwt-session?qlik-web-integration-id=${WEBINTEGRATIONID}`, {
                credentials: 'include',
                mode: 'cors',
                method: 'POST',
                headers: {
                    'Authorization': authHeader,
                    'qlik-web-integration-id': WEBINTEGRATIONID
                },
            })
        }

        async function getQCSHeaders() {
            const response = await fetch(`https://${TENANT}/api/v1/csrf-token`, {
                mode: 'cors',
                credentials: 'include',
                headers: {
                    'qlik-web-integration-id': WEBINTEGRATIONID
                },
            })

            const csrfToken = new Map(response.headers).get('qlik-csrf-token');
            return {
                'qlik-web-integration-id': WEBINTEGRATIONID,
                'qlik-csrf-token': csrfToken,
            };
        }

        //    ENIGMA ENGINE CONNECTION

        async function connectEnigma(qcsHeaders, appId, identity) {
            const [session, app] = await getEnigmaSessionAndApp(qcsHeaders, appId, identity);
            return [session, app];
        }

        async function getEnigmaSessionAndApp(headers, appId, identity) {
            const params = Object.keys(headers)
                .map((key) => `${key}=${headers[key]}`)
                .join('&');

            return (async () => {
                const schema = await (await fetch('https://unpkg.com/enigma.js@2.7.0/schemas/12.612.0.json')).json();

                try {
                    return await createEnigmaAppSession(schema, appId, identity, params);
                }
                catch {
                    // Handle race condition with new users who do not have permissions to access the application. The code makes another attempt after a 1.5 seconds.
                    const waitSecond = await new Promise(resolve => setTimeout(resolve, 1500));
                    try {
                        return await createEnigmaAppSession(schema, appId, identity, params);
                    }
                    catch (e) {
                        throw new Error(e);
                    }
                }
            })();
        }

        async function createEnigmaAppSession(schema, appId, identity, params) {
            const session = enigma.create({
                schema,
                url: `wss://${TENANT}/app/${appId}/identity/${identity}?${params}`
            });
            const enigmaGlobal = await session.open();
            const enigmaApp = await enigmaGlobal.openDoc(appId);
            return [session, enigmaApp];
        }

        //    BONUS! DYNAMICALLY FETCH THEME

        async function getTheme(enigmaApp) {
            const createAppProps = await enigmaApp.createSessionObject({
                qInfo: {
                    qId: "AppPropsList",
                    qType: "AppPropsList"
                },
                qAppObjectListDef: {
                    qType: "appprops",
                    qData: {
                        theme: "/theme"
                    }
                }
            });
            const appProps = await enigmaApp.getObject('AppPropsList');
            const appPropsLayout = await appProps.getLayout();
            const theme = appPropsLayout.qAppObjectList.qItems[0].qData.theme;
            return theme;
        }

        //    HANDLE ENGINE SESSION CLOSURE

        function handleDisconnect(session) {
            session.on('closed', () => {
                const message = '<Your text here> Due to inactivity or loss of connection, this session has ended.';
                document.getElementById('qlik_frame').style.display = "none";
                document.getElementById('message').innerHTML = message;
            });

            session.on('suspended', () => {
                const message = '<Your text here> Due to loss of connection, this session has been suspended.';
                document.getElementById('qlik_frame').style.display = "none";
                document.getElementById('message').innerHTML = message;
            });

            window.addEventListener('offline', () => {
                session.close();
            });
        }

        //    HELPER FUNCTION TO GENERATE IFRAME

        function renderSingleIframe(frameId, appId, sheetId, theme, identity) {
            const frameUrl = `https://${TENANT}/single/?appid=${appId}&sheet=${sheetId}&theme=${theme}&identity=${identity}&opt=ctxmenu,currsel`;
            document.getElementById(frameId).setAttribute('src', frameUrl);
        }


    </script>

</body>

</html>

 

Customizing Integration Parameters

Before proceeding, let's customize a few essential parameters that will tie your integration together seamlessly. These values act as the foundation for your Qlik Cloud integration, enabling it to function smoothly. Let's take a moment to personalize them:

  • TENANT: Replace with your Qlik Cloud tenant URL.
  • JWTENDPOINT: Set this to the endpoint where your JWT token will be generated, for instance: https://www.example.com/qlikcloud/jwt_token.php.
  • WEBINTEGRATIONID: Input your unique web integration ID.
  • APPID: Insert the App ID of the Qlik Cloud app you want to embed.
  • SHEETID: Specify the Sheet ID of the specific sheet you wish to display.
  • IDENTITY: Optionally, add a unique identity to enhance tracking and management.

With these values tailored to your integration's requirements, you're ready to proceed to the next step.

Witness Your Integration in Action

Now that everything is in place, it's time to witness the magic unfold. By serving your index.php or index.html file, you'll see your Qlik Cloud integration come to life.

Prepare to be amazed as the iframe showcases your chosen app and sheet, all seamlessly authenticated without any hassle. Sit back and enjoy the smooth presentation of data within the iframe.

Step 14: Conclusion

And there you have it! By following this thorough guide, you've accomplished the intricate process of embedding Qlik Cloud content using iframes and JWT authentication. Your users can now seamlessly access the content without encountering any login hurdles.

Remember, every placeholder like <...> needs to be replaced with your specific details to make the entire process work seamlessly.

Enjoy the enhanced user experience and feel free to reach out if you encounter any challenges or need further assistance!

Labels (2)
Comments
SemBL83
Partner Ambassador
Partner Ambassador

Nice work Glenn!

david_hg96
Partner - Contributor III
Partner - Contributor III

Hi! Thanks for the post, is very useful. 

I am in Step 11: Validating the JWT Token

I also updated my node.js to generate token, but when I send it to postman, it gives me the error:

...<head>
    <title>406 Not Acceptable</title>
...</head>
 
Do I need to put an specific origin for postman? (in web integration config)
 
And in this part of Step 5:
You'll find a segment like "****-ugW-**-nGH8sRxq97ksBpwx." Remove the asterisks and utilize this value for the "sub" entry

In my case accessing to url ../users/me it brings me as follow:
{"id":"647e3205d8c3c7345f7fce2c"
I think, I am taking incorrect field.
 
Regards.
-DHG
glennvancauter
Partner - Contributor II
Partner - Contributor II

Hi @david_hg96 ,

Can you share your .js file that is responsible for generating the token? Please remove any personal details when doing so. Also, the the value:

{"id":"647e3205d8c3c7345f7fce2c"

 Is not correct. You need to take the value from the URL once you've gone to users/me, it will display the correct "sub".

david_hg96
Partner - Contributor III
Partner - Contributor III

Hi @glennvancauter , thanks for the help.

I configured web integration with issuer and key id in blank. That solve my problem, and also I used the user id without problem:

david_hg96_0-1693845307056.png

status 200.

I will continue with the instructions.

 

Regards

pixlics12
Contributor III
Contributor III

Hello @glennvancauter @SemBL83 @david_hg96,
This post is very helpful for the users/developers like me, who are still struggling and find various challenges in embed Qlik app/sheet. It solves some of my doubts and issues.

I am using same code and steps mentioned in the above post.

Embed app/sheet is working as expected but currently, the logged in Qlik user got following (welcome) popup on each run of embed web page. As I understood, this is happening because I used same user's email to generate JWT token. So Qlik treat that this user is newly added and logged in first time. But my customers are not happy with this scenario.

Can you please help me on this. How can I stop this:
pixlics12_0-1711188255089.png

Thank You

Contributors
Version history
Last update:
‎2023-08-29 04:57 AM
Updated by: