<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Sam Bauch</title>
    <description>The latest articles on Forem by Sam Bauch (@sammybauch).</description>
    <link>https://forem.com/sammybauch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F559888%2F4eb92075-8286-4f49-a42f-00c5f37a71f0.jpg</url>
      <title>Forem: Sam Bauch</title>
      <link>https://forem.com/sammybauch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sammybauch"/>
    <language>en</language>
    <item>
      <title>Twitter Cards for onchain SVG NFTs</title>
      <dc:creator>Sam Bauch</dc:creator>
      <pubDate>Fri, 18 Aug 2023 16:49:06 +0000</pubDate>
      <link>https://forem.com/sammybauch/twitter-cards-for-onchain-svg-nfts-582h</link>
      <guid>https://forem.com/sammybauch/twitter-cards-for-onchain-svg-nfts-582h</guid>
      <description>&lt;p&gt;I've had 2 projects recently where I wanted to encourage users to post a Tweet that would display an image for an onchain SVG NFT. Twitter doesn't provide an easy way to prompt users to Tweet an image unless you have your users authenticate their Twitter account. But OpenGraph meta tags on your page will append a Twitter Card with an image of the developer's choosing to any tweet that links to your site.&lt;/p&gt;

&lt;p&gt;This is a superior alternative to telling users to screenshot the page, but if the image content you want shared in-feed is an SVG, you're gonna have a bad time.&lt;/p&gt;

&lt;p&gt;Let's look at this Tweet that shares a link to &lt;a href="https://opensea.io/assets/ethereum/0xf9bdddba8011262382182cdfa7c92327aff15279/46"&gt;an onchain SVG NFT on OpenSea&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oaw8HGti--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zwg50sj5kues9jhed1qb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oaw8HGti--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zwg50sj5kues9jhed1qb.png" alt="Screenshot of a Tweet that does not display an image for the NFT being linked to" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No image is displayed here because the OpenGraph metatags on the linked URL include an SVG image as the content for the &lt;code&gt;twitter:image&lt;/code&gt; metatag:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--B5uaD5MF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kvyy7y0kulam70fc5coa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--B5uaD5MF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kvyy7y0kulam70fc5coa.png" alt="Web page source code for a page that won't render an image attached to a tweet" width="800" height="113"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order to get an image to display in-feed, we need to provide a URL to a supported image type. I don't know what the supported types are, but I do know that SVG is not supported, while JPG and PNG are. So if we only have an SVG image, and want users to be able to share that image on Twitter without screenshotting and without signing into their Twitter account, we need to replace the SVG image referenced in the metatag with a link to a PNG or JPG version of the image.&lt;/p&gt;

&lt;p&gt;I don't particularly like managing servers and certainly don't want to create a whole SVG -&amp;gt; PNG asset pipeline for this functionality. And things are further complicated if you're using NextJS or similar frameworks - the Twitter bots won't execute client-side JS, so the metatags with the supported image URL need to be rendered server-side.&lt;/p&gt;

&lt;p&gt;Let's see how we can solve this using NextJS's &lt;code&gt;getServerSideProps&lt;/code&gt; and an NFT indexing service &lt;a href="https://simplehash.com/"&gt;SimpleHash&lt;/a&gt; that provides great coverage and handles caching and &lt;a href="https://docs.simplehash.com/reference/overview#media-previews"&gt;serving cached images specifically for OpenGraph&lt;/a&gt; usage in the NFT data they index and serve over an API.&lt;/p&gt;

&lt;h2&gt;
  
  
  getServerSideProps
&lt;/h2&gt;

&lt;p&gt;This isn't a NextJS tutorial, so if you're unfamiliar with NextJS SSR and &lt;code&gt;getServerSideProps&lt;/code&gt; you should review the NextJS docs. Simply put, &lt;code&gt;getServerSideProps&lt;/code&gt; allows us to write some code that executes on the server, and passes the return value of that code as props to a NextJS page component served at a specific URL, allowing dynamic code to be rendered by the server rather than the client.&lt;/p&gt;

&lt;p&gt;This is useful to us here because we need our server to render the metatags in order for Twitter to know what to display in-feed when a Tweet links to your site. If you tried to run the same code in a React component's &lt;code&gt;render&lt;/code&gt; method, Twitter's bots will not index the result.&lt;/p&gt;

&lt;p&gt;We'll walk through a simplified example of a NextJS page component that uses &lt;code&gt;getServerSideProps&lt;/code&gt; to render dynamic metatags.&lt;/p&gt;

&lt;p&gt;Let's start with a simple skeleton for a page that represents a single NFT displayed at a dynamic route. We'll set it up to expect an &lt;code&gt;imageUrl&lt;/code&gt; prop for the NFT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pages/[nftContractAddress]/[nftTokenId].tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GetServerSideProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Head&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/head&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MetaTagData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;NFTDetailPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;MetaTagData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Head&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* our metatags will go here */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Head&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* rest of markup */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that skeleton in place, we can next add to the same file our &lt;code&gt;getServerSideProps&lt;/code&gt; implementation. This code will run on the server, and needs to return an object that satisfies the page's typed props. For now we'll use a placeholder image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getServerSideProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetServerSideProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MetaTagData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://placekitten.com/600/400&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Image Fetching
&lt;/h2&gt;

&lt;p&gt;With our skeleton in place, we can now replace our placeholder with an image that is supported by Twitter for in-feed image display that represents the NFT at the requested dynamic URL.&lt;/p&gt;

&lt;p&gt;SimpleHash is great for this - their API returns a url to a JPG they host that Twitter is happy to display and which is optimized specifically for this OpenGraph purpose. You'll need an API key (free up to 20k monthly requests).&lt;/p&gt;

