cancel
Showing results for 
Search instead for 
Did you mean: 
Reply
Helper II
Helper II

How to configure SSO in PVA?

Hi,

 

I am trying to connect SSO in PVA on the Sharepoint website. I see the below error on the chrome console.

p1.png

I see a syntax error from the below line of code. I am following the SSO configuration by the doc provided below: "https://docs.microsoft.com/en-us/power-virtual-agents/configure-sso"

 

The below code is from the doc.

 

 var userID = clientApplication.account?.accountIdentifier != null ? ("Your-customized-prefix-max-20-characters" + clientApplication.account.accountIdentifier).substr(0,64) : (Math.random().toString() + Date.now().toString().substr(0,64)

 

I had posted by query before as well but no luck.. https://powerusers.microsoft.com/t5/General/How-to-configure-Single-Sign-On-in-PVA-with-Sharepoint-a...

 

Can anyone please help me out here?

 

Regards,

Hemanth

31 REPLIES 31

Hi @BoLi ,

 

Thanks a lot for responding back. I am able to achieve SSO using the code provided on github. I have few concerns regarding how the PVA is working from the code.

I have added two button on my Sharepoint Home Page.

 

1 for PVA to pop-up on the Home Page by clicking the button Power Virtual Agent the other button for Login as per the code from github.

 

I am able to achieve SSO only when i click on the login button which is from the github code. If i try accessing to PVA by directly clicking the button Power Virtual Agent. It asks for the login prompt even if i have login to Sharepoint Home Page which is the redirect URI that was added on the Azure SSO App registration. 

REF::

e1.png

When clicked on the button Log In it displays a new window of the existing Sharepoint homepage which doesn't asks for login as i have already login to the home page. Please refer the below snapshot. 

e4.png

 

Below is the snapshot of the PVA directly sign in without asking for login prompt. This works only when i have already clicked on the Login button that is added on my Sharepoint homepage provided form the github code, else if i click on the Power Virtual Agent button directly without accessing the Login button it gives me a login prompt.

e3.png

 

Ideally what i am trying to achieve is PVA has to login automatically when i have already login to my SharePoint home page. I don't want it to work on the basis of clicking on the Login button again even after i have logged to my SharePoint home page.

 

Can this be achieved? Thanks in advance.

 

Regards,

Hemanth 

Thanks Hemanth,

 

Still, you need to debug the code and check which step failed. From your descirption, it sounds like you didn't pass the initial login token to AAD to exchange for an On-behalf-of token. 

 

To clarify, SSO currently only work for AAD authentication.  if your own website use AAD to handle logging in, when your customer logged in to your website, you should be able to get a login token issued by AAD and you will need to use that token to exchange for an OBO token and pass to PVA backend to complete sign-in process.

Hello,

 

Sorry to dig up this post but I've been trying to configure SSO for our use and while I am trying to use the github example it's still not working as intended (also not throwing errors).  Here's the code, which is mostly from the github page.  Any recommendations would be appreciated.

 

@page
@model Bot.Website.IndexModel
@{
    ViewData["Title"] = "Home";
}

<!--Home page, currently the only page.  Displays some helpful links and the embedded bot in an iframe.-->
<div class="text-center">
    <h1 class="display-4"><img src="~/assets/icon-image.png" alt="" height="50" width="50" /> (removed)</h1>
</div>
<div class="row row-main">
    <div class="row-webchat" id="webchat">
        <!--https://docs.microsoft.com/en-us/power-virtual-agents/configure-bot-greeting-->
        <!--Adding code to have the bot start the greeting without a prompt-->
        <script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
        <script type="text/javascript" src="https://alcdn.msauth.net/lib/1.4.0/js/msal.js"></script>
        <script type="text/javascript" src="~/lib/azure/storage-blob/browser/azure-storage.blob.min.js"></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>
    </div>
   

<script>

    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: {
                // Client/tenant ID from (removed)

                clientId: '(removed)',
                authority: '(removed)'
            },
            cache: {
                cacheLocation: 'localStorage',
                storeAuthStateInCookie: false
            }
        };
        if (!clientApplication) {
            clientApplication = new Msal.UserAgentApplication(msalConfig);
        }
    }());

    (async function main() {

    // Bot ID - prod environment = (removed)
    // Bot ID - test environment = (removed)
    // TODO:  Set up prod and dev as variables

    var BOT_ID = "(removed)";

    var theURL = "https://powerva.microsoft.com/api/botmanagement/v1/directline/directlinetoken?botId=" + BOT_ID;

    //once this notifyMessage is checked means the conversationId variable is initialized, could set its value
    var notifyMessage = "(removed)";

    // Note that the prefix (cosine-es-bot) has to be 20 characters and the entire user ID cannot be more than 64 characters
    var userId = clientApplication.account?.accountIdentifier != null ?
        ("(removed)" + clientApplication.account.accountIdentifier).substr(0, 64)
        : (Math.random().toString() + Date.now().toString()).substr(0, 64);

    const { token } = await fetchJSON(theURL);
    const conversationId = token.conversationId;
    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);
        }
        // Make sure to add in the conversationId work that Allen did
        if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
            if (String(action.payload.activity.text).toLowerCase() === notifyMessage) {
                // the bot.conversationId has been initialized through the "Init conversationId" flow
                // set the bot global variable value through dispatch "SEND_EVENT"
                // For details about setting variables externally: see the link:
                // docs.microsoft.com/en-us/power-virtual-agents/authoring-variables-bot#set-a-bot-variables-value-from-external-sources
                dispatch({
                    type: "WEB_CHAT/SEND_EVENT",
                    payload: {
                        name: "pvaSetContext",
                        value: {
                            "conversationId": conversationId
                        }
                    }
                });

            }
            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>

