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
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."
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.
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?"
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
Static Web App has URL https://delightful-moss-0ad02620f.5.azurestaticapps.net
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).