Power Apps (Part 17)

Configure Copilot single sign-on for Web

Configure Copilot single sign-on for Web

Microsoft Copilot Studio supports single sign-on (SSO). SSO allows copilots on your website to sign customers in if they're already signed in to the page or app where the copilot is deployed.

I created a new copilot

I named the copilot "Blog Site Copilot"

The copilot was setup

"On Unknown Intent" the "Conversational boosting" Topic will generate an answer based on the contents of https://haddley.github.io (the Data source).

To test the Copilot I asked the question "What is Docker?"

I added a Message to display "System.Activity.Text"

Now the text provided by the user is echoed back to them before the generated answer is displayed

Restarting the session displays the message specified in the "Conversation Start" Topic 

Publish|Configure channels

I wanted to publish the Copilot on a custom website.

I clicked the Publish|Go to Channels link

"Because you chose Teams Authentication, only Teams channel is available. To use other channels, change your authentication settings. Go to authentication settings."

Settings|Security|Authentication

By default the Copilot was configured with Authentication "Only for Teams and Power Apps".

I clicked on the Security menu item

I clicked on the Authentication tile

The "Only for Teams and Power Apps" authentication option is selected (by default).

Notice that a "Blog Site Copilot (Power Virtual Agents)" application registration was automatically created in Azure.

Security|Authentication|No Authentication

I set the Authentication setting to No authentication. I navigated to the Demo Website channel.

I selected the No authentication option

I clicked the Save button

I clicked the Copy button

I navigated to the Publish tab and clicked the Publish button

I clicked the Publish button

I returned to the demo website and asked the question "What is NGRX?"

Setting|Security|Manual

To Configure Copilot with single sign-on for I created two Application Registrations.

A "Blog Site Copilot (Canvas App)" Application Registration for the Web Page https://delightful-moss-0ad02620f.5.azurestaticapps.net; and

A "Blog Site Copilot (Authenticated App)" Application Registration for Copilot Studio https://token.botframework.com/.auth/web/redirect

The Canvas App Registration has a Web Redirect URI https://delightful-moss-0ad02620f.5.azurestaticapps.net/

The Access tokens and ID tokens options are both checked 

The Canvas App Registration has a generated Client secret

The Canvas App Registration Overview

The Authenticated App Registration has a Web Redirect URI https://token.botframework.com/.auth/web/redirect

Notice that the Redirect URL can be copied from the Copilot Studio Security|Authentication page

The Access tokens and ID tokens options are both checked

The Authenticated App Registration has a generated Client secret

The Authenticated App Registration includes a scope. The Authenticated App is configured to trust the Canvas App Registration. Notice that the Canvas App Client ID "ff20..." has been added.

The Authenticated App Registration includes scope:  api://69552d8b-12ac-4e44-96a6-1dc285405aa5/Readfiles

The Authenticated App Registration Overview

Notice that the Authenticated
The Authenticated App Registration Client ID and Client Secret have been added to the Copilot Studio Security|Authentication page.
The "api://6955..." scope has been added to the Copilot Studio Security|Authentication page. 

    
<!DOCTYPE html>
<html>

<head>
    <title>Contoso Sample Web Chat</title>
    <script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
    <script type="text/javascript" src="https://alcdn.msauth.net/lib/1.2.0/js/msal.js"></script>
    <script src="https://unpkg.com/@azure/storage-blob@10.3.0/browser/azure-storage.blob.min.js"
        integrity="sha384-fsfhtLyVQo3L3Bh73qgQoRR328xEeXnRGdoi53kjo1uectCfAHFfavrBBN2Nkbdf"
        crossorigin="anonymous"></script>
    <script type="text/javascript">
        if (typeof Msal === 'undefined') document.write(unescape("%3Cscript src='https://alcdn.msftauth.net/lib/1.2.0/js/msal.js' type='text/javascript' %3E%3C/script%3E"));
    </script>

    <style>
        html,
        body {
            height: 100%;
        }

        body {
            margin: 0;
        }

        h1 {
            font-size: 16px;
            font-family: Segoe UI;
            line-height: 20px;
            color: whitesmoke;
            display: table-cell;
            padding: 13px 0px 0px 20px;
        }

        #heading {
            background-color: black;
            height: 50px;
        }

        .main {
            margin: 18px;
            border-radius: 4px;
        }

        div[role="form"] {
            background-color: black;
        }

        #webchat {
            position: fixed;
            height: calc(100% - 50px);
            width: 100%;
            top: 50px;
            overflow: hidden;
        }

        #heading {
            background-color: black;
            background-repeat: no-repeat;
            background-size: cover;
            background-attachment: fixed;
            background-position: center;
            height: 50px;
            width: 100%;
            overflow: hidden;
            position: fixed;
        }

        h1 {
            font-size: 14px;
            font-family: Segoe UI;
            font-style: normal;
            font-weight: 600;
            font-size: 14px;
            line-height: 20px;
            color: #F3F2F1;
            letter-spacing: 0.005em;
            display: table-cell;
            vertical-align: middle;
            padding: 13px 0px 0px 20px;
        }

        #chatwindow {
            height: 80%;
            width: 100%;
            overflow: hidden;
            position: fixed;
        }

        #loginButton {

            height: 100px;
            width: 100%;
            position: fixed;
        }
    </style>

</head>

