Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions 3rdPartySSOWithOKTA/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# 3rd Party SSO with OKTA

This custom canvas demonstates how an access token obtained from a 3rd party identity provider, like OKTA, can be used in the context of a Copilot Studio sign-in flow.

## Getting started

To run this sample, including the end-to-end SSO flow with OKTA, you will need to:

1. Deploy [index.html](./public/index.html) and [signout.html](./public/signout.html) on a remote or local server
2. Create an OKTA developer account, or use an existing one
3. Create a new app integration in OKTA
4. Configure the default access policy in the OTKA authorization server
5. Retrieve the token endpoint for a custom copilot that is configured with manual authentication
6. Update configuration values in index.html

## Detailed instructions

### Deploy the sample files

Deploy [index.html](./public/index.html) and [signout.html](./public/signout.html) on a local or a remote server, so they are availabe via two URLs. For example: [http://localhost:8080/index.html](http://localhost:8080/index.html) and [http://localhost:8080/signout.html](http://localhost:8080/signout.html)

### Configure OKTA

1. Sign up for an [OKTA developer account](https://developer.okta.com/signup/)
2. Sign in to the OKTA admin dashboard at **https://{your domain}-admin.okta.com/** and create a new app integration with the following details.


| Application Property | Value |
| ---------------------- | ------------------------------------------------------------------- |
| Sign-in method | OIDC - OpenID Connect |
| Application type | Single-Page Application |
| Grant type | Authorization Code, Interaction Code |
| Sign-in redirect URIs | the URL to index.html |
| Sign-out redirect URIs | the URL to signout.html |
| Trusted origins | your base URL, for example http://localhost:8080 |
| Assignments | allow access to specific users or groups based on your requirements |

3. After creating the app integration, note its Client ID
4. **Index.html** uses the OKTA sign-in widget which relies on the Interaction Code sign-in flow. To enable the Interaction Code flow:

1. Navigate to the API settings page in **https://{your domain}.okta.com/admin/oauth2/as**
2. Under Authorization Servers, edit the default authorization server
3. Under Access Policies, edit the default policy rule
4. Under "IF Grant type is" -> Other grants, click on **Interaction Code**.
5. Update the rule


5. You should also make shoure that CORS has been enabled for your base URL. In the OKTA admin center, navigate to Security -> API -> Trusted Origins. You base url (e.g. http://localhost:8080) should appear under "Trusted Origins" with CORS enabled. In case your base url is missing, add the url with CORS enabled.


### Configure authentication in Copilot Studio, and obtain the token endpoint

1. This SSO pattern will work for copilots configured with [manual authentication and any OAuth authentication provider](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#manual-authentication-fields). Since it is a passthrough pattern, in which the token is sent to Copilot Studio, but not validated, it will even work when no values are provided for an authentication provider. To configure manual authentication without providing any real values, select "Azure Active Directory v2" and enter **placeholder** in both client ID and secret.

<p align="center">
<img src="./img/placeholder.png" alt="Manual authentication without real values">
<br>
<em>Manual authentication without real values</em>
</p>

> [!IMPORTANT]
> When using "placeholder" instead of real values, SSO will not work in the test canvas.
> After making any changes to the copilot's authentication settings, publish the copilot.

2. Obtain the copilot's token endpoint from Settings -> Channels -> Mobile App

### Populate configuration values in index.html

1. Populate the following values in index.html, based on your configuration

| Variable | Value |
| --------------------- | ----------------------------------------------------------------------------------------- |
| baseUrl | your OKTA domain, for example: https://mydomain.okta.com/ |
| clientID | The Client ID of the OKTA application |
| redirectUri | the URL for index.html, for example: http://localhost:8080/src/index.html |
| issuer | {your OKTA domain}/oauth2/default, for example: https://mydomain.okta.com/oauth2/default |
| tokenEndpoint | Your copilot's token endpoint |
| postLogoutRedirectUri | he URL for signout.html, for example: http://localhost:8080/src/signout.html |

2. Publish or save index.html, depending if it is deployed locally or remotely

### Test the SSO flow

After signing-in using the OKTA sign-in widget, the user's access token will be sent to Copilot Studio and stored in ***System.User.AccessToken***, which can be used to make calls to protected APIs


<p align="center">
<img src="./img/widget.png" alt="The OKTA sign-in widget" width="400px">
<br>
<em>The OKTA sign-in widget</em>
</p>


<p align="center">
<img src="./img/token.png" alt="The user's access token" width="400px">
<br>
<em>System.User.AccessToken is set</em>
</p>



Binary file added 3rdPartySSOWithOKTA/img/placeholder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 3rdPartySSOWithOKTA/img/token.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added 3rdPartySSOWithOKTA/img/widget.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
261 changes: 261 additions & 0 deletions 3rdPartySSOWithOKTA/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
<!DOCTYPE html>
<html lang="en">

<head>
<title>Copilot Studio SSO with OKTA</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<script crossorigin="anonymous" src="https://global.oktacdn.com/okta-signin-widget/7.2.1/js/okta-sign-in.min.js"
type="text/javascript"></script>
<link href="https://global.oktacdn.com/okta-signin-widget/7.2.1/css/okta-sign-in.min.css" type="text/css"
rel="stylesheet" />
<script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
<!-- This styling is for the canvas demonstration purposes. It is recommended
that style is moved to separate file for organization in larger projects -->
<style>
html,
body {
height: 100%;
}

body {
margin: 0;
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
}

#header {
background-color: rgb(11, 85, 106);
color: rgb(255, 255, 255);
font-weight: 600;
height: 48px;
padding: 0px 13px;
display: flex;
justify-content: space-between;
align-items: center;
}

#subheader {
background-color: rgb(243, 242, 241);
padding: 7px 13px;
font-size: 12px;
font-weight: 400;
}

a {
color: rgb(0, 90, 158);
}

a:hover {
color: rgb(0, 69, 120);
}

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

#sign-out-button {
font-size: 16px;
border: none;
color: black;
cursor: pointer;
visibility: hidden;
position: fixed;
top: 5px;
right: 10px;
/* Add the following lines */
display: inline-block;
width: auto;
padding: 10px 20px;
/* Adjust as needed */
border-radius: 5px;
}

#chatwindow {
visibility: hidden;
}
</style>

