Skip to content

Commit 2f8ad03

Browse files
adileiadilei
andauthored
Added a sample demonstrating 3rd party authentication with OKTA (microsoft#205)
* Added a sample demonstrating 3rd party authentication with OKTA * Typos, added steps to README, cleaning up code * Added some details to README * Added screenshots to README --------- Co-authored-by: adilei <adileibowiz@microsoft.com>
1 parent c187baa commit 2f8ad03

7 files changed

Lines changed: 396 additions & 22 deletions

File tree

3rdPartySSOWithOKTA/README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# 3rd Party SSO with OKTA
2+
3+
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.
4+
5+
## Getting started
6+
7+
To run this sample, including the end-to-end SSO flow with OKTA, you will need to:
8+
9+
1. Deploy [index.html](./public/index.html) and [signout.html](./public/signout.html) on a remote or local server
10+
2. Create an OKTA developer account, or use an existing one
11+
3. Create a new app integration in OKTA
12+
4. Configure the default access policy in the OTKA authorization server
13+
5. Retrieve the token endpoint for a custom copilot that is configured with manual authentication
14+
6. Update configuration values in index.html
15+
16+
## Detailed instructions
17+
18+
### Deploy the sample files
19+
20+
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)
21+
22+
### Configure OKTA
23+
24+
1. Sign up for an [OKTA developer account](https://developer.okta.com/signup/)
25+
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.
26+
27+
28+
| Application Property | Value |
29+
| ---------------------- | ------------------------------------------------------------------- |
30+
| Sign-in method | OIDC - OpenID Connect |
31+
| Application type | Single-Page Application |
32+
| Grant type | Authorization Code, Interaction Code |
33+
| Sign-in redirect URIs | the URL to index.html |
34+
| Sign-out redirect URIs | the URL to signout.html |
35+
| Trusted origins | your base URL, for example http://localhost:8080 |
36+
| Assignments | allow access to specific users or groups based on your requirements |
37+
38+
3. After creating the app integration, note its Client ID
39+
4. **Index.html** uses the OKTA sign-in widget which relies on the Interaction Code sign-in flow. To enable the Interaction Code flow:
40+
41+
1. Navigate to the API settings page in **https://{your domain}.okta.com/admin/oauth2/as**
42+
2. Under Authorization Servers, edit the default authorization server
43+
3. Under Access Policies, edit the default policy rule
44+
4. Under "IF Grant type is" -> Other grants, click on **Interaction Code**.
45+
5. Update the rule
46+
47+
48+
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.
49+
50+
51+
### Configure authentication in Copilot Studio, and obtain the token endpoint
52+
53+
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.
54+
55+
<p align="center">
56+
<img src="./img/placeholder.png" alt="Manual authentication without real values">
57+
<br>
58+
<em>Manual authentication without real values</em>
59+
</p>
60+
61+
> [!IMPORTANT]
62+
> When using "placeholder" instead of real values, SSO will not work in the test canvas.
63+
> After making any changes to the copilot's authentication settings, publish the copilot.
64+
65+
2. Obtain the copilot's token endpoint from Settings -> Channels -> Mobile App
66+
67+
### Populate configuration values in index.html
68+
69+
1. Populate the following values in index.html, based on your configuration
70+
71+
| Variable | Value |
72+
| --------------------- | ----------------------------------------------------------------------------------------- |
73+
| baseUrl | your OKTA domain, for example: https://mydomain.okta.com/ |
74+
| clientID | The Client ID of the OKTA application |
75+
| redirectUri | the URL for index.html, for example: http://localhost:8080/src/index.html |
76+
| issuer | {your OKTA domain}/oauth2/default, for example: https://mydomain.okta.com/oauth2/default |
77+
| tokenEndpoint | Your copilot's token endpoint |
78+
| postLogoutRedirectUri | he URL for signout.html, for example: http://localhost:8080/src/signout.html |
79+
80+
2. Publish or save index.html, depending if it is deployed locally or remotely
81+
82+
### Test the SSO flow
83+
84+
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
85+
86+
87+
<p align="center">
88+
<img src="./img/widget.png" alt="The OKTA sign-in widget" width="400px">
89+
<br>
90+
<em>The OKTA sign-in widget</em>
91+
</p>
92+
93+
94+
<p align="center">
95+
<img src="./img/token.png" alt="The user's access token" width="400px">
96+
<br>
97+
<em>System.User.AccessToken is set</em>
98+
</p>
99+
100+
101+
71 KB
Loading

3rdPartySSOWithOKTA/img/token.png

144 KB
Loading

3rdPartySSOWithOKTA/img/widget.png

77.3 KB
Loading
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<title>Copilot Studio SSO with OKTA</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
7+
<script crossorigin="anonymous" src="https://global.oktacdn.com/okta-signin-widget/7.2.1/js/okta-sign-in.min.js"
8+
type="text/javascript"></script>
9+
<link href="https://global.oktacdn.com/okta-signin-widget/7.2.1/css/okta-sign-in.min.css" type="text/css"
10+
rel="stylesheet" />
11+
<script src="https://cdn.botframework.com/botframework-webchat/latest/webchat.js"></script>
12+
<!-- This styling is for the canvas demonstration purposes. It is recommended
13+
that style is moved to separate file for organization in larger projects -->
14+
<style>
15+
html,
16+
body {
17+
height: 100%;
18+
}
19+
20+
body {
21+
margin: 0;
22+
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
23+
-webkit-font-smoothing: antialiased;
24+
}
25+
26+
#header {
27+
background-color: rgb(11, 85, 106);
28+
color: rgb(255, 255, 255);
29+
font-weight: 600;
30+
height: 48px;
31+
padding: 0px 13px;
32+
display: flex;
33+
justify-content: space-between;
34+
align-items: center;
35+
}
36+
37+
#subheader {
38+
background-color: rgb(243, 242, 241);
39+
padding: 7px 13px;
40+
font-size: 12px;
41+
font-weight: 400;
42+
}
43+
44+
a {
45+
color: rgb(0, 90, 158);
46+
}
47+
48+
a:hover {
49+
color: rgb(0, 69, 120);
50+
}
51+
52+
#webchat {
53+
position: fixed;
54+
height: calc(100% - 75px);
55+
width: 100%;
56+
top: 75px;
57+
overflow: hidden;
58+
}
59+
60+
#sign-out-button {
61+
font-size: 16px;
62+
border: none;
63+
color: black;
64+
cursor: pointer;
65+
visibility: hidden;
66+
position: fixed;
67+
top: 5px;
68+
right: 10px;
69+
/* Add the following lines */
70+
display: inline-block;
71+
width: auto;
72+
padding: 10px 20px;
73+
/* Adjust as needed */
74+
border-radius: 5px;
75+
}
76+
77+
#chatwindow {
78+
visibility: hidden;
79+
}
80+
</style>
81+
82+
</head>
83+
84+
<body>
85+
<div id="okta-signin-container"></div>
86+
87+
<div id="chatwindow">
88+
<div id="header">
89+
Copilot Studio SSO with OKTA
90+
</div>
91+
<button id="sign-out-button" onclick=signOut()>Sign Out</button>
92+
<div id="webchat"> </div>
93+
</div>
94+
95+
<script>
96+
97+
var oktaSignIn = new OktaSignIn({
98+
baseUrl: "{your okta domain}", //e.g. https://mydomain.okta.com}
99+
clientId: "{your OKTA app configuration client ID}",
100+
redirectUri: "{the URL for index.html}", // For example http://localhost:5501/CopilotStudioSamples/3rdPartySSOWithOKTA/public/index.html}
101+
authParams: {
102+
responseType: ['code'],
103+
issuer: "{your OKTA domain}/oauth2/default", //for example: https://mydomain.okta.com/oauth2/default
104+
display: 'page',
105+
pkce: true,
106+
scopes: ['openid', 'email', 'profile'],
107+
storage: 'sessionStorage'
108+
}
109+
});
110+
111+
// If the user is already authenticated, render the chat widget
112+
// If the user isn't authentictated, render the OKTA login widget first
113+
oktaSignIn.authClient.token.getUserInfo().then(function (user) {
114+
console.log('User is already authenticated - rendering chat widget');
115+
renderChatWidget();
116+
}, function (error) {
117+
console.log('User is not - rendering login widget');
118+
oktaSignIn.showSignInToGetTokens({
119+
el: '#okta-signin-container'
120+
}).then(function (tokens) {
121+
oktaSignIn.authClient.tokenManager.setTokens(tokens);
122+
oktaSignIn.remove();
123+
console.log('Authentication successful - rendering chat widget');
124+
renderChatWidget();
125+
}).catch(function (err) {
126+
console.log('error logging in', err);
127+
});
128+
});
129+
130+
// Sign out and redirect to sign out page
131+
function signOut() {
132+
oktaSignIn.authClient.signOut({
133+
clearTokensBeforeRedirect: true,
134+
postLogoutRedirectUri: '{the URL for signout.html}'// For example, 'http://127.0.0.1:5501/CopilotStudioSamples/3rdPartySSOWithOKTA/public/signout.html'
135+
});
136+
}
137+
138+
// Utility function
139+
async function fetchJSON(url, options = {}) {
140+
const res = await fetch(url, {
141+
...options,
142+
headers: {
143+
...options.headers,
144+
accept: 'application/json'
145+
}
146+
});
147+
148+
if (!res.ok) {
149+
throw new Error(`Failed to fetch JSON due to ${res.status}`);
150+
}
151+
152+
return await res.json();
153+
}
154+
155+
async function renderChatWidget() {
156+
157+
const accessToken = oktaSignIn.authClient.getAccessToken();
158+
const idToken = await oktaSignIn.authClient.tokenManager.get('idToken');
159+
const userEmail = idToken.claims.email;
160+
161+
const tokenEndpoint = "https://8c40032d041be8659e3538508ad528.1a.environment.api.powerplatform.com/powervirtualagents/botsbyschema/cr148_okta/directline/token?api-version=2022-03-01-preview"
162+
163+
const userID = userEmail != null ?
164+
(userEmail).substr(0, 36) :
165+
(Math.random().toString() + Date.now().toString()).substr(0, 64);
166+
167+
168+
// Generate a direct line token
169+
const { token } = await fetchJSON(tokenEndpoint);
170+
const directLine = window.WebChat.createDirectLine({ token });
171+
172+
const store = WebChat.createStore(
173+
{},
174+
({ dispatch }) => next => action => {
175+
const { type } = action;
176+
177+
// Configure your bot to start the conversation automatically
178+
// See https://learn.microsoft.com/en-us/power-virtual-agents/configure-bot-greeting
179+
if (action.type === "DIRECT_LINE/CONNECT_FULFILLED") {
180+
dispatch({
181+
meta: {
182+
method: "keyboard",
183+
},
184+
payload: {
185+
activity: {
186+
channelData: {
187+
postBack: true,
188+
},
189+
// Web Chat will show the Start Conversation System Topic message
190+
name: 'startConversation',
191+
type: "event"
192+
},
193+
},
194+
type: "DIRECT_LINE/POST_ACTIVITY",
195+
});
196+
}
197+
198+
// Filter incoming activities from Direct Line to intercept the Login Card
199+
// If user is logged-in, post the access token obtained from OKTA to the token post URI
200+
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
201+
const activity = action.payload.activity;
202+
203+
if (activity.attachments?.[0]?.contentType === 'application/vnd.microsoft.card.oauth') {
204+
205+
// Extract the endpoint to which the token is posted
206+
const postEndpoint = activity.attachments?.[0].content.tokenPostResource.sasUrl;
207+
208+
if (accessToken) {
209+
fetch(postEndpoint, {
210+
method: 'POST',
211+
headers: {
212+
'Content-Type': 'application/json'
213+
},
214+
body: JSON.stringify({
215+
token: accessToken
216+
})
217+
}).then(() => {
218+
// Token sent successfully, do not show the login card
219+
}).catch((error) => {
220+
console.error('An error occurred:', error);
221+
// Token was not sent successfully, display the login card to the user
222+
return next(action);
223+
});
224+
return;
225+
}
226+
else {
227+
return next(action);
228+
}
229+
230+
}
231+
else {
232+
return next(action);
233+
}
234+
}
235+
else {
236+
return next(action);
237+
}
238+
});
239+
240+
const styleOptions = {
241+
// Add styleOptions to customize Web Chat canvas
242+
hideUploadButton: true,
243+
};
244+
245+
document.getElementById('chatwindow').style.visibility = 'visible';
246+
document.getElementById('sign-out-button').style.visibility = 'visible';
247+
248+
window.WebChat.renderWebChat(
249+
{
250+
directLine: directLine,
251+
store,
252+
userID: userID,
253+
styleOptions
254+
},
255+
document.getElementById('webchat')
256+
);
257+
}
258+
</script>
259+
</body>
260+
261+
</html>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>User Signed Out</title>
5+
</head>
6+
<body>
7+
<h1>User Signed Out</h1>
8+
<p>The user has been successfully signed out.</p>
9+
</body>
10+
</html>

0 commit comments

Comments
 (0)