<body>
    <div id="chatwindow">
        <div id="heading">
            <div><span>SSO Test Bot</span></div>
        </div>
        <div style="z-index: 100;position: absolute;margin-top: 50px;width: 100%;">
            <div>
                <label id="userName" name="userName" style="width:75%;height:15px;border-color: Transparent;">Not logged
                    in.</label>
                <button id="login" name="login" onclick="onSignInClick()"
                    style="float: right;background-color: aliceblue;">Log In</button>
            </div>
        </div>
        <div id="webchat">
        </div>

    </div>

    <script>
        function onSignin(idToken) {
            let user = clientApplication.getAccount();
            document.getElementById("userName").innerHTML = "Currently logged in as " + user.name;
            let requestObj1 = {
                scopes: ["user.read", 'openid', 'profile']
            };
        }

        function onSignInClick() {
            let requestObj = {
                scopes: ["user.read", 'openid', 'profile']
            };

            clientApplication.loginPopup(requestObj).then(onSignin).catch(function (error) { console.log(error) });
        }

        function getOAuthCardResourceUri(activity) {
            if (activity &&
                activity.attachments &&
                activity.attachments[0] &&
                activity.attachments[0].contentType === 'application/vnd.microsoft.card.oauth' &&
                activity.attachments[0].content.tokenExchangeResource) {
                // asking for token exchange with AAD
                return activity.attachments[0].content.tokenExchangeResource.uri;
            }
        }

        function exchangeTokenAsync(resourceUri) {
            let user = clientApplication.getAccount();
            if (user) {
                let requestObj = {
                    scopes: [resourceUri]
                };
                return clientApplication.acquireTokenSilent(requestObj)
                    .then(function (tokenResponse) {
                        return tokenResponse.accessToken;
                    })
                    .catch(function (error) {
                        console.log(error);
                    });
            }
            else {
                return Promise.resolve(null);
            }
        }

        async function fetchJSON(url, options = {}) {
            const res = await fetch(url, {
                ...options,
                headers: {
                    ...options.headers,
                    accept: 'application/json'
                }
            });

            if (!res.ok) {
                throw new Error(`Failed to fetch JSON due to ${res.status}`);
            }

            return await res.json();
        } 
    </script>

    <script>
        var clientApplication;
        (function () {
            var msalConfig = {
                auth: {
                    clientId: 'ff20bd23-60f5-42c0-af50-0e941194dce8',
                    authority: 'https://login.microsoftonline.com/1661e837-0a95-4bc6-a655-865365c2419b'
                },
                cache: {
                    cacheLocation: 'localStorage',
                    storeAuthStateInCookie: false
                }
            };
            if (!clientApplication) {
                clientApplication = new Msal.UserAgentApplication(msalConfig);
            }
        }());

        (async function main() {

            // Add your BOT ID below 
            var theURL = "https://b838e0443024ea32b2f47862b85e99.03.environment.api.powerplatform.com/powervirtualagents/botsbyschema/cr74e_blogSiteCopilot/directline/token?api-version=2022-03-01-preview" // you can find the token URL via the mobile app channel configuration

            var userId = clientApplication.account?.accountIdentifier != null ?
                ("You-customized-prefix" + clientApplication.account.accountIdentifier).substr(0, 64)
                : (Math.random().toString() + Date.now().toString()).substr(0, 64);

            const { token } = await fetchJSON(theURL);
            const directLine = window.WebChat.createDirectLine({ token });
            const store = WebChat.createStore({}, ({ dispatch }) => next => action => {
                const { type } = action;
                if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
                    dispatch({
                        type: 'WEB_CHAT/SEND_EVENT',
                        payload: {
                            name: 'startConversation',
                            type: 'event',
                            value: { text: "hello" }
                        }
                    });
                    return next(action);
                }
                if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
                    const activity = action.payload.activity;
                    let resourceUri;
                    if (activity.from && activity.from.role === 'bot' &&
                        (resourceUri = getOAuthCardResourceUri(activity))) {
                        exchangeTokenAsync(resourceUri).then(function (token) {
                            if (token) {
                                directLine.postActivity({
                                    type: 'invoke',
                                    name: 'signin/tokenExchange',
                                    value: {
                                        id: activity.attachments[0].content.tokenExchangeResource.id,
                                        connectionName: activity.attachments[0].content.connectionName,
                                        token
                                    },
                                    "from": {
                                        id: userId,
                                        name: clientApplication.account.name,
                                        role: "user"
                                    }
                                }).subscribe(
                                    id => {
                                        if (id === 'retry') {
                                            // bot was not able to handle the invoke, so display the oauthCard
                                            return next(action);
                                        }
                                        // else: tokenexchange successful and we do not display the oauthCard
                                    },
                                    error => {
                                        // an error occurred to display the oauthCard
                                        return next(action);
                                    }
                                );
                                return;
                            }
                            else
                                return next(action);
                        });
                    }
                    else
                        return next(action);
                }
                else
                    return next(action);
            });

            const styleOptions = {

                // Add styleOptions to customize Web Chat canvas
                hideUploadButton: true
            };


            window.WebChat.renderWebChat(
                {
                    directLine: directLine,
                    store,
                    userID: userId,
                    styleOptions
                },
                document.getElementById('webchat')
            );
        })().catch(err => console.error("An error occurred: " + err));
    </script>
</body>

</html>

Code is from https://github.com/microsoft/CopilotStudioSamples/blob/master/BuildYourOwnCanvasSamples/3.single-sign-on/index.html

To get this example to work I needed to load the web page and to click the "log in" button (promptly).

Whoami Topic

This part of the code posts a "signin/tokenExchange" message to the Copilot (avoiding the need for the web page logged in user to login to the Copilot themselves).