Okta Developer Secure, scalable, and highly available authentication and user management for any app. https://developer.okta.com Develop a XAA-Enabled Resource Application and Test with Okta <p>From an enterprise resource app owner’s perspective, Cross App Access (XAA) is a game-changer because it allows their resources to be “AI-ready” without compromising on security. In the XAA model, resource apps rely on the enterprise’s Identity Provider (IdP) to manage access. Instead of building out interactive OAuth flows, they defer to the IdP to check enterprise policies and user groups, assign AI agent permissions, and log and audit AI agent requests as they occur. In return, the app’s OAuth server needs only to perform a few checks:</p> <ul> <li>When the app’s OAuth server receives a POST request to its token endpoint from an AI agent, the app fetches the IdP’s public keys (via the JWKS endpoint) to ensure the ID-JAG token attached to the request was actually minted by the trusted company IdP.</li> <li>It confirms the token was intended for this app specifically. If the <code class="language-plaintext highlighter-rouge">aud</code> claim doesn’t match the app’s own identifier, it rejects the request.</li> <li>Finally, it checks the end user ID in the token’s <code class="language-plaintext highlighter-rouge">sub</code> claim to know whose data to look up in your database. It must map to the same IdP identity. It will reject the request if the user isn’t recognized.</li> </ul> <p>You can read in depth about XAA to better understand how this works and examine the token exchange flow.</p> <article class="link-container" style="border: 1px solid silver; border-radius: 3px; padding: 12px 15px"> <a href="/blog/2025/06/23/enterprise-ai" style="font-size: 1.375em; margin-bottom: 20px;"> <span>Integrate Your Enterprise AI Tools with Cross-App Access</span> </a> <p>Manage user and non-human identities, including AI in the enterprise with Cross App Access</p> <div><div class="BlogPost-attribution"> <a href="/blog/authors/semona-igama/"> <img src="/assets-jekyll/avatar-semona-igama-03eb4c28aca3765f862b574e032d32f6f8186d04ae9f0db75bed9c74f48a9a3f.jpg" alt="avatar-avatar-semona-igama.jpeg" class="BlogPost-avatar" /> </a> <span class="BlogPost-author"> <a href="/blog/authors/semona-igama/">Semona Igama</a> </span> </div></div> </article> <p>Or watch the video about Cross App Access:</p> <div class="jekyll-youtube-plugin" style="text-align: center; margin-bottom: 1.25rem"> <iframe width="700" height="394" style="max-width: 100%" src="https://www.youtube.com/embed/3VLzeT1EGrg" allowfullscreen="" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" frameborder="0"></iframe> </div> <p>In this tutorial, we’ll demonstrate how to test that an XAA-enabled resource app you have created (<strong>TaskFlow</strong>) is correctly using Okta as an <strong>enterprise Identity Provider (IdP)</strong> to sign users in, and we’ll demonstrate how a sample AI app (<strong>Agent0</strong>) uses XAA to get access to TaskFlow. To do this, you’ll:</p> <ul> <li>Enable Cross App Access in your Okta org</li> <li>Register and configure the resource app (TaskFlow) in your org</li> <li>Register the requesting app (Agent0) in your org as a known XAA app and connect it to TaskFlow.</li> <li>Test that the XAA flow is working correctly when Agent0 requests access to TaskFlow.</li> </ul> <blockquote> <p>Note that the apps (TaskFlow or Agent0) do not use Okta as their authorization server.</p> </blockquote> <h1 id="enable-cross-app-access-in-your-okta-org">Enable Cross App Access in your Okta org</h1> <p>To register your resource app with Okta, and set up secure agent-to-app connections, you’ll need an Okta Developer org enabled with XAA:</p> <ul> <li>If you don’t already have an account, sign up for a new one here: <a href="https://developer.okta.com/signup">Okta Integrator Free Plan</a></li> <li>Once created, sign in to your new Integrator Free Plan org</li> <li>In the Okta Admin Console, select <strong>Settings &gt; Features</strong></li> <li>Navigate to <strong>Early access</strong></li> <li>Find <strong>Cross App Access</strong> and select <strong>Turn on</strong> (enable the toggle)</li> <li>Refresh the Admin Console</li> </ul> <blockquote> <p>Note: Cross App Access is currently a self-service Early Access (EA) feature. You must enable it through the Admin Console before the apps appear in the catalog. If you don’t see the option right away, refresh and confirm you have the necessary admin permissions. Learn more in the <a href="https://help.okta.com/oie/en-us/content/topics/security/manage-ea-and-beta-features.htm">Okta documentation on managing EA and beta features</a>.</p> </blockquote> <p><img src="/assets-jekyll/blog/xaa-resource-app/image3-c9a95bb9918d5d631678622b2343d7776e314875e72ecd70fea836d264d8164c.jpg" alt=" " width="800" class="center-image" /></p> <h1 id="register-your-requesting-app-agent0">Register your requesting app (Agent0)</h1> <p>To test whether your resource app is working correctly, Okta provides a placeholder entry in the Okta Integration Network catalog. It is called <strong><em>Agent0 - Cross App Access (XAA) Sample Requesting App</em></strong>. Add this to your org’s integrations.</p> <ul> <li>Still in Admin Console, go to <strong>Applications &gt; Applications</strong></li> <li>Select <strong>Browse App Catalog</strong></li> <li>Search for “Agent0 - Cross App Access (XAA) Sample Requesting App”, and select it</li> <li>Select <strong>Add Integration</strong></li> </ul> <p>Now to configure it correctly. First, assign user access to Agent0.</p> <ul> <li>Change the <strong>Application</strong> label if required, and select <strong>Done</strong>,</li> <li>Select the Assignments tab <ul> <li>To assign it to a single user, select <strong>Assign &gt; Assign to People</strong> and choose your user</li> <li>To assign it to a user group, select <strong>Assign &gt; Assign to Groups</strong> and choose your user group</li> </ul> </li> <li>Click Done</li> </ul> <p>Finally, configure Agent0 with the redirect URI you will use to test Agent0</p> <ul> <li>Select the <strong>Sign On tab</strong></li> <li>Select <strong>Edit</strong>, and locate the Advanced Sign-on Settings section.</li> <li>Set the <strong>Redirect URI</strong> to the URL that your app will use. For example, <a href="http://localhost:8080/redirect">http://localhost:8080/redirect</a></li> <li>Click Save.</li> <li>Locate and copy the Client ID and Client secret in the Sign-On methods section. Your app must use these when signing users in through Okta.</li> </ul> <blockquote> <p>Note: Only the org authorization server can be used to exchange ID-JAG tokens. Ensure you are using the org authorization server and not an Okta “custom authorization server”.</p> </blockquote> <p><img src="/assets-jekyll/blog/xaa-resource-app/image2-ad5eed8a0e6b495caa26809fb24390efdd64204ac8c638c84d249a028882537b.jpg" alt=" " width="800" class="center-image" /></p> <h2 id="get-a-xaa-client-id-for-agent0-from-the-resource-apps-auth-server">Get a (XAA) Client ID for Agent0 from the Resource app’s Auth Server</h2> <p>To allow the exchange of an ID-JAG token between Agent0 and your resource app, Agent0 must be registered as an OAuth client in your resource app’s OAuth server.</p> <ul> <li>Register your requesting app (<strong>Agent0</strong>) as an OAuth client in your resource app’s OAuth server.</li> <li>Make a note of the Client ID for your requesting app (<strong>Agent0</strong>). You’ll need this as you set up your resource app.</li> </ul> <blockquote> <p>Note: The process for registering a client ID from your resource app’s OAuth server will vary depending on the product.</p> </blockquote> <h1 id="set-up-your-resource-app-taskflow">Set up your resource app (TaskFlow)</h1> <p>To set up your resource app in your org, you can use the placeholder integration in the OIN catalog called <strong><em>Todo0 - Cross App Access (XAA) Sample Resource App</em></strong> and configure it as your resource app.</p> <ul> <li>Still in Admin Console, navigate to <strong>Applications &gt; Applications</strong></li> <li>Select <strong>Browse App Catalog</strong></li> <li>Search for <strong>Todo0 - Cross App Access (XAA) Sample Resource App</strong>, and select it</li> <li>Select <strong>Add Integration</strong></li> </ul> <p>Now give it a helpful name and assign user access to TaskFlow.</p> <ul> <li>Set the Application label to <strong><em>TaskFlow</em></strong>, and click Done.</li> <li>Select the <strong>Assignments</strong> tab <ul> <li>To assign it to a single user, select <strong>Assign &gt; Assign to People</strong> and choose your user</li> <li>To assign it to a user group, select <strong>Assign &gt; Assign to Groups</strong> and choose your user group</li> </ul> </li> <li>Click <strong>Done</strong></li> </ul> <h2 id="update-the-audience-value-of-your-resource-apps-auth-server">Update the audience value of your Resource app’s auth server</h2> <p>By default, Okta will issue an ID-JAG token for Agent0 with the audience (<code class="language-plaintext highlighter-rouge">aud</code>) value set to that of the sample resource app (Todo0): <code class="language-plaintext highlighter-rouge">http://localhost:5001/</code>. You must change this so the ID-JAG token includes an audience value that identifies your actual resource app’s authorization server.</p> <p>To do this, contact the Okta XAA team to replace your app’s audience value in Okta by sending an email to [email protected]. Provide the following information to the Okta XAA team:</p> <p><strong><em>Okta Integrator Org URL:</em></strong> ‘https://{yourOktaDomain}’<br /> <strong><em>Audience:</em></strong> ‘http://yourresourceapps.authserver.org’ <strong><em>Client ID from your own OAuth server:</em></strong> [Agent0’s XAA client ID you created earlier]</p> <p>Please note that the Client ID you provide must be the client ID from your own OAuth server that was created earlier.</p> <h1 id="establish-connections-between-agent0-and-your-resource-app">Establish Connections between Agent0 and your resource app</h1> <p>Now that you have set up both requesting and resource apps, you need to establish that Agent0 can be trusted to make requests to your resource app.</p> <ul> <li>Still in Admin Console, navigate to <strong>Applications &gt; Applications &gt; Agent0</strong></li> <li>Go to the <strong>Manage Connections</strong> tab</li> <li>Under <strong>Apps providing consent</strong>, select <strong>Add resource apps</strong>, select <strong>TaskFlow</strong>, then <strong>Save</strong></li> <li>Confirm that your resource app appears under <strong>Apps providing consent</strong></li> </ul> <p>Now Agent0 and TaskFlow are connected.</p> <p><img src="/assets-jekyll/blog/xaa-resource-app/image1-723faeb6e83b3230d953dadecfc1e00b0483aa4db06c25d6c1ca30a265f504ae.jpg" alt=" " width="800" class="center-image" /></p> <h1 id="validate-that-your-resource-app-and-auth-server-work-as-intended">Validate that your Resource App and Auth Server work as intended</h1> <p>Once the Okta XAA team confirms that your app’s audience value has been updated in Okta, Agent0 can make a Token Exchange request to Okta and will receive an ID-JAG with the correct audience.</p> <p>To test the end-to-end XAA flow with Agent0 to your authorization server, create a testing client that completes the following steps:</p> <ol> <li>Agent0 signs the user in with OIDC.</li> <li>Agent0 exchanges the ID token for an ID-JAG at Okta</li> <li>Agent0 makes a token request with the ID-JAG at your authorization server</li> </ol> <p>If you need support with taking the steps above, contact [email protected].</p> <p>With testing complete, consider publicizing your resource app on the Okta Integration Network (OIN) catalog. Adding it to the catalog makes it easy for Okta’s roughly 18000 enterprise customers to learn about and add it to the suite of tools on their Okta dashboards.</p> <h1 id="learn-more-about-cross-app-access-oauth-20-and-securing-your-applications">Learn more about Cross App Access, OAuth 2.0, and securing your applications</h1> <p>If this walkthrough helped you understand more about how Cross App Access works in practice, consider learning more about</p> <p>📘 <a href="https://xaa.dev/">xaa.dev</a> - a free, open sandbox that lets you explore Cross App Access end-to-end. No local setup. No infrastructure to provision. Just a working environment where you can see the protocol in action.<br /> 📘 <a href="https://help.okta.com/oie/en-us/content/topics/apps/apps-cross-app-access.htm">Okta’s Cross App Access Documentation</a> – official guides and admin docs to configure and manage Cross App Access in production<br /> 🎙️ <a href="https://www.youtube.com/watch?v=qKs4k5Y1x_s">Okta Developer Podcast on MCP and Cross App Access</a> – hear the backstory, use cases, and why this matters for developers<br /> 📄 <a href="https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/">OAuth Identity Assertion Authorization Grant (IETF Draft)</a> – the emerging standard that powers this flow</p> Tue, 17 Feb 2026 00:00:00 -0500 https://developer.okta.com/blog/2026/02/17/xaa-resource-app https://developer.okta.com/blog/2026/02/17/xaa-resource-app Make Secure App-to-App Connections Using Cross App Access <p>Imagine you built a note-taking app. It’s so successful that LargeCorp, an aptly named large enterprise corporation, signed on as a customer. To make it a power tool for your enterprise customers, you need to allow your app to integrate with other productivity tools, such as turning a note into a task in a to-do app.</p> <p>While common integration patterns work well for individual users, these patterns create security and compliance hurdles for large organizations.</p> <h2 id="limitations-of-api-keys-and-oauth-in-enterprise-app-to-app-connectivity">Limitations of API keys and OAuth in enterprise app-to-app connectivity</h2> <p>Connecting independent apps usually involves one of two common strategies. Both have significant drawbacks when used in a corporate environment:</p> <ul> <li><strong>API keys and service accounts</strong> These lack user context. They often lead to over-privileged access and create challenging rotation requirements.</li> <li><strong>Standard OAuth 2.0</strong> A much better, industry-standard best practice over API keys and service accounts, but this relies on individual user consent. IT admins cannot see or control which apps employees connect to, creating shadow IT risks and compliance and security concerns.</li> </ul> <h2 id="cross-app-access-xaa-extends-oauth-flows-to-manage-application-access">Cross App Access (XAA) extends OAuth flows to manage application access</h2> <p>Cross App Access is an OAuth extension based on the <a href="https://drafts.oauth.net/oauth-identity-assertion-authz-grant/draft-ietf-oauth-identity-assertion-authz-grant.html">Identity Assertion Authorization Grant</a>. It addresses these challenges by using the Enterprise Identity Provider (IdP) as a central broker and was proposed by a collaborative group of organizations and interested individuals.</p> <p>With XAA, the Identity Provider (IdP) facilitates a secure token exchange. This provides three main benefits.</p> <ul> <li>IT Governance - Admins centrally manage and approve app-to-app connections</li> <li>Reduced friction - Users avoid repeated and confusing consent prompts</li> <li>Granular security - Access is limited to specific users and specific tasks.</li> </ul> <p>You can read in depth about XAA in <a href="blog/2025/06/23/enterprise-ai">Integrate Your Enterprise AI Tools with Cross App Access</a> to better understand how this works and to look at the token exchange flow</p> <article class="link-container" style="border: 1px solid silver; border-radius: 3px; padding: 12px 15px"> <a href="/blog/2025/06/23/enterprise-ai" style="font-size: 1.375em; margin-bottom: 20px;"> <span>Integrate Your Enterprise AI Tools with Cross-App Access</span> </a> <p>Manage user and non-human identities, including AI in the enterprise with Cross App Access</p> <div><div class="BlogPost-attribution"> <a href="/blog/authors/semona-igama/"> <img src="/assets-jekyll/avatar-semona-igama-03eb4c28aca3765f862b574e032d32f6f8186d04ae9f0db75bed9c74f48a9a3f.jpg" alt="avatar-avatar-semona-igama.jpeg" class="BlogPost-avatar" /> </a> <span class="BlogPost-author"> <a href="/blog/authors/semona-igama/">Semona Igama</a> </span> </div></div> </article> <p>In this tutorial, we’ll add XAA to connect a note-taking app to a to-do app using <a href="https://xaa.dev">xaa.dev</a> as our testing ground.</p> <p><strong class="hide">Table of Contents</strong></p> <ul id="markdown-toc"> <li><a href="#limitations-of-api-keys-and-oauth-in-enterprise-app-to-app-connectivity" id="markdown-toc-limitations-of-api-keys-and-oauth-in-enterprise-app-to-app-connectivity">Limitations of API keys and OAuth in enterprise app-to-app connectivity</a></li> <li><a href="#cross-app-access-xaa-extends-oauth-flows-to-manage-application-access" id="markdown-toc-cross-app-access-xaa-extends-oauth-flows-to-manage-application-access">Cross App Access (XAA) extends OAuth flows to manage application access</a></li> <li><a href="#make-app-to-app-requests-using-cross-app-access" id="markdown-toc-make-app-to-app-requests-using-cross-app-access">Make app-to-app requests using Cross App Access</a></li> <li><a href="#bring-your-own-requestor-app-to-the-xaadev-testing-site" id="markdown-toc-bring-your-own-requestor-app-to-the-xaadev-testing-site">Bring your own requestor app to the xaa.dev testing site</a></li> <li><a href="#get-the-nestjs-project-with-oauth-and-openid-connect-oidc-started" id="markdown-toc-get-the-nestjs-project-with-oauth-and-openid-connect-oidc-started">Get the NestJS project with OAuth and OpenID Connect (OIDC) started</a></li> <li><a href="#exchanging-an-id-token-for-an-access-token-for-another-app" id="markdown-toc-exchanging-an-id-token-for-an-access-token-for-another-app">Exchanging an ID token for an access token for another app</a> <ul> <li><a href="#exchange-the-id-token-for-an-intermediary-id-jag-token-type" id="markdown-toc-exchange-the-id-token-for-an-intermediary-id-jag-token-type">Exchange the ID token for an intermediary ID-JAG token type</a></li> <li><a href="#use-the-id-jag-token-to-request-an-access-token-for-a-separate-app" id="markdown-toc-use-the-id-jag-token-to-request-an-access-token-for-a-separate-app">Use the ID-JAG token to request an access token for a separate app</a></li> </ul> </li> <li><a href="#inspecting-the-xaa-token-exchange" id="markdown-toc-inspecting-the-xaa-token-exchange">Inspecting the XAA token exchange</a></li> <li><a href="#learn-more-about-xaa-and-elevating-identity-security-using-oauth" id="markdown-toc-learn-more-about-xaa-and-elevating-identity-security-using-oauth">Learn more about XAA and elevating identity security using OAuth</a></li> </ul> <h2 id="make-app-to-app-requests-using-cross-app-access">Make app-to-app requests using Cross App Access</h2> <p>We’re using <a href="https://nestjs.com/">NestJS</a> in this project. The tech stack relies on TypeScript, and we’ll use an OpenID Connect (OIDC) client library to communicate with the IdP and the to-do app’s OAuth Authorization server. Using a well-maintained OIDC client library is a best practice when creating apps that use OAuth flows, as it helps ensure you don’t make subtle errors in OAuth handshakes that compromise security.</p> <p>For this workshop, you need the following required tooling:</p> <p><strong>Required tools</strong></p> <ul> <li><a href="https://nodejs.org/en">Node.js</a> LTS version (v22 or higher at the time of this post)</li> <li>Command-line terminal application</li> <li>A code editor/Integrated development environment (IDE), such as <a href="https://code.visualstudio.com/">Visual Studio Code</a> (VS Code)</li> <li><a href="https://git-scm.com/">Git</a></li> </ul> <blockquote> <p><strong>Note</strong></p> <p>This code project is best for developers with web development and TypeScript experience and familiarity with OAuth and OpenID Connect (OIDC) flows at a high level.</p> </blockquote> <p>If you want to skip directly to the working project, you can find it <a href="https://github.com/oktadev/okta-js-xaa-requestor-example">in the GitHub repo</a>.</p> <h2 id="bring-your-own-requestor-app-to-the-xaadev-testing-site">Bring your own requestor app to the xaa.dev testing site</h2> <p>The <a href="https://xaa.dev">xaa.dev</a> testing site supports testing local client apps. It’s IdP-agnostic, meaning it’s focused on the spec and education, not on a specific company’s product line. In this scenario, we can verify whether our client app, the note-taking app, handles the token exchange with an IdP and the resource app’s authorization server. The best part about this testing site is that it’s self-contained and works out of the box. So you don’t need to create an account with an IdP, nor do you have a resource app with a conformant OAuth authorization server! We just have to bring our client code for testing! Yay for simplicity!</p> <p>You can read more about the site here:</p> <article class="link-container" style="border: 1px solid silver; border-radius: 3px; padding: 12px 15px"> <a href="/blog/2026/01/20/xaa-dev-playground" style="font-size: 1.375em; margin-bottom: 20px;"> <span>Introducing xaa.dev: A Playground for Cross App Access</span> </a> <p>Explore Cross App Access end-to-end with xaa.dev, a free, open playground that lets you test the XAA protocol without any local setup or infrastructure.</p> <div><div class="BlogPost-attribution"> <a href="/blog/authors/sohail-pathan/"> <img src="/assets-jekyll/avatar-sohail-pathan-fa148e78133752dcc86034268bffe3367e2708874b1ea957b09712e8937b8cc7.jpg" alt="avatar-avatar-sohail-pathan.jpeg" class="BlogPost-avatar" /> </a> <span class="BlogPost-author"> <a href="/blog/authors/sohail-pathan/">Sohail Pathan</a> </span> </div></div> </article> <p>Let’s register our note-taking app now.</p> <p>In your browser, navigate to <a href="https://xaa.dev">xaa.dev</a>. The main site provides information about the players in this flow, and you can test the XAA flow step by step there. Please take a moment to step through the flow to get a better sense of the code we’ll build.</p> <p>When you’re ready, navigate to <strong>Developer</strong> &gt; <strong>Register Client</strong>. Add a totally made-up email for more fun when registering.</p> <p>Select <strong>+ Register New Client</strong> and fill out the required information:</p> <ul> <li><strong>Application Name</strong> - I used “Notes App”</li> <li><strong>Redirect URIs</strong> - Enter <code class="language-plaintext highlighter-rouge">http://localhost:3000/auth/callback</code></li> <li><strong>Post-Logout Redirect URIs</strong> - Enter <code class="language-plaintext highlighter-rouge">http://localhost:3000</code></li> </ul> <p>Leave the remaining defaults as is and select <strong>Register Client</strong>.</p> <p>You’ll see a modal with the client ID and client secret. Copy both values. We need to add these to our project.</p> <h2 id="get-the-nestjs-project-with-oauth-and-openid-connect-oidc-started">Get the NestJS project with OAuth and OpenID Connect (OIDC) started</h2> <p>You’ll use a starter note-taking app project written in NestJS. Before you get too excited, remember this is a demo app. While the note-taking features are minimal, it does include built-in authentication.</p> <p>Open a terminal window and run the following commands to get a local copy of the project in a directory named <code class="language-plaintext highlighter-rouge">okta-xaa-project</code> and install dependencies. Feel free to fork the repo to track your changes.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">-b</span> starter https://github.com/oktadev/okta-js-xaa-requestor-example.git okta-xaa-project <span class="nb">cd </span>okta-xaa-project npm ci </code></pre></div></div> <p>Open the project in your IDE. Let’s go over the main components and framework choices so you don’t have to discover everything on your own:</p> <ol> <li>The NestJS project depends on <a href="https://expressjs.com/">Express</a> as the base engine and uses <a href="https://www.typescriptlang.org/">TypeScript</a>.</li> <li>Views for the landing page and the notes interface use <a href="https://mozilla.github.io/nunjucks/">Nunjucks</a> as the templating engine.</li> <li>Relies on the <a href="https://github.com/panva/openid-client/tree/main">openid-client</a> to handle all OAuth handshakes. It’s an OIDC client library for JavaScript runtimes.</li> <li>There’s a basic interceptor implementation that logs HTTP requests and responses to the console. This way, we can see the token exchange flow.</li> </ol> <p>The app requires a client ID and a client secret to run. Let’s add those to the project.</p> <p>Rename the <code class="language-plaintext highlighter-rouge">.env.example</code> file to <code class="language-plaintext highlighter-rouge">.env</code>. It already has variables defined and values added to match the URI of the XAA testing site components. Replace the <code class="language-plaintext highlighter-rouge">CLIENT_ID</code> and <code class="language-plaintext highlighter-rouge">CLIENT_SECRET</code> values with the values from the XAA testing site.</p> <p>The app should now run, but it still won’t make a successful cross-app access request. Serve the app using the command shown:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm start </code></pre></div></div> <p>Navigate to <a href="http://localhost:3000">http://localhost:3000</a>. You should see a landing page that looks like this:</p> <p><img src="/assets-jekyll/blog/xaa-client/notes-app-fab860469237f856ce30c43a5701eb5b3d3d2d578cbfa873bcc4c1879315153c.jpg" alt="The notes app landing page with a log in button in the top header" width="800" class="center-image" /></p> <p>Feel free to sign in. You’re redirected to the XAA testing site’s IdP for the user challenge. Enter the email address and any combination of numbers for the one-time password. You’ll redirect to the notes view and see something like this:</p> <p><img src="/assets-jekyll/blog/xaa-client/notes-start-ed0d8d62e286046a35c7dc90aac2f6eb1a9b2dea0416bc99185471b0c0dc80c7.jpg" alt="The notes app after signing in. The left nav has notes, the middle section displays the selected note, and the right side shows an empty todo pane" width="800" class="center-image" /></p> <p>There are no todos yet, and in the IDE’s console we see logging and errors. Each request and response to the XAA testing site’s components has a corresponding log entry. We see the IdP’s redirect with the authorization code, the <code class="language-plaintext highlighter-rouge">POST</code> to get tokens along with the request params, and a request to the todo API, which returns a <code class="language-plaintext highlighter-rouge">401 Unauthorized</code> HTTP status code. We need to add the code for the XAA token exchange. Stop serving the app by entering <kbd>Ctrl</kbd>+<kbd>C</kbd> in the terminal.</p> <h2 id="exchanging-an-id-token-for-an-access-token-for-another-app">Exchanging an ID token for an access token for another app</h2> <p>When you sign in to the note-taking app, the IdP issues an ID token. From here, the XAA token flow is a two-step process:</p> <ol> <li>The note-taking app requests the IDP’s OAuth authorization server to exchange the ID token for a trustworthy intermediary token type, an Identity Assertion JSON Web Token (JWT) also known as ID-JAG, that the todo app recognizes and supports.</li> <li>The todo app’s OAuth authorization server exchanges the intermediary token and issues an access token.</li> </ol> <p>With the access token in hand, the note-taking app can make resource requests to the todo app’s resource server.</p> <p>First, we request the trustworthy intermediary token type, the ID-JAG token.</p> <h3 id="exchange-the-id-token-for-an-intermediary-id-jag-token-type">Exchange the ID token for an intermediary ID-JAG token type</h3> <p>In the IDE, open the <code class="language-plaintext highlighter-rouge">src/auth/auth.service.ts</code> file. This file contains code for authentication and the OAuth exchange, along with some utility functions. You already have the code to sign in and have the ID token. We’ll continue using the <code class="language-plaintext highlighter-rouge">openid-client</code> library for the XAA token exchanges. Find the private helper method <code class="language-plaintext highlighter-rouge">exchangeIdTokenForIdJag()</code>. The body of the method has a comment:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// add logic to return an ID-JAG token given the user's ID token</span> </code></pre></div></div> <p>We need to replace the inner workings of this method to return the ID-JAG token instead of an empty promise. No empty promises for us! Our promises are as good as tokens. 👻</p> <p>Replace the code within the method as shown, then I’ll walk through each code block.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/** * Exchange ID token for ID-JAG token (step 1 of ID-JAG flow) */</span> <span class="k">private</span> <span class="k">async</span> <span class="nx">exchangeIdTokenForIdJag</span><span class="p">(</span> <span class="nx">config</span><span class="p">:</span> <span class="nx">openidClient</span><span class="p">.</span><span class="nx">Configuration</span><span class="p">,</span> <span class="nx">idToken</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">authServerUrl</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">resourceUrl</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">scope</span><span class="p">:</span> <span class="kr">string</span><span class="p">[],</span> <span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">tokenExchangeParams</span> <span class="o">=</span> <span class="p">{</span> <span class="na">requested_token_type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">urn:ietf:params:oauth:token-type:id-jag</span><span class="dl">'</span><span class="p">,</span> <span class="na">audience</span><span class="p">:</span> <span class="nx">authServerUrl</span><span class="p">,</span> <span class="na">resource</span><span class="p">:</span> <span class="nx">resourceUrl</span><span class="p">,</span> <span class="na">subject_token</span><span class="p">:</span> <span class="nx">idToken</span><span class="p">,</span> <span class="na">subject_token_type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">urn:ietf:params:oauth:token-type:id_token</span><span class="dl">'</span><span class="p">,</span> <span class="na">scope</span><span class="p">:</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="p">),</span> <span class="p">};</span> <span class="kd">const</span> <span class="nx">tokenExchangeResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">openidClient</span><span class="p">.</span><span class="nx">genericGrantRequest</span><span class="p">(</span> <span class="nx">config</span><span class="p">,</span> <span class="dl">'</span><span class="s1">urn:ietf:params:oauth:grant-type:token-exchange</span><span class="dl">'</span><span class="p">,</span> <span class="nx">tokenExchangeParams</span><span class="p">,</span> <span class="p">);</span> <span class="k">return</span> <span class="nx">tokenExchangeResponse</span><span class="p">.</span><span class="nx">access_token</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>In this first exchange, we call the IdP. The IdP acts as the broker between the two apps as it’s the trusted source.</p> <p>Let’s step through the key parts of the first code block where we set the token exchange parameters:</p> <ul> <li><strong><code class="language-plaintext highlighter-rouge">requested_token_type</code></strong> - we’re asking the IDP for the ID-JAG token</li> <li><strong><code class="language-plaintext highlighter-rouge">audience</code></strong> and <strong><code class="language-plaintext highlighter-rouge">resource</code></strong> - the authorization server and the todo API we’re requesting resources from</li> <li><strong><code class="language-plaintext highlighter-rouge">subject_token</code></strong> - the token we’re using for this exchange</li> <li><strong><code class="language-plaintext highlighter-rouge">subject_token_type</code></strong> - the type of the token we’re using for the exchange</li> <li><strong><code class="language-plaintext highlighter-rouge">scopes</code></strong> - the requested scopes, such as reading todos</li> </ul> <p>Once we have all these parameters set, we can call the IdP. The <code class="language-plaintext highlighter-rouge">openid-client</code> library has a function for making generic grant requests. We can use it to request the token exchange grant type. While the return value is not an access token, the grant request relies on existing OAuth models that defined the <code class="language-plaintext highlighter-rouge">access_token</code> response parameter.</p> <p>Let’s call the method so we can test it out. Find the comment:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Step 1: Exchange ID token for ID-JAG token </code></pre></div></div> <p>in the <code class="language-plaintext highlighter-rouge">exchangeIdTokenForAccessToken()</code> method.</p> <p>Add the call to the method like this:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Step 1: Exchange ID token for ID-JAG token</span> <span class="kd">const</span> <span class="nx">idJagToken</span> <span class="o">=</span> <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">exchangeIdTokenForIdJag</span><span class="p">(</span> <span class="nx">idpConfig</span><span class="p">,</span> <span class="nx">idToken</span><span class="p">,</span> <span class="nx">authServerUrl</span><span class="p">,</span> <span class="nx">resourceUrl</span><span class="p">,</span> <span class="nx">scope</span><span class="p">,</span> <span class="p">);</span> </code></pre></div></div> <p>We’re adding configuration information, including the IdP, client ID, and client secret. And we have some other required configuration values pulled from the <code class="language-plaintext highlighter-rouge">.env</code> file, such as the servers for the todo app and the scopes.</p> <p>We’ll get the signed Identity Assertion JWT Authorization grant when the call succeeds. This is a signed token from the IdP, so whenever we exchange it in the next step, the recipient knows it’s trustworthy. Step one complete. ✅</p> <p>Feel free to start the app and check the console log for your first exchange request. You should see the call to <code class="language-plaintext highlighter-rouge">LOG [OAuth HTTP] → POST idp.xaa.dev/token</code> in the console. Below that, you’ll see the token exchange parameters that look something like this:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DEBUG [OAuth HTTP] body: requested_token_type=urn:ietf:params:oauth:token-type:id-jag audience=https://auth.resource.xaa.dev resource=https://api.resource.xaa.dev/todos subject_token=eyJhbGc...IdoRppJyZmV9Q subject_token_type=urn:ietf:params:oauth:token-type:id_token scope=todos.read grant_type=urn:ietf:params:oauth:grant-type:token-exchange </code></pre></div></div> <p>The call to get todos will still fail, but you can see the first exchange request in action! 🚀</p> <h3 id="use-the-id-jag-token-to-request-an-access-token-for-a-separate-app">Use the ID-JAG token to request an access token for a separate app</h3> <p>With the ID-JAG token in hand, we can now move on to the second exchange, exchanging the ID-JAG intermediary token for an access token to the todo app. We make this exchange with the todo app’s OAuth authorization server. The IdP oversees both the note-taking app and the todo app, and trust domains between the two apps facilitate this flow. Remember, in our first exchange, we had to specify the audience for the ID-JAG token in our request - the todo app.</p> <p>Back in <code class="language-plaintext highlighter-rouge">src/auth/auth.service.ts</code>, find the comment:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// add logic to return an access token given the ID-JAG token </code></pre></div></div> <p>This comment is in the placeholder code for the <code class="language-plaintext highlighter-rouge">exchangeIdJagForAccessToken()</code> method.</p> <p>Replace the placeholder code to make the exchange. Your code will look like this:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/** * Exchange ID-JAG token for access token (step 2 of ID-JAG flow) */</span> <span class="k">private</span> <span class="k">async</span> <span class="nx">exchangeIdJagForAccessToken</span><span class="p">(</span> <span class="nx">config</span><span class="p">:</span> <span class="nx">openidClient</span><span class="p">.</span><span class="nx">Configuration</span><span class="p">,</span> <span class="nx">idJagToken</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">scope</span><span class="p">:</span> <span class="kr">string</span><span class="p">[],</span> <span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="kr">string</span><span class="o">&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">jwtBearerParams</span> <span class="o">=</span> <span class="p">{</span> <span class="na">assertion</span><span class="p">:</span> <span class="nx">idJagToken</span><span class="p">,</span> <span class="na">scope</span><span class="p">:</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="p">),</span> <span class="p">};</span> <span class="kd">const</span> <span class="nx">resourceTokenResponse</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">openidClient</span><span class="p">.</span><span class="nx">genericGrantRequest</span><span class="p">(</span> <span class="nx">config</span><span class="p">,</span> <span class="dl">'</span><span class="s1">urn:ietf:params:oauth:grant-type:jwt-bearer</span><span class="dl">'</span><span class="p">,</span> <span class="nx">jwtBearerParams</span><span class="p">,</span> <span class="p">);</span> <span class="k">return</span> <span class="nx">resourceTokenResponse</span><span class="p">.</span><span class="nx">access_token</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>We’re following a similar pattern to the first exchange, with a difference in the grant request. This time, the parameters include an assertion, the ID-JAG token. And we make the grant request to the todo app’s OAuth authorization server with the <code class="language-plaintext highlighter-rouge">urn:ietf:params:oauth:grant-type:jwt-bearer</code> grant type. This exchanges relies upon a pre-existing spec where one can use a bearer JWT for as a grant type to request an access token. That’s what we’re doing in this step.</p> <p>Next, we’ll call this method in <code class="language-plaintext highlighter-rouge">exchangeIdTokenForAccessToken()</code>.</p> <p>Find the comment:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Step 2: Exchange ID-JAG token for access token </code></pre></div></div> <p>Because we’re calling a new authorization server, the todo app’s OAuth authorization server, we first need to read the well-known discovery docs. The discovery docs include information about the authorization server, such as the server’s capabilities and endpoints, including the token endpoint. We’ve been using a custom <code class="language-plaintext highlighter-rouge">fetch</code> implementation to capture the logging you see, so we must include that implementation in <code class="language-plaintext highlighter-rouge">openid-client</code> too. Then make the call to <code class="language-plaintext highlighter-rouge">exchangeIdJagForAccessToken()</code> helper method. Your code will look like this:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Step 2: Exchange ID-JAG token for access token</span> <span class="kd">const</span> <span class="nx">resourceAuthConfig</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">openidClient</span><span class="p">.</span><span class="nx">discovery</span><span class="p">(</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">authServerUrl</span><span class="p">),</span> <span class="nx">clientId</span><span class="p">,</span> <span class="nx">clientSecret</span><span class="p">,</span> <span class="p">);</span> <span class="nx">resourceAuthConfig</span><span class="p">[</span><span class="nx">openidClient</span><span class="p">.</span><span class="nx">customFetch</span><span class="p">]</span> <span class="o">=</span> <span class="nx">loggedFetch</span><span class="p">;</span> <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">exchangeIdJagForAccessToken</span><span class="p">(</span> <span class="nx">resourceAuthConfig</span><span class="p">,</span> <span class="nx">idJagToken</span><span class="p">,</span> <span class="nx">scope</span><span class="p">,</span> <span class="p">);</span> </code></pre></div></div> <p>Make sure to remove any placeholder implementation. Step two complete. ✅</p> <p>The code to make a request to the todo API using the bearer token already exists in the project. Let’s try running the app now using <code class="language-plaintext highlighter-rouge">npm start</code>.</p> <h2 id="inspecting-the-xaa-token-exchange">Inspecting the XAA token exchange</h2> <p>After you authenticate, you’ll see the notes and the todos! 🎉</p> <p><img src="/assets-jekyll/blog/xaa-client/notes-todos-631e2643399d8e41702e2d3460371f7d6e112fc1c0f03a7b097aec8e5217e649.jpg" alt="The notes app with todos listed on the side" width="800" class="center-image" /></p> <p>In the terminal console, you’ll see each step of the handshake and requests:</p> <ol> <li>Authentication in the notes app with the IdP returning the ID token</li> <li>Exchanging the ID token for an ID-JAG token with the IDP’s OAuth authorization server</li> <li>Exchanging the IG-JAG token for an access token with the todo app’s OAuth authorization server</li> <li>Call the todo app’s resource server (the API)</li> </ol> <p>Feel free to inspect each step of this flow, the request parameters, and the responses.</p> <p>These steps allow an app to make requests to a third-party app within enterprise systems securely. You can find the completed project <a href="https://github.com/oktadev/okta-js-xaa-requestor-example">in the GitHub repo</a>.</p> <h2 id="learn-more-about-xaa-and-elevating-identity-security-using-oauth">Learn more about XAA and elevating identity security using OAuth</h2> <p>I hope you enjoyed this post on making secure cross-app requests for enterprise use cases. If you found this post interesting, I encourage you to check out these links:</p> <ul> <li><a href="/blog/2025/09/03/cross-app-access">Build Secure Agent-to-App Connections with Cross App Access (XAA)</a></li> <li><a href="https://drafts.oauth.net/oauth-identity-assertion-authz-grant/draft-ietf-oauth-identity-assertion-authz-grant.html">Identity Assertion JWT Authorization Grant</a></li> <li><a href="/blog/2024/04/30/express-universal-logout">How to Instantly Sign a User Out across All Your Apps</a></li> <li><a href="blog/2024/02/29/net-scim">How to Manage User Lifecycle with .NET and SCIM</a></li> <li><a href="/blog/2023/09/25/oauth-api-tokens">Why You Should Migrate to OAuth 2.0 From Static API Tokens</a></li> </ul> <p>Remember to follow us on <a href="https://twitter.com/oktadev">Twitter</a> and subscribe to our <a href="https://www.youtube.com/c/OktaDev/">YouTube channel</a> for more exciting content. We also want to hear from you about the topics you’d like to see and any questions you may have. Leave us a comment below!</p> Tue, 10 Feb 2026 00:00:00 -0500 https://developer.okta.com/blog/2026/02/10/xaa-client https://developer.okta.com/blog/2026/02/10/xaa-client Take User Provisioning to the Next Level with Entitlements <p>When you work on B2B SaaS apps used by large customer organizations, synchronizing those customers’ users within your software system is tricky! You must synchronize user profile information and the user attributes required for access control management. Customers with large workforces may have thousands of users to manage. They demand a speedy onboarding process, including automated user provisioning from their identity provider!</p> <p>Managing users across domains is critical to making B2B apps enterprise-scalable. In the <a href="/blog/tags/enterprise-ready-workshops/">Enterprise-Ready and Enterprise-Maturity on-demand workshop series</a>, we tackle the dilemmas faced by developers of SaaS products wanting to scale their apps to enterprise customers. We iterate on a fictitious B2B Todo app more secure and capable for enterprise customers using industry-recognized standards such as OpenID Connect (OIDC) authentication and System for Cross-Domain Identity Management (SCIM) for user provisioning. In this workshop, you build upon a previous workshop introducing automated user provisioning to add support for users’ access management and permissions attributes—their entitlements.</p> <table> <tr> <td style="font-size: 3rem;">️ℹ️</td> <td> <strong>Note</strong> <br /> This post requires Okta Identity Governance (OIG) features in your Okta org. <a href="https://developer.okta.com/signup/">Sign up for a new Integrator Free plan</a> to continue. </td> </tr> </table> <table> <thead> <tr> <th>Posts in the on-demand workshop series</th> </tr> </thead> <tbody> <tr> <td>1. <a href="/blog/2023/07/27/enterprise-ready-getting-started">How to Get Going with the On-Demand SaaS Apps Workshops</a></td> </tr> <tr> <td>2. <a href="/blog/2023/07/28/oidc_workshop">Enterprise-Ready Workshop: Authenticate with OpenID Connect</a></td> </tr> <tr> <td>3. <a href="/blog/2023/07/28/scim-workshop">Enterprise-Ready Workshop: Manage Users with SCIM</a></td> </tr> <tr> <td>4. <a href="/blog/2023/07/28/terraform-workshop">Enterprise Maturity Workshop: Terraform</a></td> </tr> <tr> <td>5. <a href="/blog/2023/09/15/workflows-workshop">Enterprise Maturity Workshop: Automate with no-code Okta Workflows</a></td> </tr> <tr> <td>6. <a href="/blog/2024/04/30/express-universal-logout">How to Instantly Sign a User Out across All Your Apps</a></td> </tr> <tr> <td>7. <strong>Take User Provisioning to the Next Level with Entitlements</strong></td> </tr> </tbody> </table> <p>This workshop walks you through adding the code to support entitlements in a sample application with three broad sections:</p> <ol> <li>Introduction to the base application, tools, and the development process</li> <li>See your application’s user and entitlements information in Okta</li> <li>Use Okta to manage user roles and custom entitlements</li> </ol> <p>If you want to skip to the completed code project for this workshop, you can find it in the <a href="https://github.com/oktadev/okta-enterprise-ready-workshops/tree/entitlements-workshop-complete"><code class="language-plaintext highlighter-rouge">entitlements-completed</code> branch on the GitHub repo</a>.</p> <p><strong class="hide">Table of Contents</strong></p> <ul id="markdown-toc"> <li><a href="#manage-users-at-scale-using-system-for-cross-domain-identity-management-scim" id="markdown-toc-manage-users-at-scale-using-system-for-cross-domain-identity-management-scim">Manage users at scale using System for Cross-domain Identity Management (SCIM)</a> <ul> <li><a href="#prepare-the-expressjs-api-project" id="markdown-toc-prepare-the-expressjs-api-project">Prepare the Express.js API project</a></li> <li><a href="#serve-the-expressjs-api-and-test-the-roles-scim-endpoint" id="markdown-toc-serve-the-expressjs-api-and-test-the-roles-scim-endpoint">Serve the Express.js API and test the <code class="language-plaintext highlighter-rouge">/Roles</code> SCIM endpoint</a></li> </ul> </li> <li><a href="#support-user-roles-in-the-database" id="markdown-toc-support-user-roles-in-the-database">Support user roles in the database</a></li> <li><a href="#connect-okta-to-the-scim-server" id="markdown-toc-connect-okta-to-the-scim-server">Connect Okta to the SCIM server</a></li> <li><a href="#create-an-okta-scim-application-for-entitlements-governance" id="markdown-toc-create-an-okta-scim-application-for-entitlements-governance">Create an Okta SCIM application for entitlements governance</a></li> <li><a href="#scim-schemas-and-resources" id="markdown-toc-scim-schemas-and-resources">SCIM schemas and resources</a> <ul> <li><a href="#harness-typescript-to-conform-to-scim-schemas" id="markdown-toc-harness-typescript-to-conform-to-scim-schemas">Harness TypeScript to conform to SCIM schemas</a></li> <li><a href="#scim-list-response" id="markdown-toc-scim-list-response">SCIM list response</a></li> <li><a href="#return-database-defined-roles-in-the-scim-roles-endpoint" id="markdown-toc-return-database-defined-roles-in-the-scim-roles-endpoint">Return database-defined roles in the SCIM <code class="language-plaintext highlighter-rouge">/Roles</code> endpoint</a></li> </ul> </li> <li><a href="#scim-resource-types" id="markdown-toc-scim-resource-types">SCIM resource types</a></li> <li><a href="#add-roles-to-the-scim-users-endpoints" id="markdown-toc-add-roles-to-the-scim-users-endpoints">Add roles to the SCIM Users endpoints</a> <ul> <li><a href="#update-the-scim-add-users-call-to-include-roles" id="markdown-toc-update-the-scim-add-users-call-to-include-roles">Update the SCIM add users call to include roles</a></li> <li><a href="#add-roles-when-getting-a-list-of-users-in-scim" id="markdown-toc-add-roles-when-getting-a-list-of-users-in-scim">Add roles when getting a list of users in SCIM</a></li> <li><a href="#update-the-users-call-so-scim-clients-can-set-their-roles" id="markdown-toc-update-the-users-call-so-scim-clients-can-set-their-roles">Update the <code class="language-plaintext highlighter-rouge">/Users</code> call so SCIM clients can set their roles</a></li> </ul> </li> <li><a href="#entitlements-discovery-in-okta" id="markdown-toc-entitlements-discovery-in-okta">Entitlements discovery in Okta</a> <ul> <li><a href="#syncing-user-entitlements" id="markdown-toc-syncing-user-entitlements">Syncing user entitlements</a></li> <li><a href="#schema-discovery-for-custom-entitlements" id="markdown-toc-schema-discovery-for-custom-entitlements">Schema discovery for custom entitlements</a></li> </ul> </li> <li><a href="#multi-tenant-use-cases-for-entitlements" id="markdown-toc-multi-tenant-use-cases-for-entitlements">Multi-tenant use cases for entitlements</a></li> <li><a href="#use-scim-to-manage-user-provisioning-and-entitlements" id="markdown-toc-use-scim-to-manage-user-provisioning-and-entitlements">Use SCIM to manage user provisioning and entitlements</a></li> </ul> <h2 id="manage-users-at-scale-using-system-for-cross-domain-identity-management-scim">Manage users at scale using System for Cross-domain Identity Management (SCIM)</h2> <p>The Todo app tech stack uses a React frontend and an Express API backend. For this workshop, you need the following required tooling:</p> <p><strong>Required tools</strong></p> <ul> <li><a href="https://nodejs.org/en">Node.js</a> v18 or higher</li> <li>Command-line terminal application</li> <li>A code editor/Integrated development environment (IDE), such as <a href="https://code.visualstudio.com/">Visual Studio Code</a> (VS Code)</li> <li>An HTTP client testing tool, such as <a href="https://www.postman.com/">Postman</a> or the <a href="https://marketplace.visualstudio.com/items?itemName=mkloubert.vscode-http-client">HTTP Client</a> VS Code extension</li> </ul> <blockquote> <p>VS Code has integrated terminals and HTTP client extensions that allow you to work out of this one application for almost everything required in this workshop. The IDE also supports TypeScript, so you’ll get quicker responses on type errors and help with importing modules.</p> </blockquote> <p>Follow the instructions in the getting started guide for installing the required tools and serving the Todo application.</p> <article class="link-container" style="border: 1px solid silver; border-radius: 3px; padding: 12px 15px"> <a href="/blog/2023/07/27/enterprise-ready-getting-started" style="font-size: 1.375em; margin-bottom: 20px;"> <span>How to Get Going with the On-Demand SaaS Apps Workshops</span> </a> <p>Start your journey to identity maturity for your SaaS applications in the enterprise-ready workshops! This post covers installing and running the base application in preparation for the upcoming workshops.</p> <div><div class="BlogPost-attribution"> <a href="/blog/authors/alisa-duncan/"> <img src="/assets-jekyll/avatar-alisa_duncan-b29fa4df50f5c99f536307c6bc0e5cb3434a922bdada7fe4f4b3cf8488299465.jpg" alt="avatar-avatar-alisa_duncan.jpeg" class="BlogPost-avatar" /> </a> <span class="BlogPost-author"> <a href="/blog/authors/alisa-duncan/">Alisa Duncan</a> </span> </div></div> </article> <p>You’ll build upon a prior workshop introducing syncing users across systems using the <a href="https://scim.cloud/">System for Cross-domain Identity Management</a> (SCIM) protocol.</p> <p>In this workshop, you’ll dive deeper into automated user provisioning by adding the user attributes required for access management, such as user roles, licensing, permissions, or something else you use to denote what actions a user has access to. The access management attributes of users are known by the generic term, user entitlements. Then, we will continue diving deeper into supporting customized user entitlements using the SCIM protocol.</p> <p>Before we get going with user entitlements, you’ll first step through the interactive and fun <a href="/blog/2023/07/28/scim-workshop">Enterprise-Ready Workshop: Manage Users with SCIM</a> workshop to get the SCIM overview, set up the code and your Okta account, and see how the protocol works. I’ll settle down with a cup of tea and a good book and wait while you learn about SCIM and are ready to continue! 🫖🍵📚</p> <article class="link-container" style="border: 1px solid silver; border-radius: 3px; padding: 12px 15px"> <a href="/blog/2023/07/28/scim-workshop" style="font-size: 1.375em; margin-bottom: 20px;"> <span>Enterprise-Ready Workshop: Manage users with SCIM</span> </a> <p>In this workshop, you will add SCIM support to a sample application, so that user changes made in your app can sync to your customer's Identity Provider!</p> <div><div class="BlogPost-attribution"> <a href="/blog/authors/semona-igama/"> <img src="/assets-jekyll/avatar-semona-igama-03eb4c28aca3765f862b574e032d32f6f8186d04ae9f0db75bed9c74f48a9a3f.jpg" alt="avatar-avatar-semona-igama.jpeg" class="BlogPost-avatar" /> </a> <span class="BlogPost-author"> <a href="/blog/authors/semona-igama/">Semona Igama</a> </span> </div></div> </article> <h3 id="prepare-the-expressjs-api-project">Prepare the Express.js API project</h3> <p>Start from a clean code project by using the SCIM workshop’s completed project code from the <a href="https://github.com/oktadev/okta-enterprise-ready-workshops/tree/scim-workshop-complete">scim-workshop-complete</a> branch. I’ll post the instructions using <a href="https://git-scm.com/">Git</a>, but you can download the code as <a href="https://github.com/oktadev/okta-enterprise-ready-workshops/archive/refs/heads/scim-workshop-complete.zip">a zip file</a> if you prefer and skip the Git command.</p> <p>Get a local copy of the completed SCIM workshop code and install dependencies by running the following commands in your terminal:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone <span class="nt">-b</span> scim-workshop-complete https://github.com/oktadev/okta-enterprise-ready-workshops.git <span class="nb">cd </span>okta-enterprise-ready-workshops npm ci </code></pre></div></div> <p>Open the code project in your IDE. We’ll work exclusively within the Express.js API for this project, and the code files for the API are in the <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src</code> directory.</p> <p>Create a file named <code class="language-plaintext highlighter-rouge">entitlements.ts</code>. We’ll define the API routes for user entitlements in the <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/entitlements.ts</code> file.</p> <p>Let’s start by hard-coding an API endpoint for <code class="language-plaintext highlighter-rouge">/Roles</code> that returns a list of roles. In the <code class="language-plaintext highlighter-rouge">entitlements.ts</code> file, add the following code:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Router</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">rolesRoute</span> <span class="o">=</span> <span class="nx">Router</span><span class="p">();</span> <span class="nx">rolesRoute</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)</span> <span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">roles</span> <span class="o">=</span> <span class="p">[</span> <span class="dl">'</span><span class="s1">Todo-er</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Admin</span><span class="dl">'</span> <span class="p">];</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">roles</span><span class="p">);</span> <span class="p">});</span> </code></pre></div></div> <p>Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/scim.ts</code>. We need to register the endpoint in the Express app by including it as part of the SCIM routes.</p> <p>At the top of the file, import <code class="language-plaintext highlighter-rouge">rolesRoutes</code></p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">rolesRoute</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./entitlements</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>At the bottom of the file below the existing code, add</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">scimRoute</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="dl">'</span><span class="s1">/Roles</span><span class="dl">'</span><span class="p">,</span> <span class="nx">rolesRoute</span><span class="p">);</span> </code></pre></div></div> <p>to register the endpoint. Let’s make sure everything works!</p> <h3 id="serve-the-expressjs-api-and-test-the-roles-scim-endpoint">Serve the Express.js API and test the <code class="language-plaintext highlighter-rouge">/Roles</code> SCIM endpoint</h3> <p>In the terminal, start the API by running</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run serve-api </code></pre></div></div> <p>This command serves the API on port 3333. Launch your HTTP client and call the <code class="language-plaintext highlighter-rouge">/Roles</code> endpoint:</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">http://localhost:3333/scim/v2/Roles</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> </code></pre></div></div> <p>Do you see a successful response with a list of roles?</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP/1.1 200 OK [ "Todo-er", "Admin" ] </code></pre></div></div> <p>Take a look at the terminal output. You’ll see output recording the <code class="language-plaintext highlighter-rouge">GET</code> request!</p> <p><img src="/assets-jekyll/blog/user-entitlements-workshop/morgan-968d824c03d48283182cce88f47f8163b3a9af02a9b98ed786a98e1a24589296.jpg" alt="Terminal output showing the GET request to the /Roles route and a 200OK HTTP response" width="800" class="center-image" /></p> <p>The project uses <a href="https://github.com/expressjs/morgan">Morgan</a>, a library that automatically adds HTTP logging to the Express API. The terminal output includes <code class="language-plaintext highlighter-rouge">POST</code> and <code class="language-plaintext highlighter-rouge">PUT</code> request payloads, so it’s an excellent way to track the SCIM calls as you work through the workshop.</p> <p>The <code class="language-plaintext highlighter-rouge">npm run serve-api</code> process watches for changes and automatically updates the API, so we don’t need to stop and restart it constantly. But we’re about to make some significant changes. Stop serving the API by entering <kbd>Ctrl</kbd>+<kbd>c</kbd> in the terminal so we can prepare the database.</p> <h2 id="support-user-roles-in-the-database">Support user roles in the database</h2> <p>The Todo app database needs to support roles; we’ve hardcoded roles so far. It’s time to bring the database to the party. A fancier SaaS app might allow each customer to define their roles. We’ll skip that level of customizability for now and focus on the simplest case. For this workshop, we’ll define supported roles for all Todo app customers instead of allowing role configurations per organization. Taking the position of application roles instead of organization roles makes our database modeling easier. I’ll discuss ways to add per-organization configurability later in the post.</p> <p>Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/prisma/schema.prisma</code>. Add the role model at the end of the file.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>model Role { id Int @id @default(autoincrement()) name String users User[] } </code></pre></div></div> <p>A user may have zero or more roles. Update the user model to add roles so that the user model looks like this:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>model User { id Int @id @default(autoincrement()) email String password String? name String Todo Todo[] org Org? @relation(fields: [orgId], references: [id]) orgId Int? externalId String? active Boolean? roles Role[] @@unique([orgId, externalId]) } </code></pre></div></div> <p>With the roles model defined, it’s time to update the database to match the model. We’ll start with a fresh, clean database for this project. In the terminal run</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx prisma migrate reset <span class="nt">-f</span> </code></pre></div></div> <p>It helps to have some seed data so we can get going. Here, we’ll define roles available within the Todo app. A user can be a “Todo-er,” “Todo Auditor,” and “Manager.” Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/prisma/seed_script.ts</code> and replace the entire file with the code below:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">PrismaClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@prisma/client</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">prisma</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">();</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">org</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">org</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">domain</span><span class="p">:</span> <span class="dl">'</span><span class="s1">gridco.example</span><span class="dl">'</span><span class="p">,</span> <span class="na">apikey</span><span class="p">:</span> <span class="dl">'</span><span class="s1">123123</span><span class="dl">'</span> <span class="p">}</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Created org Portal</span><span class="dl">'</span><span class="p">,</span> <span class="nx">org</span><span class="p">);</span> <span class="c1">// Roles defined by the Todo app</span> <span class="kd">const</span> <span class="nx">roles</span> <span class="o">=</span> <span class="p">[</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Todo-er</span><span class="dl">'</span> <span class="p">},</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Todo Auditor</span><span class="dl">'</span> <span class="p">},</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Manager</span><span class="dl">'</span><span class="p">}</span> <span class="p">];</span> <span class="kd">const</span> <span class="nx">createdRoles</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span> <span class="nx">roles</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">data</span> <span class="o">=&gt;</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">role</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span><span class="nx">data</span><span class="p">}))</span> <span class="p">);</span> <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">role</span> <span class="k">of</span> <span class="nx">createdRoles</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Created role </span><span class="dl">'</span><span class="p">,</span> <span class="nx">role</span><span class="p">);</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">somnusUser</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Somnus Henderson</span><span class="dl">'</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="dl">'</span><span class="s1">[email protected]</span><span class="dl">'</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="dl">'</span><span class="s1">correct horse battery staple</span><span class="dl">'</span><span class="p">,</span> <span class="na">orgId</span><span class="p">:</span> <span class="nx">org</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="na">externalId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">31</span><span class="dl">'</span><span class="p">,</span> <span class="na">active</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Created user Somnus</span><span class="dl">'</span><span class="p">,</span> <span class="nx">somnusUser</span><span class="p">)</span> <span class="kd">const</span> <span class="nx">trinityUser</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Trinity JustTrinity</span><span class="dl">'</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="dl">'</span><span class="s1">[email protected]</span><span class="dl">'</span><span class="p">,</span> <span class="na">password</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Zion</span><span class="dl">'</span><span class="p">,</span> <span class="na">orgId</span><span class="p">:</span> <span class="nx">org</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="na">externalId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">32</span><span class="dl">'</span><span class="p">,</span> <span class="na">active</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">roles</span><span class="p">:</span> <span class="p">{</span> <span class="na">connect</span><span class="p">:</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="nx">createdRoles</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">r</span> <span class="o">=&gt;</span> <span class="nx">r</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Todo-er</span><span class="dl">'</span><span class="p">)?.</span><span class="nx">id</span> <span class="p">}</span> <span class="p">}</span> <span class="p">},</span> <span class="p">})</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Created user Trinity</span><span class="dl">'</span><span class="p">,</span> <span class="nx">trinityUser</span><span class="p">)</span> <span class="p">}</span> <span class="nx">main</span><span class="p">()</span> <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">$disconnect</span><span class="p">()</span> <span class="p">})</span> <span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">$disconnect</span><span class="p">()</span> <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">})</span> </code></pre></div></div> <p>Save the file and run the npm script in the terminal to seed the database.</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run init-db </code></pre></div></div> <p>You’ll see console output for each newly created database record. 🎉</p> <p><strong>Inspect the database records</strong></p> <p>You can inspect the database records using <a href="https://www.prisma.io/studio">Prisma Studio</a>. In a separate terminal, run</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx prisma studio </code></pre></div></div> <p>which launches a web interface to view the database. The site URL is usually <code class="language-plaintext highlighter-rouge">http://localhost:5555</code>, shown in the terminal output. Open the site in your browser to view the database tables, records, and relationships.</p> <h2 id="connect-okta-to-the-scim-server">Connect Okta to the SCIM server</h2> <p>The SCIM Client (the identity provider, Okta) makes requests upon objects held by the SCIM Server (the Todo app).</p> <p><img src="/assets-jekyll/blog/scim-workshop/scim-diagram-4cfb2cb57d2031d826a9221b3fdf59284e2df35657a79c53118f3bb776be0440.jpg" alt="SCIM workflow showing the Identity Provider requests the SCIM server with GET, POST, PUT, and DEL user calls and the SCIM server responds with a standard SCIM interface" width="800" class="center-image" /></p> <p>First, we need to serve the API so Okta can access it. You’ll use a temporary tunnel for local development that makes <code class="language-plaintext highlighter-rouge">localhost:3333</code> publicly accessible so that Okta, the SCIM client, can call your API, the SCIM server. I’ll include the instructions using an NPM library that we don’t have to install or sign up for, but feel free to use your favorite tunneling system if you have one.</p> <p>You need two terminal sessions.</p> <p>In one terminal, serve the API using the command:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run serve-api </code></pre></div></div> <p>In the second terminal, you’ll run the local tunnel. Run the command:</p> <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx localtunnel <span class="nt">--port</span> 3333 </code></pre></div></div> <p>This creates a tunnel for the application serving on port 3333. The console output displays the tunnel URL in the format <code class="language-plaintext highlighter-rouge">https://{yourTunnelSubdomain}.loca.lt</code>, such as:</p> <div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">your URL is: https://awesome-devs-club.loca.lt </span></code></pre></div></div> <p>You’ll need this tunnel URL to configure the Okta application.</p> <h2 id="create-an-okta-scim-application-for-entitlements-governance">Create an Okta SCIM application for entitlements governance</h2> <p>In the prerequisite SCIM workshop, you added a SCIM application in Okta to connect to the Todo app. We must do something similar to connect SCIM with entitlements support.</p> <p>Sign into your <a href="https://developer.okta.com/login/">Okta Integrator Free account</a>. In the Admin Console, navigate to <strong>Applications</strong> &gt; <strong>Applications</strong>. Press the <strong>Browse App Catalog</strong> button to create a new Okta SCIM application.</p> <p>In the search bar, search for “(Header Auth) Governance with SCIM 2.0” and select the app. Press <strong>Add Integration</strong>.</p> <p>You’ll see a configuration view with two tabs. Press <strong>Next</strong> on the <strong>General settings</strong> tab. Leave default settings on the <strong>Sign-On Options</strong> tab and press <strong>Done</strong>.</p> <p>You’ll navigate to your newly created Okta application to add specific configurations about the Todo app.</p> <p>First, you need to enable Identity Governance. Navigate to the <strong>General</strong> tab and find the <strong>Identity Governance</strong> section. Press <strong>Edit</strong> to select <strong>Enabled</strong> for <strong>Governance Engine</strong>. Remember to <strong>Save</strong> your change.</p> <p>Navigate to the <strong>Provisioning</strong> tab and press the <strong>Configure API Integration</strong> button. Check the <strong>Enable API integration</strong> checkbox—two more form fields display.</p> <ul> <li> <p>In <strong>Base URL</strong> field, enter <code class="language-plaintext highlighter-rouge">https://{yourTunnelSubdomain}.loca.lt/scim/v2</code>.</p> <p>It will look like <code class="language-plaintext highlighter-rouge">https://awesome-devs-club.loca.lt/scim/v2</code></p> </li> <li> <p>In the <strong>API Token</strong> field, enter <code class="language-plaintext highlighter-rouge">Bearer 123123</code></p> </li> </ul> <p>press <strong>Save</strong>.</p> <p>The <strong>Provisioning</strong> tab has more options to configure within the <strong>Settings</strong> side nav.</p> <p>Navigate to the <strong>To App</strong> option and press <strong>Edit</strong>.</p> <ul> <li>Enable <strong>Create Users</strong></li> <li>Enable <strong>Update User Attributes</strong></li> <li>Enable <strong>Deactivate Users</strong></li> </ul> <p>Press <strong>Save</strong>.</p> <p>Import users from the todo app into Okta. Navigate to the <strong>Import</strong> tab and press the <strong>Import Now</strong> button. Okta discovers users in your app and tries to match them with users already defined in Okta. A dialog shows Okta discovered the two users you added using the DB script. Select both users and press the <strong>Confirm Assignments</strong> to confirm the assignments.</p> <p>You’ll see the imported users in the <strong>Assignments</strong> tab. But what about entitlements? They’re coming right up!</p> <p>Stop the tunnel and the API using the <kbd>Ctrl</kbd>+<kbd>c</kbd> command in the terminal windows. We’ll make some changes to the API that won’t automatically reflect in the local tunnel, so we’ll get all our entitlements changes made and resynchronize with Okta.</p> <h2 id="scim-schemas-and-resources">SCIM schemas and resources</h2> <p>In the first SCIM workshop, you learned about SCIM’s <code class="language-plaintext highlighter-rouge">User</code> resource and built out operations around the user. You updated only a handful of user properties in the workshop, but SCIM is way more powerful thanks to its superpower – <em>extensibility</em>. ✨ User is not the only resource type defined in SCIM.</p> <p>A <code class="language-plaintext highlighter-rouge">Resource</code> represents an object SCIM operates on, such as a user or group. SCIM identified core properties each <code class="language-plaintext highlighter-rouge">Resource</code> must define, such as <code class="language-plaintext highlighter-rouge">id</code> and a link to the resource’s schema definition. From there, a user extends from the core properties and adds attributes specific to the object, such as adding <code class="language-plaintext highlighter-rouge">userName</code> and their emails. A standard published schema exists for all those user-specific attributes within the SCIM spec. You can continue extending resources as needed to represent new resources, such as another SCIM standard-defined schema for Enterprise User.</p> <p><img src="/assets-jekyll/blog/user-entitlements-workshop/resource-class-diagram-696cc8f04feb6e62c81cc68743f76254c52319cfd1c26411d6933db9274a183d.svg" alt="Class diagram representing core Resource properties, User class extending from core Resource adds username and emails properties. The Enterprise User class extends from User adds department and costCenter properties. Group class extends from core Resource and adds displayName and members properties. Other class extends from core Resource demonstrating new resource representations." width="800" class="center-image" /></p> <p>What’s an example resource other than a user or group? If you said “role” or an “entitlement,” you’re correct! Those resource types must have an <code class="language-plaintext highlighter-rouge">id</code> and <code class="language-plaintext highlighter-rouge">schemas</code>. Here, Okta used SCIM’s extensibility to define a new resource type.</p> <p><img src="/assets-jekyll/blog/user-entitlements-workshop/resource-role-class-diagram-db8aa8eb1908e9bbbcfe2bae7f02a814ec732e4dc925ae761a629c159839dfd2.svg" alt="Class diagram representing core Resource properties. The User, Group, OktaRole, and Other class extends from core Resource." width="800" class="center-image" /></p> <p>Okta defines a schema for the <code class="language-plaintext highlighter-rouge">Role</code> representation. We can use the schema to ensure we conform to the definition.</p> <h3 id="harness-typescript-to-conform-to-scim-schemas">Harness TypeScript to conform to SCIM schemas</h3> <p>We can define an interface to model the <code class="language-plaintext highlighter-rouge">Role</code> representation. Add a new file to the project named <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/scim-types.ts</code> and open it up in the IDE. This file will contain the SCIM schema definitions, such as the SCIM core <code class="language-plaintext highlighter-rouge">Resource</code>. Each interface defines required and optional properties and the property’s type.</p> <p>Copy and paste the first interface for the SCIM resource into the <code class="language-plaintext highlighter-rouge">scim-types.ts</code> file.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">IScimResource</span> <span class="p">{</span> <span class="nl">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">schemas</span><span class="p">:</span> <span class="kr">string</span><span class="p">[];</span> <span class="nl">meta</span><span class="p">?:</span> <span class="nx">IMetadata</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>A SCIM resource has an optional meta property containing the resource’s metadata. Your IDE shows errors, so we can fix this by adding the <code class="language-plaintext highlighter-rouge">IMetadata</code> definition to the file below the <code class="language-plaintext highlighter-rouge">IScimResource</code>:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">IMetadata</span> <span class="p">{</span> <span class="nl">resourceType</span><span class="p">:</span> <span class="nx">RESOURCE_TYPES</span><span class="p">;</span> <span class="nl">location</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>You’ll have a new error for <code class="language-plaintext highlighter-rouge">RESOURCE_TYPES</code>. We’ll fix it soon.</p> <p>Now, on to the Okta <code class="language-plaintext highlighter-rouge">Role</code> representation. The role representation extends from the core SCIM resource and adds extra properties. Okta’s schema overlaps with the SCIM standard User <code class="language-plaintext highlighter-rouge">roles</code> field, which includes a property for <code class="language-plaintext highlighter-rouge">display</code> text. Define the interface and add it to <code class="language-plaintext highlighter-rouge">IMetadata</code> below.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">IOktaRole</span> <span class="kd">extends</span> <span class="nx">IScimResource</span><span class="p">{</span> <span class="nl">displayName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">IOktaRole</code> extends from the core <code class="language-plaintext highlighter-rouge">IScimResource</code> interface and adds a new required property, <code class="language-plaintext highlighter-rouge">displayName</code>. Each resource requires a schema, a Uniform Resource Namespace (URN) string. Instead of repeatedly typing the string for each role resource, define it below the <code class="language-plaintext highlighter-rouge">IOktaRole</code> interface for reusability</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">SCHEMA_OKTA_ROLE</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">urn:okta:scim:schemas:core:1.0:Role</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>Let’s fix the <code class="language-plaintext highlighter-rouge">RESOURCE_TYPES</code> error. Below the <code class="language-plaintext highlighter-rouge">SCHEMA_OKTA_ROLE</code> constant, add the following:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">type</span> <span class="nx">RESOURCE_TYPES</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Role</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>You can use the <code class="language-plaintext highlighter-rouge">IOktaRole</code> interface in the <code class="language-plaintext highlighter-rouge">/Roles</code> endpoint to ensure the response matches the expected structure. Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/entitlements.ts</code>, and update the code to use the interface.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Router</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">IOktaRole</span><span class="p">,</span> <span class="nx">SCHEMA_OKTA_ROLE</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./scim-types</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">rolesRoute</span> <span class="o">=</span> <span class="nx">Router</span><span class="p">();</span> <span class="nx">rolesRoute</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)</span> <span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="na">roles</span><span class="p">:</span> <span class="nx">IOktaRole</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_OKTA_ROLE</span><span class="p">],</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">one</span><span class="dl">'</span><span class="p">,</span> <span class="na">displayName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Todo-er</span><span class="dl">'</span> <span class="p">}];</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">roles</span><span class="p">);</span> <span class="p">});</span> </code></pre></div></div> <blockquote> <p><strong>Why use TypeScript and interfaces?</strong></p> <p>TypeScript, a superset of JavaScript, supports type safety. Type safety means we’ll catch errors within the IDE or at build time instead of getting caught by surprise with a runtime error. Here, we state the <code class="language-plaintext highlighter-rouge">roles</code> array is of type <code class="language-plaintext highlighter-rouge">IOktaRole[]</code>. Try commenting out the required <code class="language-plaintext highlighter-rouge">schemas</code> property. You’ll see an error in an IDE that supports TypeScript or when you try to serve the API as console output. We can use type safety to ensure we meet the expectations of required SCIM properties in our calls.</p> <p><img src="/assets-jekyll/blog/user-entitlements-workshop/type-error-e1d841636e4957ba250a1e85269ef5807987fd47bd070653fd8289d6737d20d6.jpg" alt="IDE and terminal showing the type error when `schemas` is commented out" width="600" class="center-image" /></p> </blockquote> <p>Every code change deserves a quick check. Serve the API and double check everything still works for you when you make the HTTP call to</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">http://localhost:3333/scim/v2/Roles</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> </code></pre></div></div> <p>Do you see the one ‘Todo-er’ role in the response? ✅</p> <h3 id="scim-list-response">SCIM list response</h3> <p>We return the array of Okta roles directly in the API response, but this format doesn’t match SCIM list responses. SCIM has a structured response format for lists and a defined schema. This way, SCIM structures all communication between the client and the server so each side knows how to format and parse data.</p> <p>Let’s define the <code class="language-plaintext highlighter-rouge">ListResponse</code> interface. Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/scim-types.ts</code>. The list response contains standard information supporting pagination, the schema for the list response, and the list of objects. Add the interface to the file. I like to organize my definitions, so I added the code between the <code class="language-plaintext highlighter-rouge">IOktaRole</code> interface and <code class="language-plaintext highlighter-rouge">SCHEMA_OKTA_ROLE</code> string constant.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">IListResponse</span> <span class="p">{</span> <span class="nl">schemas</span><span class="p">:</span> <span class="kr">string</span><span class="p">[];</span> <span class="nl">totalResults</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">startIndex</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">itemsPerPage</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">Resources</span><span class="p">:</span> <span class="nx">IOktaRole</span><span class="p">[];</span> <span class="p">}</span> </code></pre></div></div> <p>The list response also has a schema URN. Create a constant for this string as you did for the Okta role and add it after the role schema string.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">SCHEMA_LIST_RESPONSE</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">urn:ietf:params:scim:api:messages:2.0:ListResponse</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>The API response must match the list format. Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/entitlements.ts</code> and add <code class="language-plaintext highlighter-rouge">IListResponse</code> and <code class="language-plaintext highlighter-rouge">SCHEMA_LIST_RESPONSE</code> to the imports from the <code class="language-plaintext highlighter-rouge">scim-types</code> file:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">IListResponse</span><span class="p">,</span> <span class="nx">IOktaRole</span><span class="p">,</span> <span class="nx">SCHEMA_LIST_RESPONSE</span><span class="p">,</span> <span class="nx">SCHEMA_OKTA_ROLE</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./scim-types</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>Change <code class="language-plaintext highlighter-rouge">rolesRoute</code> response to use the list response:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">rolesRoute</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)</span> <span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="na">roles</span><span class="p">:</span> <span class="nx">IOktaRole</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_OKTA_ROLE</span><span class="p">],</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">one</span><span class="dl">'</span><span class="p">,</span> <span class="na">displayName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Todo-er</span><span class="dl">'</span> <span class="p">}];</span> <span class="kd">const</span> <span class="na">listResponse</span><span class="p">:</span> <span class="nx">IListResponse</span> <span class="o">=</span> <span class="p">{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_LIST_RESPONSE</span><span class="p">],</span> <span class="na">totalResults</span><span class="p">:</span> <span class="nx">roles</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="na">itemsPerPage</span><span class="p">:</span> <span class="nx">roles</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="na">startIndex</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">Resources</span><span class="p">:</span> <span class="nx">roles</span> <span class="p">};</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">listResponse</span><span class="p">);</span> <span class="p">});</span> </code></pre></div></div> <p>Double-check everything still works. Send the HTTP request to your API. ✅</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">http://localhost:3333/scim/v2/Roles</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> </code></pre></div></div> <h3 id="return-database-defined-roles-in-the-scim-roles-endpoint">Return database-defined roles in the SCIM <code class="language-plaintext highlighter-rouge">/Roles</code> endpoint</h3> <p>Each role has an ID and a name. We can retrieve the roles from the database and populate the <code class="language-plaintext highlighter-rouge">/Roles</code> response.</p> <p>Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/entitlements.ts</code> and make the changes to retrieve the roles from the database and map the database results to the <code class="language-plaintext highlighter-rouge">IOktaRole</code> properties. You’ll need to import some dependencies, so ensure the import statements match. The SCIM <code class="language-plaintext highlighter-rouge">ListResponse</code> supports pagination, so we’ll add the required code to consider the query parameters.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Router</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">PrismaClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@prisma/client</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">IListResponse</span><span class="p">,</span> <span class="nx">IOktaRole</span><span class="p">,</span> <span class="nx">SCHEMA_LIST_RESPONSE</span><span class="p">,</span> <span class="nx">SCHEMA_OKTA_ROLE</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./scim-types</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">prisma</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">();</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">rolesRoute</span> <span class="o">=</span> <span class="nx">Router</span><span class="p">();</span> <span class="nx">rolesRoute</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)</span> <span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">startIndex</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">query</span><span class="p">.</span><span class="nx">startIndex</span> <span class="k">as</span> <span class="kr">string</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">recordLimit</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">query</span><span class="p">.</span><span class="nx">recordLimit</span> <span class="k">as</span> <span class="kr">string</span> <span class="o">??</span> <span class="dl">'</span><span class="s1">100</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">roles</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">role</span><span class="p">.</span><span class="nx">findMany</span><span class="p">({</span> <span class="na">take</span><span class="p">:</span> <span class="nx">recordLimit</span><span class="p">,</span> <span class="na">skip</span><span class="p">:</span> <span class="nx">startIndex</span> <span class="o">-</span> <span class="mi">1</span> <span class="p">});</span> <span class="kd">const</span> <span class="na">listResponse</span><span class="p">:</span> <span class="nx">IListResponse</span> <span class="o">=</span> <span class="p">{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_LIST_RESPONSE</span><span class="p">],</span> <span class="na">totalResults</span><span class="p">:</span> <span class="nx">roles</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="nx">startIndex</span><span class="p">,</span> <span class="na">itemsPerPage</span><span class="p">:</span> <span class="nx">recordLimit</span><span class="p">,</span> <span class="na">Resources</span><span class="p">:</span> <span class="nx">roles</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">role</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_OKTA_ROLE</span><span class="p">],</span> <span class="na">id</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="na">displayName</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">name</span> <span class="p">}))</span> <span class="p">};</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">listResponse</span><span class="p">);</span> <span class="p">});</span> </code></pre></div></div> <p>Run a quick check to ensure everything still works. Serve the API and call the <code class="language-plaintext highlighter-rouge">/Roles</code> endpoint using your HTTP client. ✅</p> <p>You should see three roles matching the roles in the database. 🎉</p> <h2 id="scim-resource-types">SCIM resource types</h2> <p>We implemented the <code class="language-plaintext highlighter-rouge">/Roles</code> endpoint and discussed how SCIM defines a resource. But how would the SCIM client know about this Okta Role type? Enter discovery—learning about a SCIM server’s capabilities and supported objects such as resources!</p> <p>SCIM clients and servers communicate about the types of resources through a standard endpoint, the<code class="language-plaintext highlighter-rouge">/ResourceType</code> endpoint. SCIM clients call the endpoint to discover what resources they can expect. The endpoint returns a SCIM list response outlining resources. You can add every resource type used, including the standard <code class="language-plaintext highlighter-rouge">User</code> and <code class="language-plaintext highlighter-rouge">EnterpriseUser</code> resources, but Okta expects resource definitions only for custom types.</p> <p>First, we’ll create the interface for the <code class="language-plaintext highlighter-rouge">ResourceType</code> and define some strings. Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/scim-types.ts</code>. Add the interface for <code class="language-plaintext highlighter-rouge">IResourceType</code> above the <code class="language-plaintext highlighter-rouge">IListResponse</code> interface.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">IResourceType</span> <span class="p">{</span> <span class="nl">id</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">schemas</span><span class="p">:</span> <span class="kr">string</span><span class="p">[];</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">description</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">endpoint</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">schema</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">meta</span><span class="p">:</span> <span class="nx">IMetadata</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Notice the <code class="language-plaintext highlighter-rouge">IResourceType</code> doesn’t extend from the <code class="language-plaintext highlighter-rouge">IScimResource</code> interface. For example, the SCIM standard doesn’t require <code class="language-plaintext highlighter-rouge">id</code> for a resource type. Since the SCIM standard treats <code class="language-plaintext highlighter-rouge">ResourceType</code> as an exception case of <code class="language-plaintext highlighter-rouge">Resource</code>, we defined it separately without the relation instead of extending from <code class="language-plaintext highlighter-rouge">IScimResource</code>.</p> <p>When following the SCIM protocol, responses that list values, such as the list of roles or resource types, use the SCIM list response format.</p> <p>The <code class="language-plaintext highlighter-rouge">IListResource</code> interface must support <code class="language-plaintext highlighter-rouge">IOktaRole</code> and <code class="language-plaintext highlighter-rouge">IResourceType</code>. Using <a href="https://www.typescriptlang.org/docs/handbook/2/generics.html">generics</a> and <a href="https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types">union types</a>, we can support different list response objects . Update the <code class="language-plaintext highlighter-rouge">IListResource</code> to match the code below.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">IListResponse</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">IScimResource</span> <span class="o">|</span> <span class="nx">IResourceType</span><span class="o">&gt;</span> <span class="p">{</span> <span class="na">schemas</span><span class="p">:</span> <span class="kr">string</span><span class="p">[];</span> <span class="nl">totalResults</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">startIndex</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">itemsPerPage</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">Resources</span><span class="p">:</span> <span class="nx">T</span><span class="p">[];</span> <span class="p">}</span> </code></pre></div></div> <p>You’ll see errors in the IDE and, if you’re running the API, within the console output. No worries; we’ll fix those errors soon!</p> <p>Resource types have a schema URN and use “ResourceType” as the <code class="language-plaintext highlighter-rouge">resourceType</code> string in the metadata. Add <code class="language-plaintext highlighter-rouge">SCHEMA_RESOURCE_TYPE</code> and edit <code class="language-plaintext highlighter-rouge">RESOURCE_TYPES</code> so your string constants section looks like the code below.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">SCHEMA_OKTA_ROLE</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">urn:okta:scim:schemas:core:1.0:Role</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">SCHEMA_LIST_RESPONSE</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">urn:ietf:params:scim:api:messages:2.0:ListResponse</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">SCHEMA_RESOURCE_TYPE</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">urn:ietf:params:scim:schemas:core:2.0:ResourceType</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="kd">type</span> <span class="nx">RESOURCE_TYPES</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Role</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">ResourceType</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/entitlements.ts</code>. Let’s fix the <code class="language-plaintext highlighter-rouge">IListResponse</code> error for the <code class="language-plaintext highlighter-rouge">/Roles</code> endpoint and specify the object type in the list, the <code class="language-plaintext highlighter-rouge">IOktaRole</code> type. The code building out the list changes to</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">const</span> <span class="nx">listResponse</span><span class="p">:</span> <span class="nx">IListResponse</span><span class="o">&lt;</span><span class="nx">IOktaRole</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_LIST_RESPONSE</span><span class="p">],</span> <span class="na">totalResults</span><span class="p">:</span> <span class="nx">roles</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="nx">startIndex</span><span class="p">,</span> <span class="na">itemsPerPage</span><span class="p">:</span> <span class="nx">recordLimit</span><span class="p">,</span> <span class="na">Resources</span><span class="p">:</span> <span class="nx">roles</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">role</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_OKTA_ROLE</span><span class="p">],</span> <span class="na">id</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="na">displayName</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">name</span> <span class="p">}))</span> <span class="p">};</span> </code></pre></div></div> <p>You shouldn’t see errors anymore! 🎉</p> <p>We have a new endpoint to add. Update the imports from the <code class="language-plaintext highlighter-rouge">./scim-types</code> file and declare a new route for resource types.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Router</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">PrismaClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@prisma/client</span><span class="dl">'</span><span class="p">;</span> <span class="k">import</span> <span class="p">{</span> <span class="nx">IListResponse</span><span class="p">,</span> <span class="nx">IOktaRole</span><span class="p">,</span> <span class="nx">IResourceType</span><span class="p">,</span> <span class="nx">SCHEMA_LIST_RESPONSE</span><span class="p">,</span> <span class="nx">SCHEMA_OKTA_ROLE</span><span class="p">,</span> <span class="nx">SCHEMA_RESOURCE_TYPE</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./scim-types</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">prisma</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PrismaClient</span><span class="p">();</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">rolesRoute</span> <span class="o">=</span> <span class="nx">Router</span><span class="p">();</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">resourceTypesRoute</span> <span class="o">=</span> <span class="nx">Router</span><span class="p">();</span> <span class="c1">// existing rolesRoute code below</span> </code></pre></div></div> <p>Then create the <code class="language-plaintext highlighter-rouge">/ResourceTypes</code> route by adding the code below the <code class="language-plaintext highlighter-rouge">rolesRoute</code></p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resourceTypesRoute</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)</span> <span class="p">.</span><span class="kd">get</span><span class="p">((</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="na">resourceTypes</span><span class="p">:</span> <span class="nx">IResourceType</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_RESOURCE_TYPE</span><span class="p">],</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Role</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Role</span><span class="dl">'</span><span class="p">,</span> <span class="na">endpoint</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/Roles</span><span class="dl">'</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Roles you can set on users of Todo App</span><span class="dl">'</span><span class="p">,</span> <span class="na">schema</span><span class="p">:</span> <span class="nx">SCHEMA_OKTA_ROLE</span><span class="p">,</span> <span class="na">meta</span><span class="p">:</span> <span class="p">{</span> <span class="na">resourceType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ResourceType</span><span class="dl">'</span> <span class="p">}</span> <span class="p">}];</span> <span class="kd">const</span> <span class="na">resourceTypesListResponse</span><span class="p">:</span> <span class="nx">IListResponse</span><span class="o">&lt;</span><span class="nx">IResourceType</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_LIST_RESPONSE</span><span class="p">],</span> <span class="na">totalResults</span><span class="p">:</span> <span class="nx">resourceTypes</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="na">startIndex</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">itemsPerPage</span><span class="p">:</span> <span class="nx">resourceTypes</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span> <span class="na">Resources</span><span class="p">:</span> <span class="nx">resourceTypes</span> <span class="p">};</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">resourceTypesListResponse</span><span class="p">);</span> <span class="p">});</span> </code></pre></div></div> <p>Next, you must register the <code class="language-plaintext highlighter-rouge">/ResourceTypes</code> route in the API. Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/scim.ts</code>.</p> <p>Update the import to include <code class="language-plaintext highlighter-rouge">resourceTypesRoute</code></p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">resourceTypesRoute</span><span class="p">,</span> <span class="nx">rolesRoute</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./entitlements</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>Add the <code class="language-plaintext highlighter-rouge">/ResourceTypes</code> endpoint to the end of the file. You should have two routes defined.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">scimRoute</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="dl">'</span><span class="s1">/Roles</span><span class="dl">'</span><span class="p">,</span> <span class="nx">rolesRoute</span> <span class="p">);</span> <span class="nx">scimRoute</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="dl">'</span><span class="s1">/ResourceTypes</span><span class="dl">'</span><span class="p">,</span> <span class="nx">resourceTypesRoute</span><span class="p">);</span> </code></pre></div></div> <p>Double-check your new route by starting the API if it’s not running. Use your HTTP client to make the call</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">http://localhost:3333/scim/v2/ResourceTypes</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> </code></pre></div></div> <p>If you see a response with the Okta role resource type, the API call works as expected! ✅</p> <h2 id="add-roles-to-the-scim-users-endpoints">Add roles to the SCIM Users endpoints</h2> <p>Let’s add roles to the existing user calls. We want to reflect a user’s roles in Okta within the Todo app, so the GET and POST <code class="language-plaintext highlighter-rouge">/Users</code> calls must support roles. Near the top of the <code class="language-plaintext highlighter-rouge">scim.ts</code> file, find <code class="language-plaintext highlighter-rouge">IUserSchema</code> interface.</p> <p>Update the interface to add the <code class="language-plaintext highlighter-rouge">roles</code> property:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">interface</span> <span class="nx">IUserSchema</span> <span class="p">{</span> <span class="nl">schemas</span><span class="p">:</span> <span class="kr">string</span><span class="p">[];</span> <span class="nl">userName</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">id</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">name</span><span class="p">?:</span> <span class="p">{</span> <span class="na">givenName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">familyName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">};</span> <span class="nl">emails</span><span class="p">?:</span> <span class="p">{</span><span class="na">primary</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">}[];</span> <span class="nl">displayName</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">locale</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">meta</span><span class="p">?:</span> <span class="p">{</span> <span class="na">resourceType</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> <span class="nl">externalId</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">groups</span><span class="p">?:</span> <span class="p">[];</span> <span class="nl">password</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">active</span><span class="p">?:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="nl">detail</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">status</span><span class="p">?:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">roles</span><span class="p">?:</span> <span class="p">{</span><span class="na">value</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">display</span><span class="p">:</span> <span class="kr">string</span><span class="p">}[];</span> <span class="p">}</span> </code></pre></div></div> <p>The User SCIM schema defines <code class="language-plaintext highlighter-rouge">roles</code> property as a list of objects that may contain properties named <code class="language-plaintext highlighter-rouge">value</code> and <code class="language-plaintext highlighter-rouge">display</code>, among others. Okta uses these properties for role data.</p> <h3 id="update-the-scim-add-users-call-to-include-roles">Update the SCIM add users call to include roles</h3> <p>The first route defined is the <code class="language-plaintext highlighter-rouge">POST /Users</code> route definition. You need to add roles when saving to the database. Find the comment</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Create the User in the database</span> </code></pre></div></div> <p>and update the database command and the as shown.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Create the User in the database</span> <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">org</span> <span class="p">:</span> <span class="p">{</span> <span class="na">connect</span><span class="p">:</span> <span class="p">{</span><span class="na">id</span><span class="p">:</span> <span class="nx">ORG_ID</span><span class="p">}},</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">email</span><span class="p">,</span> <span class="nx">password</span><span class="p">,</span> <span class="nx">externalId</span><span class="p">,</span> <span class="nx">active</span><span class="p">,</span> <span class="na">roles</span><span class="p">:</span> <span class="p">{</span> <span class="na">connect</span><span class="p">:</span> <span class="nx">newUser</span><span class="p">.</span><span class="nx">roles</span><span class="p">?.</span><span class="nx">map</span><span class="p">(</span><span class="nx">role</span> <span class="o">=&gt;</span> <span class="p">({</span><span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">role</span><span class="p">.</span><span class="nx">value</span><span class="p">)}))</span> <span class="o">||</span> <span class="p">[]</span> <span class="p">}</span> <span class="p">},</span> <span class="na">include</span><span class="p">:</span> <span class="p">{</span> <span class="na">roles</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">});</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Account Created ID: </span><span class="dl">'</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span> </code></pre></div></div> <p>One more place to update in the <code class="language-plaintext highlighter-rouge">POST /Users</code> call. We need to return the roles in the response. Right below the <code class="language-plaintext highlighter-rouge">console.log()</code> update the <code class="language-plaintext highlighter-rouge">userResponse</code> to</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">userResponse</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">defaultUserSchema</span><span class="p">,</span> <span class="na">id</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">user</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="na">userName</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="p">{</span> <span class="nx">givenName</span><span class="p">,</span> <span class="nx">familyName</span> <span class="p">},</span> <span class="na">emails</span><span class="p">:</span> <span class="p">[{</span> <span class="na">primary</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">work</span><span class="dl">"</span> <span class="p">}],</span> <span class="na">displayName</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span> <span class="na">externalId</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">externalId</span><span class="p">,</span> <span class="na">active</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">active</span><span class="p">,</span> <span class="na">roles</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">roles</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">role</span> <span class="o">=&gt;</span> <span class="p">({</span><span class="na">display</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">toString</span><span class="p">()}))</span> <span class="p">};</span> </code></pre></div></div> <h3 id="add-roles-when-getting-a-list-of-users-in-scim">Add roles when getting a list of users in SCIM</h3> <p>Continuing to the <code class="language-plaintext highlighter-rouge">GET /Users</code> call, search for the code to find users in the database</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">findMany</span><span class="p">({...});</span> </code></pre></div></div> <p>to add <code class="language-plaintext highlighter-rouge">roles</code> to the <code class="language-plaintext highlighter-rouge">select</code> argument.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">findMany</span><span class="p">({</span> <span class="na">take</span><span class="p">:</span> <span class="nx">recordLimit</span><span class="p">,</span> <span class="na">skip</span><span class="p">:</span> <span class="nx">startIndex</span><span class="p">,</span> <span class="na">select</span><span class="p">:</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">externalId</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">active</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">roles</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="nx">where</span> <span class="p">});</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">GET /Users</code> response also needs roles, so update the</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">usersResponse</span><span class="p">[</span><span class="dl">'</span><span class="s1">Resources</span><span class="dl">'</span><span class="p">]</span> <span class="o">=</span> <span class="nx">users</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">user</span> <span class="o">=&gt;</span> <span class="p">{...});</span> </code></pre></div></div> <p>like this.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">usersResponse</span><span class="p">[</span><span class="dl">'</span><span class="s1">Resources</span><span class="dl">'</span><span class="p">]</span> <span class="o">=</span> <span class="nx">users</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">user</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="p">[</span><span class="nx">givenName</span><span class="p">,</span> <span class="nx">familyName</span><span class="p">]</span> <span class="o">=</span> <span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">"</span><span class="s2"> </span><span class="dl">"</span><span class="p">);</span> <span class="k">return</span> <span class="p">{</span> <span class="p">...</span><span class="nx">defaultUserSchema</span><span class="p">,</span> <span class="na">id</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="na">userName</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="p">{</span> <span class="nx">givenName</span><span class="p">,</span> <span class="nx">familyName</span> <span class="p">},</span> <span class="na">emails</span><span class="p">:</span> <span class="p">[{</span> <span class="na">primary</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">work</span><span class="dl">'</span> <span class="p">}],</span> <span class="na">displayName</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">externalId</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">externalId</span><span class="p">,</span> <span class="na">active</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">active</span><span class="p">,</span> <span class="na">roles</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">roles</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">role</span> <span class="o">=&gt;</span> <span class="p">({</span><span class="na">display</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">toString</span><span class="p">()}))</span> <span class="p">}</span> <span class="p">});</span> </code></pre></div></div> <h4 id="update-the-response-for-an-individual-user">Update the response for an individual user</h4> <p>On to the next call, <code class="language-plaintext highlighter-rouge">GET /Users/:userId</code>. We need to add <code class="language-plaintext highlighter-rouge">roles</code> to the</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">findFirst</span><span class="p">({...});</span> </code></pre></div></div> <p>database command. Update it to match the code below.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">findFirst</span><span class="p">({</span> <span class="na">select</span><span class="p">:</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">externalId</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">active</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">roles</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="na">where</span><span class="p">:</span> <span class="p">{</span> <span class="nx">id</span><span class="p">,</span> <span class="na">org</span><span class="p">:</span> <span class="p">{</span><span class="na">id</span><span class="p">:</span> <span class="nx">ORG_ID</span><span class="p">},</span> <span class="p">}</span> <span class="p">});</span> </code></pre></div></div> <p>Then, find the comment</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// If no response from DB, return 404</span> </code></pre></div></div> <p>to update the <code class="language-plaintext highlighter-rouge">userResponse</code> object inside the <code class="language-plaintext highlighter-rouge">if</code> statement. Update the <code class="language-plaintext highlighter-rouge">userResponse</code> to match the code shown.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">userResponse</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">defaultUserSchema</span><span class="p">,</span> <span class="na">id</span><span class="p">:</span> <span class="nx">id</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="na">userName</span><span class="p">:</span> <span class="nx">email</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="p">{</span> <span class="nx">givenName</span><span class="p">,</span> <span class="nx">familyName</span> <span class="p">},</span> <span class="na">emails</span><span class="p">:</span> <span class="p">[{</span> <span class="na">primary</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="nx">email</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">work</span><span class="dl">'</span> <span class="p">}],</span> <span class="na">displayName</span><span class="p">:</span> <span class="nx">name</span><span class="p">,</span> <span class="na">externalId</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">externalId</span><span class="p">,</span> <span class="na">active</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">active</span><span class="p">,</span> <span class="na">roles</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">roles</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">role</span> <span class="o">=&gt;</span> <span class="p">({</span><span class="na">display</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">toString</span><span class="p">()}))</span> <span class="p">}</span> <span class="nx">satisfies</span> <span class="nx">IUserSchema</span><span class="p">;</span> </code></pre></div></div> <h3 id="update-the-users-call-so-scim-clients-can-set-their-roles">Update the <code class="language-plaintext highlighter-rouge">/Users</code> call so SCIM clients can set their roles</h3> <p>Another endpoint down, but there’s one more left, the <code class="language-plaintext highlighter-rouge">PUT /Users/:userId</code>.</p> <p>Find the code</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">emails</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">updatedUserRequest</span><span class="p">;</span> </code></pre></div></div> <p>and change it to the following code so we can work with the user’s updated roles and save the changes in the database.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">emails</span><span class="p">,</span> <span class="nx">roles</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">updatedUserRequest</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">updatedUser</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">update</span><span class="p">({</span> <span class="na">data</span><span class="p">:</span> <span class="p">{</span> <span class="na">email</span><span class="p">:</span> <span class="nx">emails</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">email</span> <span class="o">=&gt;</span> <span class="nx">email</span><span class="p">.</span><span class="nx">primary</span><span class="p">).</span><span class="nx">value</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="s2">`</span><span class="p">${</span><span class="nx">name</span><span class="p">.</span><span class="nx">givenName</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">name</span><span class="p">.</span><span class="nx">familyName</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="na">roles</span><span class="p">:</span> <span class="p">{</span> <span class="na">set</span><span class="p">:</span> <span class="nx">roles</span><span class="p">?.</span><span class="nx">map</span><span class="p">(</span><span class="nx">role</span> <span class="o">=&gt;</span> <span class="p">({</span><span class="na">id</span><span class="p">:</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">role</span><span class="p">.</span><span class="nx">value</span><span class="p">)}))</span> <span class="o">||</span> <span class="p">[]</span> <span class="p">}</span> <span class="p">},</span> <span class="na">where</span> <span class="p">:</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">},</span> <span class="na">include</span><span class="p">:</span> <span class="p">{</span> <span class="na">roles</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">});</span> </code></pre></div></div> <p>Lastly, we need to update the response from the <code class="language-plaintext highlighter-rouge">PUT /Users/:userId</code> call. Update the <code class="language-plaintext highlighter-rouge">userResponse</code> object to look like this.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">userResponse</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">defaultUserSchema</span><span class="p">,</span> <span class="na">id</span><span class="p">:</span> <span class="nx">id</span><span class="p">.</span><span class="nx">toString</span><span class="p">(),</span> <span class="na">userName</span><span class="p">:</span> <span class="nx">updatedUser</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="p">{</span> <span class="nx">givenName</span><span class="p">,</span> <span class="nx">familyName</span> <span class="p">},</span> <span class="na">emails</span><span class="p">:</span> <span class="p">[{</span> <span class="na">primary</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="nx">updatedUser</span><span class="p">.</span><span class="nx">email</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">work</span><span class="dl">'</span> <span class="p">}],</span> <span class="na">displayName</span><span class="p">:</span> <span class="nx">updatedUser</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">externalId</span><span class="p">:</span> <span class="nx">updatedUser</span><span class="p">.</span><span class="nx">externalId</span><span class="p">,</span> <span class="na">active</span><span class="p">:</span> <span class="nx">updatedUser</span><span class="p">.</span><span class="nx">active</span><span class="p">,</span> <span class="na">roles</span><span class="p">:</span> <span class="nx">updatedUser</span><span class="p">.</span><span class="nx">roles</span><span class="p">?.</span><span class="nx">map</span><span class="p">(</span><span class="nx">role</span> <span class="o">=&gt;</span> <span class="p">({</span><span class="na">display</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">value</span><span class="p">:</span> <span class="nx">role</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">toString</span><span class="p">()}))</span> <span class="p">}</span> <span class="nx">satisfies</span> <span class="nx">IUserSchema</span><span class="p">;</span> </code></pre></div></div> <p>Serve the API if you aren’t running it using <code class="language-plaintext highlighter-rouge">npm run serve-api</code>. Let’s make an HTTP call to get all users to double-check our work.</p> <div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET http://localhost:3333/scim/v2/Users Authorization: Bearer 123123 </span></code></pre></div></div> <p>You will see the list of users. Each user object has a <code class="language-plaintext highlighter-rouge">roles</code> and <code class="language-plaintext highlighter-rouge">entitlements</code> property. ✅</p> <h2 id="entitlements-discovery-in-okta">Entitlements discovery in Okta</h2> <p>What can Okta do with user entitlements? Okta can discover defined entitlements, such as the roles you define for the Todo app, and applies existing roles on users. Now that you have all the endpoints needed for a SCIM client to discover resources held by a SCIM server, you can see this in action on Okta.</p> <p>You’ll need to serve the API and create a local tunnel. Serve the API using the <code class="language-plaintext highlighter-rouge">npm run serve-api</code> command. In a second terminal window, run <code class="language-plaintext highlighter-rouge">npx localtunnel --port 3333</code>. Take note of your tunnel URL.</p> <p>Sign into your <a href="https://developer.okta.com/login/">Okta Developer Edition account</a>. Navigate to <strong>Applications</strong> &gt; <strong>Applications</strong> and select the “(Header Auth) Governance with SCIM 2.0” app. Navigate to the <strong>Provisioning</strong> tab and select <strong>Integration</strong>. Press <strong>Edit</strong>.</p> <p>Update the <strong>Base URL</strong> field by replacing the tunnel URL with your new tunnel URL. Make sure you keep the <code class="language-plaintext highlighter-rouge">/scim/v2</code> path. Your base URL might look something like <code class="language-plaintext highlighter-rouge">https://beep-bop-boop.loca.lt/scim/v2</code>. Press <strong>Save</strong>.</p> <p>Updating the API integration kicks off a discovery process. Okta automatically looks for roles as a possible entitlement type. It then matches the roles it discovers for the Todo application and matches them again with roles defined on the users. You can see Okta working by looking at the terminal window serving the API. You can see the calls Okta makes by inspecting the HTTP requests and their payloads written to the console. 🔍</p> <p>Make sure to keep the API running! There’s more work to do here!</p> <p>Navigate to the <strong>Governance</strong> tab. The tab you see is <strong>Entitlements</strong>. Do you see <strong>Role</strong> in the sidenav below the <strong>Search</strong> input? If not, hang tight. Because an app may have many defined entitlements, Okta starts a background job to discover roles asynchronously. It could take up to 10 minutes for the roles to populate.</p> <p>Eventually, you’ll see <strong>Role</strong>; when you select it, you’ll see metadata about it, such as the variable name, data type, and description. We also see the values: “Manager,” “Todo Auditor,” and “Todo-er.”</p> <p><img src="/assets-jekyll/blog/user-entitlements-workshop/entitlements-roles-7344da8e7387c37c7b6f3c9d16abcbf326e6477fe02ebb172b2eaf2666ee2958.jpg" alt="Governance tab with roles discovered by Okta" width="800" class="center-image" /></p> <p>You can define policies for users that automatically assign their entitlements when adding them to this integration app. While that’s pretty nifty, this post focuses on building out the SCIM endpoints for entitlements, so I’ll include links to resources that explain this feature in more detail at the end of the post.</p> <p>Press <strong>&lt; Back to application</strong> to return to the SCIM Okta app.</p> <h3 id="syncing-user-entitlements">Syncing user entitlements</h3> <p>When you use an identity provider, you want that system to be the source of truth for managing the users’ identities and access levels. You want to set the roles you defined for the Todo app onto users within Okta. That would be pretty sweet, right?</p> <p>Since we last ran our user import with hardcoded roles, let’s ensure we’ve synchronized everything from the starting state of the application before we start managing with Okta.</p> <p>Within the SCIM application tab, navigate to <strong>Import</strong> and press the <strong>Import Now</strong> button. Okta scans the users in the todo app, but since there are no new users, there’s no confirmation process. The user scan synced the existing users and the roles!</p> <p>Navigate to <strong>Assignments</strong>. Each user has a vertical 3-dot menu icon to display a context menu allowing you to <strong>Edit user assignment</strong>, <strong>View access details **, and **Unassign</strong>. Find “Trinity” and **View access details ** on them. A panel shows you Trinity’s role pre-assigned in the Todo app. 🎉 Exit the side panel by clicking outside the side panel.</p> <p>Let’s assign a new role to “Somnus” using Okta. Open the context menu for “Somnus” and <strong>View access details</strong>. Press the <strong>Edit access</strong> button. You’ll see a page titled <strong>Edit access</strong>. Press the <strong>Customize entitlements</strong> button. You’ll see a warning followed by a section called <strong>Custom Entitlements</strong>.</p> <p>You’ll see <strong>Role</strong> and a dropdown list with values. Select a role, such as “Todo-er,” and press <strong>Save</strong> to add the role to the user.</p> <p>But how about the Todo app? Take a look at the terminal output where you’re serving the API. The HTTP call tracing shows a <code class="language-plaintext highlighter-rouge">PUT</code> request on the user adding the role. Can you see the role of the user in the database? You can check it out by opening another terminal window, running <code class="language-plaintext highlighter-rouge">npx prisma studio</code>, and navigating to the website. ✅</p> <p>You can now use Okta to manage user roles centrally and automatically update the user’s grants!</p> <p>Stop serving the local tunnel and API for this next section.</p> <h3 id="schema-discovery-for-custom-entitlements">Schema discovery for custom entitlements</h3> <p>What if we have something other than roles in the application? Can SCIM support custom entitlement strategies? SCIM is extensible, meaning it has the structure for custom schemas and extends beyond the core resources. A SCIM server can publish a custom schema if it defines custom resource types.</p> <p>Let’s say you have user roles but want to add a custom entitlement, such as licenses, profiles, or something else. Let’s walk through the example where we want to add a custom entitlement. We will call this “Characteristic,” such as whether the user is tall. We know Trinity is tall, so it’s logical to note their tallness as part of their user attributes.</p> <p>SCIM clients must discover resources through schemas. So, we first need to define the schema describing “Characteristics.” Note that I came up with “Characteristics” as the name of this attribute, but you will need to change it for your user entitlements model, whether it be some sort of permissions system or something else. Custom schemas can extend from an existing schema, such as Okta’s entitlement schema, which tracks data as a key-value pair, and add our own flavoring to it.</p> <p>In the IDE, open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/scim-types.ts</code>.</p> <p>Add new schema URNs after the <code class="language-plaintext highlighter-rouge">SCHEMA_OKTA_ROLE</code> definition towards the end of the file:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">SCHEMA_OKTA_ENTITLEMENT</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">urn:okta:scim:schemas:core:1.0:Entitlement</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">SCHEMA_CHARACTERISTIC</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">urn:bestapps:scim:schemas:extension:todoapp:1.0:Characteristic</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>We defined a new schema URN for the characteristic SCIM resource. Following naming conventions for extension schemas, we substituted our company name (Best Apps) and added the app’s name (Todo app). The format looks like this</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>urn:&lt;Company name&gt;:scim:schemas:extension:&lt;App name&gt;:1.0:&lt;Custom entitlement&gt; </code></pre></div></div> <p>Right now, there’s a custom TypeScript type for <code class="language-plaintext highlighter-rouge">RESOURCE_TYPES</code>. Since we’ll have custom schemas as a resource type, update the code.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">type</span> <span class="nx">RESOURCE_TYPES</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Role</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">ResourceType</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">Schema</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>SCIM defines required and optional attributes to describe a schema resource. We’ll define the interfaces for a schema resource. Add the following interfaces to the <code class="language-plaintext highlighter-rouge">scim-types.ts</code> file. I added mine after the other interfaces and before the URNs.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">ISchema</span> <span class="p">{</span> <span class="nl">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">name</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">description</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">attributes</span><span class="p">:</span> <span class="nx">IAttribute</span><span class="p">[];</span> <span class="nl">meta</span><span class="p">:</span> <span class="nx">IMetadata</span><span class="p">;</span> <span class="p">}</span> <span class="k">export</span> <span class="kr">interface</span> <span class="nx">IAttribute</span> <span class="p">{</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">description</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">multiValued</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="nl">required</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="nl">caseExact</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span> <span class="nl">mutability</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">returned</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">uniqueness</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Characteristic is a unique resource type because it’s a new, custom type extending from an existing schema. We must explicitly show this relationship for consuming SCIM clients, like Okta. Find the <code class="language-plaintext highlighter-rouge">IResourceType</code> interface. We’ll add a new optional property, <code class="language-plaintext highlighter-rouge">schemaExtensions</code> and inline the type definition.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">IResourceType</span> <span class="p">{</span> <span class="nl">id</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">schemas</span><span class="p">:</span> <span class="kr">string</span><span class="p">[];</span> <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">description</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">endpoint</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">schema</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">schemaExtensions</span><span class="p">?:</span> <span class="p">{</span><span class="na">schema</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="na">required</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">}[];</span> <span class="nl">meta</span><span class="p">:</span> <span class="nx">IMetadata</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>SCIM clients expect a list of schemas that you offer in the SCIM server. You might’ve guessed what that means. You must wrap all the schemas in a SCIM <code class="language-plaintext highlighter-rouge">ListResponse</code>. Find <code class="language-plaintext highlighter-rouge">IListResponse</code> and add <code class="language-plaintext highlighter-rouge">ISchema</code> as a supported type. The <code class="language-plaintext highlighter-rouge">IListResponse</code> interface changes to:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">IListResponse</span><span class="o">&lt;</span><span class="nx">T</span> <span class="kd">extends</span> <span class="nx">IScimResource</span> <span class="o">|</span> <span class="nx">IResourceType</span> <span class="o">|</span> <span class="nx">ISchema</span><span class="o">&gt;</span> <span class="p">{</span> <span class="na">schemas</span><span class="p">:</span> <span class="kr">string</span><span class="p">[];</span> <span class="nl">totalResults</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">startIndex</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">itemsPerPage</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span> <span class="nl">Resources</span><span class="p">:</span> <span class="nx">T</span><span class="p">[];</span> <span class="p">}</span> </code></pre></div></div> <p>Finally, we define what a characteristic attribute looks like by adding the interface shown below.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kr">interface</span> <span class="nx">ICharacteristic</span> <span class="kd">extends</span> <span class="nx">IScimResource</span> <span class="p">{</span> <span class="nl">type</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="nl">displayName</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>With all the types and interfaces defined, it’s time to write the code for the route. Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/entitlements.ts</code>.</p> <p>Update the import array from <code class="language-plaintext highlighter-rouge">./scim-types.ts</code>:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">ICharacteristic</span><span class="p">,</span> <span class="nx">IListResponse</span><span class="p">,</span> <span class="nx">IOktaRole</span><span class="p">,</span> <span class="nx">IResourceType</span><span class="p">,</span> <span class="nx">ISchema</span><span class="p">,</span> <span class="nx">SCHEMA_CHARACTERISTIC</span><span class="p">,</span> <span class="nx">SCHEMA_LIST_RESPONSE</span><span class="p">,</span> <span class="nx">SCHEMA_OKTA_ENTITLEMENT</span><span class="p">,</span> <span class="nx">SCHEMA_OKTA_ROLE</span><span class="p">,</span> <span class="nx">SCHEMA_RESOURCE_TYPE</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./scim-types</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>Below the other route definitions, add two new route definitions.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">schemasRoute</span> <span class="o">=</span> <span class="nx">Router</span><span class="p">();</span> <span class="k">export</span> <span class="kd">const</span> <span class="nx">characteristicsRoute</span> <span class="o">=</span> <span class="nx">Router</span><span class="p">();</span> </code></pre></div></div> <p>Now, it’s time to define the <code class="language-plaintext highlighter-rouge">/Schemas</code> route. The <code class="language-plaintext highlighter-rouge">/Schemas</code> endpoint returns a list of schemas. You can return schemas for all the resources you use, even for <code class="language-plaintext highlighter-rouge">User</code>, but Okta allows us to skip the strict SCIM requirements and only return custom schemas. The custom schema we’ll return has metadata about a user characteristic, specifically whether the user is tall. Add the following code at the end of the file.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">schemasRoute</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)</span> <span class="p">.</span><span class="kd">get</span><span class="p">((</span><span class="nx">_</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="na">characteristic</span><span class="p">:</span> <span class="nx">ISchema</span> <span class="o">=</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="nx">SCHEMA_CHARACTERISTIC</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Characteristic</span><span class="dl">'</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">'</span><span class="s1">User characteristics for entitlements</span><span class="dl">'</span><span class="p">,</span> <span class="na">attributes</span><span class="p">:</span> <span class="p">[{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">is_tall</span><span class="dl">'</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Profile entitlement extension for tallness factor</span><span class="dl">'</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span><span class="p">,</span> <span class="na">multiValued</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">required</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">mutability</span><span class="p">:</span> <span class="dl">'</span><span class="s1">readWrite</span><span class="dl">'</span><span class="p">,</span> <span class="na">returned</span><span class="p">:</span> <span class="dl">'</span><span class="s1">default</span><span class="dl">'</span><span class="p">,</span> <span class="na">caseExact</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">uniqueness</span><span class="p">:</span> <span class="dl">'</span><span class="s1">none</span><span class="dl">'</span> <span class="p">}],</span> <span class="na">meta</span><span class="p">:</span> <span class="p">{</span> <span class="na">resourceType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Schema</span><span class="dl">'</span><span class="p">,</span> <span class="na">location</span><span class="p">:</span> <span class="s2">`/v2/Schemas/</span><span class="p">${</span><span class="nx">SCHEMA_CHARACTERISTIC</span><span class="p">}</span><span class="s2">`</span> <span class="p">}</span> <span class="p">};</span> <span class="kd">const</span> <span class="nx">schemas</span> <span class="o">=</span> <span class="p">{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_LIST_RESPONSE</span><span class="p">],</span> <span class="na">totalResults</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">startIndex</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">itemsPerPage</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">Resources</span><span class="p">:</span> <span class="p">[</span> <span class="nx">characteristic</span> <span class="p">]</span> <span class="p">};</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">schemas</span><span class="p">);</span> <span class="p">});</span> </code></pre></div></div> <p>And we must define a route for <code class="language-plaintext highlighter-rouge">/Characteristics</code>, in the same way one exists for <code class="language-plaintext highlighter-rouge">/Roles</code>. We won’t worry about updating the database for this as I don’t want to detract from the SCIM concepts. We’ll hardcode the characteristic for now so you can see what this looks like within Okta. Feel free to add the required code to connect it to the database as homework. 🏆 Add the following code below the schemas route:</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">characteristicsRoute</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)</span> <span class="p">.</span><span class="kd">get</span><span class="p">((</span><span class="nx">_</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="na">characteristicsListResponse</span><span class="p">:</span> <span class="nx">IListResponse</span><span class="o">&lt;</span><span class="nx">ICharacteristic</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span> <span class="nx">SCHEMA_OKTA_ENTITLEMENT</span><span class="p">,</span> <span class="nx">SCHEMA_CHARACTERISTIC</span> <span class="p">],</span> <span class="na">totalResults</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">startIndex</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">itemsPerPage</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">Resources</span><span class="p">:</span> <span class="p">[{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_CHARACTERISTIC</span><span class="p">],</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Characteristic</span><span class="dl">"</span><span class="p">,</span> <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">is_tall</span><span class="dl">"</span><span class="p">,</span> <span class="na">displayName</span><span class="p">:</span> <span class="dl">"</span><span class="s2">This user is so tall</span><span class="dl">"</span> <span class="p">}]</span> <span class="p">};</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">characteristicsListResponse</span><span class="p">);</span> <span class="p">});</span> </code></pre></div></div> <p>Notice the ID is the string “is_tall”. I modeled it to look like an enum here so that it’s distinct from roles, but IDs in your system may be a UUID or an integer.</p> <p>Lastly, we must add the new characteristic resource type to the <code class="language-plaintext highlighter-rouge">/ResourceTypes</code> response so that Okta knows the resource exists. Find the <code class="language-plaintext highlighter-rouge">resourceTypes.route('/')</code> definition and update the <code class="language-plaintext highlighter-rouge">resourceTypes</code> array to include both roles and characteristics.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">const</span> <span class="nx">resourceTypes</span><span class="p">:</span> <span class="nx">IResourceType</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_RESOURCE_TYPE</span><span class="p">],</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Role</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Role</span><span class="dl">'</span><span class="p">,</span> <span class="na">endpoint</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/Roles</span><span class="dl">'</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Roles you can set on users of Todo App</span><span class="dl">'</span><span class="p">,</span> <span class="na">schema</span><span class="p">:</span> <span class="nx">SCHEMA_OKTA_ROLE</span><span class="p">,</span> <span class="na">meta</span><span class="p">:</span> <span class="p">{</span> <span class="na">resourceType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ResourceType</span><span class="dl">'</span> <span class="p">}</span> <span class="p">},</span> <span class="p">{</span> <span class="na">schemas</span><span class="p">:</span> <span class="p">[</span><span class="nx">SCHEMA_RESOURCE_TYPE</span><span class="p">],</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Characteristic</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Characteristic</span><span class="dl">'</span><span class="p">,</span> <span class="na">endpoint</span><span class="p">:</span> <span class="dl">'</span><span class="s1">/Characteristics</span><span class="dl">'</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">'</span><span class="s1">This resource type is user characteristics</span><span class="dl">'</span><span class="p">,</span> <span class="na">schema</span><span class="p">:</span> <span class="dl">'</span><span class="s1">urn:okta:scim:schemas:core:1.0:Entitlement</span><span class="dl">'</span><span class="p">,</span> <span class="na">schemaExtensions</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="na">schema</span><span class="p">:</span> <span class="nx">SCHEMA_CHARACTERISTIC</span><span class="p">,</span> <span class="na">required</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span> <span class="p">],</span> <span class="na">meta</span><span class="p">:</span> <span class="p">{</span> <span class="na">resourceType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ResourceType</span><span class="dl">'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">];</span> </code></pre></div></div> <p>Now, we must register the routes in the API. Open <code class="language-plaintext highlighter-rouge">okta-enterprise-ready-workshops/apps/api/src/scim.ts</code>. At the top of the file, update the imports from <code class="language-plaintext highlighter-rouge">./entitlements</code> to</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">characteristicsRoute</span><span class="p">,</span> <span class="nx">resourceTypesRoute</span><span class="p">,</span> <span class="nx">rolesRoute</span><span class="p">,</span> <span class="nx">schemasRoute</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./entitlements</span><span class="dl">'</span><span class="p">;</span> </code></pre></div></div> <p>At the end of the file, add the code to register the <code class="language-plaintext highlighter-rouge">/Schemas</code> and <code class="language-plaintext highlighter-rouge">/Charactertistics</code> routes to the API.</p> <div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">scimRoute</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="dl">'</span><span class="s1">/Schemas</span><span class="dl">'</span><span class="p">,</span> <span class="nx">schemasRoute</span><span class="p">);</span> <span class="nx">scimRoute</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="dl">'</span><span class="s1">/Characteristics</span><span class="dl">'</span><span class="p">,</span> <span class="nx">characteristicsRoute</span><span class="p">);</span> </code></pre></div></div> <p>Serve the API by running <code class="language-plaintext highlighter-rouge">npm run serve-api</code> in a terminal window. In a second terminal window, run <code class="language-plaintext highlighter-rouge">npx localtunnel --port 3333</code> to create a local tunnel for the API. Keep track of the tunnel URL.</p> <p>Back in the <a href="https://developer.okta.com/login/">Okta Admin console</a>, navigate to <strong>Applications</strong> &gt; <strong>Applications</strong> and open the SCIM with governance Okta app. Navigate to <strong>Provisioning</strong> &gt; <strong>Integration</strong>. Press <strong>Edit</strong> and update the <strong>Base URL</strong> using the new tunnel URL. Don’t forget to keep the <code class="language-plaintext highlighter-rouge">/scim/v2</code> at the end of the URL. The URL should look something like</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://{yourTunnelSubdomain}.loca.lt/scim/v2 </code></pre></div></div> <p>Press <strong>Save</strong>.</p> <p>Okta discovers schemas and resource types when updating the provisioning configuration. If you look at the HTTP call tracing in the terminal window serving the API, you’ll see that Okta made a GET request to both <code class="language-plaintext highlighter-rouge">/Schemas</code> and <code class="language-plaintext highlighter-rouge">/Characteristics</code>.</p> <p>Navigate to the <strong>Governance</strong>. <strong>Characteristic</strong> may take 10-15 minutes to populate, but you’ll see the display name and value when it does. Go <strong>&lt; Back to application</strong> and navigate to <strong>Assignments</strong>. Open the user context menu for “Trinity” by pressing the three vertical dots icon menu and opening <strong>View entitlements</strong>. Press <strong>Edit</strong> and <strong>Customize entitlements</strong> to add the <code class="language-plaintext highlighter-rouge">is_tall</code> user characteristic. <strong>Save</strong> the changes and navigate back to the Okta SCIM app.</p> <p>Check out the terminal serving the API for the HTTP call tracing. You’ll see a <code class="language-plaintext highlighter-rouge">PUT</code> request on Trinity adding the new characteristic. The field goes into the core SCIM User <code class="language-plaintext highlighter-rouge">entitlements</code> property. Check it out by inspecting the HTTP tracing in the console output. ✅</p> <h2 id="multi-tenant-use-cases-for-entitlements">Multi-tenant use cases for entitlements</h2> <p>In this workshop, we defined roles for the entire Todo app. But what if your SaaS app supports tenant-configurable roles? You must make structural changes to the Todo app database to support organization roles. Notice that an organization has a unique API key, and we included this API as a <code class="language-plaintext highlighter-rouge">Bearer</code> token value in the <code class="language-plaintext highlighter-rouge">Authorization</code> header. All the SCIM calls from Okta can target a specific organization in the Todo app, including the organization’s custom roles.</p> <table> <tr> <td style="font-size: 3rem;">️ℹ️</td> <td> <strong>Note</strong> <br /> We used an API key for demonstration purposes, but we recommend using OAuth to secure the calls from Okta to your API for production applications. </td> </tr> </table> <h2 id="use-scim-to-manage-user-provisioning-and-entitlements">Use SCIM to manage user provisioning and entitlements</h2> <p>In this workshop, you dived deeper into SCIM and learned about resources and schemas. You also synced users and their pre-existing entitlements from the Todo app and provisioned users within Okta. I hope you enjoyed this workshop and have ideas for using it for your SaaS applications! Check out the <a href="https://help.okta.com/oie/en-us/content/topics/identity-governance/iga.htm">Identity Governance</a> help docs to learn about Okta Identity Governance.</p> <p>You can find the completed code project in the <a href="https://github.com/oktadev/okta-enterprise-ready-workshops/tree/entitlements-workshop-complete"><code class="language-plaintext highlighter-rouge">entitlements-workshop-completed</code> branch within the GitHub repo</a>.</p> <p>If you want to learn more about what it means to be enterprise-ready and to have enterprise maturity, check out the other workshops in this series</p> <table> <thead> <tr> <th>Posts in the on-demand workshop series</th> </tr> </thead> <tbody> <tr> <td>1. <a href="/blog/2023/07/27/enterprise-ready-getting-started">How to Get Going with the On-Demand SaaS Apps Workshops</a></td> </tr> <tr> <td>2. <a href="/blog/2023/07/28/oidc_workshop">Enterprise-Ready Workshop: Authenticate with OpenID Connect</a></td> </tr> <tr> <td>3. <a href="/blog/2023/07/28/scim-workshop">Enterprise-Ready Workshop: Manage Users with SCIM</a></td> </tr> <tr> <td>4. <a href="/blog/2023/07/28/terraform-workshop">Enterprise Maturity Workshop: Terraform</a></td> </tr> <tr> <td>5. <a href="/blog/2023/09/15/workflows-workshop">Enterprise Maturity Workshop: Automate with no-code Okta Workflows</a></td> </tr> <tr> <td>6. <a href="/blog/2024/04/30/express-universal-logout">How to Instantly Sign a User Out across All Your Apps</a></td> </tr> <tr> <td>7. <strong>Take User Provisioning to the Next Level with Entitlements</strong></td> </tr> </tbody> </table> <p>Want to learn about more exciting topics? Let us know by commenting below. To get notified about exciting new content, follow us on <a href="https://twitter.com/oktadev">Twitter</a> and subscribe to our <a href="https://www.youtube.com/c/oktadev">YouTube</a> channel.</p> Wed, 21 Jan 2026 00:00:00 -0500 https://developer.okta.com/blog/2026/01/21/user-entitlements-workshop https://developer.okta.com/blog/2026/01/21/user-entitlements-workshop Introducing xaa.dev: A Playground for Cross App Access <p>AI agents are quickly becoming part of everyday enterprise development. They summarize emails, coordinate calendars, query internal systems, and automate workflows across tools.</p> <p>But once an AI agent needs to access an enterprise application <em>on behalf of a user</em>, things get complicated.</p> <p>How do you securely let an AI-powered app act for a user without exposing credentials, spamming consent prompts, or losing administrative control?</p> <p>This is the problem <strong>Cross App Access (XAA)</strong> is designed to solve.</p> <p>Today, we’re introducing <strong><a href="https://xaa.dev">xaa.dev</a></strong>, a free, open playground that lets you explore Cross App Access end-to-end. <strong>No local setup. No infrastructure to provision.</strong> Just a working environment where you can see the protocol in action.</p> <p><img src="/assets-jekyll/blog/xaa-dev-playground/xaa-dev-homepage-1ced782c627da4675bc483f0b21b24dbb583b30b0fc3978cf753233df80a5cda.jpg" alt="xaa.dev playground homepage showing the Cross App Access flow" width="800" class="center-image" /></p> <blockquote> <p><strong>Note:</strong> xaa.dev is currently in beta. We’re actively developing new features for the next release, and your feedback helps shape what comes next.</p> </blockquote> <p><strong class="hide">Table of Contents</strong></p> <ul id="markdown-toc"> <li><a href="#what-is-cross-app-access" id="markdown-toc-what-is-cross-app-access">What is Cross App Access?</a></li> <li><a href="#the-problem-testing-xaa-is-hard" id="markdown-toc-the-problem-testing-xaa-is-hard">The problem: testing XAA is hard</a></li> <li><a href="#what-you-can-do-on-xaadev" id="markdown-toc-what-you-can-do-on-xaadev">What you can do on xaa.dev</a> <ul> <li><a href="#requesting-app" id="markdown-toc-requesting-app">Requesting App</a></li> <li><a href="#resource-app" id="markdown-toc-resource-app">Resource App</a></li> <li><a href="#identity-provider" id="markdown-toc-identity-provider">Identity Provider</a></li> <li><a href="#resource-mcp-server" id="markdown-toc-resource-mcp-server">Resource MCP Server</a></li> <li><a href="#bring-your-own-requesting-app" id="markdown-toc-bring-your-own-requesting-app">Bring your own Requesting App</a></li> </ul> </li> <li><a href="#how-to-get-started" id="markdown-toc-how-to-get-started">How to get started</a></li> <li><a href="#why-we-built-a-testing-site-for-cross-app-access" id="markdown-toc-why-we-built-a-testing-site-for-cross-app-access">Why we built a testing site for cross app access</a></li> <li><a href="#inspect-the-xaa-flow" id="markdown-toc-inspect-the-xaa-flow">Inspect the XAA flow</a></li> <li><a href="#learn-more" id="markdown-toc-learn-more">Learn more</a></li> </ul> <h2 id="what-is-cross-app-access">What is Cross App Access?</h2> <p>Cross App Access refers to a typical enterprise pattern: <strong>one application accesses another application’s resources on behalf of a user.</strong></p> <p>For example:</p> <ul> <li>An internal AI assistant fetching updates from a project management system</li> <li>A workflow engine booking meetings through a calendar API</li> <li>An agent querying internal data sources to complete a task</li> </ul> <p>Traditionally, OAuth consent flows handle this. That approach works well for consumer-based apps, but it creates friction in enterprise environments where organizations require workforce oversight:</p> <ul> <li>Applications and their access levels are centrally managed</li> <li>IT teams need visibility into trust relationships</li> <li>Access must be revocable without user involvement</li> </ul> <p>Cross App Access shifts responsibility from end users to the enterprise identity layer.</p> <p>Instead of prompting users for consent, the <strong>Identity Provider (IdP)</strong> issues a signed identity assertion called an <strong>ID-JAG (Identity JWT Authorization Grant)</strong>. This assertion cryptographically represents the user and the requesting application. Resource applications trust the IdP’s assertion and issue access accordingly.</p> <p>The result:</p> <ul> <li>No interactive consent screens making application access seamless for employees</li> <li>Clear, auditable trust boundaries</li> <li>Complete administrative control over app-to-app access</li> </ul> <p>For a deeper dive into why this matters for enterprise AI, read more about Cross App Access in this post:</p> <article class="link-container" style="border: 1px solid silver; border-radius: 3px; padding: 12px 15px"> <a href="/blog/2025/06/23/enterprise-ai" style="font-size: 1.375em; margin-bottom: 20px;"> <span>Integrate Your Enterprise AI Tools with Cross-App Access</span> </a> <p>Manage user and non-human identities, including AI in the enterprise with Cross App Access</p> <div><div class="BlogPost-attribution"> <a href="/blog/authors/semona-igama/"> <img src="/assets-jekyll/avatar-semona-igama-03eb4c28aca3765f862b574e032d32f6f8186d04ae9f0db75bed9c74f48a9a3f.jpg" alt="avatar-avatar-semona-igama.jpeg" class="BlogPost-avatar" /> </a> <span class="BlogPost-author"> <a href="/blog/authors/semona-igama/">Semona Igama</a> </span> </div></div> </article> <h2 id="the-problem-testing-xaa-is-hard">The problem: testing XAA is hard</h2> <p>XAA is built on an emerging OAuth extension called the <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant">Identity Assertion JWT Authorization Grant</a> – an IETF draft that Okta, along with public and industry contributors, has been actively contributing to. It’s powerful, but it’s also new, and new protocols need experimentation.</p> <p>Here’s the challenge: to test XAA locally, you’d need to spin up:</p> <ul> <li>An Identity Provider (IdP)</li> <li>An Authorization Server for the resource application</li> <li>The resource API itself</li> <li>A requesting application (the agent or client app)</li> </ul> <p>That’s hours (or days) of configuration before you can even see a single token exchange. Most developers give up before getting to the interesting part.</p> <p><strong>xaa.dev changes that.</strong></p> <p>We pre-configured all the components so you can focus on understanding the flow, not debugging dev environments. Go from zero to a working XAA token exchange in under 60 seconds.</p> <p><strong><a href="https://xaa.dev">Launch the playground</a></strong>. It’s free and requires no signup.</p> <h2 id="what-you-can-do-on-xaadev">What you can do on xaa.dev</h2> <p>The playground gives you hands-on access to every role in the Cross App Access flow:</p> <h3 id="requesting-app">Requesting App</h3> <p>Step into the shoes of an AI agent or client application. Authenticate a user, request an ID-JAG from the IdP, and exchange it for an access token at the resource server.</p> <h3 id="resource-app">Resource App</h3> <p>See the other side of the transaction. Watch how a resource server validates the identity assertion, verifies the trust relationship, and issues scoped access tokens.</p> <h3 id="identity-provider">Identity Provider</h3> <p>We’ve built a simulated IdP with pre-configured test users. Log in, see how ID-JAGs are minted, and inspect the cryptographic claims that make XAA secure.</p> <h3 id="resource-mcp-server">Resource MCP Server</h3> <p>Connect your AI agents using the Model Context Protocol (MCP). The playground provides a ready-to-use MCP server that acts as a resource application, letting you test how AI agents can securely access protected resources through the Cross App Access flow.</p> <h3 id="bring-your-own-requesting-app">Bring your own Requesting App</h3> <p>The built-in Requesting App is great for learning, but the real power comes when you test with your own application, whether it’s a traditional app or an MCP client. <a href="https://xaa.dev/developer/register">Register a client</a> on the playground, grab the configuration, and integrate it into your local app. This lets you validate your XAA implementation against a working IdP and Resource App without spinning up your own infrastructure. The <a href="https://xaa.dev/docs">playground documentation</a> walks you through the setup step-by-step.</p> <h2 id="how-to-get-started">How to get started</h2> <p>Getting started with xaa.dev takes less than a minute:</p> <p><strong>Step 1: Open the playground</strong></p> <p>Visit <a href="https://xaa.dev">xaa.dev</a>. No account required.</p> <p><strong>Step 2: Explore the components</strong></p> <p>The playground has three components (Requesting App, Resource App, and Identity Provider), each with its own URL. Visit any component to see its configuration and understand how it participates in the XAA flow.</p> <p><strong>Step 3: Follow the guided flow</strong></p> <p>Walk through the four steps of the XAA flow: User Authentication (SSO), Token Exchange, Access Token Request, and Access Resource. Inspect the requests and responses at each step to see exactly how XAA works under the hood.</p> <p>That’s it. No local tools installations, Docker containers, environment variables, or CORS headaches.</p> <p>Watch this walkthrough video of the playground if you’d like a guided tour:</p> <div class="jekyll-youtube-plugin" style="text-align: center; margin-bottom: 1.25rem"> <iframe width="700" height="394" style="max-width: 100%" src="https://www.youtube.com/embed/hXZ4o2Oasc0" allowfullscreen="" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" frameborder="0"></iframe> </div> <h2 id="why-we-built-a-testing-site-for-cross-app-access">Why we built a testing site for cross app access</h2> <p>XAA is built on an emerging IETF specification, the Identity Assertion JWT Authorization Grant. As enterprise AI adoption accelerates, there’s a clear need: developers want to understand XAA, but the barrier to entry is too high.</p> <p>xaa.dev lowers the barrier. It helps you:</p> <ul> <li><strong>Learn faster</strong> – See the protocol in action before writing any code</li> <li><strong>Build confidently</strong> – Understand exactly what tokens to expect and validate</li> <li><strong>Experiment safely</strong> – Test edge cases without affecting production systems</li> </ul> <h2 id="inspect-the-xaa-flow">Inspect the XAA flow</h2> <p>XAA is how enterprise applications will securely connect in an AI-first world. Whether you’re building agents, integrating SaaS tools, or just curious about modern OAuth patterns, <a href="https://xaa.dev">xaa.dev</a> gives you a risk-free environment to learn. Check it out and let us know how it works for you!</p> <h2 id="learn-more">Learn more</h2> <p>Ready to go deeper? Check out these resources:</p> <ul> <li><a href="https://www.okta.com/integrations/cross-app-access/">Checkout Cross App Access Integration in Okta </a> – Securing AI-driven access together</li> <li><a href="/blog/2025/09/03/cross-app-access">Build Secure Agent-to-App Connections with Cross App Access</a> – Hands-on implementation guide</li> <li><a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-identity-assertion-authz-grant">Identity Assertion JWT Authorization Grant (IETF Draft)</a> – The specification behind XAA</li> </ul> <p>Have questions or feedback? Reach out to us on <a href="https://twitter.com/oktadev">Twitter</a>, join the conversation on the <a href="https://devforum.okta.com/">Okta Developer Forums</a>, or drop a comment below. We’re actively improving xaa.dev based on developer input – your feedback shapes what we build next.</p> <p>Follow us on <a href="https://twitter.com/oktadev">Twitter</a> and subscribe to our <a href="https://youtube.com/oktadev">YouTube channel</a> for more content on identity, security, and building with Okta.</p> Tue, 20 Jan 2026 00:00:00 -0500 https://developer.okta.com/blog/2026/01/20/xaa-dev-playground https://developer.okta.com/blog/2026/01/20/xaa-dev-playground Okta Developer Connect Recap <p>Identity has become one of the most important control points in modern systems. As applications grow more distributed and AI-driven automation becomes part of everyday workflows, identity increasingly defines how secure, predictable, and trustworthy those systems are. Decisions about access, scope, and lifecycle now shape not only the user experience, but also how well security holds up as systems scale.</p> <p>With this shift in mind, we hosted our first flagship <strong><a href="https://regionalevents.okta.com/oktadeveloperconnect">Okta Developer Connect event</a></strong> in India, in Bengaluru. Led by the Okta Developer Advocacy team, the event brought together developers, architects, engineering managers, IAM practitioners, and technology leaders for a day of focused conversations on how identity needs to evolve as systems scale, and as AI agents begin to act alongside humans.</p> <h2 id="where-identity-security-and-ai-connect">Where identity, security, and AI connect</h2> <p>Throughout the day, the theme stood out clearly: identity now sits at the intersection of application architecture, security decisions, and system behavior. As organizations integrate across ecosystems and introduce AI-driven workflows, identity increasingly determines how safely systems interact and how confidently access can be controlled.</p> <p>Identity was discussed as foundational infrastructure, spanning users, applications, APIs, services, and AI agents. The conversations focused on how identity decisions ripple across systems as they become more interconnected, and why those decisions matter long after the first sign-in.</p> <p>The sessions balanced identity fundamentals with how teams are applying them today. Core identity standards and practices were discussed as the foundation for building secure, interoperable systems that scale. From there, the conversations expanded into modern use cases across Okta’s platform, including identity for AI-driven systems using the Okta MCP Server, Cross App Access, secure integrations through the Okta Integration Network, automation with Okta Workflows, lifecycle management, and emerging capabilities such as Verifiable Digital Credentials.</p> <p><img src="/assets-jekyll/blog/okta-developer-connect-recap/image-1-063315f1150424b260fa798d1e4ebe0d6e781da15af736d3db6e68fbe4b2d234.jpg" alt="speakers" width="700" class="center-image" /></p> <h2 id="a-shared-conversation-with-the-community">A shared conversation with the community</h2> <p>Beyond the talks, Okta Developer Connect was intentionally designed to be interactive, with open discussions, audience Q&amp;A, quizzes, interactive sessions, surveys, and a research panel created space for participation beyond listening. Attendees engaged directly with Okta engineers, product leaders, and advocates, exchanging perspectives shaped by the systems they build and operate. Those exchanges added important context to the day, grounding the conversations in real systems and practical implementations.</p> <p><img src="/assets-jekyll/blog/okta-developer-connect-recap/image-2-0ac56c03de4e4dcbf532f691d9ab6992ca6c26c3470cdf839cf04cbf1e271abf.jpg" alt="community" width="700" class="center-image" /></p> <h2 id="looking-ahead">Looking ahead</h2> <p>Okta Developer Connect Bengaluru marked the beginning of an ongoing series focused on practical identity education and open technical dialogue. The series aims to help teams think more clearly about building enterprise-ready systems, how identity standards and practices are reflected across the stack, and how AI-driven systems impact access and security assumptions.</p> <p>As identity continues to sit at the intersection of security, system architecture, and AI-driven automation, these conversations will only become more important.</p> <p>We’re excited to continue building this forum with the developer community, grounded in standards, informed by real-world systems, and focused on designing identity that scales with what comes next.</p> <p>Stay tuned for upcoming Okta Developer Connect events, and follow OktaDev on <a href="https://www.linkedin.com/company/oktadev">LinkedIn</a> and <a href="https://twitter.com/oktadev">Twitter</a> for the latest updates.</p> Mon, 19 Jan 2026 00:00:00 -0500 https://developer.okta.com/blog/2026/01/19/okta-developer-connect-recap https://developer.okta.com/blog/2026/01/19/okta-developer-connect-recap Unlock the Secrets of a Custom Sign-In Page with Tailwind and JavaScript <p>We recommend redirecting users to authenticate via the Okta-hosted sign-in page powered by the Okta Identity Engine (OIE) for your custom-built applications. It’s the most secure method for authenticating. You don’t have to manage credentials in your code and can take advantage of the strongest authentication factors without requiring any code changes.</p> <p>The Okta Sign-In Widget (SIW) built into the sign-in page does the heavy lifting of supporting the authentication factors required by your organization. Did I mention policy changes won’t need any code changes?</p> <p>But you may think the sign-in page and the SIW are a little bland. And maybe too Okta for your needs? What if you can have a page like this?</p> <p><img src="/assets-jekyll/blog/okta-custom-sign-in-page/final-siw-desktop-211b475e04926250d77e2a72779c27ea2c22a65ad47f43322c22ba81006f5df2.jpg" alt="A customized Okta-hosted Sign-In Widget with custom elements, colors, and styles" width="800" class="center-image" /></p> <p>With a bright and colorful responsive design change befitting a modern lifestyle.</p> <p><img src="/assets-jekyll/blog/okta-custom-sign-in-page/final-siw-responsive-c258e3e1daf1d1fdfe36dfbaa4eb61afd13e38e5234b3781af4ac08bbd6baa13.jpg" alt="A customized Okta-hosted Sign-In Widget with custom elements, colors, and styles for smaller form factors" width="800" class="center-image" /></p> <p>Let’s add some color, life, and customization to the sign-in page.</p> <p>In this tutorial, we will customize the sign-in page for a fictional to-do app. We’ll make the following changes:</p> <ul> <li>Use <a href="https://tailwindcss.com/">Tailwind</a> CSS framework to create a responsive sign-in page layout</li> <li>Add a footer for custom brand links</li> <li>Display a terms and conditions modal using <a href="https://alpinejs.dev">Alpine.js</a> that the user must accept before authenticating</li> </ul> <p>Take a moment to read this post on customizing the Sign-In Widget if you aren’t familiar with the process, as we will be expanding from customizing the widget to enhancing the entire sign-in page experience.</p> <article class="link-container" style="border: 1px solid silver; border-radius: 3px; padding: 12px 15px"> <a href="/blog/2025/11/12/custom-signin" style="font-size: 1.375em; margin-bottom: 20px;"> <span>Stretch Your Imagination and Build a Delightful Sign-In Experience</span> </a> <p>Customize your Gen3 Okta Sign-In Widget to match your brand. Learn to use design tokens, CSS, and JavaScript for a seamless user experience.</p> <div></div> </article> <p>In the post, we covered how to style the Gen3 SIW using design tokens and customize the widget elements using the <code class="language-plaintext highlighter-rouge">afterTransform()</code> method. You’ll want to combine elements of both posts for the most customized experience.</p> <p><strong class="hide">Table of Contents</strong></p> <ul id="markdown-toc"> <li><a href="#customize-your-okta-hosted-sign-in-page" id="markdown-toc-customize-your-okta-hosted-sign-in-page">Customize your Okta-hosted sign-in page</a></li> <li><a href="#use-tailwind-css-to-build-a-responsive-layout" id="markdown-toc-use-tailwind-css-to-build-a-responsive-layout">Use Tailwind CSS to build a responsive layout</a></li> <li><a href="#use-tailwind-for-custom-html-elements-on-your-okta-hosted-sign-in-page" id="markdown-toc-use-tailwind-for-custom-html-elements-on-your-okta-hosted-sign-in-page">Use Tailwind for custom HTML elements on your Okta-hosted sign-in page</a></li> <li><a href="#add-custom-interactivity-on-the-okta-hosted-sign-in-page-using-an-external-library" id="markdown-toc-add-custom-interactivity-on-the-okta-hosted-sign-in-page-using-an-external-library">Add custom interactivity on the Okta-hosted sign-in page using an external library</a></li> <li><a href="#customize-okta-hosted-sign-in-page-behavior-using-web-apis" id="markdown-toc-customize-okta-hosted-sign-in-page-behavior-using-web-apis">Customize Okta-hosted sign-in page behavior using Web APIs</a></li> <li><a href="#add-tailwind-web-apis-and-javascript-libraries-to-customize-your-okta-hosted-sign-in-page" id="markdown-toc-add-tailwind-web-apis-and-javascript-libraries-to-customize-your-okta-hosted-sign-in-page">Add Tailwind, Web APIs, and JavaScript libraries to customize your Okta-hosted sign-in page</a></li> </ul> <p><strong>Prerequisites</strong></p> <p>To follow this tutorial, you need:</p> <ul> <li>An Okta account with the Identity Engine, such as the <a href="https://developer.okta.com/signup/">Integrator Free account</a>.</li> <li>Your own domain name</li> <li>A basic understanding of HTML, CSS, and JavaScript</li> <li>A brand design in mind. Feel free to tap into your creativity!</li> <li>An understanding of customizing the sign-in page by following the previous blog post</li> </ul> <p>Let’s get started!</p> <p>Before we begin, you must configure your Okta org to use your custom domain. Custom domains enable code customizations, allowing us to style more than just the default logo, background, favicon, and two colors. Sign in as an admin and open the Okta Admin Console, navigate to <strong>Customizations</strong> &gt; <strong>Brands</strong> and select <strong>Create Brand +</strong>.</p> <p>Follow the <a href="https://developer.okta.com/docs/guides/custom-url-domain/main/">Customize domain and email</a> developer docs to set up your custom domain on the new brand.</p> <h2 id="customize-your-okta-hosted-sign-in-page">Customize your Okta-hosted sign-in page</h2> <p>We’ll first apply the base configuration using the built-in configuration options in the UI. Add your favorite primary and secondary colors, then upload your favorite logo, favicon, and background image for the page. Select <strong>Save</strong> when done. Everyone has a favorite favicon, right?</p> <p>I’ll use <code class="language-plaintext highlighter-rouge">#ea3eda</code> and <code class="language-plaintext highlighter-rouge">#ffa738</code> as the primary and secondary colors, respectively.</p> <p>On to the code. In the <strong>Theme</strong> tab:</p> <ol> <li>Select <strong>Sign-in Page</strong> in the dropdown menu</li> <li>Select the <strong>Customize</strong> button</li> <li>On the <strong>Page Design</strong> tab, select the <strong>Code editor</strong> toggle to see a HTML page</li> </ol> <blockquote> <p><strong>Note</strong></p> <p>You can only enable the code editor if you configure a <a href="https://developer.okta.com/docs/guides/custom-url-domain/">custom domain</a>.</p> </blockquote> <p>You’ll see the lightweight IDE already has code scaffolded. Press <strong>Edit</strong> and replace the existing code with the following.</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;meta</span> <span class="na">http-equiv=</span><span class="s">"Content-Type"</span> <span class="na">content=</span><span class="s">"text/html; charset=UTF-8"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"viewport"</span> <span class="na">content=</span><span class="s">"width=device-width, initial-scale=1.0"</span> <span class="nt">/&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"robots"</span> <span class="na">content=</span><span class="s">"noindex,nofollow"</span> <span class="nt">/&gt;</span> <span class="c">&lt;!-- Styles generated from theme --&gt;</span> <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"{{themedStylesUrl}}"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">type=</span><span class="s">"text/css"</span><span class="nt">&gt;</span> <span class="c">&lt;!-- Favicon from theme --&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"shortcut icon"</span> <span class="na">href=</span><span class="s">"{{faviconUrl}}"</span> <span class="na">type=</span><span class="s">"image/x-icon"</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"preconnect"</span> <span class="na">href=</span><span class="s">"https://fonts.googleapis.com"</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"preconnect"</span> <span class="na">href=</span><span class="s">"https://fonts.gstatic.com"</span> <span class="na">crossorigin</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&amp;family=Manrope:[email protected]&amp;display=swap"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span> <span class="nt">&lt;title&gt;</span>{{pageTitle}}<span class="nt">&lt;/title&gt;</span> {{{SignInWidgetResources}}} <span class="nt">&lt;style </span><span class="na">nonce=</span><span class="s">"{{nonceValue}}"</span><span class="nt">&gt;</span> <span class="nd">:root</span> <span class="p">{</span> <span class="py">--font-header</span><span class="p">:</span> <span class="s2">'Inter Tight'</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span> <span class="py">--font-body</span><span class="p">:</span> <span class="s2">'Manrope'</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span> <span class="py">--color-gray</span><span class="p">:</span> <span class="m">#4f4f4f</span><span class="p">;</span> <span class="py">--color-fuchsia</span><span class="p">:</span> <span class="m">#ea3eda</span><span class="p">;</span> <span class="py">--color-orange</span><span class="p">:</span> <span class="m">#ffa738</span><span class="p">;</span> <span class="py">--color-azul</span><span class="p">:</span> <span class="m">#016fb9</span><span class="p">;</span> <span class="py">--color-cherry</span><span class="p">:</span> <span class="m">#ea3e84</span><span class="p">;</span> <span class="py">--color-purple</span><span class="p">:</span> <span class="m">#b13fff</span><span class="p">;</span> <span class="py">--color-black</span><span class="p">:</span> <span class="m">#191919</span><span class="p">;</span> <span class="py">--color-white</span><span class="p">:</span> <span class="m">#fefefe</span><span class="p">;</span> <span class="py">--color-bright-white</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="py">--border-radius</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span> <span class="py">--color-gradient</span><span class="p">:</span> <span class="n">linear-gradient</span><span class="p">(</span><span class="m">12deg</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-fuchsia</span><span class="p">)</span> <span class="m">0%</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-orange</span><span class="p">)</span> <span class="m">100%</span><span class="p">);</span> <span class="p">}</span> <span class="p">{</span><span class="err">{#useSiwGen3</span><span class="p">}</span><span class="err">}</span> <span class="nt">html</span> <span class="p">{</span> <span class="nl">font-size</span><span class="p">:</span> <span class="m">87.5%</span><span class="p">;</span> <span class="p">}</span> <span class="p">{</span><span class="err">{/useSiwGen3</span><span class="p">}</span><span class="err">}</span> <span class="nf">#okta-auth-container</span> <span class="p">{</span> <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span> <span class="nl">background-image</span><span class="p">:</span> <span class="err">{{</span><span class="n">bgImageUrl</span><span class="p">}</span><span class="err">}</span><span class="o">;</span> <span class="err">}</span> <span class="nf">#okta-login-container</span> <span class="p">{</span> <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span> <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span> <span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100vh</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">50vw</span><span class="p">;</span> <span class="nl">background</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-white</span><span class="p">);</span> <span class="p">}</span> <span class="nt">&lt;/style&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-auth-container"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-login-container"</span><span class="nt">&gt;&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="c">&lt;!-- "OktaUtil" defines a global OktaUtil object that contains methods used to complete the Okta login flow. --&gt;</span> {{{OktaUtil}}} <span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript"</span> <span class="na">nonce=</span><span class="s">"{{nonceValue}}"</span><span class="nt">&gt;</span> <span class="c1">// "config" object contains default widget configuration</span> <span class="c1">// with any custom overrides defined in your admin settings.</span> <span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="nx">OktaUtil</span><span class="p">.</span><span class="nx">getSignInWidgetConfig</span><span class="p">();</span> <span class="nx">config</span><span class="p">.</span><span class="nx">theme</span> <span class="o">=</span> <span class="p">{</span> <span class="na">tokens</span><span class="p">:</span> <span class="p">{</span> <span class="na">BorderColorDisplay</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-bright-white)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryMain</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-fuchsia)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryDark</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-purple)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryDarker</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-purple)</span><span class="dl">'</span><span class="p">,</span> <span class="na">BorderRadiusTight</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--border-radius)</span><span class="dl">'</span><span class="p">,</span> <span class="na">BorderRadiusMain</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--border-radius)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryDark</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-orange)</span><span class="dl">'</span><span class="p">,</span> <span class="na">FocusOutlineColorPrimary</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-azul)</span><span class="dl">'</span><span class="p">,</span> <span class="na">TypographyFamilyBody</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--font-body)</span><span class="dl">'</span><span class="p">,</span> <span class="na">TypographyFamilyHeading</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--font-header)</span><span class="dl">'</span><span class="p">,</span> <span class="na">TypographyFamilyButton</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--font-header)</span><span class="dl">'</span><span class="p">,</span> <span class="na">BorderColorDangerControl</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-cherry)</span><span class="dl">'</span> <span class="p">}</span> <span class="p">}</span> <span class="nx">config</span><span class="p">.</span><span class="nx">i18n</span> <span class="o">=</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">en</span><span class="dl">'</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">primaryauth.title</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Log in to create tasks</span><span class="dl">'</span><span class="p">,</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// Render the Okta Sign-In Widget</span> <span class="kd">const</span> <span class="nx">oktaSignIn</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">OktaSignIn</span><span class="p">(</span><span class="nx">config</span><span class="p">);</span> <span class="nx">oktaSignIn</span><span class="p">.</span><span class="nx">renderEl</span><span class="p">({</span> <span class="na">el</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#okta-login-container</span><span class="dl">'</span> <span class="p">},</span> <span class="nx">OktaUtil</span><span class="p">.</span><span class="nx">completeLogin</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Logs errors that occur when configuring the widget.</span> <span class="c1">// Remove or replace this with your own custom error handler.</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span> <span class="p">}</span> <span class="p">);</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span> </code></pre></div></div> <p>This code adds style configuration to the SIW elements and configures the text for the title when signing in. Press <strong>Save to draft</strong>.</p> <p>We must allow Okta to load font resources from an external source, Google, by adding the domains to the allowlist in the Content Security Policy (CSP).</p> <p>Navigate to the <strong>Settings</strong> tab for your brand’s <strong>Sign-in page</strong>. Find the <strong>Content Security Policy</strong> and press <strong>Edit</strong>. Add the domains for external resources. In our example, we only load resources from Google Fonts, so we added the following two domains:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*.googleapis.com *.gstatic.com </code></pre></div></div> <p>Select <strong>Save to draft</strong>, then <strong>Publish</strong> to view your changes.</p> <p>The sign-in page looks more stylized than before. If you try resizing the browser window, we see it’s not handling different form factors well. Let’s use Tailwind CSS to add a responsive layout.</p> <h2 id="use-tailwind-css-to-build-a-responsive-layout">Use Tailwind CSS to build a responsive layout</h2> <p>Tailwind makes delivering cool-looking websites much faster than writing our CSS manually. We’ll load Tailwind via CDN for our demonstration purposes.</p> <p>Add the CDN to your CSP allowlist:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://cdn.jsdelivr.net </code></pre></div></div> <p>Navigate to <strong>Page Design</strong>, then <strong>Edit</strong> the page. Add the script to load the Tailwind resources in the <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code>. I added it after the <code class="language-plaintext highlighter-rouge">&lt;style&gt;&lt;/style&gt;</code> definitions before the <code class="language-plaintext highlighter-rouge">&lt;/head&gt;</code>.</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"</span> <span class="na">nonce=</span><span class="s">"{{nonceValue}}"</span><span class="nt">&gt;&lt;/script&gt;</span> </code></pre></div></div> <p>Loading external resources, like styles and scripts, requires a CSP nonce to mitigate cross-site scripting (XSS). You can read more about the CSP nonce on the <a href="https://content-security-policy.com/nonce/">CSP Quick Reference Guide</a>.</p> <blockquote> <p><strong>Note</strong></p> <p>Don’t use Tailwind from NPM CDN for production use cases. The Tailwind documentation notes this is for experimentation and prototyping only, as the CDN has rate limits. If your brand uses Tailwind for other production sites, you’ve most likely defined custom mixins and themes in Tailwind. Therefore, reference your production Tailwind resources in place of the CDN we’re using in this post.</p> </blockquote> <p>Remove the styles for <code class="language-plaintext highlighter-rouge">#okta-auth-container</code> and <code class="language-plaintext highlighter-rouge">#okta-login-container</code> from the <code class="language-plaintext highlighter-rouge">&lt;style&gt;&lt;/style&gt;</code> section. We can use Tailwind to handle it. The <code class="language-plaintext highlighter-rouge">&lt;style&gt;&lt;/style&gt;</code> section should only contain the CSS custom properties defined in <code class="language-plaintext highlighter-rouge">:root</code> and the directive to use SIW Gen3.</p> <p>Add the styles for Tailwind. We’ll add the classes to show the login container without the hero image in smaller form factors, then display the hero image with different widths depending on the breakpoints.</p> <p>The two <code class="language-plaintext highlighter-rouge">div</code> containers look like this:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-auth-container"</span> <span class="na">class=</span><span class="s">"h-screen flex bg-(--color-gray) bg-[{{bgImageUrl}}]"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-login-container"</span> <span class="na">class=</span><span class="s">"w-full min-w-sm lg:w-2/3 xl:w-1/2 bg-(image:--color-gradient) lg:bg-none bg-(--color-white) flex justify-center items-center"</span><span class="nt">&gt;&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre></div></div> <p>Save the file and publish the changes. Feel free to test it out!</p> <h2 id="use-tailwind-for-custom-html-elements-on-your-okta-hosted-sign-in-page">Use Tailwind for custom HTML elements on your Okta-hosted sign-in page</h2> <p>Tailwind excels at adding styled HTML elements to websites. We can also take advantage of this. Let’s say you want to maintain continuity of the webpage from your site through the sign-in page by adding a footer with links to your brand’s sites. Adding this new section involves changing the HTML node structure and styling the elements.</p> <p>We want a footer pinned to the bottom of the view, so we’ll need a new parent container with vertical stacking and ensure the height of the footer stays consistent. Replace the HTML node structure to look like this:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"flex flex-col min-h-screen"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-auth-container"</span> <span class="na">class=</span><span class="s">"flex grow bg-(--color-gray) bg-[{{bgImageUrl}}]"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"w-full min-w-sm lg:w-2/3 xl:w-1/2 bg-(image:--color-gradient) lg:bg-none bg-(--color-white) flex justify-center items-center"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-login-container"</span><span class="nt">&gt;&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;footer</span> <span class="na">class=</span><span class="s">"font-(family-name:--font-body)"</span><span class="nt">&gt;</span> <span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"h-12 flex justify-evenly items-center text-(--color-azul)"</span><span class="nt">&gt;</span> <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">class=</span><span class="s">"hover:text-(--color-orange) hover:underline"</span> <span class="na">href=</span><span class="s">"https://developer.okta.com"</span><span class="nt">&gt;</span>Terms<span class="nt">&lt;/a&gt;&lt;/li&gt;</span> <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">class=</span><span class="s">"hover:text-(--color-orange) hover:underline"</span> <span class="na">href=</span><span class="s">"https://developer.okta.com"</span><span class="nt">&gt;</span>Docs<span class="nt">&lt;/a&gt;&lt;/li&gt;</span> <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">class=</span><span class="s">"hover:text-(--color-orange) hover:underline"</span> <span class="na">href=</span><span class="s">"https://developer.okta.com/blog"</span><span class="nt">&gt;</span>Blog<span class="nt">&lt;/a&gt;&lt;/li&gt;</span> <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">class=</span><span class="s">"hover:text-(--color-orange) hover:underline"</span> <span class="na">href=</span><span class="s">"https://devforum.okta.com"</span><span class="nt">&gt;</span>Community<span class="nt">&lt;/a&gt;&lt;/li&gt;</span> <span class="nt">&lt;/ul&gt;</span> <span class="nt">&lt;/footer&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre></div></div> <p>Everything redirects to the Okta Developer sites. 😊 I also maintained the style of font, text colors, and text decoration styles to match the SIW elements. CSS custom properties make consistency manageable.</p> <p>Feel free to save and publish to check it out!</p> <h2 id="add-custom-interactivity-on-the-okta-hosted-sign-in-page-using-an-external-library">Add custom interactivity on the Okta-hosted sign-in page using an external library</h2> <p>Tailwind is great at styling HTML elements, but it’s not a JavaScript library. If we want interactive elements on the sign-in page, we must rely on Web APIs or libraries to assist us. Let’s say we want to ensure that users who sign in to the to-do app agree to the terms and conditions. We want a modal that blocks interaction with the SIW until the user agrees.</p> <p>We’ll use Alpine for the heavy lifting because it’s a lightweight JavaScript library that suits this need. We add the library via the NPM CDN, as we have already allowed the domain in our CSP. Add the following to the <code class="language-plaintext highlighter-rouge">&lt;head&gt;&lt;/head&gt;</code> section of the HTML. I added mine directly after the Tailwind script.</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">defer</span> <span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"</span> <span class="na">nonce=</span><span class="s">"{{nonceValue}}"</span><span class="nt">&gt;&lt;/script&gt;</span> </code></pre></div></div> <blockquote> <p><strong>Note</strong></p> <p>We’re including Alpine from the NPM CDN for demonstration and experimentation. For production applications, use a CDN that supports production scale. The NPM CDN applies rate limiting to prevent production-grade use.</p> </blockquote> <p>Next, we add the HTML tags to support the modal. Replace the HTML node structure to look like this:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"flex flex-col min-h-screen"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"modal"</span> <span class="na">x-data</span> <span class="na">x-cloak</span> <span class="na">x-show=</span><span class="s">"$store.modal.open"</span> <span class="na">x-transition:enter=</span><span class="s">"transition ease-out duration-300"</span> <span class="na">x-transition:enter-start=</span><span class="s">"opacity-0"</span> <span class="na">x-transition:enter-end=</span><span class="s">"opacity-100"</span> <span class="na">x-transition:leave=</span><span class="s">"transition ease-in duration-200"</span> <span class="na">x-transition:leave-start=</span><span class="s">"opacity-100"</span> <span class="na">x-transition:leave-end=</span><span class="s">"opacity-0 hidden"</span> <span class="na">class=</span><span class="s">"fixed inset-0 z-50 flex items-center justify-center bg-(--color-black)/80 bg-opacity-50"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">x-transition:enter=</span><span class="s">"transition ease-out duration-300"</span> <span class="na">x-transition:enter-start=</span><span class="s">"opacity-0 scale-90"</span> <span class="na">x-transition:enter-end=</span><span class="s">"opacity-100 scale-100"</span> <span class="na">x-transition:leave=</span><span class="s">"transition ease-in duration-200"</span> <span class="na">x-transition:leave-start=</span><span class="s">"opacity-100 scale-100"</span> <span class="na">x-transition:leave-end=</span><span class="s">"opacity-0 scale-90"</span> <span class="na">class=</span><span class="s">"bg-(--color-white) rounded-(--border-radius) shadow-lg p-8 max-w-md w-full mx-4"</span><span class="nt">&gt;</span> <span class="nt">&lt;h2</span> <span class="na">class=</span><span class="s">"text-2xl font-(family-name:--font-header) text-(--color-black) mb-4 text-center"</span><span class="nt">&gt;</span>Welcome to to-do app<span class="nt">&lt;/h2&gt;</span> <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"text-(--color-black) mb-6"</span><span class="nt">&gt;</span>This app is in beta. Thank you for agreeing to our terms and conditions.<span class="nt">&lt;/p&gt;</span> <span class="nt">&lt;button</span> <span class="err">@</span><span class="na">click=</span><span class="s">"$store.modal.hide()"</span> <span class="na">class=</span><span class="s">"w-full bg-(--color-fuchsia) hover:bg-(--color-orange) text-(--color-bright-white) font-medium py-2 px-4 rounded-(--border-radius) transition duration-200"</span><span class="nt">&gt;</span> Agree <span class="nt">&lt;/button&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-auth-container"</span> <span class="na">class=</span><span class="s">"flex grow bg-(--color-gray) bg-[{{bgImageUrl}}]"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"w-full min-w-sm lg:w-2/3 xl:w-1/2 bg-(image:--color-gradient) lg:bg-none bg-(--color-white) flex justify-center items-center"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-login-container"</span><span class="nt">&gt;&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;footer</span> <span class="na">class=</span><span class="s">"font-(family-name:--font-body)"</span><span class="nt">&gt;</span> <span class="nt">&lt;ul</span> <span class="na">class=</span><span class="s">"h-12 flex justify-evenly items-center text-(--color-azul)"</span><span class="nt">&gt;</span> <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">class=</span><span class="s">"hover:text-(--color-orange) hover:underline"</span> <span class="na">href=</span><span class="s">"https://developer.okta.com"</span><span class="nt">&gt;</span>Terms<span class="nt">&lt;/a&gt;&lt;/li&gt;</span> <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">class=</span><span class="s">"hover:text-(--color-orange) hover:underline"</span> <span class="na">href=</span><span class="s">"https://developer.okta.com"</span><span class="nt">&gt;</span>Docs<span class="nt">&lt;/a&gt;&lt;/li&gt;</span> <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">class=</span><span class="s">"hover:text-(--color-orange) hover:underline"</span> <span class="na">href=</span><span class="s">"https://developer.okta.com/blog"</span><span class="nt">&gt;</span>Blog<span class="nt">&lt;/a&gt;&lt;/li&gt;</span> <span class="nt">&lt;li&gt;&lt;a</span> <span class="na">class=</span><span class="s">"hover:text-(--color-orange) hover:underline"</span> <span class="na">href=</span><span class="s">"https://devforum.okta.com"</span><span class="nt">&gt;</span>Community<span class="nt">&lt;/a&gt;&lt;/li&gt;</span> <span class="nt">&lt;/ul&gt;</span> <span class="nt">&lt;/footer&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre></div></div> <p>It’s a lot to add, but I want the smooth transition animations. 😅 The built-in enter and leave states make adding the transition animation so much easier than doing it manually.</p> <p>Notice we’re using a state value to determine whether to show the modal. We’re using global state management, and setting it up is the next step. We’ll add initializing the state when Alpine initializes. Find the comment <code class="language-plaintext highlighter-rouge">// Render the Okta Sign-In Widget</code> within the <code class="language-plaintext highlighter-rouge">&lt;script&gt;&lt;/script&gt;</code> section, and add the following code that runs after Alpine initializes:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">alpine:init</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">Alpine</span><span class="p">.</span><span class="nx">store</span><span class="p">(</span><span class="dl">'</span><span class="s1">modal</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">open</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">show</span><span class="p">()</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">open</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="p">},</span> <span class="nx">hide</span><span class="p">()</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">open</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="p">});</span> <span class="p">});</span> </code></pre></div></div> <p>The event listener watches for the <code class="language-plaintext highlighter-rouge">alpine:init</code> event and runs a function that defines an element in Alpine’s store, <code class="language-plaintext highlighter-rouge">modal</code>. The <code class="language-plaintext highlighter-rouge">modal</code> store contains a property to track whether it’s open and some helper methods for showing and hiding.</p> <p>When you save and publish, you’ll see the modal upon site reload!</p> <p><img src="/assets-jekyll/blog/okta-custom-sign-in-page/modal-siw-2379ac6fcdef9e9e55634f4f98d033535efcbf959ab46ad793c1e3be94a69c84.jpg" alt="A modal which displays on top of the sign-in page where the user must accept terms before continuing" width="800" class="center-image" /></p> <p>We made the modal fixed even if the user presses <kbd>Esc</kbd> or selects the scrim. Users must agree to the terms to continue.</p> <h2 id="customize-okta-hosted-sign-in-page-behavior-using-web-apis">Customize Okta-hosted sign-in page behavior using Web APIs</h2> <p>We display the modal as soon as the webpage loads. It works, but we can also display the modal after the Sign-In Widget renders. Doing so allows us to use the nice enter and leave CSS transitions Alpine supports. We want to watch for changes to the DOM within the <code class="language-plaintext highlighter-rouge">&lt;div id="okta-login-container"&gt;&lt;/div&gt;</code>. This is the parent container that renders the SIW. We can use the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver"><code class="language-plaintext highlighter-rouge">MutationObserver</code> Web API</a> and watch for DOM mutations within the <code class="language-plaintext highlighter-rouge">div</code>.</p> <p>In the <code class="language-plaintext highlighter-rouge">&lt;script&gt;&lt;/script&gt;</code> section, after the event listener for <code class="language-plaintext highlighter-rouge">alpine:init</code>, add the following code:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">loginContainer</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#okta-login-container</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// Use MutationObserver to watch for auth container element</span> <span class="kd">const</span> <span class="nx">mutationObserver</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">element</span> <span class="o">=</span> <span class="nx">loginContainer</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">[data-se*="auth-container"]</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">element</span><span class="p">)</span> <span class="p">{</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">modal</span><span class="dl">'</span><span class="p">).</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">hidden</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// Open modal using Alpine store</span> <span class="nx">Alpine</span><span class="p">.</span><span class="nx">store</span><span class="p">(</span><span class="dl">'</span><span class="s1">modal</span><span class="dl">'</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span> <span class="c1">// Clean up the observer</span> <span class="nx">mutationObserver</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span> <span class="p">}</span> <span class="p">});</span> <span class="nx">mutationObserver</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">loginContainer</span><span class="p">,</span> <span class="p">{</span> <span class="na">childList</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">subtree</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span> </code></pre></div></div> <p>Let’s walk through what the code does. First, we’re creating a variable to reference the parent container for the SIW, as we’ll use it as the root element to target our work. Mutation observers can negatively impact performance, so it’s essential to limit the scope of the observer as much as possible.</p> <p><strong>Create the observer</strong></p> <p>We create the observer and define the behavior for observation. The observer first looks for the element with the data attribute named <code class="language-plaintext highlighter-rouge">se</code>, which includes the value <code class="language-plaintext highlighter-rouge">auth-container</code>. Okta adds a node with the data attribute for internal operations. We’ll do the same for our internal operations. 😎</p> <p><strong>Define the behavior upon observation</strong></p> <p>Once we have an element matching the <code class="language-plaintext highlighter-rouge">auth-container</code> data attribute, we show the modal, which triggers the enter transition animation. Then we clean up the observer.</p> <p><strong>Identify what to observe</strong></p> <p>We begin by observing the DOM and pass in the element to use as the root, along with a configuration specifying what to watch for. We want to look for changes in child elements and the subtree from the root to find the SIW elements.</p> <p>Lastly, let’s enable the modal to trigger based on the observer. I intentionally provided you with code snippets that force the modal to display before the SIW renders, so you could take sneak peeks at your work as we went along.</p> <p>In the HTML node structure, find the <code class="language-plaintext highlighter-rouge">&lt;div id="modal"&gt;</code>. It’s missing a class that hides the modal initially. Add the class <code class="language-plaintext highlighter-rouge">hidden</code> to the class list. The class list for the <code class="language-plaintext highlighter-rouge">&lt;div&gt;</code> should look like</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"modal"</span> <span class="na">x-data</span> <span class="na">x-cloak</span> <span class="na">x-show=</span><span class="s">"$store.modal.open"</span> <span class="na">x-transition:enter=</span><span class="s">"transition ease-out duration-300"</span> <span class="na">x-transition:enter-start=</span><span class="s">"opacity-0"</span> <span class="na">x-transition:enter-end=</span><span class="s">"opacity-100"</span> <span class="na">x-transition:leave=</span><span class="s">"transition ease-in duration-200"</span> <span class="na">x-transition:leave-start=</span><span class="s">"opacity-100"</span> <span class="na">x-transition:leave-end=</span><span class="s">"opacity-0 hidden"</span> <span class="na">class=</span><span class="s">"hidden fixed inset-0 z-50 flex items-center justify-center bg-(--color-black)/80 bg-opacity-50"</span><span class="nt">&gt;</span> <span class="c">&lt;!-- Remaining modal structure here. Compare your work to the class list above --&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre></div></div> <p>Then, in the <code class="language-plaintext highlighter-rouge">alpine:init</code> event listener, change the modal’s <code class="language-plaintext highlighter-rouge">open</code> property to default to <code class="language-plaintext highlighter-rouge">false</code>:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">alpine:init</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="nx">Alpine</span><span class="p">.</span><span class="nx">store</span><span class="p">(</span><span class="dl">'</span><span class="s1">modal</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">open</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">show</span><span class="p">()</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">open</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="p">},</span> <span class="nx">hide</span><span class="p">()</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">open</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="p">});</span> <span class="p">});</span> </code></pre></div></div> <p>Save and publish your changes. You’ll now notice a slight delay before the modal eases into view. So smooth!</p> <p><img src="/assets-jekyll/blog/okta-custom-sign-in-page/final-siw-desktop-211b475e04926250d77e2a72779c27ea2c22a65ad47f43322c22ba81006f5df2.jpg" alt="A customized Okta-hosted Sign-In Widget with custom elements, colors, and styles" width="800" class="center-image" /></p> <p>It’s worth noting that our solution isn’t foolproof; a savvy user can hide the modal and continue interacting with the sign-in widget by manipulating elements in the browser’s debugger. You’ll need to add extra checks and more robust code for foolproof methods. Still, this example provides a general idea of capabilities and how one might approach adding interactive components to the sign-in experience.</p> <p>Don’t forget to test any implementation changes to the sign-in page for accessibility. The default site and the sign-in widget are accessible. Any changes or customizations we make may alter the accessibility of the site.</p> <p>You can connect your brand to one of our sample apps to see it work end-to-end. Follow the instructions in the README of our <a href="https://github.com/okta-samples/okta-react-sample">Okta React Sample</a> to run the app locally. You’ll need to update your Okta OpenID Connect (OIDC) application to work with the domain. In the Okta Admin Console, navigate to <strong>Applications</strong> &gt; <strong>Applications</strong> and find the Okta application for your custom app. Navigate to the <strong>Sign On</strong> tab. You’ll see a section for <strong>OpenID Connect ID Token</strong>. Select <strong>Edit</strong> and select <strong>Custom URL</strong> for your brand’s sign-in URL as the <strong>Issuer</strong> value.</p> <p>You’ll use the issuer value, which matches your brand’s custom URL, and the Okta application’s client ID in your custom app’s OIDC configuration.</p> <h2 id="add-tailwind-web-apis-and-javascript-libraries-to-customize-your-okta-hosted-sign-in-page">Add Tailwind, Web APIs, and JavaScript libraries to customize your Okta-hosted sign-in page</h2> <p>I hope you found this post interesting and unlocked the potential of how much you can customize the Okta-hosted Sign-In Widget experience.</p> <p>You can find the final code for this project in the <a href="https://github.com/oktadev/okta-js-siw-customization-example/tree/main/custom-signin-blog-post">GitHub repo</a>.</p> <p>If you liked this post, check out these resources.</p> <ul> <li><a href="/blog/2025-11-12-custom-signin">Stretch Your Imagination and Build a Delightful Sign-In Experience</a></li> <li><a href="https://developer.okta.com/docs/concepts/sign-in-widget/">The Okta Sign-In Widget</a></li> </ul> <p>Remember to follow us on <a href="https://www.linkedin.com/company/oktadev">LinkedIn</a> and subscribe to our <a href="https://www.youtube.com/c/oktadev">YouTube</a> for more exciting content. Let us know how you customized the Okta-hosted sign-in page. We’d love to see what you came up with.</p> <p>We also want to hear from you about topics you want to see and questions you may have. Leave us a comment below!</p> Mon, 24 Nov 2025 00:00:00 -0500 https://developer.okta.com/blog/2025/11/24/okta-custom-sign-in-page https://developer.okta.com/blog/2025/11/24/okta-custom-sign-in-page Secure Authentication with a Push Notification in Your iOS Device <p>Building secure and seamless sign-in experiences is a core challenge for today’s iOS developers. Users expect authentication that feels instant, yet protects them with strong safeguards like multi-factor authentication (MFA). With Okta’s DirectAuth and push notification support, you can achieve both – delivering native, phishing-resistant MFA flows without ever leaving your app.</p> <p>In this post, we’ll walk you through how to:</p> <ol> <li>Set up your Okta developer account</li> <li>Configure your Okta org for DirectAuth and push notification factor</li> <li>Enable your iOS app to drive DirectAuth flows natively</li> <li>Create an AuthService with the support of DirectAuth</li> <li>Build a fully working SwiftUI demo leveraging the AuthService</li> </ol> <p>Note: This guide assumes you’re comfortable developing in Xcode using Swift and have basic familiarity with Okta’s identity flows.</p> <p>If you want to skip the tutorial and run the project, you can <a href="https://github.com/oktadev/okta-ios-swift-directauth-example">follow the instructions in the project’s README</a>.</p> <p><strong class="hide">Table of Contents</strong></p> <ul id="markdown-toc"> <li><a href="#use-okta-directauth-with-push-notification-factor" id="markdown-toc-use-okta-directauth-with-push-notification-factor">Use Okta DirectAuth with push notification factor</a></li> <li><a href="#prefer-phishing-resistant-authentication-factors" id="markdown-toc-prefer-phishing-resistant-authentication-factors">Prefer phishing-resistant authentication factors</a></li> <li><a href="#set-up-your-ios-project-with-oktas-mobile-sdks" id="markdown-toc-set-up-your-ios-project-with-oktas-mobile-sdks">Set up your iOS project with Okta’s mobile SDKs</a></li> <li><a href="#authenticate-your-ios-app-using-okta-directauth" id="markdown-toc-authenticate-your-ios-app-using-okta-directauth">Authenticate your iOS app using Okta DirectAuth</a></li> <li><a href="#add-the-oidc-configuration-to-your-ios-app" id="markdown-toc-add-the-oidc-configuration-to-your-ios-app">Add the OIDC configuration to your iOS app</a></li> <li><a href="#add-authentication-in-your-ios-app-without-a-browser-redirect-using-okta-directauth" id="markdown-toc-add-authentication-in-your-ios-app-without-a-browser-redirect-using-okta-directauth">Add authentication in your iOS app without a browser redirect using Okta DirectAuth</a> <ul> <li><a href="#secure-native-sign-in-in-ios" id="markdown-toc-secure-native-sign-in-in-ios">Secure, native sign-in in iOS</a></li> <li><a href="#sign-out-users-when-using-directauth" id="markdown-toc-sign-out-users-when-using-directauth">Sign-out users when using DirectAuth</a></li> <li><a href="#refresh-access-tokens-securely" id="markdown-toc-refresh-access-tokens-securely">Refresh access tokens securely</a></li> </ul> </li> <li><a href="#display-the-authenticated-users-information" id="markdown-toc-display-the-authenticated-users-information">Display the authenticated user’s information</a></li> <li><a href="#build-the-swiftui-views-to-display-authenticated-state" id="markdown-toc-build-the-swiftui-views-to-display-authenticated-state">Build the SwiftUI views to display authenticated state</a> <ul> <li><a href="#read-id-token-info" id="markdown-toc-read-id-token-info">Read ID token info</a></li> </ul> </li> <li><a href="#view-the-authenticated-users-profile-info" id="markdown-toc-view-the-authenticated-users-profile-info">View the authenticated user’s profile info</a> <ul> <li><a href="#keeping-tokens-refreshed-and-maintaining-user-sessions" id="markdown-toc-keeping-tokens-refreshed-and-maintaining-user-sessions">Keeping tokens refreshed and maintaining user sessions</a></li> </ul> </li> <li><a href="#build-your-own-secure-native-sign-in-ios-app" id="markdown-toc-build-your-own-secure-native-sign-in-ios-app">Build your own secure native sign-in iOS app</a></li> </ul> <h2 id="use-okta-directauth-with-push-notification-factor">Use Okta DirectAuth with push notification factor</h2> <p>The first step in implementing Direct Authentication with push-based MFA is setting up your Okta org and enabling the Push Notification factor. DirectAuth allows your app to handle authentication entirely within its own native UI – no browser redirection required – while still leveraging Okta’s secure OAuth 2.0 and OpenID Connect (OIDC) standards under the hood.</p> <p>This means your app can seamlessly verify credentials, obtain tokens, and trigger a push notification challenge without switching contexts or relying on the <code class="language-plaintext highlighter-rouge">SafariViewController</code>.</p> <p>Before you begin, you’ll need an Okta Integrator Free Plan account. To get one, sign up for an <a href="https://developer.okta.com/login">Integrator account</a>. Once you have an account, sign in to your <a href="https://developer.okta.com/login">Integrator account</a>. Next, in the Admin Console:</p> <ol> <li>Go to <strong>Applications</strong> &gt; <strong>Applications</strong></li> <li>Select <strong>Create App Integration</strong></li> <li>Select <strong>OIDC - OpenID Connect</strong> as the sign-in method</li> <li>Select <strong>Native Application</strong> as the application type, then select <strong>Next</strong></li> <li>Enter an app integration name</li> <li>Configure the redirect URIs: <ul> <li><strong>Redirect URI</strong>: <code class="language-plaintext highlighter-rouge">com.okta.{yourOktaDomain}:/callback</code></li> <li><strong>Post Logout Redirect URI</strong>: <code class="language-plaintext highlighter-rouge">com.okta.{yourOktaDomain}:/</code> (where <code class="language-plaintext highlighter-rouge">{yourOktaDomain}.okta.com</code> is your Okta domain name). Your domain name is reversed to provide a unique scheme to open your app on a device.</li> </ul> </li> <li>Select <strong>Advanced v</strong>. <ul> <li>Select the <strong>OOB</strong> and <strong>MFA OOB</strong> grant types.</li> </ul> </li> <li>In the <strong>Controlled access</strong> section, select the appropriate access level</li> <li>Select <strong>Save</strong></li> </ol> <p>NOTE: When using a custom authorization server, you need to set up authorization policies. Complete these additional steps:</p> <ol> <li>In the Admin Console, go to <strong>Security</strong> &gt; <strong>API</strong> &gt; <strong>Authorization Servers</strong></li> <li>Select your custom authorization server (<code class="language-plaintext highlighter-rouge">default</code>)</li> <li>On the Access Policies tab, ensure you have at least one policy: <ul> <li>If no policies exist, select <strong>Add New Access Policy</strong></li> <li>Give it a name like “Default Policy”</li> <li>Set <strong>Assign</strong> to “All clients”</li> <li>Click <strong>Create Policy</strong></li> </ul> </li> <li>For your policy, ensure you have at least one rule: <ul> <li>Select <strong>Add Rule</strong> if no rules exist</li> <li>Give it a name like “Default Rule”</li> <li>Set <strong>Grant type</strong> is to “Authorization Code”</li> <li>Select <strong>Advanced</strong> and enable “MFA OOB”</li> <li>Set <strong>User is</strong> to “Any user assigned the app”</li> <li>Set <strong>Scopes requested</strong> to “Any scopes”</li> <li>Select <strong>Create Rule</strong></li> </ul> </li> </ol> <p>For more details, see the <a href="https://developer.okta.com/docs/concepts/auth-servers/#custom-authorization-server">Custom Authorization Server</a> documentation.</p> <details> <summary>Where are my new app's credentials?</summary> <div> <p>Creating an OIDC Native App manually in the Admin Console configures your Okta Org with the application settings.</p> <p>After creating the app, you can find the configuration details on the app’s <strong>General</strong> tab:</p> <ul> <li><strong>Client ID</strong>: Found in the <strong>Client Credentials</strong> section</li> <li><strong>Issuer</strong>: Found in the <strong>Issuer URI</strong> field for the authorization server that appears by selecting <strong>Security</strong> &gt; <strong>API</strong> from the navigation pane.</li> </ul> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Issuer: https://dev-133337.okta.com/oauth2/default Client ID: 0oab8eb55Kb9jdMIr5d6 </code></pre></div> </div> <p><strong>NOTE</strong>: You can also use the <a href="https://github.com/okta/okta-cli-client">Okta CLI Client</a> or <a href="https://github.com/okta/okta-powershell-cli">Okta PowerShell Module</a> to automate this process. See this guide for more information about setting up your app.</p> </div> </details> <h2 id="prefer-phishing-resistant-authentication-factors">Prefer phishing-resistant authentication factors</h2> <p>When implementing DirectAuth with push notifications, security remains your top priority. Every new Okta Integrator Free Plan account requires admins to configure multi-factor authentication (MFA) using Okta Verify by default. We’ll keep these default settings for this tutorial, as they already support Okta Verify Push, the recommended factor for a native and secure authentication experience.</p> <p>Push notifications through Okta Verify provide strong, phishing-resistant protection by requiring the user to approve sign-in attempts directly from a trusted device. Combined with biometric verification (Face ID or Touch ID) or device PIN enforcement, Okta Verify Push ensures that only the legitimate user can complete the authentication flow – even if credentials are compromised.</p> <p>By default, push factor isn’t enabled in the Integrator Free org. Let’s enable it now.</p> <p>Navigate to <strong>Security</strong> &gt; <strong>Authenticators</strong>. Find <strong>Okta Verify</strong> and select <strong>Actions</strong> &gt; <strong>Edit</strong>. In the <strong>Okta Verify</strong> modal, find <strong>Verification options</strong> and select <strong>Push notification (Android and iOS only)</strong>. Select <strong>Save</strong>.</p> <h2 id="set-up-your-ios-project-with-oktas-mobile-sdks">Set up your iOS project with Okta’s mobile SDKs</h2> <p>Before integrating Okta DirectAuth and Push Notification MFA, make sure your development environment meets the following requirements:</p> <ul> <li>Xcode 15.0 or later – This guide assumes you’re comfortable developing iOS apps in Swift using Xcode.</li> <li>Swift 5+ – All examples use modern Swift language features.</li> <li>Swift Package Manager (SPM) – Dependency manager handled through SPM, which is built into Xcode.</li> </ul> <p>Once your environment is ready, create a new iOS project in Xcode and prepare it for integration with Okta’s mobile libraries.</p> <h2 id="authenticate-your-ios-app-using-okta-directauth">Authenticate your iOS app using Okta DirectAuth</h2> <p>If you are starting from scratch, create a new iOS app:</p> <ol> <li>Open Xcode</li> <li>Go to <strong>File</strong> &gt; <strong>New</strong> &gt; <strong>Project</strong></li> <li>Select <strong>iOS App</strong> and select <strong>Next</strong></li> <li>Enter the name of the project, such as “okta-mfa-direct-auth”</li> <li>Set the Interface to SwiftUI</li> <li>Select <strong>Next</strong> and save your project locally</li> </ol> <p>To integrate Okta’s Direct Authentication SDK into your iOS app, we’ll use Swift Package Manager (SPM) – the recommended and modern way to manage dependencies in Xcode.</p> <p>Follow these steps:</p> <ol> <li>Open your project in Xcode (or create a new one if needed)</li> <li>Go to <strong>File</strong> &gt; <strong>Add Package Dependencies</strong></li> <li>In the search field at the top-right, enter: <code class="language-plaintext highlighter-rouge">https://github.com/okta/okta-mobile-swift</code> and press <kbd>Return</kbd>. Xcode will automatically fetch the available packages.</li> <li>Select the <strong>latest version</strong> (recommended) or specify a compatible version with your setup</li> <li>When prompted to choose which products to add, ensure that you select your app target next to <strong>OktaDirectAuth</strong> and <strong>AuthFoundation</strong></li> <li>Select <strong>Add Package</strong></li> </ol> <p>These packages provide all the tools you need to implement native authentication flows using OAuth 2.0 and OpenID Connect (OIDC) with DirectAuth, including secure token handling and MFA challenge management – without relying on a browser session.</p> <p>Once the integration is complete, you’ll see <strong>OktaMobileSwift</strong> and its dependencies listed under your project’s <strong>Package Dependencies</strong> section in Xcode.</p> <h2 id="add-the-oidc-configuration-to-your-ios-app">Add the OIDC configuration to your iOS app</h2> <p>The cleanest and most scalable way to manage configuration is to use a property list file for Okta stored in your app bundle.</p> <p>Create the property list for your OIDC and app config by following these steps:</p> <ol> <li>Right-click on the root folder of the project</li> <li>Select <strong>New File from Template</strong> (<strong>New File</strong> in legacy Xcode versions)</li> <li>Ensure you have iOS selected on the top picker</li> <li>Select <strong>Property List template</strong> and select <strong>Next</strong></li> <li>Name the template <code class="language-plaintext highlighter-rouge">Okta</code> and select Create to create an <code class="language-plaintext highlighter-rouge">Okta.plist</code> file</li> </ol> <p>You can edit the file in XML format by right-clicking and selecting <strong>Open As</strong> &gt; <strong>Source Code</strong>. Copy and paste the following code into the file.</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span> <span class="cp">&lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt;</span> <span class="nt">&lt;plist</span> <span class="na">version=</span><span class="s">"1.0"</span><span class="nt">&gt;</span> <span class="nt">&lt;dict&gt;</span> <span class="nt">&lt;key&gt;</span>scopes<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;string&gt;</span>openid profile offline_access<span class="nt">&lt;/string&gt;</span> <span class="nt">&lt;key&gt;</span>redirectUri<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;string&gt;</span>com.okta.{yourOktaDomain}:/callback<span class="nt">&lt;/string&gt;</span> <span class="nt">&lt;key&gt;</span>clientId<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;string&gt;</span>{yourClientID}<span class="nt">&lt;/string&gt;</span> <span class="nt">&lt;key&gt;</span>issuer<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;string&gt;</span>{yourOktaDomain}/oauth2/default<span class="nt">&lt;/string&gt;</span> <span class="nt">&lt;key&gt;</span>logoutRedirectUri<span class="nt">&lt;/key&gt;</span> <span class="nt">&lt;string&gt;</span>com.okta.{yourOktaDomain}:/<span class="nt">&lt;/string&gt;</span> <span class="nt">&lt;/dict&gt;</span> <span class="nt">&lt;/plist&gt;</span> </code></pre></div></div> <p>Replace <code class="language-plaintext highlighter-rouge">{yourOktaDomain}</code> and <code class="language-plaintext highlighter-rouge">{yourClientID}</code> with the values from your Okta org.</p> <p>If you use something like this in your code, you can directly access the <code class="language-plaintext highlighter-rouge">DirectAuth</code> shared instance, which is already initialized and ready to handle authentication requests.</p> <h2 id="add-authentication-in-your-ios-app-without-a-browser-redirect-using-okta-directauth">Add authentication in your iOS app without a browser redirect using Okta DirectAuth</h2> <p>Now that you’ve added the SDK and property list file, let’s implement the main authentication logic for your app.</p> <p>We’ll build a dedicated service called <code class="language-plaintext highlighter-rouge">AuthService</code>, responsible for logging users in and out, refreshing tokens, and managing session state.</p> <p>This service will rely on OktaDirectAuth for native authentication and <code class="language-plaintext highlighter-rouge">AuthFoundation</code> for secure token handling.</p> <p>To set it up, create a new folder named <code class="language-plaintext highlighter-rouge">Auth</code> under your project’s folder structure, then add a new Swift file called <code class="language-plaintext highlighter-rouge">AuthService.swift</code>.</p> <p>Here, you’ll define your authentication protocol and a concrete class that integrates directly with the Okta SDK – making it easy to use across your SwiftUI or UIKit views.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">AuthFoundation</span> <span class="kd">import</span> <span class="kt">OktaDirectAuth</span> <span class="kd">import</span> <span class="kt">Observation</span> <span class="kd">import</span> <span class="kt">Foundation</span> <span class="kd">protocol</span> <span class="kt">AuthServicing</span> <span class="p">{</span> <span class="c1">// The accessToken of the logged in user</span> <span class="k">var</span> <span class="nv">accessToken</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span> <span class="c1">// State for driving SwiftUI</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">AuthService</span><span class="o">.</span><span class="kt">State</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span> <span class="c1">// Sign in (Password + Okta Verify Push)</span> <span class="kd">func</span> <span class="nf">signIn</span><span class="p">(</span><span class="nv">username</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">password</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="k">async</span> <span class="k">throws</span> <span class="c1">// Sign out &amp; revoke tokens</span> <span class="kd">func</span> <span class="nf">signOut</span><span class="p">()</span> <span class="k">async</span> <span class="c1">// Refresh access token if possible (returns updated token if refreshed)</span> <span class="kd">func</span> <span class="nf">refreshTokenIfNeeded</span><span class="p">()</span> <span class="k">async</span> <span class="k">throws</span> <span class="c1">// Getting the userInfo out of the Credential</span> <span class="kd">func</span> <span class="nf">userInfo</span><span class="p">()</span> <span class="k">async</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="kt">UserInfo</span><span class="p">?</span> <span class="p">}</span> </code></pre></div></div> <p>With this added, you will get an error that <code class="language-plaintext highlighter-rouge">AuthService</code> can’t be found. That’s because we haven’t created the class yet. Below this code, add the following declarations of the <code class="language-plaintext highlighter-rouge">AuthService</code> class:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">AuthService</span><span class="p">:</span> <span class="kt">AuthServicing</span> <span class="p">{</span> <span class="p">}</span> </code></pre></div></div> <p>After doing so, we next need to confirm the <code class="language-plaintext highlighter-rouge">AuthService</code> class to the <code class="language-plaintext highlighter-rouge">AuthServicing</code> protocol and also create the <code class="language-plaintext highlighter-rouge">State</code> enum, which will hold all the states of our Authentication process.</p> <p>To do that, first let’s create the <code class="language-plaintext highlighter-rouge">State</code> enum inside the <code class="language-plaintext highlighter-rouge">AuthService</code> class like this:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">AuthService</span><span class="p">:</span> <span class="kt">AuthServicing</span> <span class="p">{</span> <span class="kd">enum</span> <span class="kt">State</span><span class="p">:</span> <span class="kt">Equatable</span> <span class="p">{</span> <span class="k">case</span> <span class="n">idle</span> <span class="k">case</span> <span class="n">authenticating</span> <span class="k">case</span> <span class="n">waitingForPush</span> <span class="k">case</span> <span class="nf">authorized</span><span class="p">(</span><span class="kt">Token</span><span class="p">)</span> <span class="k">case</span> <span class="nf">failed</span><span class="p">(</span><span class="nv">errorMessage</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The new code resolved the two errors about the <code class="language-plaintext highlighter-rouge">AuthService</code> and the <code class="language-plaintext highlighter-rouge">State</code> enum. We only have one error to fix, which is confirming the class to the protocol.</p> <p>We will start implementing the functions top to bottom. Let’s first add the two variables from the protocol, accessToken and state. After the definition of the enum, we will add the properties:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">AuthService</span><span class="p">:</span> <span class="kt">AuthServicing</span> <span class="p">{</span> <span class="kd">enum</span> <span class="kt">State</span><span class="p">:</span> <span class="kt">Equatable</span> <span class="p">{</span> <span class="k">case</span> <span class="n">idle</span> <span class="k">case</span> <span class="n">authenticating</span> <span class="k">case</span> <span class="n">waitingForPush</span> <span class="k">case</span> <span class="nf">authorized</span><span class="p">(</span><span class="kt">Token</span><span class="p">)</span> <span class="k">case</span> <span class="nf">failed</span><span class="p">(</span><span class="nv">errorMessage</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">}</span> <span class="kd">private(set)</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">State</span> <span class="o">=</span> <span class="o">.</span><span class="n">idle</span> <span class="k">var</span> <span class="nv">accessToken</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>For now, we will leave the <code class="language-plaintext highlighter-rouge">accessToken</code> getter with a return value of <code class="language-plaintext highlighter-rouge">nil</code>, as we are not using the token yet. We’ll add the implementation later.</p> <p>Next, we’ll add a private property to hold a reference to the <code class="language-plaintext highlighter-rouge">DirectAuthenticationFlow</code> instance.</p> <p>This object manages the entire DirectAuth process, including credential verification, MFA challenges, and token issuance. The object must persist across authentication steps.</p> <p>Insert the following variable between the existing <code class="language-plaintext highlighter-rouge">state</code> and <code class="language-plaintext highlighter-rouge">accessToken</code> properties:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private(set)</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">State</span> <span class="o">=</span> <span class="o">.</span><span class="n">idle</span> <span class="kd">@ObservationIgnored</span> <span class="kd">private</span> <span class="k">let</span> <span class="nv">flow</span><span class="p">:</span> <span class="kt">DirectAuthenticationFlow</span><span class="p">?</span> <span class="k">var</span> <span class="nv">accessToken</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> </code></pre></div></div> <p>To allocate the flow variable, we will need to implement an initializer for the <code class="language-plaintext highlighter-rouge">AuthService</code> class. Inside, we’ll allocate the flow using the <code class="language-plaintext highlighter-rouge">PropertyListConfiguration</code> that we introduced earlier. Just after the <code class="language-plaintext highlighter-rouge">accessToken</code> getter, add the following function:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MARK: Init</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{</span> <span class="c1">// Prefer PropertyListConfiguration if Okta.plist exists; otherwise fall back</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">configuration</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">OAuth2Client</span><span class="o">.</span><span class="kt">PropertyListConfiguration</span><span class="p">()</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">flow</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">DirectAuthenticationFlow</span><span class="p">(</span><span class="nv">client</span><span class="p">:</span> <span class="kt">OAuth2Client</span><span class="p">(</span><span class="n">configuration</span><span class="p">))</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">flow</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">DirectAuthenticationFlow</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>This will try to fetch the Okta.plist file from the project’s folder, and if not found, will fall back to the default initializer of <code class="language-plaintext highlighter-rouge">the DirectAuthenticationFlow</code>. We have now successfully allocated the <code class="language-plaintext highlighter-rouge">DirectAuthenticationFlow</code>, and we can proceed with implementing the next functions of the protocol.</p> <p>Moving down to the first function in the protocol, which is the <code class="language-plaintext highlighter-rouge">signIn(username: String, password: String)</code>.</p> <p>The <code class="language-plaintext highlighter-rouge">signIn</code> method below performs the full authentication flow using Okta DirectAuth and Auth Foundation. It authenticates a user with their username and password, handles MFA challenges (in this case, Okta Verify Push), and securely stores the resulting token for future API calls. Add the following code just under the Init that we just added.</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MARK: AuthServicing</span> <span class="kd">func</span> <span class="nf">signIn</span><span class="p">(</span><span class="nv">username</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">password</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Task</span> <span class="p">{</span> <span class="kd">@MainActor</span> <span class="k">in</span> <span class="c1">// 1️⃣ Start the Sign-In Process</span> <span class="c1">// Update UI state and begin the DirectAuth flow with username/password.</span> <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">authenticating</span> <span class="k">do</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">result</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="n">flow</span><span class="p">?</span><span class="o">.</span><span class="nf">start</span><span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="nv">with</span><span class="p">:</span> <span class="o">.</span><span class="nf">password</span><span class="p">(</span><span class="n">password</span><span class="p">))</span> <span class="k">switch</span> <span class="n">result</span> <span class="p">{</span> <span class="c1">// 2️⃣ Handle Successful Authentication</span> <span class="c1">// Okta validated credentials, return access/refresh/ID tokens.</span> <span class="k">case</span> <span class="o">.</span><span class="nf">success</span><span class="p">(</span><span class="k">let</span> <span class="nv">token</span><span class="p">):</span> <span class="k">let</span> <span class="nv">newCred</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">Credential</span><span class="o">.</span><span class="nf">store</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="o">=</span> <span class="n">newCred</span> <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="nf">authorized</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="c1">// 3️⃣ Handle MFA with Push Notification</span> <span class="c1">// Okta requires MFA, wait for push approval via Okta Verify.</span> <span class="k">case</span> <span class="o">.</span><span class="nv">mfaRequired</span><span class="p">:</span> <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">waitingForPush</span> <span class="k">let</span> <span class="nv">status</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="n">flow</span><span class="p">?</span><span class="o">.</span><span class="nf">resume</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="o">.</span><span class="nf">oob</span><span class="p">(</span><span class="nv">channel</span><span class="p">:</span> <span class="o">.</span><span class="n">push</span><span class="p">))</span> <span class="k">if</span> <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">success</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="o">=</span> <span class="n">status</span> <span class="p">{</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">Credential</span><span class="o">.</span><span class="nf">store</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="nf">authorized</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="p">}</span> <span class="k">default</span><span class="p">:</span> <span class="k">break</span> <span class="p">}</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span> <span class="c1">// 4️⃣ Handle Errors Gracefully</span> <span class="c1">// Update state with a descriptive error message for the UI.</span> <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="nf">failed</span><span class="p">(</span><span class="nv">errorMessage</span><span class="p">:</span> <span class="n">error</span><span class="o">.</span><span class="n">localizedDescription</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Let’s break down what’s happening step by step:</p> <p><strong>1. Start the sign-in process</strong></p> <p>When the function is called, it launches a new asynchronous Task and sets the UI state to .authenticating. It then initiates the DirectAuth flow using the provided username and password:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">result</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="n">flow</span><span class="p">?</span><span class="o">.</span><span class="nf">start</span><span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="nv">with</span><span class="p">:</span> <span class="o">.</span><span class="nf">password</span><span class="p">(</span><span class="n">password</span><span class="p">))</span> </code></pre></div></div> <p>This sends the user’s credentials to Okta’s Direct Authentication API and waits for a response.</p> <p><strong>2. Handle successful authentication</strong></p> <p>If Okta validates the credentials and no additional verification is needed, the result will be <code class="language-plaintext highlighter-rouge">.success(token)</code>.</p> <p>The returned Token object contains access, refresh, and ID tokens.</p> <p>We securely persist the credentials using AuthFoundation:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">newCred</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">Credential</span><span class="o">.</span><span class="nf">store</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="o">=</span> <span class="n">newCred</span> <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="nf">authorized</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> </code></pre></div></div> <p>This marks the user as authenticated and updates the app state, allowing your UI to transition to the signed-in experience.</p> <p><strong>3. Handle MFA with push notification</strong></p> <p>If Okta determines that an MFA challenge is required, the result will be .mfaRequired. The app updates its state to .waitingForPush, prompting the user to approve the login on their Okta Verify app:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">waitingForPush</span> <span class="k">let</span> <span class="nv">status</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="n">flow</span><span class="p">?</span><span class="o">.</span><span class="nf">resume</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="o">.</span><span class="nf">oob</span><span class="p">(</span><span class="nv">channel</span><span class="p">:</span> <span class="o">.</span><span class="n">push</span><span class="p">))</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">.oob(channel: .push)</code> parameter resumes the authentication flow by waiting for the push approval event from Okta Verify.</p> <p>Once the user approves, Okta returns a new token:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">success</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="o">=</span> <span class="n">status</span> <span class="p">{</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">Credential</span><span class="o">.</span><span class="nf">store</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="nf">authorized</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="p">}</span> </code></pre></div></div> <p><strong>4. Handle errors</strong></p> <p>If any step fails (e.g., invalid credentials, network issues, or push timeout), the catch block updates the UI to show an error message:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="nf">failed</span><span class="p">(</span><span class="nv">errorMessage</span><span class="p">:</span> <span class="n">error</span><span class="o">.</span><span class="n">localizedDescription</span><span class="p">)</span> </code></pre></div></div> <p>The error function allows your app to display user-friendly error states while preserving robust error handling for debugging.</p> <h3 id="secure-native-sign-in-in-ios">Secure, native sign-in in iOS</h3> <p>This function demonstrates a complete native sign-in experience with Okta DirectAuth, no web views, no redirects.</p> <p>It authenticates the user, manages token storage securely, and handles push-based MFA all within your app’s Swift layer – making the authentication flow fast, secure, and frictionless.</p> <p>The following diagram illustrates how the authentication flow works under the hood when using Okta DirectAuth with push notification authentication factor:</p> <p><img src="/assets-jekyll/blog/okta-ios-directauth/diagram-25e524254ab609c95e5c606597336aec6c1d0f8cdb02af6cd085b813d2ab9356.svg" alt="Flowchart showing the sequence of steps for authentication flow" width="800" /></p> <h3 id="sign-out-users-when-using-directauth">Sign-out users when using DirectAuth</h3> <p>Next from the protocol functions is the sign-out method. This method provides a clean and secure way to log the user out of the app.</p> <p>It revokes the user’s active tokens from Okta and resets the local authentication state, ensuring that no stale credentials remain on the device. Add the following code right below the <code class="language-plaintext highlighter-rouge">signIn</code> method:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">signOut</span><span class="p">()</span> <span class="k">async</span> <span class="p">{</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">credential</span> <span class="o">=</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="p">{</span> <span class="k">try</span><span class="p">?</span> <span class="k">await</span> <span class="n">credential</span><span class="o">.</span><span class="nf">revoke</span><span class="p">()</span> <span class="p">}</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="o">=</span> <span class="kc">nil</span> <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">idle</span> <span class="p">}</span> </code></pre></div></div> <p>Let’s look at what each step does: <strong>1. Check for an existing credential</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="k">let</span> <span class="nv">credential</span> <span class="o">=</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="p">{</span> </code></pre></div></div> <p>The method first checks if a stored credential (token) exists in memory. <code class="language-plaintext highlighter-rouge">Credential.default</code> represents the current authenticated session created earlier during sign-in.</p> <p><strong>2. Revoke the tokens from Okta</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">?</span> <span class="k">await</span> <span class="n">credential</span><span class="o">.</span><span class="nf">revoke</span><span class="p">()</span> </code></pre></div></div> <p>This line tells Okta to invalidate the access and refresh tokens associated with that credential. Calling <code class="language-plaintext highlighter-rouge">revoke()</code> ensures that the user’s session terminates locally and in the authorization server, preventing further API access with those tokens.</p> <p>The <code class="language-plaintext highlighter-rouge">try?</code> operator is used to safely ignore any errors (e.g., network failure during logout), since token revocation is a best-effort operation.</p> <p><strong>3. Clear local credential data</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="o">=</span> <span class="kc">nil</span> </code></pre></div></div> <p>After revoking the tokens, the app clears the local credential object.</p> <p>This removes any sensitive authentication data from memory, ensuring that no valid tokens remain on the device.</p> <p><strong>4. Reset the authentication state</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">idle</span> </code></pre></div></div> <p>Finally, the app updates its internal state back to <code class="language-plaintext highlighter-rouge">.idle</code>, which tells the UI that the user is now logged out and ready to start a new session.</p> <p>You can use this state to trigger a transition back to the login screen or turn off authenticated features.</p> <p>The protocol confirmation is almost complete, and we only have two functions remaining to implement.</p> <h3 id="refresh-access-tokens-securely">Refresh access tokens securely</h3> <p>Access tokens issued by Okta have a limited lifetime to reduce the risk of misuse if compromised. OAuth clients that can’t maintain secrets, like mobile apps, require short access token lifetimes for security.</p> <p>To maintain a seamless user experience, your app should refresh tokens automatically before they expire. The <code class="language-plaintext highlighter-rouge">refreshTokenIfNeeded()</code> method handles this process securely using <code class="language-plaintext highlighter-rouge">AuthFoundation</code>’s built-in token management APIs.</p> <p>Let’s walk through what it does. Add the following code right after the <code class="language-plaintext highlighter-rouge">signOut</code> method:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">refreshTokenIfNeeded</span><span class="p">()</span> <span class="k">async</span> <span class="k">throws</span> <span class="p">{</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">credential</span> <span class="o">=</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="k">try</span> <span class="k">await</span> <span class="n">credential</span><span class="o">.</span><span class="nf">refresh</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p><strong>1. Check for an existing credential</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">guard</span> <span class="k">let</span> <span class="nv">credential</span> <span class="o">=</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> </code></pre></div></div> <p>Before attempting a token refresh, the method checks whether a valid credential exists. If no credential is stored (e.g., the user hasn’t signed in yet or has logged out), the method exits early.</p> <p><strong>2. Refresh the token</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="k">await</span> <span class="n">credential</span><span class="o">.</span><span class="nf">refresh</span><span class="p">()</span> </code></pre></div></div> <p>This line tells Okta to exchange the refresh token for a new access token and ID token.</p> <p>The <code class="language-plaintext highlighter-rouge">refresh()</code> method automatically updates the <code class="language-plaintext highlighter-rouge">Credential</code> object with the new tokens and securely persists them using <code class="language-plaintext highlighter-rouge">AuthFoundation</code>.</p> <p>If the refresh token has expired or is invalid, this call throws an error – allowing your app to detect the issue and prompt the user to sign in again.</p> <h2 id="display-the-authenticated-users-information">Display the authenticated user’s information</h2> <p>Lastly, let’s look at the <code class="language-plaintext highlighter-rouge">userInfo()</code> function. After authenticating, your app can access the user’s profile information – such as their name, email, or user ID – from Okta using a standard OIDC endpoint.</p> <p>The <code class="language-plaintext highlighter-rouge">userInfo()</code> method retrieves this data from the ID token or by calling the authorization server’s <code class="language-plaintext highlighter-rouge">/userinfo</code> endpoint. The ID token doesn’t necessarily include all of the profile information though, as the ID token is intentionally lightweight.</p> <p>Here’s how it works. Add the following code after the end of <code class="language-plaintext highlighter-rouge">refreshTokenIfNeeded()</code>:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">userInfo</span><span class="p">()</span> <span class="k">async</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="kt">UserInfo</span><span class="p">?</span> <span class="p">{</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">userInfo</span> <span class="o">=</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span><span class="p">?</span><span class="o">.</span><span class="n">userInfo</span> <span class="p">{</span> <span class="k">return</span> <span class="n">userInfo</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">do</span> <span class="p">{</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">userInfo</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span><span class="p">?</span><span class="o">.</span><span class="nf">userInfo</span><span class="p">()</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> <span class="k">return</span> <span class="n">userInfo</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p><strong>1. Return the cached user info</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="k">let</span> <span class="nv">userInfo</span> <span class="o">=</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span><span class="p">?</span><span class="o">.</span><span class="n">userInfo</span> <span class="p">{</span> <span class="k">return</span> <span class="n">userInfo</span> <span class="p">}</span> </code></pre></div></div> <p>If the user’s profile information has already been fetched and stored in memory, the method returns it immediately.</p> <p>This avoids unnecessary network calls, providing a fast and responsive experience.</p> <p><strong>2. Fetch user info</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">guard</span> <span class="k">let</span> <span class="nv">userInfo</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span><span class="p">?</span><span class="o">.</span><span class="nf">userInfo</span><span class="p">()</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> </code></pre></div></div> <p>If the cached data isn’t available, the method fetches it directly from Okta using the <code class="language-plaintext highlighter-rouge">UserInfo</code> endpoint.</p> <p>This endpoint returns standard OpenID Connect claims such as:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sub (the user's unique ID) name email preferred_username etc... </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">AuthFoundation</code> SDK handles the request and parsing for you, returning a <code class="language-plaintext highlighter-rouge">UserInfo</code> object.</p> <p><strong>3. Handle errors gracefully</strong></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">catch</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> </code></pre></div></div> <p>If the request fails (for example, due to a network issue or expired token), the function returns <code class="language-plaintext highlighter-rouge">nil</code>. This prevents your app from crashing and allows you to handle the error by displaying a default user state or prompting re-authentication.</p> <p>With this implemented, you’ve resolved all the errors and should be able to build the app. 🎉</p> <h2 id="build-the-swiftui-views-to-display-authenticated-state">Build the SwiftUI views to display authenticated state</h2> <p>Now that we’ve built the <code class="language-plaintext highlighter-rouge">AuthService</code> to handle sign-in, sign-out, token management, and user info retrieval, let’s see how to integrate it into your app’s UI.</p> <p>To maintain consistency in your architecture, rename the default <code class="language-plaintext highlighter-rouge">ContentView</code> to <code class="language-plaintext highlighter-rouge">AuthView</code> and update all references accordingly.</p> <p>This clarifies the purpose of the view – it will serve as the primary authentication interface. Then, create a <code class="language-plaintext highlighter-rouge">Views</code> folder under your project’s folder, drag and drop the <code class="language-plaintext highlighter-rouge">AuthView</code> into the newly created folder, and create a new file named <code class="language-plaintext highlighter-rouge">AuthViewModel.swift</code> in the same folder.</p> <p>The <code class="language-plaintext highlighter-rouge">AuthViewModel</code> will encapsulate all authentication-related state and actions, acting as the communication layer between your view and the underlying <code class="language-plaintext highlighter-rouge">AuthService</code>.</p> <p>Add the following code in <code class="language-plaintext highlighter-rouge">AuthViewModel.swift</code>:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Foundation</span> <span class="kd">import</span> <span class="kt">Observation</span> <span class="kd">import</span> <span class="kt">AuthFoundation</span> <span class="c1">/// The `AuthViewModel` acts as the bridge between your app's UI and the authentication layer (`AuthService`).</span> <span class="c1">/// It coordinates user actions such as signing in, signing out, refreshing tokens, and fetching user profile data.</span> <span class="c1">/// This class uses Swift's `@Observable` macro so that your SwiftUI views can automatically react to state changes.</span> <span class="kd">@Observable</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">AuthViewModel</span> <span class="p">{</span> <span class="c1">// MARK: - Dependencies</span> <span class="c1">/// The authentication service responsible for handling DirectAuth sign-in,</span> <span class="c1">/// push-based MFA, token management, and user info retrieval.</span> <span class="kd">private</span> <span class="k">let</span> <span class="nv">authService</span><span class="p">:</span> <span class="kt">AuthServicing</span> <span class="c1">// MARK: - UI State Properties</span> <span class="c1">/// Stores the user's token, which can be used for secure communication</span> <span class="c1">/// with backend services that validate the user's identity.</span> <span class="k">var</span> <span class="nv">accessToken</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span> <span class="c1">/// Represents a loading statex. Set to `true` when background operations are running</span> <span class="c1">/// (such as sign-in, sign-out, or token refresh) to display a progress indicator.</span> <span class="k">var</span> <span class="nv">isLoading</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span> <span class="c1">/// Holds any human-readable error messages that should be displayed in the UI</span> <span class="c1">/// (for example, invalid credentials or network errors).</span> <span class="k">var</span> <span class="nv">errorMessage</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span> <span class="c1">/// The username and password properties are bound to text fields in the UI.</span> <span class="c1">/// As the user types, these values update automatically thanks to SwiftUI's reactive data binding.</span> <span class="c1">/// The view model then uses them to perform DirectAuth sign-in when the user submits the form.</span> <span class="k">var</span> <span class="nv">username</span><span class="p">:</span> <span class="kt">String</span> <span class="o">=</span> <span class="s">""</span> <span class="k">var</span> <span class="nv">password</span><span class="p">:</span> <span class="kt">String</span> <span class="o">=</span> <span class="s">""</span> <span class="c1">/// Exposes the current authentication state (idle, authenticating, waitingForPush, authorized, failed)</span> <span class="c1">/// as defined by the `AuthService.State` enum. The view can use this to display the correct UI.</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">AuthService</span><span class="o">.</span><span class="kt">State</span> <span class="p">{</span> <span class="n">authService</span><span class="o">.</span><span class="n">state</span> <span class="p">}</span> <span class="c1">// MARK: - Initialization</span> <span class="c1">/// Initializes the view model with a default instance of `AuthService`.</span> <span class="c1">/// You can inject a mock `AuthServicing` implementation for testing.</span> <span class="nf">init</span><span class="p">(</span><span class="nv">authService</span><span class="p">:</span> <span class="kt">AuthServicing</span> <span class="o">=</span> <span class="kt">AuthService</span><span class="p">())</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">authService</span> <span class="o">=</span> <span class="n">authService</span> <span class="p">}</span> <span class="c1">// MARK: - Authentication Actions</span> <span class="c1">/// Attempts to authenticate the user with the provided credentials.</span> <span class="c1">/// This triggers the full DirectAuth flow -- including password verification,</span> <span class="c1">/// push notification MFA (if required), and secure token storage via AuthFoundation.</span> <span class="kd">@MainActor</span> <span class="kd">func</span> <span class="nf">signIn</span><span class="p">()</span> <span class="k">async</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="k">defer</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span> <span class="p">{</span> <span class="k">try</span> <span class="k">await</span> <span class="n">authService</span><span class="o">.</span><span class="nf">signIn</span><span class="p">(</span><span class="nv">username</span><span class="p">:</span> <span class="n">username</span><span class="p">,</span> <span class="nv">password</span><span class="p">:</span> <span class="n">password</span><span class="p">)</span> <span class="n">accessToken</span> <span class="o">=</span> <span class="n">authService</span><span class="o">.</span><span class="n">accessToken</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span> <span class="n">errorMessage</span> <span class="o">=</span> <span class="n">error</span><span class="o">.</span><span class="n">localizedDescription</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">/// Signs the user out by revoking active tokens, clearing local credentials,</span> <span class="c1">/// and resetting the app's authentication state.</span> <span class="kd">@MainActor</span> <span class="kd">func</span> <span class="nf">signOut</span><span class="p">()</span> <span class="k">async</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="k">defer</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span> <span class="p">}</span> <span class="k">await</span> <span class="n">authService</span><span class="o">.</span><span class="nf">signOut</span><span class="p">()</span> <span class="p">}</span> <span class="c1">// MARK: - Token Handling</span> <span class="c1">/// Refreshes the user's access token using their refresh token.</span> <span class="c1">/// This allows the app to maintain a valid session without requiring</span> <span class="c1">/// the user to log in again after the access token expires.</span> <span class="kd">@MainActor</span> <span class="kd">func</span> <span class="nf">refreshToken</span><span class="p">()</span> <span class="k">async</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span> <span class="k">defer</span> <span class="p">{</span> <span class="nf">setLoading</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span> <span class="p">}</span> <span class="k">do</span> <span class="p">{</span> <span class="k">try</span> <span class="k">await</span> <span class="n">authService</span><span class="o">.</span><span class="nf">refreshTokenIfNeeded</span><span class="p">()</span> <span class="n">accessToken</span> <span class="o">=</span> <span class="n">authService</span><span class="o">.</span><span class="n">accessToken</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span> <span class="n">errorMessage</span> <span class="o">=</span> <span class="n">error</span><span class="o">.</span><span class="n">localizedDescription</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// MARK: - User Info Retrieval</span> <span class="c1">/// Fetches the authenticated user's profile information from Okta.</span> <span class="c1">/// Returns a `UserInfo` object containing standard OIDC claims (such as `name`, `email`, and `sub`).</span> <span class="c1">/// If fetching fails (e.g., due to expired tokens or network issues), it returns `nil`.</span> <span class="kd">@MainActor</span> <span class="kd">func</span> <span class="nf">fetchUserInfo</span><span class="p">()</span> <span class="k">async</span> <span class="o">-&gt;</span> <span class="kt">UserInfo</span><span class="p">?</span> <span class="p">{</span> <span class="k">do</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">userInfo</span> <span class="o">=</span> <span class="k">try</span> <span class="k">await</span> <span class="n">authService</span><span class="o">.</span><span class="nf">userInfo</span><span class="p">()</span> <span class="k">return</span> <span class="n">userInfo</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span> <span class="n">errorMessage</span> <span class="o">=</span> <span class="n">error</span><span class="o">.</span><span class="n">localizedDescription</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// MARK: - UI Helpers</span> <span class="c1">/// Updates the `isLoading` property. This is used to show or hide</span> <span class="c1">/// a loading spinner in your SwiftUI view while background work is in progress.</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">setLoading</span><span class="p">(</span><span class="n">_</span> <span class="nv">value</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="p">{</span> <span class="n">isLoading</span> <span class="o">=</span> <span class="n">value</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>With the view model in place, the next step is to bind it to your SwiftUI view. The <code class="language-plaintext highlighter-rouge">AuthView</code> will observe the <code class="language-plaintext highlighter-rouge">AuthViewModel</code>, updating automatically as the authentication state changes.</p> <p>It will show the user’s ID token when authenticated and provide controls for signing in, signing out, and refreshing the token.</p> <p>Open <code class="language-plaintext highlighter-rouge">AuthView.swift</code>, remove the existing template code, and insert the following implementation:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span> <span class="kd">import</span> <span class="kt">AuthFoundation</span> <span class="c1">/// A simple wrapper for `UserInfo` used to present user profile data in a full-screen modal.</span> <span class="c1">/// Conforms to `Identifiable` so it can be used with `.fullScreenCover(item:)`.</span> <span class="kd">struct</span> <span class="kt">UserInfoModel</span><span class="p">:</span> <span class="kt">Identifiable</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">id</span> <span class="o">=</span> <span class="kt">UUID</span><span class="p">()</span> <span class="k">let</span> <span class="nv">user</span><span class="p">:</span> <span class="kt">UserInfo</span> <span class="p">}</span> <span class="c1">/// The main SwiftUI view for managing the authentication experience.</span> <span class="c1">/// This view observes the `AuthViewModel`, displays different UI states</span> <span class="c1">/// based on the current authentication flow, and provides controls for</span> <span class="c1">/// signing in, signing out, refreshing tokens, and viewing user or token information.</span> <span class="kd">struct</span> <span class="kt">AuthView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="c1">// MARK: - View Model</span> <span class="c1">/// The view model that manages all authentication logic and state transitions.</span> <span class="c1">/// It uses `@Observable` from Swift's Observation framework, so changes here</span> <span class="c1">/// automatically trigger UI updates.</span> <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">viewModel</span> <span class="o">=</span> <span class="kt">AuthViewModel</span><span class="p">()</span> <span class="c1">// MARK: - State and Presentation</span> <span class="c1">/// Holds the currently fetched user information (if available).</span> <span class="c1">/// When this value is set, the `UserInfoView` is displayed as a full-screen sheet.</span> <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">userInfo</span><span class="p">:</span> <span class="kt">UserInfoModel</span><span class="p">?</span> <span class="c1">/// Controls whether the Token Info screen is presented as a full-screen modal.</span> <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">showTokenInfo</span> <span class="o">=</span> <span class="kc">false</span> <span class="c1">// MARK: - View Body</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span> <span class="p">{</span> <span class="c1">// Render the UI based on the current authentication state.</span> <span class="c1">// Each case corresponds to a different phase of the DirectAuth flow.</span> <span class="k">switch</span> <span class="n">viewModel</span><span class="o">.</span><span class="n">state</span> <span class="p">{</span> <span class="k">case</span> <span class="o">.</span><span class="n">idle</span><span class="p">,</span> <span class="o">.</span><span class="nv">failed</span><span class="p">:</span> <span class="n">loginForm</span> <span class="k">case</span> <span class="o">.</span><span class="nv">authenticating</span><span class="p">:</span> <span class="kt">ProgressView</span><span class="p">(</span><span class="s">"Signing in..."</span><span class="p">)</span> <span class="k">case</span> <span class="o">.</span><span class="nv">waitingForPush</span><span class="p">:</span> <span class="c1">// Waiting for Okta Verify push approval</span> <span class="kt">WaitingForPushView</span> <span class="p">{</span> <span class="kt">Task</span> <span class="p">{</span> <span class="k">await</span> <span class="n">viewModel</span><span class="o">.</span><span class="nf">signOut</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="k">case</span> <span class="o">.</span><span class="nv">authorized</span><span class="p">:</span> <span class="n">successView</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// MARK: - Login Form View</span> <span class="kd">private</span> <span class="kd">extension</span> <span class="kt">AuthView</span> <span class="p">{</span> <span class="c1">/// The initial sign-in form displayed when the user is not authenticated.</span> <span class="c1">/// Captures username and password input and triggers the DirectAuth sign-in flow.</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">loginForm</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Okta DirectAuth (Password + Okta Verify Push)"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">headline</span><span class="p">)</span> <span class="c1">// Email input field (bound to view model's username property)</span> <span class="kt">TextField</span><span class="p">(</span><span class="s">"Email"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">viewModel</span><span class="o">.</span><span class="n">username</span><span class="p">)</span> <span class="o">.</span><span class="nf">keyboardType</span><span class="p">(</span><span class="o">.</span><span class="n">emailAddress</span><span class="p">)</span> <span class="o">.</span><span class="nf">textContentType</span><span class="p">(</span><span class="o">.</span><span class="n">username</span><span class="p">)</span> <span class="o">.</span><span class="nf">textInputAutocapitalization</span><span class="p">(</span><span class="o">.</span><span class="n">never</span><span class="p">)</span> <span class="o">.</span><span class="nf">autocorrectionDisabled</span><span class="p">()</span> <span class="c1">// Secure password input field</span> <span class="kt">SecureField</span><span class="p">(</span><span class="s">"Password"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">viewModel</span><span class="o">.</span><span class="n">password</span><span class="p">)</span> <span class="o">.</span><span class="nf">textContentType</span><span class="p">(</span><span class="o">.</span><span class="n">password</span><span class="p">)</span> <span class="c1">// Triggers authentication via DirectAuth and Push MFA</span> <span class="kt">Button</span><span class="p">(</span><span class="s">"Sign In"</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Task</span> <span class="p">{</span> <span class="k">await</span> <span class="n">viewModel</span><span class="o">.</span><span class="nf">signIn</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">buttonStyle</span><span class="p">(</span><span class="o">.</span><span class="n">borderedProminent</span><span class="p">)</span> <span class="o">.</span><span class="nf">disabled</span><span class="p">(</span><span class="n">viewModel</span><span class="o">.</span><span class="n">username</span><span class="o">.</span><span class="n">isEmpty</span> <span class="o">||</span> <span class="n">viewModel</span><span class="o">.</span><span class="n">password</span><span class="o">.</span><span class="n">isEmpty</span><span class="p">)</span> <span class="c1">// Display error message if sign-in fails</span> <span class="k">if</span> <span class="k">case</span> <span class="o">.</span><span class="nf">failed</span><span class="p">(</span><span class="k">let</span> <span class="nv">message</span><span class="p">)</span> <span class="o">=</span> <span class="n">viewModel</span><span class="o">.</span><span class="n">state</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="o">.</span><span class="n">red</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">footnote</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// MARK: - Authorized State View</span> <span class="kd">private</span> <span class="kd">extension</span> <span class="kt">AuthView</span> <span class="p">{</span> <span class="c1">/// Displayed once the user has successfully signed in and completed MFA.</span> <span class="c1">/// Shows the user's ID token and provides actions for token refresh, user info,</span> <span class="c1">/// token details, and sign-out.</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">successView</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Signed in 🎉"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">title2</span><span class="p">)</span> <span class="o">.</span><span class="nf">bold</span><span class="p">()</span> <span class="c1">// Scrollable ID token display (for demo purposes)</span> <span class="kt">ScrollView</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="kt">Credential</span><span class="o">.</span><span class="k">default</span><span class="p">?</span><span class="o">.</span><span class="n">token</span><span class="o">.</span><span class="n">idToken</span><span class="p">?</span><span class="o">.</span><span class="n">rawValue</span> <span class="p">??</span> <span class="s">"(no id token)"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">footnote</span><span class="p">)</span> <span class="o">.</span><span class="nf">textSelection</span><span class="p">(</span><span class="o">.</span><span class="n">enabled</span><span class="p">)</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="o">.</span><span class="n">thinMaterial</span><span class="p">)</span> <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxHeight</span><span class="p">:</span> <span class="mi">220</span><span class="p">)</span> <span class="c1">// Authenticated user actions</span> <span class="n">signoutButton</span> <span class="p">}</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// MARK: - Action Buttons</span> <span class="kd">private</span> <span class="kd">extension</span> <span class="kt">AuthView</span> <span class="p">{</span> <span class="c1">/// Signs the user out, revoking tokens and returning to the login form.</span> <span class="k">var</span> <span class="nv">signoutButton</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Button</span><span class="p">(</span><span class="s">"Sign Out"</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Task</span> <span class="p">{</span> <span class="k">await</span> <span class="n">viewModel</span><span class="o">.</span><span class="nf">signOut</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="nf">system</span><span class="p">(</span><span class="nv">size</span><span class="p">:</span> <span class="mi">14</span><span class="p">))</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>With this added, you will receive an error stating that <code class="language-plaintext highlighter-rouge">WaitingForPushView</code> can’t be found in scope. To fix this, we need to add that view next. Add a new empty Swift file in the <code class="language-plaintext highlighter-rouge">Views</code> folder and name it <code class="language-plaintext highlighter-rouge">WaitingForPushView</code>. When complete, add the following implementation inside:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span> <span class="kd">struct</span> <span class="kt">WaitingForPushView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">onCancel</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> <span class="p">{</span> <span class="kt">ProgressView</span><span class="p">()</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Approve the Okta Verify push on your device."</span><span class="p">)</span> <span class="o">.</span><span class="nf">multilineTextAlignment</span><span class="p">(</span><span class="o">.</span><span class="n">center</span><span class="p">)</span> <span class="kt">Button</span><span class="p">(</span><span class="s">"Cancel"</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="n">onCancel</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Now you can run the application on a simulator, and it should present you with the option to log in first with a username and password. After selecting <strong>SignIn</strong>, it will redirect to the “Waiting for push notification” screen and remain active until you acknowledge the request from the Okta Verify App. If you’re logged in, you’ll see the access token and a sign-out button.</p> <h3 id="read-id-token-info">Read ID token info</h3> <p>Once your app authenticates a user with Okta DirectAuth, the resulting credentials are securely stored in the device’s keychain through <code class="language-plaintext highlighter-rouge">AuthFoundation</code>.</p> <p>These credentials include access, ID, and (optionally) refresh tokens – all essential for securely calling APIs or verifying user identity.</p> <p>In this section, we’ll create a skeleton <code class="language-plaintext highlighter-rouge">TokenInfoView</code> that reads the current tokens from <code class="language-plaintext highlighter-rouge">Credential.default</code> and displays them in a developer-friendly format.</p> <p>This view helps visualize the credential in the store and to inspect the scope. And it helps verify that the authentication flow works.</p> <p>Create a new Swift file in the <code class="language-plaintext highlighter-rouge">Views</code> folder and name it <code class="language-plaintext highlighter-rouge">TokenInfoView</code>. Add the following code:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span> <span class="kd">import</span> <span class="kt">AuthFoundation</span> <span class="c1">/// Displays detailed information about the tokens stored in the current</span> <span class="c1">/// `Credential.default` instance. This view is helpful for debugging and</span> <span class="c1">/// validating your DirectAuth flow -- confirming that tokens are correctly</span> <span class="c1">/// issued, stored, and refreshed.</span> <span class="c1">///</span> <span class="c1">/// ⚠️ **Important:** Avoid showing full token strings in production apps.</span> <span class="c1">/// Tokens should be treated as sensitive secrets.</span> <span class="kd">struct</span> <span class="kt">TokenInfoView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="c1">/// Retrieves the current credential object managed by `AuthFoundation`.</span> <span class="c1">/// If the user is signed in, this will contain their access, ID, and refresh tokens.</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">credential</span><span class="p">:</span> <span class="kt">Credential</span><span class="p">?</span> <span class="p">{</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span> <span class="p">}</span> <span class="c1">/// Used to dismiss the current view when the close button is tapped.</span> <span class="kd">@Environment</span><span class="p">(\</span><span class="o">.</span><span class="n">dismiss</span><span class="p">)</span> <span class="k">var</span> <span class="nv">dismiss</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ScrollView</span> <span class="p">{</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">,</span> <span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// MARK: - Close Button</span> <span class="c1">// Dismisses the token info view when tapped.</span> <span class="kt">Button</span> <span class="p">{</span> <span class="nf">dismiss</span><span class="p">()</span> <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span> <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"xmark.circle.fill"</span><span class="p">)</span> <span class="o">.</span><span class="nf">resizable</span><span class="p">()</span> <span class="o">.</span><span class="nf">foregroundStyle</span><span class="p">(</span><span class="o">.</span><span class="n">black</span><span class="p">)</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">40</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">40</span><span class="p">)</span> <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">leading</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// MARK: - Token Display</span> <span class="c1">// Displays the token information as formatted monospaced text.</span> <span class="c1">// If no credential is available, a "No token found" message is shown.</span> <span class="kt">Text</span><span class="p">(</span><span class="n">credential</span><span class="p">?</span><span class="o">.</span><span class="nf">toString</span><span class="p">()</span> <span class="p">??</span> <span class="s">"No token found"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="nf">system</span><span class="p">(</span><span class="o">.</span><span class="n">body</span><span class="p">,</span> <span class="nv">design</span><span class="p">:</span> <span class="o">.</span><span class="n">monospaced</span><span class="p">))</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="p">(</span><span class="o">.</span><span class="n">systemGroupedBackground</span><span class="p">))</span> <span class="o">.</span><span class="nf">navigationTitle</span><span class="p">(</span><span class="s">"Token Info"</span><span class="p">)</span> <span class="o">.</span><span class="nf">navigationBarTitleDisplayMode</span><span class="p">(</span><span class="o">.</span><span class="n">inline</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// MARK: - Credential Display Helper</span> <span class="kd">extension</span> <span class="kt">Credential</span> <span class="p">{</span> <span class="c1">/// Returns a formatted string representation of the stored token values.</span> <span class="c1">/// Includes access, ID, and refresh tokens as well as their associated scopes.</span> <span class="c1">///</span> <span class="c1">/// - Returns: A multi-line string suitable for debugging and display in `TokenInfoView`.</span> <span class="kd">func</span> <span class="nf">toString</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">String</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">result</span> <span class="o">=</span> <span class="s">""</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"Token type: </span><span class="se">\(</span><span class="n">token</span><span class="o">.</span><span class="n">tokenType</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"Access Token: </span><span class="se">\(</span><span class="n">token</span><span class="o">.</span><span class="n">accessToken</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"Scopes: </span><span class="se">\(</span><span class="n">token</span><span class="o">.</span><span class="n">scope</span><span class="p">?</span><span class="o">.</span><span class="nf">joined</span><span class="p">(</span><span class="nv">separator</span><span class="p">:</span> <span class="s">","</span><span class="p">)</span> <span class="p">??</span> <span class="s">"No scopes found"</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">idToken</span> <span class="o">=</span> <span class="n">token</span><span class="o">.</span><span class="n">idToken</span> <span class="p">{</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"ID Token: </span><span class="se">\(</span><span class="n">idToken</span><span class="o">.</span><span class="n">rawValue</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span> <span class="p">}</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">refreshToken</span> <span class="o">=</span> <span class="n">token</span><span class="o">.</span><span class="n">refreshToken</span> <span class="p">{</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"Refresh Token: </span><span class="se">\(</span><span class="n">refreshToken</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span> <span class="p">}</span> <span class="k">return</span> <span class="n">result</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>To view this on screen, we need to instruct SwiftUI to present it. We added the <code class="language-plaintext highlighter-rouge">State</code> variable in the <code class="language-plaintext highlighter-rouge">AuthView</code> for this purpose - it’s named <code class="language-plaintext highlighter-rouge">showTokenInfo</code>. Next, we need to add a button to present the <code class="language-plaintext highlighter-rouge">TokenInfoView</code>. Go to the <code class="language-plaintext highlighter-rouge">AuthView.swift</code> and scroll down to the last private extension where it says “Action Buttons” and add the following button:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// Opens the full-screen view showing token info.</span> <span class="k">var</span> <span class="nv">tokenInfoButton</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Button</span><span class="p">(</span><span class="s">"Token Info"</span><span class="p">)</span> <span class="p">{</span> <span class="n">showTokenInfo</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">}</span> <span class="o">.</span><span class="nf">disabled</span><span class="p">(</span><span class="n">viewModel</span><span class="o">.</span><span class="n">isLoading</span><span class="p">)</span> <span class="p">}</span> </code></pre></div></div> <p>Now that this is in place, we need to tell SwiftUI that we want to present <code class="language-plaintext highlighter-rouge">TokenInfoView</code> whenever the <code class="language-plaintext highlighter-rouge">showTokenInfo</code> boolean is true. In the <code class="language-plaintext highlighter-rouge">AuthView</code>, find the body and add this code at the end below the <code class="language-plaintext highlighter-rouge">.padding()</code>:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Show Token Info full screen</span> <span class="o">.</span><span class="nf">fullScreenCover</span><span class="p">(</span><span class="nv">isPresented</span><span class="p">:</span> <span class="err">$</span><span class="n">showTokenInfo</span><span class="p">)</span> <span class="p">{</span> <span class="kt">TokenInfoView</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p>If you build and run the app, you’ll no longer see the <strong>Token Info</strong> button when logged in. To keep the button visible, we also need to reference the <code class="language-plaintext highlighter-rouge">tokenInfoButton</code> in the <code class="language-plaintext highlighter-rouge">successView</code>. In the <code class="language-plaintext highlighter-rouge">AuthView</code> file, scroll down to “Authorized State View” (<code class="language-plaintext highlighter-rouge">successView</code>) and reference the button just above the <code class="language-plaintext highlighter-rouge">signoutButton</code> like this:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="k">var</span> <span class="nv">successView</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Signed in 🎉"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">title2</span><span class="p">)</span> <span class="o">.</span><span class="nf">bold</span><span class="p">()</span> <span class="c1">// Scrollable ID token display (for demo purposes)</span> <span class="kt">ScrollView</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="kt">Credential</span><span class="o">.</span><span class="k">default</span><span class="p">?</span><span class="o">.</span><span class="n">token</span><span class="o">.</span><span class="n">idToken</span><span class="p">?</span><span class="o">.</span><span class="n">rawValue</span> <span class="p">??</span> <span class="s">"(no id token)"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">footnote</span><span class="p">)</span> <span class="o">.</span><span class="nf">textSelection</span><span class="p">(</span><span class="o">.</span><span class="n">enabled</span><span class="p">)</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="o">.</span><span class="n">thinMaterial</span><span class="p">)</span> <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxHeight</span><span class="p">:</span> <span class="mi">220</span><span class="p">)</span> <span class="c1">// Authenticated user actions</span> <span class="n">tokenInfoButton</span> <span class="c1">// this is added</span> <span class="n">signoutButton</span> <span class="p">}</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p>Try building and running the app. You should now see the <strong>Token Info</strong> button after logging in. Tapping the button should open the Token Info View.</p> <h2 id="view-the-authenticated-users-profile-info">View the authenticated user’s profile info</h2> <p>Once your app authenticates a user with Okta DirectAuth, it can use the stored credentials to request profile information from the <code class="language-plaintext highlighter-rouge">UserInfo</code> endpoint securely.</p> <p>This endpoint returns standard OpenID Connect (OIDC) claims, including the user’s name, email address, and unique identifier (<code class="language-plaintext highlighter-rouge">sub</code>).</p> <p>In this section, you’ll add a <strong>User Info</strong> button to your authenticated view and implement a corresponding <code class="language-plaintext highlighter-rouge">UserInfoView</code> that displays these profile details.</p> <p>This is a quick and powerful way to confirm the validity of the access token and that your app can retrieve user data after sign-in.</p> <p>Create a new empty Swift file in the <code class="language-plaintext highlighter-rouge">Views</code> folder and name it <code class="language-plaintext highlighter-rouge">UserInfoView</code>. Then add the following code:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span> <span class="kd">import</span> <span class="kt">AuthFoundation</span> <span class="c1">/// A view that displays the authenticated user's profile information</span> <span class="c1">/// retrieved from Okta's **UserInfo** endpoint.</span> <span class="c1">///</span> <span class="c1">/// The `UserInfo` object is provided by **AuthFoundation** and contains</span> <span class="c1">/// standard OpenID Connect (OIDC) claims such as `name`, `preferred_username`,</span> <span class="c1">/// and `sub` (subject identifier). This view is shown after the user has</span> <span class="c1">/// successfully authenticated, allowing you to confirm that your access token</span> <span class="c1">/// can retrieve user data.</span> <span class="kd">struct</span> <span class="kt">UserInfoView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="c1">/// The user information returned by the Okta UserInfo endpoint.</span> <span class="k">let</span> <span class="nv">userInfo</span><span class="p">:</span> <span class="kt">UserInfo</span> <span class="c1">/// Used to dismiss the view when the close button is tapped.</span> <span class="kd">@Environment</span><span class="p">(\</span><span class="o">.</span><span class="n">dismiss</span><span class="p">)</span> <span class="k">var</span> <span class="nv">dismiss</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ScrollView</span> <span class="p">{</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">,</span> <span class="nv">spacing</span><span class="p">:</span> <span class="mi">20</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// MARK: - Close Button</span> <span class="c1">// Dismisses the full-screen user info view.</span> <span class="kt">Button</span> <span class="p">{</span> <span class="nf">dismiss</span><span class="p">()</span> <span class="p">}</span> <span class="nv">label</span><span class="p">:</span> <span class="p">{</span> <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"xmark.circle.fill"</span><span class="p">)</span> <span class="o">.</span><span class="nf">resizable</span><span class="p">()</span> <span class="o">.</span><span class="nf">foregroundStyle</span><span class="p">(</span><span class="o">.</span><span class="n">black</span><span class="p">)</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">40</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">40</span><span class="p">)</span> <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">leading</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// MARK: - User Information Text</span> <span class="c1">// Displays formatted user claims (name, username, subject, etc.)</span> <span class="kt">Text</span><span class="p">(</span><span class="n">formattedData</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="nf">system</span><span class="p">(</span><span class="nv">size</span><span class="p">:</span> <span class="mi">14</span><span class="p">))</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">leading</span><span class="p">)</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="p">(</span><span class="o">.</span><span class="n">systemBackground</span><span class="p">))</span> <span class="o">.</span><span class="nf">navigationTitle</span><span class="p">(</span><span class="s">"User Info"</span><span class="p">)</span> <span class="o">.</span><span class="nf">navigationBarTitleDisplayMode</span><span class="p">(</span><span class="o">.</span><span class="n">inline</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// MARK: - Data Formatting</span> <span class="c1">/// Builds a simple multi-line string of readable user information.</span> <span class="c1">/// Extracts common OIDC claims and formats them for display.</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">formattedData</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">result</span> <span class="o">=</span> <span class="s">""</span> <span class="c1">// User's full name</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"Name: "</span> <span class="o">+</span> <span class="p">(</span><span class="n">userInfo</span><span class="o">.</span><span class="n">name</span> <span class="p">??</span> <span class="s">"No name set"</span><span class="p">))</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span> <span class="c1">// Preferred username (email or login identifier)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"Username: "</span> <span class="o">+</span> <span class="p">(</span><span class="n">userInfo</span><span class="o">.</span><span class="n">preferredUsername</span> <span class="p">??</span> <span class="s">"No username set"</span><span class="p">))</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span> <span class="c1">// Subject identifier (unique Okta user ID)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"User ID: "</span> <span class="o">+</span> <span class="p">(</span><span class="n">userInfo</span><span class="o">.</span><span class="n">subject</span> <span class="p">??</span> <span class="s">"No ID found"</span><span class="p">))</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">)</span> <span class="c1">// Last updated timestamp (if available)</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">updatedAt</span> <span class="o">=</span> <span class="n">userInfo</span><span class="o">.</span><span class="n">updatedAt</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">dateFormatter</span> <span class="o">=</span> <span class="kt">DateFormatter</span><span class="p">()</span> <span class="n">dateFormatter</span><span class="o">.</span><span class="n">dateStyle</span> <span class="o">=</span> <span class="o">.</span><span class="n">medium</span> <span class="n">dateFormatter</span><span class="o">.</span><span class="n">timeStyle</span> <span class="o">=</span> <span class="o">.</span><span class="n">short</span> <span class="k">let</span> <span class="nv">formattedDate</span> <span class="o">=</span> <span class="n">dateFormatter</span><span class="o">.</span><span class="nf">string</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">updatedAt</span><span class="p">)</span> <span class="n">result</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="s">"Updated at: "</span> <span class="o">+</span> <span class="p">(</span><span class="n">formattedDate</span> <span class="p">??</span> <span class="s">""</span><span class="p">))</span> <span class="p">}</span> <span class="k">return</span> <span class="n">result</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Once again, to display this in our app, we need to add a new button to show the new view. To do that, open the <code class="language-plaintext highlighter-rouge">AuthView.swift</code>, scroll down to the last private extension where it says “Action Buttons”, and add the following button just below the <code class="language-plaintext highlighter-rouge">tokenInfoButton</code>:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// Loads user info and presents it full screen.</span> <span class="kd">@MainActor</span> <span class="k">var</span> <span class="nv">userInfoButton</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Button</span><span class="p">(</span><span class="s">"User Info"</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Task</span> <span class="p">{</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">user</span> <span class="o">=</span> <span class="k">await</span> <span class="n">viewModel</span><span class="o">.</span><span class="nf">fetchUserInfo</span><span class="p">()</span> <span class="p">{</span> <span class="n">userInfo</span> <span class="o">=</span> <span class="kt">UserInfoModel</span><span class="p">(</span><span class="nv">user</span><span class="p">:</span> <span class="n">user</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="nf">system</span><span class="p">(</span><span class="nv">size</span><span class="p">:</span> <span class="mi">14</span><span class="p">))</span> <span class="o">.</span><span class="nf">disabled</span><span class="p">(</span><span class="n">viewModel</span><span class="o">.</span><span class="n">isLoading</span><span class="p">)</span> <span class="p">}</span> </code></pre></div></div> <p>Next, we need to add the button to the <code class="language-plaintext highlighter-rouge">successView</code> like we did with the <code class="language-plaintext highlighter-rouge">tokenInfoButton</code>. Then, we will use the <code class="language-plaintext highlighter-rouge">userInfo</code> property in the <code class="language-plaintext highlighter-rouge">AuthView</code>, which we added at the start. Navigate to the <code class="language-plaintext highlighter-rouge">AuthView.swift</code> file and find the <code class="language-plaintext highlighter-rouge">successView</code> in the “Authorized State View” mark and reference the <code class="language-plaintext highlighter-rouge">userInfoButton</code> after the <code class="language-plaintext highlighter-rouge">tokenInfoButton</code> like this:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="k">var</span> <span class="nv">successView</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Signed in 🎉"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">title2</span><span class="p">)</span> <span class="o">.</span><span class="nf">bold</span><span class="p">()</span> <span class="c1">// Scrollable ID token display (for demo purposes)</span> <span class="kt">ScrollView</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="kt">Credential</span><span class="o">.</span><span class="k">default</span><span class="p">?</span><span class="o">.</span><span class="n">token</span><span class="o">.</span><span class="n">idToken</span><span class="p">?</span><span class="o">.</span><span class="n">rawValue</span> <span class="p">??</span> <span class="s">"(no id token)"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">footnote</span><span class="p">)</span> <span class="o">.</span><span class="nf">textSelection</span><span class="p">(</span><span class="o">.</span><span class="n">enabled</span><span class="p">)</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="o">.</span><span class="n">thinMaterial</span><span class="p">)</span> <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxHeight</span><span class="p">:</span> <span class="mi">220</span><span class="p">)</span> <span class="c1">// Authenticated user actions</span> <span class="n">tokenInfoButton</span> <span class="n">userInfoButton</span> <span class="c1">// this is added</span> <span class="n">signoutButton</span> <span class="p">}</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p>We need to tell SwiftUI that we want to open a new <code class="language-plaintext highlighter-rouge">UserInfoView</code> whenever the value on the <code class="language-plaintext highlighter-rouge">userInfo</code> property changes. To do so, open the <code class="language-plaintext highlighter-rouge">AuthView</code> and find the body variable, add the following code after the last closing bracket:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Show User Info full screen</span> <span class="o">.</span><span class="nf">fullScreenCover</span><span class="p">(</span><span class="nv">item</span><span class="p">:</span> <span class="err">$</span><span class="n">userInfo</span><span class="p">)</span> <span class="p">{</span> <span class="n">info</span> <span class="k">in</span> <span class="kt">UserInfoView</span><span class="p">(</span><span class="nv">userInfo</span><span class="p">:</span> <span class="n">info</span><span class="o">.</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span> </code></pre></div></div> <p>The body of your <code class="language-plaintext highlighter-rouge">AuthView</code> should look like this now:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span> <span class="p">{</span> <span class="c1">// Render the UI based on the current authentication state.</span> <span class="c1">// Each case corresponds to a different phase of the DirectAuth flow.</span> <span class="k">switch</span> <span class="n">viewModel</span><span class="o">.</span><span class="n">state</span> <span class="p">{</span> <span class="k">case</span> <span class="o">.</span><span class="n">idle</span><span class="p">,</span> <span class="o">.</span><span class="nv">failed</span><span class="p">:</span> <span class="n">loginForm</span> <span class="k">case</span> <span class="o">.</span><span class="nv">authenticating</span><span class="p">:</span> <span class="kt">ProgressView</span><span class="p">(</span><span class="s">"Signing in..."</span><span class="p">)</span> <span class="k">case</span> <span class="o">.</span><span class="nv">waitingForPush</span><span class="p">:</span> <span class="c1">// Waiting for Okta Verify push approval</span> <span class="kt">WaitingForPushView</span> <span class="p">{</span> <span class="kt">Task</span> <span class="p">{</span> <span class="k">await</span> <span class="n">viewModel</span><span class="o">.</span><span class="nf">signOut</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="k">case</span> <span class="o">.</span><span class="nv">authorized</span><span class="p">:</span> <span class="n">successView</span> <span class="p">}</span> <span class="k">if</span> <span class="n">viewModel</span><span class="o">.</span><span class="n">isLoading</span> <span class="p">{</span> <span class="kt">ProgressView</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="c1">// Show Token Info full screen</span> <span class="o">.</span><span class="nf">fullScreenCover</span><span class="p">(</span><span class="nv">isPresented</span><span class="p">:</span> <span class="err">$</span><span class="n">showTokenInfo</span><span class="p">)</span> <span class="p">{</span> <span class="kt">TokenInfoView</span><span class="p">()</span> <span class="p">}</span> <span class="c1">// Show User Info full screen</span> <span class="o">.</span><span class="nf">fullScreenCover</span><span class="p">(</span><span class="nv">item</span><span class="p">:</span> <span class="err">$</span><span class="n">userInfo</span><span class="p">)</span> <span class="p">{</span> <span class="n">info</span> <span class="k">in</span> <span class="kt">UserInfoView</span><span class="p">(</span><span class="nv">userInfo</span><span class="p">:</span> <span class="n">info</span><span class="o">.</span><span class="n">user</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <h3 id="keeping-tokens-refreshed-and-maintaining-user-sessions">Keeping tokens refreshed and maintaining user sessions</h3> <p>Access tokens have a limited lifetime to ensure your app’s security. When a token expires, the user shouldn’t have to sign-in again – instead, your app can request a new access token using the refresh token stored in the credential.</p> <p>In this section, you’ll add support for token refresh, allowing users to stay authenticated without repeating the entire sign-in and MFA flow.</p> <p>You’ll add an action in the UI that calls the <code class="language-plaintext highlighter-rouge">refreshTokenIfNeeded()</code> method from your <code class="language-plaintext highlighter-rouge">AuthService</code>, which silently exchanges the refresh token for a new set of valid tokens. We’re making this call manually, but you can watch for upcoming expiry and refresh the token before it happens preemptively. While we don’t show it here, you should use <strong>Refresh Token Rotation</strong> to ensure refresh tokens are also short-lived as a security measure.</p> <p>First, we need to add the <code class="language-plaintext highlighter-rouge">refreshTokenButton</code>, which we’ll add to the <code class="language-plaintext highlighter-rouge">AuthView</code>. Open the <code class="language-plaintext highlighter-rouge">AuthView</code>, scroll down to the last private extension in the “Action Buttons” mark, and add the following button at the end of the extension:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// Refresh Token if needed</span> <span class="k">var</span> <span class="nv">refreshTokenButton</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Button</span><span class="p">(</span><span class="s">"Refresh Token"</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Task</span> <span class="p">{</span> <span class="k">await</span> <span class="n">viewModel</span><span class="o">.</span><span class="nf">refreshToken</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="nf">system</span><span class="p">(</span><span class="nv">size</span><span class="p">:</span> <span class="mi">14</span><span class="p">))</span> <span class="o">.</span><span class="nf">disabled</span><span class="p">(</span><span class="n">viewModel</span><span class="o">.</span><span class="n">isLoading</span><span class="p">)</span> <span class="p">}</span> </code></pre></div></div> <p>Next, we need to reference the button somewhere in our view. We will do that inside the <code class="language-plaintext highlighter-rouge">successView</code>, like we did with the other buttons. Find the <code class="language-plaintext highlighter-rouge">successView</code> and add the button. Your <code class="language-plaintext highlighter-rouge">successView</code> should look like this:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="k">var</span> <span class="nv">successView</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Signed in 🎉"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">title2</span><span class="p">)</span> <span class="o">.</span><span class="nf">bold</span><span class="p">()</span> <span class="c1">// Scrollable ID token display (for demo purposes)</span> <span class="kt">ScrollView</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="kt">Credential</span><span class="o">.</span><span class="k">default</span><span class="p">?</span><span class="o">.</span><span class="n">token</span><span class="o">.</span><span class="n">idToken</span><span class="p">?</span><span class="o">.</span><span class="n">rawValue</span> <span class="p">??</span> <span class="s">"(no id token)"</span><span class="p">)</span> <span class="o">.</span><span class="nf">font</span><span class="p">(</span><span class="o">.</span><span class="n">footnote</span><span class="p">)</span> <span class="o">.</span><span class="nf">textSelection</span><span class="p">(</span><span class="o">.</span><span class="n">enabled</span><span class="p">)</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="o">.</span><span class="n">thinMaterial</span><span class="p">)</span> <span class="o">.</span><span class="nf">cornerRadius</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxHeight</span><span class="p">:</span> <span class="mi">220</span><span class="p">)</span> <span class="c1">// Authenticated user actions</span> <span class="n">tokenInfoButton</span> <span class="n">userInfoButton</span> <span class="n">refreshTokenButton</span> <span class="c1">// this is added</span> <span class="n">signoutButton</span> <span class="p">}</span> <span class="o">.</span><span class="nf">padding</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p>Now, if you run the app and tap the <code class="language-plaintext highlighter-rouge">refreshTokenButton</code>, you should see your token change in the token preview label.</p> <p>One thing that we didn’t implement and left with a default implementation to return <code class="language-plaintext highlighter-rouge">nil</code> is the <code class="language-plaintext highlighter-rouge">accessToken</code> property on the <code class="language-plaintext highlighter-rouge">AuthService</code>. Navigate to the <code class="language-plaintext highlighter-rouge">AuthService</code>, find the <code class="language-plaintext highlighter-rouge">accessToken</code> property, and replace the code so it looks like this:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">accessToken</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span> <span class="p">{</span> <span class="k">switch</span> <span class="n">state</span> <span class="p">{</span> <span class="k">case</span> <span class="o">.</span><span class="nf">authorized</span><span class="p">(</span><span class="k">let</span> <span class="nv">token</span><span class="p">):</span> <span class="k">return</span> <span class="n">token</span><span class="o">.</span><span class="n">accessToken</span> <span class="k">default</span><span class="p">:</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Currently, if you restart the app, you’ll get a prompt to log in each time. This is not a good user experience, and the user should remain logged in. We can add this feature by adding code in the <code class="language-plaintext highlighter-rouge">AuthService</code> initializer. Open your <code class="language-plaintext highlighter-rouge">AuthService</code> class and replace the <code class="language-plaintext highlighter-rouge">init</code> function with the following:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">init</span><span class="p">()</span> <span class="p">{</span> <span class="c1">// Prefer PropertyListConfiguration if Okta.plist exists; otherwise fall back</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">configuration</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">OAuth2Client</span><span class="o">.</span><span class="kt">PropertyListConfiguration</span><span class="p">()</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">flow</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">DirectAuthenticationFlow</span><span class="p">(</span><span class="nv">client</span><span class="p">:</span> <span class="kt">OAuth2Client</span><span class="p">(</span><span class="n">configuration</span><span class="p">))</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">flow</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">DirectAuthenticationFlow</span><span class="p">()</span> <span class="p">}</span> <span class="c1">// Added</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">token</span> <span class="o">=</span> <span class="kt">Credential</span><span class="o">.</span><span class="k">default</span><span class="p">?</span><span class="o">.</span><span class="n">token</span> <span class="p">{</span> <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="nf">authorized</span><span class="p">(</span><span class="n">token</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <h2 id="build-your-own-secure-native-sign-in-ios-app">Build your own secure native sign-in iOS app</h2> <p>You’ve now built a fully native authentication flow on iOS using Okta DirectAuth with push notification MFA – no browser redirects required. You can check your work against <a href="https://github.com/oktadev/okta-ios-swift-directauth-example">the GitHub repo</a> for this project.</p> <p>Your app securely signs users in, handles multi-factor verification through Okta Verify, retrieves user profile details, displays token information, and refreshes tokens to maintain an active session. By combining <code class="language-plaintext highlighter-rouge">AuthFoundation</code> and <code class="language-plaintext highlighter-rouge">OktaDirectAuth</code>, you’ve implemented a modern, phishing-resistant authentication system that balances strong security with a seamless user experience – all directly within your SwiftUI app.</p> <p>If you found this post interesting, you may want to check out these resources:</p> <ul> <li><a href="/blog/2025/08/20/ios-mfa">How to Build a Secure iOS App with MFA</a></li> <li><a href="/blog/2022/08/30/introducing-the-new-okta-mobile-sdks">Introducing the New Okta Mobile SDKs</a></li> <li><a href="/blog/2022/01/13/mobile-sso">A History of the Mobile SSO (Single Sign-On) Experience in iOS</a></li> </ul> <p>Follow OktaDev on <a href="https://twitter.com/oktadev">Twitter</a> and subscribe to our <a href="https://www.youtube.com/c/OktaDev/">YouTube channel</a> to learn about secure authentication and other exciting content. We also want to hear from you about topics you want to see and questions you may have. Leave us a comment below!</p> Tue, 18 Nov 2025 00:00:00 -0500 https://developer.okta.com/blog/2025/11/18/okta-ios-directauth https://developer.okta.com/blog/2025/11/18/okta-ios-directauth Stretch Your Imagination and Build a Delightful Sign-In Experience <p>When you choose Okta as your IAM provider, one of the features you get access to is customizing your Okta-hosted Sign-In Widget (SIW), which is our recommended method for the highest levels of identity security. It’s a customizable JavaScript component that provides a ready-made login interface you can use immediately as part of your web application.</p> <p>The Okta Identity Engine (OIE) utilizes authentication policies to drive authentication challenges, and the SIW supports various authentication factors, ranging from basic username and password login to more advanced scenarios, such as multi-factor authentication, biometrics, passkeys, social login, account registration, account recovery, and more. Under the hood, it interacts with Okta’s APIs, so you don’t have to build or manage complex auth logic yourself. It’s all handled for you!</p> <p>One of the perks of using the Okta SIW, especially with the 3rd Generation Standard (Gen3), is that customization is a configuration thanks to <a href="https://m3.material.io/foundations/design-tokens/overview">design tokens</a>, so you don’t have to write CSS to style the widget elements.</p> <h2 id="style-the-okta-sign-in-widget-to-match-your-brand">Style the Okta Sign-In Widget to match your brand</h2> <p>In this tutorial, we will customize the Sign In Widget for a fictional to-do app. We’ll make the following changes:</p> <ul> <li>Replace font selections</li> <li>Define border, error, and focus colors</li> <li>Remove elements from the SIW, such as the horizontal rule and add custom elements</li> <li>Shift the control to the start of the site and add a background panel</li> </ul> <p>Without any changes, when you try to sign in to your Okta account, you see something like this:</p> <p><img src="/assets-jekyll/blog/custom-signin/default-siw-ca3d7bddb94294c86b7cf43444d188b3738323c9ae4e11ce6ecc2b5979fffb15.jpg" alt="Default Okta-hosted Sign-In Widget" width="800" class="center-image" /></p> <p>At the end of the tutorial, your login screen will look something like this 🎉</p> <p><img src="/assets-jekyll/blog/custom-signin/final-siw-84d06d435ba3788183f7327b043d8dcaede89e6470bd0444c8862dc875c4b219.jpg" alt="A customized Okta-hosted Sign-In Widget with custom elements, colors, and styles" width="800" class="center-image" /></p> <p>We’ll use the SIW gen3 along with new recommendations to customize form elements and style using design tokens.</p> <p><strong class="hide">Table of Contents</strong></p> <ul id="markdown-toc"> <li><a href="#style-the-okta-sign-in-widget-to-match-your-brand" id="markdown-toc-style-the-okta-sign-in-widget-to-match-your-brand">Style the Okta Sign-In Widget to match your brand</a></li> <li><a href="#customize-your-okta-hosted-sign-in-page" id="markdown-toc-customize-your-okta-hosted-sign-in-page">Customize your Okta-hosted sign-in page</a></li> <li><a href="#understanding-the-okta-hosted-sign-in-widget-default-code" id="markdown-toc-understanding-the-okta-hosted-sign-in-widget-default-code">Understanding the Okta-hosted Sign-In Widget default code</a></li> <li><a href="#customize-the-ui-elements-within-the-okta-sign-in-widget" id="markdown-toc-customize-the-ui-elements-within-the-okta-sign-in-widget">Customize the UI elements within the Okta Sign-In Widget</a></li> <li><a href="#organize-your-sign-in-widget-customizations-with-css-custom-properties" id="markdown-toc-organize-your-sign-in-widget-customizations-with-css-custom-properties">Organize your Sign-In Widget customizations with CSS Custom properties</a></li> <li><a href="#extending-the-siw-theme-with-a-custom-color-palette" id="markdown-toc-extending-the-siw-theme-with-a-custom-color-palette">Extending the SIW theme with a custom color palette</a></li> <li><a href="#add-custom-html-elements-to-the-sign-in-widget" id="markdown-toc-add-custom-html-elements-to-the-sign-in-widget">Add custom HTML elements to the Sign-In Widget</a></li> <li><a href="#overriding-okta-sign-in-widget-element-styles" id="markdown-toc-overriding-okta-sign-in-widget-element-styles">Overriding Okta Sign-In Widget element styles</a></li> <li><a href="#change-the-layout-of-the-okta-hosted-sign-in-page" id="markdown-toc-change-the-layout-of-the-okta-hosted-sign-in-page">Change the layout of the Okta-hosted Sign-In page</a></li> <li><a href="#customize-your-gen3-okta-hosted-sign-in-widget" id="markdown-toc-customize-your-gen3-okta-hosted-sign-in-widget">Customize your Gen3 Okta-hosted Sign-In Widget</a></li> </ul> <p><strong>Prerequisites</strong> To follow this tutorial, you need:</p> <ul> <li>An Okta account with the Identity Engine, such as the <a href="https://developer.okta.com/signup/">Integrator Free account</a>. The SIW version in the org we’re using is 7.36.</li> <li>Your own domain name</li> <li>A basic understanding of HTML, CSS, and JavaScript</li> <li>A brand design in mind. Feel free to tap into your creativity!</li> </ul> <p>Let’s get started!</p> <h2 id="customize-your-okta-hosted-sign-in-page">Customize your Okta-hosted sign-in page</h2> <p>Before we begin, you must configure your Okta org to use your custom domain. Custom domains enable code customizations, allowing us to style more than just the default logo, background, favicon, and two colors. Sign in as an admin and open the Okta Admin Console, navigate to <strong>Customizations</strong> &gt; <strong>Brands</strong> and select <strong>Create Brand +</strong>.</p> <p>Follow the <a href="https://developer.okta.com/docs/guides/custom-url-domain/main/">Customize domain and email</a> developer docs to set up your custom domain on the new brand.</p> <p>You can also follow this post if you prefer.</p> <article class="link-container" style="border: 1px solid silver; border-radius: 3px; padding: 12px 15px"> <a href="/blog/2023/01/12/signin-custom-domain" style="font-size: 1.375em; margin-bottom: 20px;"> <span>A Secure and Themed Sign-in Page</span> </a> <p>Redirecting to the Okta-hosted sign-in page is the most secure way to authenticate users in your application. But the default configuration yield a very neutral sign-in page. This post walks you through customization options and setting up a custom domain so the personality of your site shines all through the user's experience.</p> <div><div class="BlogPost-attribution"> <a href="/blog/authors/alisa-duncan/"> <img src="/assets-jekyll/avatar-alisa_duncan-b29fa4df50f5c99f536307c6bc0e5cb3434a922bdada7fe4f4b3cf8488299465.jpg" alt="avatar-avatar-alisa_duncan.jpeg" class="BlogPost-avatar" /> </a> <span class="BlogPost-author"> <a href="/blog/authors/alisa-duncan/">Alisa Duncan</a> </span> </div></div> </article> <p>Once you have a working brand with a custom domain, select your brand to configure it. First, navigate to <strong>Settings</strong> and select <strong>Use third generation</strong> to enable the SIW Gen3. <strong>Save</strong> your selection.</p> <blockquote> <p>⚠️ <strong>Note</strong></p> <p>The code in this post relies on using SIW Gen3. It will not work on SIW Gen2.</p> </blockquote> <p>Navigate to <strong>Theme</strong>. You’ll see a default brand page that looks something like this:</p> <p><img src="/assets-jekyll/blog/custom-signin/default-siw-styles-049249e12debf07e4d791d667958a3c417e1b438a8a5b6057f3f715d8895ce07.jpg" alt="Default styles for the Okta-hosted SIW" width="800" class="center-image" /></p> <p>Let’s start making it more aligned with the theme we have in mind. Change the primary and secondary colors, then the logo and favicon images with your preferred options</p> <p>To change either color, click on the text field and enter the hex code for each. We’re going for a bold and colorful approach, so we’ll use <code class="language-plaintext highlighter-rouge">#ea3eda</code> as the primary color and <code class="language-plaintext highlighter-rouge">#ffa738</code> as the secondary color, and upload the logo and favicon images for the brand. Select <strong>Save</strong>.</p> <p>Take a look at your sign-in page now by navigating to the sign-in URL for the brand. With your configuration, the sign-in widget looks more interesting than the default view, but we can make things even more exciting.</p> <p>Let’s dive into the main task, customizing the signup page. On the <strong>Theme</strong> tab:</p> <ol> <li>Select <strong>Sign-in Page</strong> in the dropdown menu</li> <li>Select the <strong>Customize</strong> button</li> <li>On the <strong>Page Design</strong> tab, select the <strong>Code editor</strong> toggle to see a HTML page</li> </ol> <blockquote> <p>Note: You can only enable the code editor if you configure a <a href="https://developer.okta.com/docs/guides/custom-url-domain/">custom domain</a>.</p> </blockquote> <h2 id="understanding-the-okta-hosted-sign-in-widget-default-code">Understanding the Okta-hosted Sign-In Widget default code</h2> <p>If you’re familiar with basic HTML, CSS, and JavaScript, the sign-in code appears standard, although it’s somewhat unusual in certain areas. There are two major blocks of code we should examine: the top of the <code class="language-plaintext highlighter-rouge">body</code> tag on the page and the sign-in configuration in the <code class="language-plaintext highlighter-rouge">script</code> tag.</p> <p>The first one looks something like this:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-login-container"</span><span class="nt">&gt;&lt;/div&gt;</span> </code></pre></div></div> <p>The second looks like this:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">config</span> <span class="o">=</span> <span class="nx">OktaUtil</span><span class="p">.</span><span class="nx">getSignInWidgetConfig</span><span class="p">();</span> <span class="c1">// Render the Okta Sign-In Widget</span> <span class="kd">var</span> <span class="nx">oktaSignIn</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">OktaSignIn</span><span class="p">(</span><span class="nx">config</span><span class="p">);</span> <span class="nx">oktaSignIn</span><span class="p">.</span><span class="nx">renderEl</span><span class="p">({</span> <span class="na">el</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#okta-login-container</span><span class="dl">'</span> <span class="p">},</span> <span class="nx">OktaUtil</span><span class="p">.</span><span class="nx">completeLogin</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Logs errors that occur when configuring the widget.</span> <span class="c1">// Remove or replace this with your own custom error handler.</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span> <span class="p">}</span> <span class="p">);</span> </code></pre></div></div> <p>Let’s take a closer look at how this code works. In the HTML, there’s a designated parent element that the <code class="language-plaintext highlighter-rouge">OktaSignIn</code> instance uses to render the SIW as a child node. This means that when the page loads, you’ll see the <code class="language-plaintext highlighter-rouge">&lt;div id="okta-login-container"&gt;&lt;/div&gt;</code> in the DOM with the HTML elements for SIW functionality as its child within the <code class="language-plaintext highlighter-rouge">div</code>. The SIW handles all authentication and user registration processes as defined by policies, allowing us to focus entirely on customization.</p> <p>To create the SIW, we need to pass in the configuration. The configuration includes properties like the theme elements and messages for labels. The method <code class="language-plaintext highlighter-rouge">renderEl()</code> identifies the HTML element to use for rendering the SIW. We’re passing in <code class="language-plaintext highlighter-rouge">#okta-login-container</code> as the identifier.</p> <p>The <code class="language-plaintext highlighter-rouge">#okta-login-container</code> is a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors">CSS selector</a>. While any correct CSS selector works, we recommend you use the ID of the element. Element IDs must be unique within the HTML document, so this is the safest and easiest method.</p> <h2 id="customize-the-ui-elements-within-the-okta-sign-in-widget">Customize the UI elements within the Okta Sign-In Widget</h2> <p>Now that we have a basic understanding of how the Okta Sign-In Widget works, let’s start customizing the code. We’ll start by customizing the elements within the SIW. To manipulate the Okta SIW DOM elements in Gen3, we use the <code class="language-plaintext highlighter-rouge">afterTransform</code> method. The <code class="language-plaintext highlighter-rouge">afterTransform</code> method allows us to remove or update elements for individual or all forms.</p> <p>Find the button <strong>Edit</strong> on the <strong>Code editor</strong> view, which makes the code editor editable and behaves like a lightweight IDE.</p> <p>Below the <code class="language-plaintext highlighter-rouge">oktaSignIn.renderEl()</code> method within the <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> tag, add</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">oktaSignIn</span><span class="p">.</span><span class="nx">afterTransform</span><span class="p">(</span><span class="dl">'</span><span class="s1">identify</span><span class="dl">'</span><span class="p">,</span> <span class="p">({</span> <span class="nx">formBag</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Title</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">title</span><span class="p">)</span> <span class="p">{</span> <span class="nx">title</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">content</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Log in and create a task</span><span class="dl">"</span><span class="p">;</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">help</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Link</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">dataSe</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">help</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">unlock</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Link</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">dataSe</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">unlock</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">divider</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Divider</span><span class="dl">'</span><span class="p">);</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="o">!</span><span class="p">[</span><span class="nx">help</span><span class="p">,</span> <span class="nx">unlock</span><span class="p">,</span> <span class="nx">divider</span><span class="p">].</span><span class="nx">includes</span><span class="p">(</span><span class="nx">ele</span><span class="p">));</span> <span class="p">});</span> </code></pre></div></div> <p>This <code class="language-plaintext highlighter-rouge">afterTransform</code> hook only runs before the ‘identify’ form. We can find and target UI elements using the <code class="language-plaintext highlighter-rouge">FormBag</code>. The <code class="language-plaintext highlighter-rouge">afterTransform</code> hook is a more streamlined way to manipulate DOM elements within the SIW before rendering the widget. For example, we can search elements by type to filter them out of the view before rendering, which is more performant than manipulating DOM elements after SIW renders. We filtered out elements such as the <code class="language-plaintext highlighter-rouge">unlock</code> account element and dividers in this snippet.</p> <p>Let’s take a look at what this looks like. Press <strong>Save to draft</strong> and <strong>Publish</strong>.</p> <p>Navigate to your sign-in URL for your brand to view the changes you made. When we compare to the default state, we no longer see the horizontal rule below the logo or the “Help” link. The account unlock element is no longer available.</p> <p>We explored how we can customize the widget elements. Now, let’s add some flair.</p> <h2 id="organize-your-sign-in-widget-customizations-with-css-custom-properties">Organize your Sign-In Widget customizations with CSS Custom properties</h2> <p>At its core, we’re styling an HTML document. This means we operate on the SIW customization in the same way as we would any HTML page, and code organization principles still apply. We can define customization values as <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties">CSS Custom properties</a> (also known as CSS variables).</p> <p>Defining styles using CSS variables keeps our code <a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a>. Setting up style values for reuse even extends beyond the Okta-hosted sign-in page. If your organization hosts stylesheets with brand color defined as CSS custom properties publicly, you can use the colors defined there and link your stylesheet.</p> <p>Before making code edits, identify the fonts you want to use for your customization. We found a header and body font to use.</p> <p>Open the SIW code editor for your brand and select <strong>Edit</strong> to make changes.</p> <p>Import the fonts into the HTML. You can <code class="language-plaintext highlighter-rouge">&lt;link&gt;</code> or <code class="language-plaintext highlighter-rouge">@import</code> the fonts based on your preference. We added the <code class="language-plaintext highlighter-rouge">&lt;link&gt;</code> instructions to the <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> of the HTML.</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"preconnect"</span> <span class="na">href=</span><span class="s">"https://fonts.googleapis.com"</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"preconnect"</span> <span class="na">href=</span><span class="s">"https://fonts.gstatic.com"</span> <span class="na">crossorigin</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&amp;family=Poiret+One&amp;display=swap"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span> </code></pre></div></div> <p>Find the <code class="language-plaintext highlighter-rouge">&lt;style nonce="{{nonceValue}}"&gt;</code> tag. Within the tag, define your properties using the <code class="language-plaintext highlighter-rouge">:root</code> selector:</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">:root</span> <span class="p">{</span> <span class="py">--color-gray</span><span class="p">:</span> <span class="m">#4f4f4f</span><span class="p">;</span> <span class="py">--color-fuchsia</span><span class="p">:</span> <span class="m">#ea3eda</span><span class="p">;</span> <span class="py">--color-orange</span><span class="p">:</span> <span class="m">#ffa738</span><span class="p">;</span> <span class="py">--color-azul</span><span class="p">:</span> <span class="m">#016fb9</span><span class="p">;</span> <span class="py">--color-cherry</span><span class="p">:</span> <span class="m">#ea3e84</span><span class="p">;</span> <span class="py">--color-purple</span><span class="p">:</span> <span class="m">#b13fff</span><span class="p">;</span> <span class="py">--color-black</span><span class="p">:</span> <span class="m">#191919</span><span class="p">;</span> <span class="py">--color-white</span><span class="p">:</span> <span class="m">#fefefe</span><span class="p">;</span> <span class="py">--color-bright-white</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="py">--border-radius</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span> <span class="py">--font-header</span><span class="p">:</span> <span class="s2">'Poiret One'</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span> <span class="py">--font-body</span><span class="p">:</span> <span class="s2">'Inter Tight'</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Feel free to add new properties or replace the property value for your brand. Now is a good opportunity to add your own brand colors and customizations!</p> <p>Let’s configure the SIW with our variables using design tokens.</p> <p>Find <code class="language-plaintext highlighter-rouge">var config = OktaUtil.getSignInWidgetConfig();</code>. After this line of code, set the values of the design tokens using your CSS Custom properties. You’ll use the <code class="language-plaintext highlighter-rouge">var()</code> function to access your variables:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">config</span><span class="p">.</span><span class="nx">theme</span> <span class="o">=</span> <span class="p">{</span> <span class="na">tokens</span><span class="p">:</span> <span class="p">{</span> <span class="na">BorderColorDisplay</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-bright-white)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryMain</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-fuchsia)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryDark</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-purple)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryDarker</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-purple)</span><span class="dl">'</span><span class="p">,</span> <span class="na">BorderRadiusTight</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--border-radius)</span><span class="dl">'</span><span class="p">,</span> <span class="na">BorderRadiusMain</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--border-radius)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryDark</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-orange)</span><span class="dl">'</span><span class="p">,</span> <span class="na">FocusOutlineColorPrimary</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-azul)</span><span class="dl">'</span><span class="p">,</span> <span class="na">TypographyFamilyBody</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--font-body)</span><span class="dl">'</span><span class="p">,</span> <span class="na">TypographyFamilyHeading</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--font-header)</span><span class="dl">'</span><span class="p">,</span> <span class="na">TypographyFamilyButton</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--font-body)</span><span class="dl">'</span><span class="p">,</span> <span class="na">BorderColorDangerControl</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-cherry)</span><span class="dl">'</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Save your changes, publish the page, and view your brand’s sign-in URI site. Yay! You see, there’s no border outline, the border radius of the widget and HTML elements changed, a different focus color, and a different color for element outlines when there’s a form error. You can inspect the HTML elements and view the computed styles. Or if you prefer, feel free to update the CSS variables to something more visible.</p> <p>When you inspect your brand’s sign-in URL site, you’ll notice that the fonts aren’t loading properly and that there are errors in your browser’s debugging console. This is because you need to configure Content Security Policies (CSP) to allow resources loaded from external sites. CSPs are a security measure to mitigate cross-site scripting (XSS) attacks. You can read <a href="/blog/2021/10/18/security-headers-best-practices">An Overview of Best Practices for Security Headers</a> to learn more about CSPs.</p> <p>Navigate to the <strong>Settings</strong> tab for your brand’s <strong>Sign-in page</strong>. Find the <strong>Content Security Policy</strong> and press <strong>Edit</strong>. Add the domains for external resources. In our example, we only load resources from Google Fonts, so we added the following two domains:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>*.googleapis.com *.gstatic.com </code></pre></div></div> <p>Press <strong>Save to draft</strong> and press <strong>Publish</strong> to view your changes. The SIW now displays the fonts you selected!</p> <h2 id="extending-the-siw-theme-with-a-custom-color-palette">Extending the SIW theme with a custom color palette</h2> <p>In our example, we selectively added colors. The SIW design system adheres to WCAG accessibility standards and relies on <a href="https://m2.material.io/">Material Design</a> color palettes.</p> <p>Okta generates colors based on your primary color that conform to accessibility standards and contrast requirements. Check out <a href="https://help.okta.com/oie/en-us/content/topics/settings/branding-siw-color-contrast.htm">Understand Sign-In Widget color customization</a> to learn more about color contrast and how Okta color generation works. You must supply accessible colors to the configuration.</p> <p>Material Design supports themes by customizing color palettes. The <a href="https://developer.okta.com/docs/guides/custom-widget-gen3/main/#use-design-tokens">list of all configurable design tokens</a> displays all available options, including <code class="language-plaintext highlighter-rouge">Hue*</code> properties for precise color control. Consider exploring color palette customization options tailored to your brand’s specific needs. You can use Material palette generators such as <a href="https://m2.material.io/inline-tools/color/">this color picker</a> from the Google team or an open source <a href="https://materialpalettes.com/">Material Design Palette Generator</a> that allows you to enter a HEX color value.</p> <p>Don’t forget to keep accessibility in mind. You can run an accessibility audit using <a href="https://developer.chrome.com/docs/lighthouse/overview">Lighthouse</a> in the Chrome browser and the <a href="https://webaim.org/resources/contrastchecker/">WebAIM Contrast Checker</a>. Our selected primary color doesn’t quite meet contrast requirements. 😅</p> <h2 id="add-custom-html-elements-to-the-sign-in-widget">Add custom HTML elements to the Sign-In Widget</h2> <p>Previously, we filtered HTML elements out of the SIW. We can also add new custom HTML elements to SIW. We’ll experiment by adding a link to the Okta Developer blog. Find the <code class="language-plaintext highlighter-rouge">afterTransform()</code> method. Update the <code class="language-plaintext highlighter-rouge">afterTransform()</code> method to look like this:</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">oktaSignIn</span><span class="p">.</span><span class="nx">afterTransform</span><span class="p">(</span><span class="dl">'</span><span class="s1">identify</span><span class="dl">'</span><span class="p">,</span> <span class="p">({</span><span class="nx">formBag</span><span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Title</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">title</span><span class="p">)</span> <span class="p">{</span> <span class="nx">title</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">content</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Log in and create a task</span><span class="dl">"</span><span class="p">;</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">help</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Link</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">dataSe</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">help</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">unlock</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Link</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">dataSe</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">unlock</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">divider</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Divider</span><span class="dl">'</span><span class="p">);</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="o">!</span><span class="p">[</span><span class="nx">help</span><span class="p">,</span> <span class="nx">unlock</span><span class="p">,</span> <span class="nx">divider</span><span class="p">].</span><span class="nx">includes</span><span class="p">(</span><span class="nx">ele</span><span class="p">));</span> <span class="kd">const</span> <span class="nx">blogLink</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Link</span><span class="dl">'</span><span class="p">,</span> <span class="na">contentType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">footer</span><span class="dl">'</span><span class="p">,</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">href</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://developer.okta.com/blog</span><span class="dl">'</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Read our blog</span><span class="dl">'</span><span class="p">,</span> <span class="na">dataSe</span><span class="p">:</span> <span class="dl">'</span><span class="s1">blogCustomLink</span><span class="dl">'</span> <span class="p">}</span> <span class="p">};</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">blogLink</span><span class="p">);</span> <span class="p">});</span> </code></pre></div></div> <p>We created a new element named <code class="language-plaintext highlighter-rouge">blogLink</code> and set properties such as the type, where the content resides, and options related to the <code class="language-plaintext highlighter-rouge">type</code>. We also added a <code class="language-plaintext highlighter-rouge">dataSe</code> property that adds the value <code class="language-plaintext highlighter-rouge">blogCustomLink</code> to an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/Use_data_attributes">HTML data attribute</a>. Doing so makes it easier for us to select the element for customization or for testing purposes.</p> <p>When you continue past the ‘identify’ form in the sign-in flow, you’ll no longer see the link to the blog.</p> <h2 id="overriding-okta-sign-in-widget-element-styles">Overriding Okta Sign-In Widget element styles</h2> <p>We should use design tokens for customizations wherever possible. In cases where a design token isn’t available for your styling needs, you can fall back to defining style manually.</p> <p>Let’s start with the element we added, the blog link. Let’s say we want to display the text in capital casing. It’s not good practice to define the label value using capital casing for accessibility. We should use CSS to transform the text.</p> <p>In the styles definition, find the <code class="language-plaintext highlighter-rouge">#login-bg-image-id</code>. After the styles for the background image, add the style to target the <code class="language-plaintext highlighter-rouge">blogCustomLink</code> data attribute and define the text transform like this:</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">a</span><span class="o">[</span><span class="nt">data-se</span><span class="o">=</span><span class="s1">"blogCustomLink"</span><span class="o">]</span> <span class="p">{</span> <span class="nl">text-transform</span><span class="p">:</span> <span class="nb">uppercase</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Save and publish the page to check out your changes.</p> <p>Now, let’s say you want to style an Okta-provided HTML element. Use design tokens wherever possible, and make style changes cautiously as doing so adds brittleness and security concerns.</p> <p>Here’s a terrible example of styling an Okta-provided HTML element that you shouldn’t emulate, as it makes the text illegible. Let’s say you want to change the background of the <strong>Next</strong> button to be a gradient. 🌈</p> <p>Inspect the SIW element you want to style. We want to style the <code class="language-plaintext highlighter-rouge">button</code> with the data attribute <code class="language-plaintext highlighter-rouge">okta-sign-in-header</code>.</p> <p>After the <code class="language-plaintext highlighter-rouge">blogCustomLink</code> style, add the following:</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">button</span><span class="o">[</span><span class="nt">data-se</span><span class="o">=</span><span class="s1">"save"</span><span class="o">]</span> <span class="p">{</span> <span class="nl">background</span><span class="p">:</span> <span class="n">linear-gradient</span><span class="p">(</span><span class="m">12deg</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-fuchsia</span><span class="p">)</span> <span class="m">0%</span><span class="p">,</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-orange</span><span class="p">)</span> <span class="m">100%</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>Save and publish the site. The button background is now a gradient.</p> <p>However, style the Okta-provided SIW elements with caution. The dangers with this approach are two-fold:</p> <ol> <li>The Okta Sign-in widget undergoes accessibility audits, and changing styles and behavior manually may decrease accessibility thresholds</li> <li>The Okta Sign-in widget is internationalized, and changing styles around text layout manually may break localization needs</li> <li>Okta can’t guarantee that the data attributes or DOM elements remain unchanged, leading to customization breaks</li> </ol> <p>In the rare case where you style an Okta-provided SIW element you may need to pin the SIW version so your customizations don’t break from under you. Navigate to the <strong>Settings</strong> tab and find the <strong>Sign-In Widget version</strong> section. Select <strong>Edit</strong> and select the most recent version of the widget, as this one should be compatible with your code. We are using widget version 7.36 in this post.</p> <blockquote> <p>⚠️ <strong>Note</strong></p> <p>When you pin the widget, you won’t get the latest and greatest updates from the SIW without manually updating the version. Pinning the version prevents any forward progress in the evolution and extensibility of the end-user experiences. For the most secure option, allow SIW to update automatically and avoid overly customizing the SIW with CSS. Use the design tokens wherever possible.</p> </blockquote> <h2 id="change-the-layout-of-the-okta-hosted-sign-in-page">Change the layout of the Okta-hosted Sign-In page</h2> <p>We left the HTML nodes defined in the SIW customization unedited so far. You can change the layout of the default <code class="language-plaintext highlighter-rouge">&lt;div&gt;</code> containers to make a significant impact. Change the <code class="language-plaintext highlighter-rouge">display</code> CSS property to make an impactful change, such as using <a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Flexbox">Flexbox</a> or <a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/CSS_layout/Grids">CSS Grid</a>. I’ll use Flexbox in this example.</p> <p>Find the <code class="language-plaintext highlighter-rouge">div</code> for the background image container and the <code class="language-plaintext highlighter-rouge">okta-login-container</code>. Replace those <code class="language-plaintext highlighter-rouge">div</code> elements with this HTML snippet:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"login-bg-image-id"</span> <span class="na">class=</span><span class="s">"login-bg-image tb--background"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"login-container-panel"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-login-container"</span><span class="nt">&gt;&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre></div></div> <p>We moved the <code class="language-plaintext highlighter-rouge">okta-login-container</code> div inside another parent container and made it a child of the background image container.</p> <p>Find <code class="language-plaintext highlighter-rouge">#login-bg-image</code> style. Add the <code class="language-plaintext highlighter-rouge">display: flex;</code> property. The styles should look like this:</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nf">#login-bg-image-id</span> <span class="p">{</span> <span class="nl">background-image</span><span class="p">:</span> <span class="err">{{</span><span class="n">bgImageUrl</span><span class="p">}</span><span class="err">}</span><span class="o">;</span> <span class="nt">display</span><span class="o">:</span> <span class="nt">flex</span><span class="o">;</span> <span class="err">}</span> </code></pre></div></div> <p>We want to style the <code class="language-plaintext highlighter-rouge">okta-login-container</code>’s parent <code class="language-plaintext highlighter-rouge">&lt;div&gt;</code> to set the background color and to center the SIW on the panel. Add new styles for the <code class="language-plaintext highlighter-rouge">login-container-panel</code> class:</p> <div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">.login-container-panel</span> <span class="p">{</span> <span class="nl">background</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-white</span><span class="p">);</span> <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span> <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span> <span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">40%</span><span class="p">;</span> <span class="nl">min-width</span><span class="p">:</span> <span class="m">400px</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>Save your changes and view the sign-in page. What do you think of the new layout? 🎊</p> <blockquote> <p>⚠️ <strong>Note</strong></p> <p>Flexbox and CSS Grid are responsive, but you may still need to add properties handling responsiveness or media queries to fit your needs.</p> </blockquote> <p>Your final code might look something like this:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;meta</span> <span class="na">http-equiv=</span><span class="s">"Content-Type"</span> <span class="na">content=</span><span class="s">"text/html; charset=UTF-8"</span><span class="nt">&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"viewport"</span> <span class="na">content=</span><span class="s">"width=device-width, initial-scale=1.0"</span> <span class="nt">/&gt;</span> <span class="nt">&lt;meta</span> <span class="na">name=</span><span class="s">"robots"</span> <span class="na">content=</span><span class="s">"noindex,nofollow"</span> <span class="nt">/&gt;</span> <span class="c">&lt;!-- Styles generated from theme --&gt;</span> <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"{{themedStylesUrl}}"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">type=</span><span class="s">"text/css"</span><span class="nt">&gt;</span> <span class="c">&lt;!-- Favicon from theme --&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"shortcut icon"</span> <span class="na">href=</span><span class="s">"{{faviconUrl}}"</span> <span class="na">type=</span><span class="s">"image/x-icon"</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"preconnect"</span> <span class="na">href=</span><span class="s">"https://fonts.googleapis.com"</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"preconnect"</span> <span class="na">href=</span><span class="s">"https://fonts.gstatic.com"</span> <span class="na">crossorigin</span><span class="nt">&gt;</span> <span class="nt">&lt;link</span> <span class="na">href=</span><span class="s">"https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&amp;family=Poiret+One&amp;display=swap"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">&gt;</span> <span class="nt">&lt;title&gt;</span>{{pageTitle}}<span class="nt">&lt;/title&gt;</span> {{{SignInWidgetResources}}} <span class="nt">&lt;style </span><span class="na">nonce=</span><span class="s">"{{nonceValue}}"</span><span class="nt">&gt;</span> <span class="nd">:root</span> <span class="p">{</span> <span class="py">--font-header</span><span class="p">:</span> <span class="s2">'Poiret One'</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span> <span class="py">--font-body</span><span class="p">:</span> <span class="s2">'Inter Tight'</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span> <span class="py">--color-gray</span><span class="p">:</span> <span class="m">#4f4f4f</span><span class="p">;</span> <span class="py">--color-fuchsia</span><span class="p">:</span> <span class="m">#ea3eda</span><span class="p">;</span> <span class="py">--color-orange</span><span class="p">:</span> <span class="m">#ffa738</span><span class="p">;</span> <span class="py">--color-azul</span><span class="p">:</span> <span class="m">#016fb9</span><span class="p">;</span> <span class="py">--color-cherry</span><span class="p">:</span> <span class="m">#ea3e84</span><span class="p">;</span> <span class="py">--color-purple</span><span class="p">:</span> <span class="m">#b13fff</span><span class="p">;</span> <span class="py">--color-black</span><span class="p">:</span> <span class="m">#191919</span><span class="p">;</span> <span class="py">--color-white</span><span class="p">:</span> <span class="m">#fefefe</span><span class="p">;</span> <span class="py">--color-bright-white</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="py">--border-radius</span><span class="p">:</span> <span class="m">4px</span><span class="p">;</span> <span class="p">}</span> <span class="p">{</span><span class="err">{</span> <span class="err">#useSiwGen3</span> <span class="p">}</span><span class="err">}</span> <span class="nt">html</span> <span class="p">{</span> <span class="nl">font-size</span><span class="p">:</span> <span class="m">87.5%</span><span class="p">;</span> <span class="p">}</span> <span class="p">{</span><span class="err">{</span> <span class="err">/useSiwGen3</span> <span class="p">}</span><span class="err">}</span> <span class="nf">#login-bg-image-id</span> <span class="p">{</span> <span class="nl">background-image</span><span class="p">:</span> <span class="err">{{</span><span class="n">bgImageUrl</span><span class="p">}</span><span class="err">}</span><span class="o">;</span> <span class="nt">display</span><span class="o">:</span> <span class="nt">flex</span><span class="o">;</span> <span class="err">}</span> <span class="nc">.login-container-panel</span> <span class="p">{</span> <span class="nl">background</span><span class="p">:</span> <span class="n">var</span><span class="p">(</span><span class="n">--color-white</span><span class="p">);</span> <span class="nl">display</span><span class="p">:</span> <span class="n">flex</span><span class="p">;</span> <span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span> <span class="nl">align-items</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span> <span class="nl">width</span><span class="p">:</span> <span class="m">40%</span><span class="p">;</span> <span class="nl">min-width</span><span class="p">:</span> <span class="m">400px</span><span class="p">;</span> <span class="p">}</span> <span class="nt">a</span><span class="o">[</span><span class="nt">data-se</span><span class="o">=</span><span class="s1">"blogCustomLink"</span><span class="o">]</span> <span class="p">{</span> <span class="nl">text-transform</span><span class="p">:</span> <span class="nb">uppercase</span><span class="p">;</span> <span class="p">}</span> <span class="nt">&lt;/style&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"login-bg-image-id"</span> <span class="na">class=</span><span class="s">"login-bg-image tb--background"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"login-container-panel"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"okta-login-container"</span><span class="nt">&gt;&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="c">&lt;!-- "OktaUtil" defines a global OktaUtil object that contains methods used to complete the Okta login flow. --&gt;</span> {{{OktaUtil}}} <span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript"</span> <span class="na">nonce=</span><span class="s">"{{nonceValue}}"</span><span class="nt">&gt;</span> <span class="c1">// "config" object contains default widget configuration</span> <span class="c1">// with any custom overrides defined in your admin settings.</span> <span class="kd">const</span> <span class="nx">config</span> <span class="o">=</span> <span class="nx">OktaUtil</span><span class="p">.</span><span class="nx">getSignInWidgetConfig</span><span class="p">();</span> <span class="nx">config</span><span class="p">.</span><span class="nx">theme</span> <span class="o">=</span> <span class="p">{</span> <span class="na">tokens</span><span class="p">:</span> <span class="p">{</span> <span class="na">BorderColorDisplay</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-bright-white)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryMain</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-fuchsia)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryDark</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-purple)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryDarker</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-purple)</span><span class="dl">'</span><span class="p">,</span> <span class="na">BorderRadiusTight</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--border-radius)</span><span class="dl">'</span><span class="p">,</span> <span class="na">BorderRadiusMain</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--border-radius)</span><span class="dl">'</span><span class="p">,</span> <span class="na">PalettePrimaryDark</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-orange)</span><span class="dl">'</span><span class="p">,</span> <span class="na">FocusOutlineColorPrimary</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-azul)</span><span class="dl">'</span><span class="p">,</span> <span class="na">TypographyFamilyBody</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--font-body)</span><span class="dl">'</span><span class="p">,</span> <span class="na">TypographyFamilyHeading</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--font-header)</span><span class="dl">'</span><span class="p">,</span> <span class="na">TypographyFamilyButton</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--font-body)</span><span class="dl">'</span><span class="p">,</span> <span class="na">BorderColorDangerControl</span><span class="p">:</span> <span class="dl">'</span><span class="s1">var(--color-cherry)</span><span class="dl">'</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// Render the Okta Sign-In Widget</span> <span class="kd">const</span> <span class="nx">oktaSignIn</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">OktaSignIn</span><span class="p">(</span><span class="nx">config</span><span class="p">);</span> <span class="nx">oktaSignIn</span><span class="p">.</span><span class="nx">renderEl</span><span class="p">({</span> <span class="na">el</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#okta-login-container</span><span class="dl">'</span> <span class="p">},</span> <span class="nx">OktaUtil</span><span class="p">.</span><span class="nx">completeLogin</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Logs errors that occur when configuring the widget.</span> <span class="c1">// Remove or replace this with your own custom error handler.</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span> <span class="p">}</span> <span class="p">);</span> <span class="nx">oktaSignIn</span><span class="p">.</span><span class="nx">afterTransform</span><span class="p">(</span><span class="dl">'</span><span class="s1">identify</span><span class="dl">'</span><span class="p">,</span> <span class="p">({</span> <span class="nx">formBag</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Title</span><span class="dl">'</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">title</span><span class="p">)</span> <span class="p">{</span> <span class="nx">title</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">content</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Log in and create a task</span><span class="dl">"</span><span class="p">;</span> <span class="p">}</span> <span class="kd">const</span> <span class="nx">help</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Link</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">dataSe</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">help</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">unlock</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Link</span><span class="dl">'</span> <span class="o">&amp;&amp;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">dataSe</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">unlock</span><span class="dl">'</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">divider</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="nx">ele</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Divider</span><span class="dl">'</span><span class="p">);</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span> <span class="o">=</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">ele</span> <span class="o">=&gt;</span> <span class="o">!</span><span class="p">[</span><span class="nx">help</span><span class="p">,</span> <span class="nx">unlock</span><span class="p">,</span> <span class="nx">divider</span><span class="p">].</span><span class="nx">includes</span><span class="p">(</span><span class="nx">ele</span><span class="p">));</span> <span class="kd">const</span> <span class="nx">blogLink</span> <span class="o">=</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Link</span><span class="dl">'</span><span class="p">,</span> <span class="na">contentType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">footer</span><span class="dl">'</span><span class="p">,</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">href</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://developer.okta.com/blog</span><span class="dl">'</span><span class="p">,</span> <span class="na">label</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Read our blog</span><span class="dl">'</span><span class="p">,</span> <span class="na">dataSe</span><span class="p">:</span> <span class="dl">'</span><span class="s1">blogCustomLink</span><span class="dl">'</span> <span class="p">}</span> <span class="p">};</span> <span class="nx">formBag</span><span class="p">.</span><span class="nx">uischema</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">blogLink</span><span class="p">);</span> <span class="p">});</span> <span class="nt">&lt;/script&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span> </code></pre></div></div> <p>You can also find the code in the <a href="https://github.com/oktadev/okta-js-siw-customization-example/tree/main/custom-signin-blog-post">GitHub repository for this blog post</a>. With these code changes, you can connect this with an app to see how it works end-to-end. You’ll need to update your Okta OpenID Connect (OIDC) application to work with the domain. In the Okta Admin Console, navigate to <strong>Applications</strong> &gt; <strong>Applications</strong> and find the Okta application for your custom app. Navigate to the <strong>Sign On</strong> tab. You’ll see a section for <strong>OpenID Connect ID Token</strong>. Select <strong>Edit</strong> and select <strong>Custom URL</strong> for your brand’s sign-in URL as the <strong>Issuer</strong> value.</p> <p>You’ll use the issuer value, which matches your brand’s custom URL, and the Okta application’s client ID in your custom app’s OIDC configuration. If you want to try this and don’t have a pre-built app, you can use one of our samples, such as the <a href="https://github.com/okta-samples/okta-react-sample">Okta React sample</a>.</p> <h2 id="customize-your-gen3-okta-hosted-sign-in-widget">Customize your Gen3 Okta-hosted Sign-In Widget</h2> <p>I hope you enjoyed customizing the sign-in experience for your brand. Using the Okta-hosted Sign-In widget is the best, most secure way to add identity security to your sites. With all the configuration options available, you can have a highly customized sign-in experience with a custom domain without anyone knowing you’re using Okta.</p> <p>If you like this post, there’s a good chance you’ll find these links helpful:</p> <ul> <li><a href="/blog/2025/07/22/react-pwa">Create a React PWA with Social Login Authentication</a></li> <li><a href="https://developer.okta.com/docs/journeys/OCI-secure-your-first-web-app/main/">Secure your first web app</a></li> <li><a href="/blog/2025/08/20/ios-mfa">How to Build a Secure iOS App with MFA</a></li> </ul> <p>Remember to follow us on <a href="https://twitter.com/oktadev">Twitter</a> and subscribe to our <a href="https://www.youtube.com/c/OktaDev/">YouTube</a> channel for fun and educational content. We also want to hear from you about topics you want to see and questions you may have. Leave us a comment below! Until next time!</p> Wed, 12 Nov 2025 00:00:00 -0500 https://developer.okta.com/blog/2025/11/12/custom-signin https://developer.okta.com/blog/2025/11/12/custom-signin