Hi @PaulCullivan 

 

The sample code provided on github works fine if you have configured the authentication part as per the doc provided for authentication and SSO. I am able to achieve this only when i click on the login button that is from the code. 

 

Regards,

Hemanth

Hi @BoLi ,

 

As I am trying to debug the code and find what is going wrong it redirects me to different CDN and files that is involved from the code provided on the github. I am not able to figure out what's going wrong here because of it, Is there anyway if the below code which is used for login button can be configured for the Power Virtual Agent button which is used to pop-up the chatbot for which I am using on the code to work without asking for the user to login I just want it to be authenticated directly without the pop-up showing up for login . Any help me much appreciated. Thanks in advance.

 

Login button code from github

 

<button id="login" name="login" onclick="onSignInClick()" style="float: right;background-color: aliceblue;">Log In</button>

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) });
    }

 

 

Below is the code I am using.

 

 

<!DOCTYPE html>
<html>
<head>
  <title>Virtual Agent</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;
    }
	
	.modal 
	{
      display: none; /* Hidden by default */
      position: fixed; /* Stay in place */
      z-index: 1; /* Sit on top */
      padding-top: 100px; /* Location of the box */
      left: 0;
      top: 0;
      width: 100%; /* Full width */
      height: 100%; /* Full height */
      overflow: auto; /* Enable scroll if needed */
      background-color: rgb(0,0,0); /* Fallback color */
      background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
    }
	
	.modal-content 
	{
      background-color: #fefefe;
      margin: auto;
      padding: 10px;
      border: 1px solid #888;
      width: 500px;
      height: 575px;
    }
    .close 
	{
      color: black;
      float: right;
      font-size: 28px;
      font-weight: bold;
    }

    .close:hover,
    .close:focus 
	{
      color: #000;
      text-decoration: none;
      cursor: pointer;
    }
	
    .main 
	{
	  margin: 18px;
	  border-radius: 4px;
    }

    div[role="form"]
	{
	  background-color: #3392FF;
    }

    #webchat 
	{
	  position: center;
      height: 530px;
      width: 100%;
      top: 60px;
      overflow: hidden;
    }
	#heading 
	{
	  padding-bottom: 5px;
	}

    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;
    }

	#login 
	{
      position: fixed;
	  margin-left: 150px;
	}
	
    .span
	{
      font-weight: bold;
    }
    
	#myBtn 
	{
      position: fixed;
	  float:right;
      outline: none;
	  width:60px;
	  height:80px;
	  margin:auto auto auto 10px;
    }
	button:hover
	{
	  background-color: transparent;
	}   

  </style>

</head>
<body>
	
	<button id="myBtn" type="button">Power Virtual Agent</button>
	<button id="login" name="login" onclick="onSignInClick()" style="background-color: aliceblue;">Log In</button>

	<div id="myModal" class="modal">
		<div class="modal-content" style=" background-color: #FFD933">
			<span class="close">&times;</span>
			<div id="chatwindow">
				<div id="heading">
					<img src="https://kennametal.sharepoint.com/sites/testing/KE/project/SiteAssets/kmt_logo.png" width="42" height="30" alt="KMT-logo">
					<span class="span"><strong>Virtual Agent</strong></span>
				</div>
				<div id="webchat"></div>
			</div>
		</div>
	</div>

	<!--Button code begins here-->
	<script>
		
		var modal = document.getElementById("myModal");
		var btn = document.getElementById("myBtn");
		var span = document.getElementsByClassName("close")[0];

		btn.onclick = function () {
			onSignInClick();
			modal.style.display = "block";
		}

		span.onclick = function () {
			modal.style.display = "none";
		}

		window.onclick = function (event) {
			if (event.target == modal) {
				modal.style.display = "none";
			}
		}
	</script>
	<!--Button code ends here-->

	<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() {
			alert("Inside onSignInClick function");
			debugger;
			let requestObj = {
				scopes: ["user.read", 'openid', 'profile']
			};
			debugger;
			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>
	
		const styleOptions = {
		botAvatarInitials: 'BT',
		userAvatarInitials: 'UR',
		accent: '#00809d',
		botAvatarBackgroundColor: "#FFFFFF",
		userAvatarBackgroundColor:"#FFFFFF",
		botAvatarImage:"<BOT_IMAGE>",
		userAvatarImage:"<USER_IMAGE>",
		hideUploadButton: true,
		};
	
		var clientApplication;
		
		(function () {
		debugger;
			var msalConfig = {
				auth: {
					clientId: '<CLIENT_ID>',
					authority: 'https://login.microsoftonline.com/<DIRECTORY_ID>'
				},
				cache: {
					cacheLocation: 'localStorage',
					storeAuthStateInCookie: false
				}
			};
			if (!clientApplication) {
				debugger;
				clientApplication = new Msal.UserAgentApplication(msalConfig);	
			}
		}());


		(async function main() {
			debugger;
			// Add your BOT ID below
			var BOT_ID = "<BOT_ID>";
			var theURL = "https://powerva.microsoft.com/api/botmanagement/v1/directline/directlinetoken?botId=" + BOT_ID;

			var userId = clientApplication.account?.accountIdentifier != null ?
				("You-customized-prefix" + clientApplication.account.accountIdentifier).substr(0, 64)
				: (Math.random().toString() + Date.now().toString()).substr(0, 64);
			debugger;
			
			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);
			});

			

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

 

 