&lt;p&gt;We pull the contract address and token ID for the NFT off of  the URL, call the SimpleHash API, and return an object that includes the optimized image url from the API response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pages/[nftContractAddress]/[nftTokenId].tsx&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getServerSideProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetServerSideProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MetaTagData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nftContractAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nftTokenId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="s2"&gt;`https://api.simplehash.com/api/v0/nfts/ethereum/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nftContractAddress&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nftTokenId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SH_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;


  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;previews&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;image_opengraph_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function will execute on the server, and pass the return value to our page component as props. Next, we'll use those values to display metatags that tell Twitter to render an image of our NFT in-feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metatags
&lt;/h2&gt;

&lt;p&gt;If you're not familiar with OpenGraph metatags, you should review the documentation to know exactly what tags you need for display and what options are available to you. For now we will hardcode some values and show how to use the &lt;code&gt;imageUrl&lt;/code&gt; we gathered.&lt;/p&gt;

&lt;p&gt;Back in our page component markup, let's add some metatags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// app/pages/[nftContractAddress]/[nftTokenId].tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GetServerSideProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Head&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/head&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MetaTagData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;NFTDetailPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;MetaTagData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Head&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twitter:card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summary_large_image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twitter:url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://mysite.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twitter:title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NFT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twitter:description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is an NFT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* &amp;lt;!-- Dynamic Image Here             vvvvvv --&amp;gt; */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twitter:image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Head&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* rest of markup */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// `getServerSideProps` implementation...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caching
&lt;/h2&gt;

&lt;p&gt;Depending on your project it might also be a good idea to cache the API response and serve the cached data rather than querying the SimpleHash API for every request. This will help keep your API requests under limits and serve your page faster once data is cached.&lt;/p&gt;

&lt;p&gt;I found Vercel's Redis offering and &lt;code&gt;kv&lt;/code&gt; package to be super simple to setup. &lt;/p&gt;

&lt;p&gt;We need to create a client and check our cache for appropriate data, returning the cached JSON if we find a hit. Otherwise, we request the API and write to our cache for subsequent requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KV_REST_API_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KV_REST_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hgetall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nftContractAddress&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nftTokenId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;MetaTagData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// .... API request&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nftContractAddress&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;nftTokenId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;previews&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;image_opengraph_url&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Shipit
&lt;/h2&gt;

&lt;p&gt;We're done! This blog post took a lot longer to write than writing the code itself. It's not a show-stopping feature, but if you want your onchain art to show on Twitter and other social networks, this feels like the best way to accomplish that goal without setting up your own image conversion pipeline.&lt;/p&gt;

&lt;p&gt;Here's what a tweet linking to an NFT page would like look, compared to the tweet linking to OpenSea we saw earlier:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2pykQU37--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/76j0k9zvh1apqwfly6qa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2pykQU37--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/76j0k9zvh1apqwfly6qa.png" alt="Screenshot of a Tweet that does display an image for the NFT being linked to" width="800" height="659"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For debugging purposes, &lt;a href="//metatags.io"&gt;metatags.io&lt;/a&gt; is a great site that will crawl your metatags and show you how your link will appear on various social platforms.&lt;/p&gt;

&lt;p&gt;I like onchain NFTs and it's frustrating that it takes a little extra work to make onchain art display on Twitter. Hopefully this helps you helps me see more onchain art on Twitter.&lt;/p&gt;

&lt;p&gt;You can follow me there &lt;a href="https://twitter.com/sammybauch"&gt;@sammybauch&lt;/a&gt;, and if you also like onchain NFTs, check out my onchain projects &lt;a href="https://v1.sudoswap.xyz/#/browse/buy/0xF9BddDBa8011262382182CdFA7c92327afF15279"&gt;Blitpop&lt;/a&gt; and &lt;a href="https://blockclock.0xessential.com/"&gt;Blockclock&lt;/a&gt; and keep an eye out for some new collections coming soon!&lt;/p&gt;

</description>
      <category>web3</category>
      <category>nextjs</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Add SAML SSO to a Rails 6 app</title>
      <dc:creator>Sam Bauch</dc:creator>
      <pubDate>Fri, 29 Jan 2021 16:32:19 +0000</pubDate>
      <link>https://forem.com/sammybauch/add-saml-sso-to-a-rails-6-app-20ld</link>
      <guid>https://forem.com/sammybauch/add-saml-sso-to-a-rails-6-app-20ld</guid>
      <description>&lt;p&gt;SAML SSO refers to an authentication mechanism preferred by enterprise companies. The SSO part stands for Single Sign-On. From the enterprise’s perspective, they desire a centralized service where their employees can authenticate, which then provides authenticated access to the applications they use for work. These services are called Identity Providers (IDPs), and they are the &lt;em&gt;Single&lt;/em&gt; place where enterprise employees &lt;em&gt;Sign-On.&lt;/em&gt; This is an alternative to employees using password-based authentication for each of the various applications they use.&lt;/p&gt;

&lt;p&gt;The SAML part stands for Secure Assertion Markup Language. SAML utilizes a domain-specific flavor of XML that describes an authenticated user, encoded to a string and passed to your application in a query parameter. Your application decodes the &lt;code&gt;SAMLResponse&lt;/code&gt; using a key that the enterprise provides to you.&lt;/p&gt;

&lt;p&gt;The details of the SAML Response are less important than understanding the general flow. When a user wants to sign in using SAML, you must send that user to their Identity Provider with a &lt;code&gt;SAMLRequest&lt;/code&gt; that identifies your application. The user signs in to their IDP, and is redirected back to you with the &lt;code&gt;SAMLResponse&lt;/code&gt;. What makes this a tiny bit complicated for a multi-tenant application is routing the user to the correct IDP, as SAML SSO breaks our common understanding of a login flow.&lt;/p&gt;

&lt;p&gt;A typical login screen might have an email and password field, then a few buttons to sign in with OAuth services, like Google or GitHub. But now, you’ll need to gather something from the user who wants to sign in such that you can send them to the correct IDP instance.  We like to say that SAML is &lt;em&gt;instance based&lt;/em&gt;, especially when&lt;a href="https://ossoapp.com/blog/saml-vs-oauth" rel="noopener noreferrer"&gt;compared to OAuth&lt;/a&gt;, which we can think of as &lt;em&gt;class based&lt;/em&gt;. Domain is a common key for this process - you can ask a user for their email, grab the domain, and use that as a key to find the correct IDP instance and redirect the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let’s Build and Iterate
&lt;/h3&gt;

&lt;p&gt;Once you wrap your head around understanding the SAML flow, it’s really not challenging to implement SAML in a Rails app - IDPs themselves offer great open source libraries to help with your integration. You can definitely get very far in a day or two. But like most things in engineering, the last 10% can take 90% of the effort, and while we all love to say “I could build that in a weekend,” we often later eat our words when we realize all of the edge cases, documentation needs, scaling challenges and unknown unknowns that make this or any other project worth doing a bit more involved than a quick weekend hack.&lt;/p&gt;

&lt;p&gt;I’m a co-founder of an open source company Osso - our microservice is a Ruby and React app that allows you to onboard SAML SSO customers, generates custom documentation for each customer to perform their onboarding tasks in their IDP, and allows your Rails app to consume Osso using OAuth - we even provide &lt;code&gt;omniauth-osso&lt;/code&gt; to make consuming an Osso instance from your Rails app incredibly simple.&lt;/p&gt;

&lt;p&gt;Osso also offers paid plans, and you can skip right to our &lt;a href="https://ossoapp.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt; to learn more about hosted Osso if you need SAML yesterday. Or you can jump right in to &lt;a href="https://ossoapp.com/docs/deploy/overview" rel="noopener noreferrer"&gt;deploying an open source Osso instance&lt;/a&gt; from our GitHub repo. There’s certainly the possibility of using &lt;code&gt;osso-rb&lt;/code&gt; in your Rails app directly — you’ll want to mount our 3 rack based apps, pull in migrations, etc. — but this post will focus instead on a microservices approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So&lt;/strong&gt;, if you’d like to begin from first principles, follow along here as we build up a production-ready SAML SSO integration, starting with a single tenant approach, then layering in multi-tenancy. Once we have multi-tenant support, we’ll address questions about scalability and serviceability — how can we repeatedly add additional tenants, without spending significant engineering or customer support cycles? I &lt;em&gt;will&lt;/em&gt; encourage you to use Osso, and show you exactly how, but will also highlight the things you need to be considering in order to roll out a production ready SAML integration &lt;em&gt;without&lt;/em&gt; Osso.&lt;/p&gt;

&lt;p&gt;We’ll use a single Rails app repository throughout this tutorial. The &lt;a href="https://github.com/enterprise-oss/saml-rails" rel="noopener noreferrer"&gt;main branch&lt;/a&gt; is a barebones Rails 6 app with Devise and a User model, and for each of our steps below we’ll move to a feature branch so we can see a full diff. Each step adds more functionality, getting you closer to a production-ready, multi-tenant integration. Each step also uses open source software, where the library used in the previous step is a dependency of the next step’s library. Neat!&lt;/p&gt;

&lt;p&gt;We’ll also make use of Osso’s &lt;a href="https://github.com/enterprise-oss/sinatra-ruby-idp" rel="noopener noreferrer"&gt;mock IDP&lt;/a&gt; and &lt;a href="https://demo.ossoapp.com" rel="noopener noreferrer"&gt;demo instance&lt;/a&gt;, especially when we get into multi-tenancy. You’ll still want to register with an IDP for a developer account in order to have access to an IDP for a second tenant - we recommend an &lt;a href="https://developer.okta.com/signup/" rel="noopener noreferrer"&gt;Okta developer account&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Base Rails application
&lt;/h3&gt;

&lt;p&gt;We’ll use the same Rails 6 application for each step - the source code is available &lt;a href="https://github.com/enterprise-oss/saml-rails" rel="noopener noreferrer"&gt;here&lt;/a&gt;. The &lt;code&gt;main&lt;/code&gt; branch adds Devise with a User model to a brand new Rails app using Postgres as the database. The application doesn’t do anything - there is an index route and a logged in route that will tell you the email for the current user. This guide assumes proficiency with Rails, so it will skip some details. We’ll also use some intentionally naive approaches to demonstrate common challenges of SAML SSO. &lt;/p&gt;

&lt;h3&gt;
  
  
  Single tenant
&lt;/h3&gt;

&lt;h4&gt;
  
  
  With ruby-saml and mock IDP
&lt;/h4&gt;

&lt;p&gt;My first experience with SAML was working on &lt;a href="https://medium.com/@tsharon/democratizing-ux-670b95fbc07f" rel="noopener noreferrer"&gt;internal software at WeWork&lt;/a&gt; where one of my Osso co-founders was a teammate. The IT department required us to use SAML SSO, which I at first was awfully annoyed by. Google OAuth was super easy, and could be restricted to domain, so what’s the deal with this OneLogin thing? Fortunately, single-tenant SAML was just as simple to implement, and admittedly did include some better security. This is a common situation — an enterprise company requires SAML, and it might seem silly to you, but them’s the rules.&lt;/p&gt;

&lt;p&gt;Our project was a Rails app, and we used a Ruby gem from OneLogin to handle the actual SAML encoding and decoding. Since we were building this app for WeWork employees, we had a single tenant, and didn’t need to worry about implementing a scalable solution for multiple tenants, documenting how to set SAML up for the app or training our teammates on how it worked. We also didn’t need to change our login form - we knew every user would be sent to the same IDP instance, so we were able to use a simple &lt;em&gt;Sign in With OneLogin&lt;/em&gt; button.&lt;/p&gt;

&lt;p&gt;That’s what we’ll do here, and if you’re building internal software this will be a fine approach for your production release! We’ll use Osso’s mock IDP for this single tenant to keep things simple too.&lt;/p&gt;

&lt;p&gt;First lets install the ruby-saml gem. We’ll include it in our Gemfile and run &lt;code&gt;$ bundle install&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'ruby-saml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 1.9.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll use this library in two ways:&lt;/p&gt;

&lt;p&gt;First, when a user wishes to sign in with SAML, we use ruby-saml to generate a url with a &lt;code&gt;SAMLRequest&lt;/code&gt; - the URL is where we will send the user to sign in, and the &lt;code&gt;SAMLRequest&lt;/code&gt; is sent along as a query param in order to identify our application to the IDP.&lt;/p&gt;

&lt;p&gt;When the user signs in to their IDP, they will be sent back to your application with a &lt;code&gt;SAMLResponse&lt;/code&gt; query param. We then use ruby-saml to decode and validate the &lt;code&gt;SAMLResponse&lt;/code&gt;, allowing us to access information describing the user, such as their email address.&lt;/p&gt;

&lt;p&gt;Lets define a couple of routes and controller actions to handle this flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'saml_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"application#saml_login"&lt;/span&gt;
  &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'saml_callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"application#saml_callback"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;saml_login&lt;/code&gt; action, we create a &lt;code&gt;OneLogin::RubySaml::Authrequest&lt;/code&gt; and redirect to the return value. Later, in the callback, we validate and decode the response, and will sign the user in and redirect them if valid. Otherwise we’ll raise the validation errors to understand where our SAML config went wrong.&lt;/p&gt;

&lt;p&gt;This callback route will also need to accept a POST request with &lt;code&gt;www-url-form-encoded&lt;/code&gt; parameters, so you will need to skip the Rails &lt;code&gt;verify_authenticity_token&lt;/code&gt; &lt;code&gt;before_action&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;skip_before_action&lt;/span&gt; &lt;span class="ss"&gt;:verify_authenticity_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: :saml_callback&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;saml_login&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OneLogin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RubySaml&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Authrequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="n"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saml_settings&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;saml_callback&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OneLogin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RubySaml&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:SAMLResponse&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;:settings&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;saml_settings&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_valid?&lt;/span&gt;
      &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_or_find_by!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nameid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;sign_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:logged_in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of these actions depend on what ruby-saml calls &lt;code&gt;settings&lt;/code&gt;. These values are created via the configuration you must perform between your application and the Identity Provider. In a real-world example, your application generates a few values which you provide to your customer. The customer uses these to configure your application in their IDP and returns some data generated by the IDP. We’ll discuss this process in more detail later. Since we are using the Osso Mock IDP we can mostly skip this configuration step, but it’s worth understanding what each of these values represents, how it functions and why it might prevent your user from signing in if misconfigured.&lt;/p&gt;

&lt;p&gt;Each of the controller actions above calls a private method &lt;code&gt;saml_settings&lt;/code&gt;. Since we are dealing with a single tenant, we will essentially hardcode the SAML configuration values to support this one IDP instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;saml_settings&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OneLogin&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RubySaml&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;

    &lt;span class="c1"&gt;# You provide to IDP&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertion_consumer_service_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;host_with_port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/saml_callback"&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sp_entity_id&lt;/span&gt;                   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-single-tenant"&lt;/span&gt;

    &lt;span class="c1"&gt;# IDP provides to you&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idp_sso_target_url&lt;/span&gt;             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://idp.ossoapp.com/saml-login"&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idp_cert&lt;/span&gt;                       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idp_cert&lt;/span&gt;

    &lt;span class="n"&gt;settings&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Values you provide&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ACS URL&lt;/strong&gt; - The Assertion Consumer Service URL is where the IDP will send the user with a SAMLResponse when they log in. Similar to a Redirect URI in OAuth, your application will generate this value according to how your routes are set up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SP Entity ID&lt;/strong&gt;  - Sometimes called the Audience URI, this is a unique identifier for the tenant in your application. This can get hairy! &lt;strong&gt;Google&lt;/strong&gt; requires that they be unique for a customer, so you can't just use domain, while &lt;strong&gt;Azure&lt;/strong&gt; won't let you use a UUID and &lt;strong&gt;Ping&lt;/strong&gt; won't let you use a url. Those are just &lt;em&gt;some&lt;/em&gt; of the edge cases we've found!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Values the IDP provides&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IDP SSO Target URL&lt;/strong&gt; - The single sign on target URL tells your application where to send a user with a &lt;code&gt;SAMLRequest&lt;/code&gt; in order to sign in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IDP Certificate&lt;/strong&gt; - An x509 certificate which includes a public key that your application uses to encode and decode the SAML request and response.&lt;/p&gt;

&lt;p&gt;Since we are using the Mock IDP, these values can be found in the Mock IDP’s federated metadata: &lt;a href="https://github.com/enterprise-oss/sinatra-ruby-idp/blob/main/metadata.xml" rel="noopener noreferrer"&gt;https://github.com/enterprise-oss/sinatra-ruby-idp/blob/main/metadata.xml&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To enforce good security practices, we put the certificate in Rails credentials. We won't show you our whole certificate here, but here's part of an example credentials file that shows both single and multiline certificates:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhktpv7ieppxfc6gdv5nr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhktpv7ieppxfc6gdv5nr.png" alt="Rails credentials file"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, we need to give a way for the user to actually log in. We’ll add a Sign In button to the index route. We’re able to use a single button due to the fact we are supporting only one tenant - we know we’ll send every user to the same SSO URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"container"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"main-content"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;Welcome&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&amp;gt;
      &amp;lt;% flash.each do |name, msg| %&amp;gt;
        &amp;lt;% if msg.is_a?(String) %&amp;gt;
          &amp;lt;div class="alert alert-&amp;lt;%= name == :notice ? "success" : "error" %&amp;gt;"&amp;gt;
            &amp;lt;%=raw content_tag :div, msg, id:"flash_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;" %&amp;gt;
          &amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="no"&gt;Add&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="n"&gt;signing&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= button_to 'Sign in with SAML SSO', action: :saml_login %&amp;gt; 
    &amp;lt;/div&amp;gt;  
  &amp;lt;/div&amp;gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it! You should now be able to sign in using SAML against the Osso Mock IDP, which takes any email / password combination. You can see the whole diff for this approach at this Pull Request: &lt;a href="https://github.com/enterprise-oss/saml-rails/pull/4" rel="noopener noreferrer"&gt;https://github.com/enterprise-oss/saml-rails/pull/4&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-tenant
&lt;/h3&gt;

&lt;h4&gt;
  
  
  with omniauth-multi-provider and omniauth-saml
&lt;/h4&gt;

&lt;p&gt;A single-tenant SAML integration is fine if you’re building internal software. But if your application is a multi-tenant SAAS app and you’re starting to sell to bigger, security-minded enterprises you need to support multi-tenancy.&lt;/p&gt;

&lt;p&gt;The main engineering requirement to think about is how to surface the relevant SAML configuration values when a user wants to sign in with SAML. In our single-tenant approach, we were able to hard code these values for the single tenant. We’ll also hard code values in this multi-tenant approach before discussing the weaknesses of this approach. We’ll also need to update our sign in UX in order to ascertain which tenant a user belongs to in order to send them to the correct IDP.&lt;/p&gt;

&lt;p&gt;We’ll also use the second tenant to demonstrate SAML configuration in an Identity Provider and discuss the documentation challenges. We recommend signing up for an Okta developer account for your testing purposes - &lt;a href="https://www.okta.com/developer/signup" rel="noopener noreferrer"&gt;https://www.okta.com/developer/signup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ll use a Ruby gem &lt;code&gt;omniauth-multi-provider&lt;/code&gt;  to support SAML multi-tenancy. &lt;code&gt;omniauth-saml&lt;/code&gt; will handle that actual SAML bits, and this library uses &lt;code&gt;ruby-saml&lt;/code&gt; internally, much in the same manner we used it in the single-tenant approach. OmniAuth is a Ruby library that “standardizes multi-provider authentication for web applications.” If you’ve implemented OAuth in a Rails app you’re likely familiar with this library. It integrates well with Devise, and offers a framework for engineers to create &lt;em&gt;Strategies&lt;/em&gt; for OmniAuth for authenticating against external services. If you’re not familiar with OmniAuth it’s worth familiarizing yourself - &lt;a href="https://github.com/omniauth/omniauth" rel="noopener noreferrer"&gt;https://github.com/omniauth/omniauth&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To begin, let’s add &lt;code&gt;omniauth-multi-provider&lt;/code&gt; and &lt;code&gt;omniauth-saml&lt;/code&gt;to our Gemfile, and we can remove the &lt;code&gt;ruby-saml&lt;/code&gt; gem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'omniauth-multi-provider'&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'omniauth-saml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'= 1.10.3'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll also extend our Devise configuration in the User model to integrate with OmniAuth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
    &lt;span class="n"&gt;devise&lt;/span&gt; &lt;span class="ss"&gt;:timeoutable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:omniauthable&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OmniAuth will handle our routes now, so let’s update our &lt;code&gt;routes.rb&lt;/code&gt;, replacing the routes and controller actions we created in our single-tenant branch. We wrap these routes in a &lt;code&gt;devise_scope&lt;/code&gt; block in order to let Devise know we want to map these routes to the User resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;devise_scope&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/auth/saml/:identity_provider_id/callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'omniauth_callbacks#saml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'user_omniauth_callback'&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/auth/saml/:identity_provider_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'omniauth_callbacks#passthru'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'user_omniauth_authorize'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We do need to create this &lt;code&gt;OmniauthCallbacks&lt;/code&gt; controller, and it will inherit the &lt;code&gt;Devise::OmniauthCallbacksController&lt;/code&gt;. We only need to define the &lt;code&gt;saml&lt;/code&gt; controller action - &lt;code&gt;omniauth-multi-provider&lt;/code&gt; acts as a sort of meta-provider as we’ll see below. We also need to skip verifying the authenticity token - some IDPs will submit the callback request as a POST with &lt;code&gt;www-url-form-encoded&lt;/code&gt; parameters, and we need to allow that on this route. Since we want actions added in the future to enforce CSRF, let's be sure to allow-list the saml action rather than disable the check for the whole controller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OmniauthCallbacksController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Devise&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OmniauthCallbacksController&lt;/span&gt;
    &lt;span class="n"&gt;protect_from_forgery&lt;/span&gt; &lt;span class="ss"&gt;with: :exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;except: :saml&lt;/span&gt; 

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;saml&lt;/span&gt;
      &lt;span class="n"&gt;auth_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'omniauth.auth'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_or_find_by!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;auth_hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'uid'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

      &lt;span class="n"&gt;sign_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="n"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:logged_in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;omniauth-multi-provider&lt;/code&gt; gem offers some Rack middleware that will intercept requests to each of these routes. You’ll want to add an initializer that allows our OmniAuth implementation to route a user to the relevant IDP. On the callback, the middleware will intercept the request, handle SAML validation, and convert the SAMLResponse into an omniauth authentication hash, passing the request on to your controller action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
  &lt;span class="no"&gt;SAML_SETTINGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'example.com'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;issuer: &lt;/span&gt;&lt;span class="s2"&gt;"my-single-tenant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;idp_sso_target_url: &lt;/span&gt;&lt;span class="s2"&gt;"https://idp.ossoapp.com/saml-login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;idp_cert: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idp_cert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="no"&gt;OmniAuth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Builder&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;OmniAuth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MultiProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;provider_name: :saml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;identity_provider_id_regex: &lt;/span&gt;&lt;span class="sr"&gt;/[a-z]*/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;path_prefix: &lt;/span&gt;&lt;span class="s1"&gt;'/users/auth/saml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;callback_suffix: &lt;/span&gt;&lt;span class="s1"&gt;'callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;identity_provider_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rack_env&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rack_env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;SAML_SETTINGS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;identity_provider_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chomp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/callback'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to_sym&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="ss"&gt;assertion_consumer_service_url: &lt;/span&gt;&lt;span class="n"&gt;acs_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acs_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request_url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chomp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/callback'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'/callback'&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This block must return the same &lt;code&gt;saml_settings&lt;/code&gt; we saw in the single-tenant approach for both the request and callback actions - &lt;code&gt;omniauth-saml&lt;/code&gt; uses &lt;code&gt;ruby-saml&lt;/code&gt; under the hood, so the keys are the same. We’ve replaced our private controller method with a class constant that includes the same attributes for the same tenant. We’re using a hash with a key of &lt;code&gt;example.com&lt;/code&gt; - in order to use these SAML settings, we need to submit a POST request to &lt;code&gt;/users/auth/saml/example.com&lt;/code&gt;. This sorta breaks Rails conventions, so we'll adjust it later.&lt;/p&gt;

&lt;p&gt;We also add a convenience method here for the ACS url - since this block is used for both the request and callback, we need to be a little hacky to ensure this block always returns a hash with the proper callback path - a better approach, as we’ll see later, would be to derive all of these config values from a model instance.&lt;/p&gt;

&lt;p&gt;With that in place, we can slightly adjust our login flow and should still be able to log in using the first tenant. We’re hardcoding the &lt;code&gt;identity_provider_id&lt;/code&gt; as &lt;code&gt;example.com&lt;/code&gt; to match the key above. We’re not quite supporting multi-tenancy yet, but most of the parts are in place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= button_to 'Sign in with SAML SSO', user_omniauth_authorize_url(identity_provider_id: 'example.com') %&amp;gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s get a second tenant onboarded! We’ll use Okta as the IDP for the second tenant. You can sign up for a free developer account at &lt;a href="https://developer.okta.com/signup/" rel="noopener noreferrer"&gt;https://developer.okta.com/signup/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you have an account, configure our Rails app in your Okta instance. We’ll wait.&lt;/p&gt;

&lt;p&gt;[some stupid waiting gif]&lt;/p&gt;

&lt;p&gt;Feeling a bit lost? That’s what your enterprise customers will experience if you don’t provide them documentation. You’ll want to create a SAML 2.0 Web App, and the Okta form even suggests that the app you’re trying to integrate should provide instructions.&lt;/p&gt;

&lt;p&gt;Lots of SaaS companies take a similar approach where they provide &lt;a href="https://learning.postman.com/docs/administration/sso/intro-sso/" rel="noopener noreferrer"&gt;one-size-fits-all documentation&lt;/a&gt; to &lt;a href="https://docs.datadoghq.com/account_management/saml/" rel="noopener noreferrer"&gt;configure&lt;/a&gt; an &lt;a href="https://docs.looker.com/admin-options/security/saml-auth#saml_auth_settings" rel="noopener noreferrer"&gt;application&lt;/a&gt; in an &lt;a href="https://support.airtable.com/hc/en-us/articles/115006448948-Configuring-SSO-with-Okta" rel="noopener noreferrer"&gt;IDP&lt;/a&gt;, and then &lt;a href="https://help.figma.com/hc/en-us/articles/360040532333-Guide-to-SAML-SSO-in-Figma" rel="noopener noreferrer"&gt;separately&lt;/a&gt; provides the &lt;a href="https://support.box.com/hc/en-us/articles/360043696514-Setting-Up-Single-Sign-On-SSO-for-your-Enterprise" rel="noopener noreferrer"&gt;configuration&lt;/a&gt; &lt;a href="https://www.intercom.com/help/en/articles/3974587-integrate-with-an-identity-provider-and-log-in-with-saml-sso" rel="noopener noreferrer"&gt;values&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Osso takes a slightly different approach and generates bespoke documentation for end users in a portable and easy to use format. Here’s a doc we generated that you can use to set up the demo app in your Okta instance:&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://www.slideshare.net/slideshow/embed_code/key/2C3ZQ16vUoS9uE" alt="2C3ZQ16vUoS9uE on slideshare.net" width="100%" height="487"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Once you configure the demo app in your Okta instance, you’ll be able to access the configuration values you need to support the second tenant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="no"&gt;SAML_SETTINGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'example.com'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;issuer: &lt;/span&gt;&lt;span class="s2"&gt;"my-single-tenant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;idp_sso_target_url: &lt;/span&gt;&lt;span class="s2"&gt;"https://idp.ossoapp.com/saml-login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;idp_cert: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idp_cert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s1"&gt;'your-email-domain.com'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;issuer: &lt;/span&gt;&lt;span class="s2"&gt;"my-single-tenant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;# SSO URL provided by Okta specific to your instance&lt;/span&gt;
      &lt;span class="ss"&gt;idp_sso_target_url: &lt;/span&gt;&lt;span class="s1"&gt;'https://dev-634049.okta.com/app/dev-634049_railsdemo_1/exk1yj3meeGRUVVT04x7/sso/saml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;idp_cert: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;okta_idp_cert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# x509 cert provided by Okta&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should be able to log in using your Okta instance by switching the hardcoded &lt;code&gt;identity_provider_id&lt;/code&gt; in the login form to match the key we just added in the &lt;code&gt;SAML_SETTINGS&lt;/code&gt; hash. These keys are somewhat arbitrary, but by using a domain we ensure uniqueness for what we can understand as a tenant, and we’ll be able to offer a nice sign in UX when we want to support both SAML and password based logins in the same form. Lets test the Okta tenant before extending this form to actually support multi-tenant logins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= button_to 'Sign in with SAML SSO', user_omniauth_authorize_url(identity_provider_id: 'your-email-domain.com') %&amp;gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you’ve successfully authenticated against the Mock IDP and your Okta instance, let’s make this login form support multi-tenancy. Due to &lt;code&gt;omniauth-multi-provider&lt;/code&gt;'s RESTful route approach, we need to add another route that at first may seem to just be adding indirection, but it will become useful as we improve our login form.&lt;/p&gt;

&lt;p&gt;The route we need to add will be responsible for receiving the login form POST request. For now, we’ll just redirect the user to the &lt;code&gt;user_omniauth_authorize&lt;/code&gt; route. We won’t want to use this in production - the recently released OmniAuth 2.0 removes support for GET requests in the request phase due to security concerns, and that’s what we’re doing here with this redirect.&lt;/p&gt;

&lt;p&gt;You’ll likely notice some other half-baked approaches here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/users/auth/saml_idp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'application#idp_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'user_idp_discovery'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="no"&gt;TENANTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'example.com'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"08909493-cc6f-4a67-9986-f8f4452ba1d4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'your-email-domain.com'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"4eaff58f-40a2-4ebe-b746-a9dbe2103864"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;idp_login&lt;/span&gt;
    &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;
    &lt;span class="n"&gt;idp_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;TENANTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;user_omniauth_authorize_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;identity_provider_id: &lt;/span&gt;&lt;span class="n"&gt;idp_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;idp_id&lt;/span&gt;

    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This action is responsible for accepting an email form parameter, parsing the domain, and using the domain to look up a UUID for a SAML tenant. In our case we’ve generated UUIDs and are looking up the domains in a hash, but we could also start to think about persisting these values in the database. We could redirect using the domain name, but that breaks some Rails conventions, and when something is named ID we should use an ID to meet our teammates expectations. We also need to copy these UUIDs over to our &lt;code&gt;SAML_SETTINGS&lt;/code&gt; hash, though you could also start to think about persisting the SAML settings in the database as well as we’re starting to get a little copy-paste messy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="no"&gt;SAML_SETTINGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'08909493-cc6f-4a67-9986-f8f4452ba1d4'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;issuer: &lt;/span&gt;&lt;span class="s2"&gt;"my-single-tenant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;idp_sso_target_url: &lt;/span&gt;&lt;span class="s2"&gt;"https://idp.ossoapp.com/saml-login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;idp_cert: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idp_cert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s1"&gt;'4eaff58f-40a2-4ebe-b746-a9dbe2103864'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;issuer: &lt;/span&gt;&lt;span class="s2"&gt;"my-single-tenant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;idp_sso_target_url: &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="c1"&gt;# SSO URL provided by Okta,&lt;/span&gt;
      &lt;span class="ss"&gt;idp_cert: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;okta_idp_cert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# x509 cert provided by Okta&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we’re now using UUIDs, we’ll also want to change the regex used by &lt;code&gt;omniauth-multi-provider&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="no"&gt;UUID_REGEXP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="sr"&gt;/[0-9a-f]{8}-[0-9a-f]{3,4}-[0-9a-f]{4}-[0-9a-f]{3,4}-[0-9a-f]{12}/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
          &lt;span class="nf"&gt;freeze&lt;/span&gt;

  &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="no"&gt;OmniAuth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Builder&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;OmniAuth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MultiProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;provider_name: :saml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;identity_provider_id_regex: &lt;/span&gt;&lt;span class="no"&gt;UUID_REGEXP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;path_prefix: &lt;/span&gt;&lt;span class="s1"&gt;'/users/auth/saml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;callback_suffix: &lt;/span&gt;&lt;span class="s1"&gt;'callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;identity_provider_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rack_env&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="c1"&gt;#...&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can adjust our login form to use an email input, and hit the &lt;code&gt;idp_login&lt;/code&gt; route&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= form_with url: idp_login_url, method: 'post', class: 'login-form' do |form| %&amp;gt;
    &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Email"&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= form.email_field :email %&amp;gt;
    &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt; &lt;span class="s2"&gt;"Sign in with SAML SSO"&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should be able to sign in using either tenant now! We’re definitely making progress - the login form scales reasonably well for SAML users, but it won’t support falling back to password. And we really should address the GET request we’re making in our redirect in order to be ready for OmniAuth 2.0.&lt;/p&gt;

&lt;p&gt;We’re going to write a little bit of Javascript to help. If you’re using a front-end framework like React this will give you the general idea of what you’ll want to do with a login form, but we’ll stick with unobtrusive vanilla JS.&lt;/p&gt;

&lt;p&gt;We’re going to update our idp_login action to accept an ajax request and return json. If we submit an email where a SAML tenant exists for the email’s domain, then we’ll return the SAML Tenant UUID, and use that to append another form on the page and submit it. We’ll now be posting to the user_omniauth_authorize_url with a UUID that we know maps to a SAML tenant. If we don’t get a UUID back, then we know the user will need to provide a password to sign in and we can display a password input. We’re a little hacky here again with things like the authenticity token, and we won’t actually implement the password login, but this should serve as a fine example for what you’ll want to do however you write JS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbolinks:load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loginForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#login-form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;loginForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ajax:success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;_rest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity_provider_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;samlLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity_provider_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nf"&gt;passwordLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loginForm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;samlLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idpId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta[name="csrf-token"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
    &lt;span class="nx"&gt;tokenInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;tokenInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authenticity_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/users/auth/saml/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;idpId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;passwordLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loginForm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passwordInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="nx"&gt;passwordInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;loginForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passwordInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;loginForm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/auth/password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;   
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s update our controller action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;idp_login&lt;/span&gt;
    &lt;span class="n"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;
    &lt;span class="n"&gt;idp_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;TENANTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;identity_provider_id: &lt;/span&gt;&lt;span class="n"&gt;idp_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we can also adjust our routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;devise_scope&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/users/auth/saml_idp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'omniauth_callbacks#idp_login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'user_idp_discovery'&lt;/span&gt;
    &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="s1"&gt;'/users/auth/saml/:identity_provider_id/callback'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;via: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'omniauth_callbacks#saml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'user_omniauth_callback'&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'/users/auth/saml/:identity_provider_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'omniauth_callbacks#passthru'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'user_omniauth_authorize'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’re now POSTing to the &lt;code&gt;user_omniauth_authorize&lt;/code&gt; route, and including the authenticity token. But we still need to protect this route from CSRF attacks, which we can use the &lt;code&gt;omniauth-rails_csrf_protection&lt;/code&gt; gem to achieve this. See &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2015-9284" rel="noopener noreferrer"&gt;this security vulnerability&lt;/a&gt; for more information on why this is important. Adding this to your Gemfile includes middleware that locks down your OmniAuth routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'omniauth-rails_csrf_protection'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that does it! We’ve now got a working proof of concept for multi-tenancy. The login form can scale well and support other auth approaches if needed. We’re using POST requests with authenticity tokens for OmniAuth so we’re ready for OmniAuth 2.0 and practicing good security. You can see the whole diff for this branch here - &lt;a href="https://github.com/enterprise-oss/saml-rails/pull/5" rel="noopener noreferrer"&gt;https://github.com/enterprise-oss/saml-rails/pull/5&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A couple of things stand out as issues that should be improved. First, we’re hardcoding things still. That does not scale well - we don’t want to ship a new release with hardcoded data every time we want to onboard a new customer. The obvious approach is to model the SAML configuration data and persist it in the database. Of course this starts to suggest you need a UI for CRUD on this data. That’s all a bit outside of the scope of this guide, and if you go down this path you’ll discover lots of more threads to pull on, like parsing Federated Metadata XML files for SAML config values.&lt;/p&gt;

&lt;p&gt;Another issue here is end-user documentation. We cheated by providing you docs generated by Osso, but if you go down this multi-tenancy path, you’ll need to create similar documentation, and figure out the best way to securely exchange the config values with your customer, recognizing that the software buyer is likely not the same person who has administrative access to their IDP.&lt;/p&gt;

&lt;p&gt;Or, you could not bother thinking about any of this, and use Osso to handle your SAML SSO needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production ready with Osso
&lt;/h3&gt;

&lt;p&gt;Osso provides an open source web app that you can use to implement SAML SSO in your Rails app. Osso handles multi-tenant SAML much like we just built in the previous section. But it also provides SAML configuration persistence and an intuitive UI to onboard customers, generating bespoke documentation for each customer to integrate your app in their IDP. In short, Osso solves all of the challenges that the previous libraries don't address. Osso provides the last 10% of a scalable SAML SSO integration that you'd normally need to implement yourself, while saving your engineering team time. Your app consumes Osso using an OAuth2 authorization code grant flow, and Osso provides &lt;a href="https://github.com/enterprise-oss/omniauth-osso" rel="noopener noreferrer"&gt;omniauth-osso&lt;/a&gt; to make consuming Osso in your Rails app incredibly simple.&lt;/p&gt;

&lt;p&gt;There are alternatives to Osso - Auth0 is a popular choice and works quite similarly to Osso, but doesn't provide documentation for your customers. AWS Cognito and Google Cloud Identity Platform also have support for SAML but also skimp on UI and documentation. Pricing for each of these services is also complicated, opaque and unpredictable.&lt;/p&gt;

&lt;p&gt;Osso is available as an open source application that you can &lt;a href="https://ossoapp.com/docs/deploy/overview" rel="noopener noreferrer"&gt;deploy yourself&lt;/a&gt;. You can also purchase an Osso subscription, and we'll maintain an Osso instance for you - see&lt;a href="https://ossoapp.com/pricing" rel="noopener noreferrer"&gt;Osso's pricing&lt;/a&gt;. We also offer a &lt;a href="https://demo.ossoap.com" rel="noopener noreferrer"&gt;demo instance&lt;/a&gt; which we will use in this guide. The demo instance is re-seeded hourly, but will always have a Demo Production OAuth Client and a customer configured against the Osso Mock IDP.&lt;/p&gt;

&lt;p&gt;Let's start by adding the &lt;code&gt;omniauth-osso&lt;/code&gt; and &lt;code&gt;omniauth-rails_csrf_protection&lt;/code&gt; gems - the former for interacting with an Osso instance, and the latter for the same CSRF and POST only protection for OmniAuth as we saw in the previous section. We also want to add &lt;code&gt;omniauth&lt;/code&gt; and pin it to a version before 2.0.0 until Devise supports OmniAuth 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'omniauth'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt; 2.0.0'&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'omniauth-osso'&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'omniauth-rails_csrf_protection'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Osso will handle all the SAML bits, we consume Osso using OAuth, so we'll need to configure the Osso strategy in our Devise initializer. These values are for the demo instance, but you'll want to use your own instance, and should use Rails credentials to store the client ID and secret rather than committing to git.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;omniauth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;:osso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'demo-client-id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'demo-client-secret'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;client_options: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="ss"&gt;site: &lt;/span&gt;&lt;span class="s1"&gt;'https://demo.ossoapp.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our sign in UX, let's iteratively build up a deeply integrated form. Osso supports varying levels fo integration for sign in UX, and we suggest starting by using Osso's hosted login page. If you submit a POST request to &lt;code&gt;/auth/users/osso&lt;/code&gt;, the user will be redirected to Osso where they will enter their email address to be routed to their IDP. Let's create a quick little form that will post to this endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"container"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"main-content login"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;Welcome&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&amp;gt;
      &amp;lt;% flash.each do |name, msg| %&amp;gt;
        &amp;lt;% if msg.is_a?(String) %&amp;gt;
          &amp;lt;div class="alert alert-&amp;lt;%= name == :notice ? "success" : "error" %&amp;gt;"&amp;gt;
            &amp;lt;%=raw content_tag :div, msg, id:"flash_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;" %&amp;gt;
          &amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= form_with(url: '/users/auth/osso', method: 'post', class: 'login-form') do |form| %&amp;gt;
        &amp;lt;button type=&lt;/span&gt;&lt;span class="s1"&gt;'submit'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;Login&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="no"&gt;SAML&lt;/span&gt; &lt;span class="no"&gt;SSO&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;
      &amp;lt;% end %&amp;gt;
    &amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this in place, we can successfully log in again using &lt;code&gt;user@example.com&lt;/code&gt;. The demo instance is configured for the &lt;code&gt;example.com&lt;/code&gt; domain to use the Mock IDP, and the Mock IDP takes any password, so you should be able to test this easily.&lt;/p&gt;

&lt;p&gt;What about a second tenant? If you're following the guide and using the demo instance, you'll be able to onboard another tenant as a customer, but since the data here gets reset, it won't be available beyond your initial testing. The Osso UI should be intuitive, but you can review our &lt;a href="https://ossoapp.com/docs/user-guide/onboarding-customers" rel="noopener noreferrer"&gt;Onboarding Customers&lt;/a&gt; guide. You can generate docs for yourself for whatever IDP you might have access to. Okta and OneLogin both offer developer accounts, and Google workspaces also allow for SAML apps. You can even add multiple IDPs for your second tenant - Osso will ask the user which service they use to sign in.&lt;/p&gt;

&lt;p&gt;If you have a few SAML customers you might stop here - there's sure to be other things you need to work on that are more central to your product, and we are absolutely supporting multi-tenant SAML at this point. But what if we wanted to improve the sign in UX? The Osso hosted login page isn't branded, so it would be best if our users didn't have to hit that.&lt;/p&gt;

&lt;p&gt;In our previous section we integrated SAML login into our email / password flow. Some companies take this approach, while others provide an entirely separate login form for SAML SSO. Let's go with the latter for now, where we nonetheless ask the user to enter their email address by adding an email input to our login form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"container"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"main-content login"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;Welcome&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&amp;gt;
      &amp;lt;% flash.each do |name, msg| %&amp;gt;
        &amp;lt;% if msg.is_a?(String) %&amp;gt;
          &amp;lt;div class="alert alert-&amp;lt;%= name == :notice ? "success" : "error" %&amp;gt;"&amp;gt;
            &amp;lt;%=raw content_tag :div, msg, id:"flash_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;" %&amp;gt;
          &amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;% end &lt;/span&gt;&lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= form_with(url: '/users/auth/osso', method: 'post', class: 'login-form') do |form| %&amp;gt;
        &amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Email"&lt;/span&gt; &lt;span class="o"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= form.email_field :email %&amp;gt;
        &amp;lt;button type=&lt;/span&gt;&lt;span class="s1"&gt;'submit'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="no"&gt;Login&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="no"&gt;SAML&lt;/span&gt; &lt;span class="no"&gt;SSO&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;
      &amp;lt;% end %&amp;gt;
    &amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can log in again using an &lt;code&gt;example.com&lt;/code&gt; email address, and we'll skip over Osso's hosted login page. The downside of this approach is that Osso will display an error if there is not a SAML configured customer for the supplied domain. &lt;/p&gt;

&lt;p&gt;To best serve our SAML SSO users, we'd only send users who belong to an onboarded SAML customer to Osso, and we wouldn't require a user to remember that they use SAML SSO to sign in. Unfortunately this issue has become &lt;em&gt;incredibly fraught&lt;/em&gt;. Non-SAML users &lt;em&gt;hate&lt;/em&gt; when you split a login form into two steps, and they usually have no idea why anyone would implement such a poor UX. There's no perfect solution if we want to serve all of our users well, just the least bad solution. We'll do our best to make our login flow frustrate the least number of users — engineering is, after all, about tradeoffs.  &lt;/p&gt;

&lt;p&gt;We should also aim to reduce the amount of code you need to write to integrate Osso - you shouldn't have to persist in your database whether one of your tenants uses SAML. A user should just be able to come to your login page and login, whatever mechanism their account is set up to use. Yes, we want to save you effort, but we also want to keep things simple, direct and easy to reason about.&lt;/p&gt;

&lt;p&gt;Osso also offers a &lt;a href="https://github.com/enterprise-oss/osso-react" rel="noopener noreferrer"&gt;React library&lt;/a&gt; that provides components like an &lt;code&gt;&amp;lt;OssoLogin /&amp;gt;&lt;/code&gt; form as well as lower level hooks for interacting with your Osso instance. Rather than you persisting data about which of your customers use SAML for auth, you can use the Osso React library to talk directly to Osso in order to build out your login form. Of course if you're not already using React on your front end it won't make a ton of sense to take this approach, but the Osso React source code should help you understand how you can implement such a form yourself.&lt;/p&gt;

&lt;p&gt;We'll skip past a bit of Rails and React setup - the result PR will show everything you need to do to get React set up with webpacker, but let's assume that we've got a JSX file we can treat as a Javascript pack that includes React and renders the React app to the DOM. We'll reuse the markup from previous steps to start building our login form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ReactDOM&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;container&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main-content login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Welcome&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&amp;gt;   &lt;/span&gt;&lt;span class="err"&gt; 
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can install Osso's React library from npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;yarn add @enterprise-oss/osso
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we need to wrap any of our Osso components in an &lt;code&gt;&amp;lt;OssoProvider /&amp;gt;&lt;/code&gt;, passing a &lt;code&gt;baseUrl&lt;/code&gt; to the client options. We're still using the Osso Demo instance here, which is an anything goes environment. If you're using your own instance, you'll need to set the &lt;code&gt;CORS_ORIGINS&lt;/code&gt; ENV var to the origin where you're using this form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OssoLogin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OssoProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@enterprise-oss/osso&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OssoProvider&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
      &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://demo.ossoapp.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;container&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main-content login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Welcome&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/OssoProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can add the &lt;code&gt;&amp;lt;OssoLogin /&amp;gt;&lt;/code&gt; component - we'll review the props afterwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OssoProvider&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
      &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://demo.ossoapp.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;container&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main-content login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Welcome&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OssoLogin&lt;/span&gt;
          &lt;span class="nx"&gt;ButtonComponent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;InputComponent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;containerClass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login-form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;onSamlFound&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;submitSaml&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;onSubmitPassword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSubmitPassword&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/OssoProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OssoLogin component displays an email input and submit button, and on submit, the component talks directly to your Osso instance to determine if that user can sign in using SAML via Osso. Osso's components are typically "headless" - they allow you to provide your own UI components to match the rest of your application, and surround them with some logic. Check out CodeSandboxes for &lt;a href="https://codesandbox.io/s/github/enterprise-oss/osso-react/tree/main/examples/ant" rel="noopener noreferrer"&gt;Ant Design&lt;/a&gt; and &lt;a href="https://codesandbox.io/s/github/enterprise-oss/osso-react/tree/main/examples/mui" rel="noopener noreferrer"&gt;Material UI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For our purposes, let's create our own basic components to use here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="nx"&gt;htmlFor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/label&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; 
        &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// Osso expects a value in change handlers rather than events&lt;/span&gt;
        &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; 
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event handler props are what makes the form work with your back end authentication. When we do find that a user can log in with SAML via Osso, the login component calls the &lt;code&gt;onSamlFound&lt;/code&gt; prop function. We don't want to send users to Osso right away - we need to send them through your back end to protect against CSRF attacks and to properly use our OmniAuth strategy. &lt;/p&gt;

&lt;p&gt;So we'll use a similar function to what we've used previously for submitting the SAML sign in form. We need to again grab the authenticity token, and submit a POST request from the browser via a form submit, including the email address for the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitSaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;meta[name="csrf-token"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="nx"&gt;tokenInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;tokenInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authenticity_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="nx"&gt;emailInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;emailInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`/users/auth/osso`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any onboarded SAML user is now able to sign in, but we also need to handle non-SAML users. The login component will display a password field, and on a form submit with an email and password, will call the &lt;code&gt;onSubmitPassword&lt;/code&gt; function. Without knowing more about your authentication approach, we can't really say how to handle these - you may want to do something similar to the SAML handler if you use session based authentication, or post an ajax request if you use something like JWTs. We'll just show you the function signature and log the values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onSubmitPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Submit a request to sign the user in 
      to your server. Email: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, Password: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final diff for integrating Osso into our Rails app &lt;a href="https://github.com/enterprise-oss/saml-rails/pull/6" rel="noopener noreferrer"&gt;can be found here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;If you need to add SAML SSO to your Rails application you should now be able to make an informed decision about how you want to approach this project. If you're working on a multi-tenant application, we've seen that the challenges of releasing a production-ready integration are due to the &lt;em&gt;instance based&lt;/em&gt; nature of SAML SSO. &lt;/p&gt;

&lt;p&gt;Existing OSS handles a lot of the nitty-gritty of SAML, but without Osso, you're responsible for building a scalable system to onboard new customers. We feel pretty strongly that you need to document this process for your customers, and unless you plan on having an engineer add database rows by hand for each new tenant, you'll also need a UI and CRUD operations for your SAML configurations. You'll need to work through edge cases for various Identity Providers and you'll need to sign up for a few of them yourself to have confidence that you've properly integrated that provider. You'll also need to think through how to approach sign in UX for your SAML users, while still providing a good UX for your non-SAML users.&lt;/p&gt;

&lt;p&gt;We hope you'll consider using Osso so we can handle all of these challenges while you focus on features that are more core to your application. We think you'll find &lt;a href="https://ossoapp.com/pricing" rel="noopener noreferrer"&gt;our pricing&lt;/a&gt; fair, transparent and predictable, and we're also more than happy to help you get going with an open source deployment.&lt;/p&gt;

&lt;p&gt;You can reach us via chat, at &lt;a href="mailto:hello@ossoapp.com"&gt;&lt;/a&gt;&lt;a href="mailto:hello@ossoapp.com"&gt;hello@ossoapp.com&lt;/a&gt;, or me personally on Twitter &lt;a href="https://twitter.com/sammybauch" rel="noopener noreferrer"&gt;@sammybauch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>webdev</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>SAML vs. OAuth</title>
      <dc:creator>Sam Bauch</dc:creator>
      <pubDate>Tue, 19 Jan 2021 17:36:48 +0000</pubDate>
      <link>https://forem.com/sammybauch/saml-vs-oauth-4cfo</link>
      <guid>https://forem.com/sammybauch/saml-vs-oauth-4cfo</guid>
      <description>&lt;h3&gt;
  
  
  An Engineer’s Guide to Enterprise-grade Single Sign-on
&lt;/h3&gt;

&lt;p&gt;OAuth and &lt;a href="https://ossoapp.com/blog/what-is-saml/" rel="noopener noreferrer"&gt;SAML&lt;/a&gt; are both open specifications for exchanging access credentials for a specific user between an identity provider and an application. When a user wants to sign in to an app using either SAML or OAuth, they are sent to a third party where the user must already be registered. They sign in to this third party, and get sent back to the application. The mechanisms differ, but both SAML and OAuth involve using secrets to securely exchange information about the user in order for the application to begin an authenticated session for the user.&lt;/p&gt;

&lt;p&gt;If you’re approaching these technologies as an engineer, you can consider OAuth to be &lt;em&gt;class based&lt;/em&gt; while SAML is &lt;em&gt;instance based&lt;/em&gt;. With OAuth, you configure your application with a provider like GitHub, and any GitHub user can now sign in to your app. But with SAML, you must configure your application with an Identity Provider (IDP) &lt;em&gt;instance&lt;/em&gt;. Each customer who wants to use SAML will have an instance with their IDP, and you and the customer must perform a configuration between your app and their IDP instance. &lt;/p&gt;

&lt;p&gt;While this might seem like a small distinction for SAML vs. OAuth, as you pull on the instance thread, you begin to understand implementing SAML to be a much bigger project than implementing OAuth - it needs CRUD and persistence, a UI to configure instances, and documentation for your customers and teammates, and you’ll need to adjust how your sign-in form works. These knock on effects are way more important to think about than the nitty gritty details of SAML — like most things in modern web dev, really strong libraries exist for the nitty gritty, but they only get you 90% of the way there when building a production-ready integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  An abridged and personal history of OAuth
&lt;/h3&gt;

&lt;p&gt;I started programming for the web right in the midst of the “social web 2.0” era and OAuth was &lt;em&gt;everywhere&lt;/em&gt;. It was the heyday of hackathons, a more fun and flippant wacky Internet, where OAuth both helped you get up and running quicker, bypassing password based auth, and also gave you a wealth of users’ data to mix and match and create experiences with - Facebook friends, Foursquare checkins and Tweets…at least when the fail whale was kept at bay.&lt;/p&gt;

&lt;p&gt;OAuth was convenient for the average web user. It wasn’t necessarily designed as a way to authenticate your users, but folks started using it that way and never really looked back. Instead of remembering passwords for hundreds of websites, the average Internet user was happy to hand over keys to their Facebook account for the convenience of signing in with Facebook. Providers, consumer engineers and users eventually got smarter with scopes, limiting what an app could do on your behalf in the account you had OAuthed.&lt;/p&gt;

&lt;h5&gt;
  
  
  So much OAuth
&lt;/h5&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F71mfmvo86mx21q5eft60.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F71mfmvo86mx21q5eft60.jpg" alt="OAuth social login buttons"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OAuth remains a popular authentication mechanism today, for both consumer and business applications. It’s incredibly easy to implement - languages and frameworks often have standardized approaches or libraries that make this easier. In Ruby, there’s &lt;a href="https://github.com/omniauth/omniauth" rel="noopener noreferrer"&gt;omniauth&lt;/a&gt;, “a library that standardizes multi-provider authentication for web applications”. Any developer can create an omniauth “strategy” for a specific web service. &lt;a href="http://www.passportjs.org/" rel="noopener noreferrer"&gt;Passport&lt;/a&gt; is a similar approach for NodeJS apps.&lt;/p&gt;

&lt;p&gt;You’ll typically register your application on the web service’s developer portal, where you’ll get a Client ID and Client Secret, specify redirect URIs and choose the scopes you want to use. Add these secrets to your application, drop in an OAuth library, and you’re done in less than an hour. OAuth is definitely convenient for users and engineers, but when it comes to large or security-focused enterprises, SAML is the best choice for them and their employees (even if it’s a headache to implement).&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping CISOs up at night
&lt;/h3&gt;

&lt;p&gt;A Chief Information Security Officer is the person ultimately responsible for an organization's information and data security. A good way to shirk this responsibility would be to allow your employees to sign up for critical apps and services using a password or through OAuth against an identity provider that the enterprise doesn’t control. Consider an employee who is terminated, but signed in to all of their services using email and password or their personal Twitter account. &lt;/p&gt;

&lt;p&gt;The IT department would need to ensure, right at the time of termination, that the employee’s access to services is cut off. They would need to go into the account settings for every service and remove that user. At enterprise scale, tens or hundreds of employees might leave in a given week. Managing account access would be a never-ending nightmare for the IT department.&lt;/p&gt;

&lt;h3&gt;
  
  
  SAML locks it down
&lt;/h3&gt;

&lt;p&gt;Once again the SAML part is not really what matters here. Many IDPs support other technologies for exchanging credentials, including OAuth. But SAML is the standard that enterprises have adopted and is supported by every enterprise-grade Identity Provider. &lt;/p&gt;

&lt;p&gt;The important aspect of enterprise-grade Identity Providers is that they centralize user access to applications. When an employee is terminated, the IT department can go to just one place where they remove the user, and the user immediately loses access to all of the services they used at work. SAML stands for “Secure Assertion Markup Language”, but in common usage, SAML is an authentication mechanism where an enterprise can easily shut off access. When an enterprise requires that you offer SAML authentication, it’s not because they love old clunky technologies or want to annoy you — they simply need to be able to manage employee access to services in a single place.&lt;/p&gt;

&lt;h5&gt;
  
  
  SAML Flow Diagram
&lt;/h5&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fc7d39uoaurz8nop9pi8l.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fc7d39uoaurz8nop9pi8l.gif" alt="SAML flow diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  SAML slows you down
&lt;/h3&gt;

&lt;p&gt;If you need to add SAML, that’s a great problem for your company to have! It means you’re moving up market and selling to bigger companies with a larger deal size and who tend to be a little stickier. If you’re thinking about adding SAML, you probably have 1,000 other things on your plate, from scaling concerns, to features you’ve wanted to add, and tech debt to address. But adding SAML might be the biggest single thing you could do to increase your company’s revenue, unlocking deals with bigger companies.&lt;/p&gt;

&lt;p&gt;The problem is that it takes &lt;em&gt;a lot more work&lt;/em&gt; to implement SAML vs. OAuth. It’s not exactly difficult work - you won’t need to write any algorithms, and lots of open source libraries exist for dealing with the actual SAML responses you receive from an IDP. But returning to our &lt;em&gt;class&lt;/em&gt; vs. &lt;em&gt;instance&lt;/em&gt; based distinction, lets review some tasks you’ll need to complete in order to ship a production-ready SAML integration.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Configuration UI and CRUD
&lt;/h4&gt;

&lt;p&gt;Every SAML-needing customer will need to be onboarded, where a teammate will provide some information to your customer, like an Assertion Consumer Service URL and an Entity ID. These are part of the SAML spec, and going into detail about their functions is beyond the scope of this post, but know that your application will need to generate these values, unique to each customer. Once your customer configures your app in their IDP, they’ll return some data to you, often in the form of a metadata XML file. Will you parse this for the keys and SSO URL you need to persist? Or have your teammates open the XML and copy out the data? Either way you’ll need to persist it all in a database table, and have a reliable way to look it up when a user wants to log in.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Documentation
&lt;/h4&gt;

&lt;p&gt;You can’t assume that your customer will know how to set up a SAML app in their IDP instance, so you’ll need to provide some instructions. That typically means signing up for all of the common IDPs, working your way through their documentation and configuring a test app, QAing with your codebase, and ultimately creating some sort of documentation to help your customers. Skip this at your peril - this will be the first thing your valuable enterprise customer experiences after signing a deal. &lt;/p&gt;

&lt;h4&gt;
  
  
  3. Sign-in UX
&lt;/h4&gt;

&lt;p&gt;Since SAML authentication is instance based, you need to route users to the right IDP via the SSO URL your customer returned to you. Unlike OAuth, you can’t just stick a Sign in With Okta button on your login page and be done. The two common ways of approaching this are to either split your login form into two steps, first ascertaining email and sending to the IDP if the user belongs to an enterprise SAML account, or to create a second SAML SSO login form that only collects email or company domain.&lt;/p&gt;

&lt;p&gt;That’s a lot of work! If you’re reading this, I think we can probably assume that your teammates in support don’t know what SAML is, so you’ll probably need some internal docs and training too. And we haven’t even gotten into the SAML spec itself! Attribute mapping, Single Log Out, there’s still more to think about here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Osso lets you treat SAML as OAuth
&lt;/h3&gt;

&lt;p&gt;You’ve probably implemented OAuth at some point in your career, you may even have it in your current stack already for things like signing in with Google. It’s simple to set up - register your client, allow-list some redirect URIs and grab a Client ID and Secret. SAML on the other hand, while conceptually similar, requires configuration per customer instance, which makes a SAML integration a bit heavier of a lift. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F6ftfpj7vm8i49ghageai.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F6ftfpj7vm8i49ghageai.png" alt="Osso Admin UI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Osso allows you to sign SAML users in using an OAuth flow - it's SAML as OAuth instead of SAML vs. OAuth. Osso handles everything SAML - our Admin UI is easy for non-technical teammates to use in order to onboard customers and generates custom documentation for your customer’s Identity Provider. When your customer returns an XML metadata file to you, your teammates can easily upload it, where the required values get parsed out and persisted. Osso provides libraries for omniauth and Passport, making your integration even easier. You can even keep your sign in UX the same - Osso offers a hosted login page so you can just add a “Sign in with SSO” button or link (though we do recommend eventually deepening your integration by sending a user to Osso with a domain or email for IDP routing). &lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>security</category>
    </item>
  </channel>
</rss>