</head>

<body>
<div id="okta-signin-container"></div>

<div id="chatwindow">
<div id="header">
Copilot Studio SSO with OKTA
</div>
<button id="sign-out-button" onclick=signOut()>Sign Out</button>
<div id="webchat"> </div>
</div>

<script>

var oktaSignIn = new OktaSignIn({
baseUrl: "{your okta domain}", //e.g. https://mydomain.okta.com}
clientId: "{your OKTA app configuration client ID}",
redirectUri: "{the URL for index.html}", // For example http://localhost:5501/CopilotStudioSamples/3rdPartySSOWithOKTA/public/index.html}
authParams: {
responseType: ['code'],
issuer: "{your OKTA domain}/oauth2/default", //for example: https://mydomain.okta.com/oauth2/default
display: 'page',
pkce: true,
scopes: ['openid', 'email', 'profile'],
storage: 'sessionStorage'
}
});

// If the user is already authenticated, render the chat widget
// If the user isn't authentictated, render the OKTA login widget first
oktaSignIn.authClient.token.getUserInfo().then(function (user) {
console.log('User is already authenticated - rendering chat widget');
renderChatWidget();
}, function (error) {
console.log('User is not - rendering login widget');
oktaSignIn.showSignInToGetTokens({
el: '#okta-signin-container'
}).then(function (tokens) {
oktaSignIn.authClient.tokenManager.setTokens(tokens);
oktaSignIn.remove();
console.log('Authentication successful - rendering chat widget');
renderChatWidget();
}).catch(function (err) {
console.log('error logging in', err);
});
});

// Sign out and redirect to sign out page
function signOut() {
oktaSignIn.authClient.signOut({
clearTokensBeforeRedirect: true,
postLogoutRedirectUri: '{the URL for signout.html}'// For example, 'http://127.0.0.1:5501/CopilotStudioSamples/3rdPartySSOWithOKTA/public/signout.html'
});
}

// Utility function
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();
}

async function renderChatWidget() {

const accessToken = oktaSignIn.authClient.getAccessToken();
const idToken = await oktaSignIn.authClient.tokenManager.get('idToken');
const userEmail = idToken.claims.email;

const tokenEndpoint = "https://8c40032d041be8659e3538508ad528.1a.environment.api.powerplatform.com/powervirtualagents/botsbyschema/cr148_okta/directline/token?api-version=2022-03-01-preview"

const userID = userEmail != null ?
(userEmail).substr(0, 36) :
(Math.random().toString() + Date.now().toString()).substr(0, 64);


// Generate a direct line token
const { token } = await fetchJSON(tokenEndpoint);
const directLine = window.WebChat.createDirectLine({ token });

const store = WebChat.createStore(
{},
({ dispatch }) => next => action => {
const { type } = action;

// Configure your bot to start the conversation automatically
// See https://learn.microsoft.com/en-us/power-virtual-agents/configure-bot-greeting
if (action.type === "DIRECT_LINE/CONNECT_FULFILLED") {
dispatch({
meta: {
method: "keyboard",
},
payload: {
activity: {
channelData: {
postBack: true,
},
// Web Chat will show the Start Conversation System Topic message
name: 'startConversation',
type: "event"
},
},
type: "DIRECT_LINE/POST_ACTIVITY",
});
}

// Filter incoming activities from Direct Line to intercept the Login Card
// If user is logged-in, post the access token obtained from OKTA to the token post URI
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
const activity = action.payload.activity;

if (activity.attachments?.[0]?.contentType === 'application/vnd.microsoft.card.oauth') {

// Extract the endpoint to which the token is posted
const postEndpoint = activity.attachments?.[0].content.tokenPostResource.sasUrl;

if (accessToken) {
fetch(postEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: accessToken
})
}).then(() => {
// Token sent successfully, do not show the login card
}).catch((error) => {
console.error('An error occurred:', error);
// Token was not sent successfully, display the login card to the user
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,
};

document.getElementById('chatwindow').style.visibility = 'visible';
document.getElementById('sign-out-button').style.visibility = 'visible';

window.WebChat.renderWebChat(
{
directLine: directLine,
store,
userID: userID,
styleOptions
},
document.getElementById('webchat')
);
}
</script>
</body>

</html>
10 changes: 10 additions & 0 deletions 3rdPartySSOWithOKTA/public/signout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>User Signed Out</title>
</head>
<body>
<h1>User Signed Out</h1>
<p>The user has been successfully signed out.</p>
</body>
</html>
Loading