Regards,

Hemanth

 

Hey HemanthN, thanks for the response.

 

We went through the documentation for authentication over the weekend:

https://docs.microsoft.com/en-us/power-virtual-agents/configure-sso

 

We confirmed that the production bot was working correctly.  I relaunched the test bot (which has the code mentioned above) and I can see that the user is logged in correctly, but this always fails and the oauthCard always gets displayed.  Is there something with how the request is being passed that's causing the problem?

 

sso.png

Hi, this can be achieved. 

what you should do is when the login button on your home page was clicked and user successfully logged in to AAD(AAD is a must because sso only support AAD V2 type IDP right now), there will be a login token return from AAD back to your home page, you should use the token and then call AAD again to exchange this for a OBO token and then put OBO token as part of invoke activity and send it to PVA. This is what the github sso sample domo does today.

Thanks for the response, and sorry for the delay.

I got the button working and thanks for the tip.  However, when we saw the post on SSO we were assuming that it was for users in the chat window that could log in without having to resort to the oauthCard.

https://docs.microsoft.com/en-us/power-virtual-agents/configure-sso

 

Do you know why the button works and the login in the script doesn't?

Whoops, sorry.  Specifically I am referring to the below.  As I mentioned earlier I was trying to get this to work but no matter what we keep failing at id === retry every time we pass this through.  Thanks again, really appreciate it.

 

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);
                    }

So in looking into this I can see that we're getting a 412 error and that I am getting href = undefined in monitorIframeForHash.  As far as I can tell the authentication settings for powerVA and the app registration appear to be valid.  The bot is logging people in as expected live.  FWIW I am testing locally.  Couldn't upload a photo so here is where I get the error.

 

static monitorIframeForHash(contentWindow: Window, timeout: number, urlNavigate: string, logger: Logger): Promise<string> {
        return new Promise((resolve, reject) => {
            /*
             * Polling for iframes can be purely timing based,
             * since we don't need to account for interaction.
             */
            const nowMark = TimeUtils.relativeNowMs();
            const timeoutMark = nowMark + timeout;

            logger.verbose("monitorWindowForIframe polling started");

            const intervalId = setInterval(() => {
                if (TimeUtils.relativeNowMs() > timeoutMark) {
                    logger.error("monitorIframeForHash unable to find hash in url, timing out");
                    logger.errorPii(`monitorIframeForHash polling timed out for url: ${urlNavigate}`);
                    clearInterval(intervalId);
                    reject(ClientAuthError.createTokenRenewalTimeoutError());
                    return;
                }

                let href;

                try {
                    /*
                     * Will throw if cross origin,
                     * which should be caught and ignored
                     * since we need the interval to keep running while on STS UI.
                     */
                    href = contentWindow.location.href;
                } catch (e) {}

                if (href && UrlUtils.urlContainsHash(href)) {
                    logger.verbose("monitorIframeForHash found url in hash");
                    clearInterval(intervalId);
                    resolve(contentWindow.location.hash);
                } 
            }, WindowUtils.POLLING_INTERVAL_MS);
        });
    }

 

Helpful resources

Announcements
Microsoft Ignite

Microsoft Ignite

Join digitally, March 2–4, 2021 to explore new tech that's ready to implement. Experience the keynote in mixed reality through AltspaceVR!

New Super Users

Meet the Power Virtual Agents Community Super Users!

Congratulations to our Season 1 2021 Super User Crew!

PVA Commnity Blog

NEW Power Virtual Agents Community Blog

View articles posted by fellow community members on the Power Virtual Agents Community Blog!

New Badges

New Solution Badges!

Check out our new profile badges recognizing authored solutions!

Users online (76,763)