<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://freelancing-gods.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://freelancing-gods.com/" rel="alternate" type="text/html" /><updated>2026-04-08T10:27:29+00:00</updated><id>https://freelancing-gods.com/feed.xml</id><title type="html">Freelancing Gods</title><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><entry><title type="html">MICF 2026 Recommendations</title><link href="https://freelancing-gods.com/2026/03/02/micf-2026.html" rel="alternate" type="text/html" title="MICF 2026 Recommendations" /><published>2026-03-02T00:00:00+00:00</published><updated>2026-03-02T00:00:00+00:00</updated><id>https://freelancing-gods.com/2026/03/02/micf-2026</id><content type="html" xml:base="https://freelancing-gods.com/2026/03/02/micf-2026.html"><![CDATA[<p>In a few weeks the Melbourne International Comedy Festival kicks off for another year. I’ve had a thorough look through the program, and there are so many superb shows to pick from - and that’s just based on the acts I know!</p>

<p>There are four shows I can strongly recommend directly, because I’ve seen these before at past festivals:</p>

<ul>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/classic-penguins/">Garry Starr’s Classic Penguins</a> won the best show of last year’s festival, and deservedly so. Only go if you’re comfortable with plenty of nudity though!</li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/josie-long/">Josie Long’s Now is the Time of Monsters</a> was one of my favourites from Edinburgh last year, with laughs and sharp politics.</li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/god-s-favourite/">Scout Boxall’s God’s Favourite</a> was a standout from last year’s festival, smart, dark, and weird in the best way.</li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/alice-fraser">Alice Fraser’s A Passion for Passion</a> was another favourite from Edinburgh - a sharply clever interrogation of romance novels and society (worth seeing even if the topic may not immediately take your fancy).</li>
</ul>

<p>And then, there’s my extended list of other acts who are consistently excellent:</p>

<ul>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/ali-mcgregor-s-late-night-variety-nite-night/">Ali McGregor</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/cassie-workman">Cassie Workman</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/gift-horse/">Celia Pacquola</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/chloe-petts/">Chloe Petts</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/daniel-kitson-work-in-progress/">Daniel Kitson</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/elf-lyons-swan">Elf Lyons</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/i-wish-i-could-come-out-of-my-shell/">Felicity Ward</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/the-evening-muse/">Hannah Gadsby</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/laura-davis-swag">Laura Davis</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/mark-watson/">Mark Watson</a></li>
  <li><a href="https://www.comedyfestival.com.au/browse-shows/sarah-keyworth/">Sarah Keyworth</a></li>
</ul>

<p>As always: if you come across any great shows, <a href="http://hachyderm.io/@pat">do let me know</a>!</p>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="comedy" /><category term="micf" /><category term="melbourne" /><category term="2026" /><summary type="html"><![CDATA[Some recommendations for the 2026 edition of the Melbourne International Comedy Festival, as per the almost-yearly tradition.]]></summary></entry><entry><title type="html">AI and moral injury</title><link href="https://freelancing-gods.com/2026/02/14/ai-and-moral-injury.html" rel="alternate" type="text/html" title="AI and moral injury" /><published>2026-02-14T00:00:00+00:00</published><updated>2026-02-14T00:00:00+00:00</updated><id>https://freelancing-gods.com/2026/02/14/ai-and-moral-injury</id><content type="html" xml:base="https://freelancing-gods.com/2026/02/14/ai-and-moral-injury.html"><![CDATA[<p>As generative AI and LLMs seem to take over the world, and as someone who has <a href="/2026/02/13/ai-is-bad">significant ethical concerns about these technologies</a>, I’ve been feeling quite despondent lately. A colleague with similar values accurately described it as “existential depression” - and then earlier this week I read <a href="https://jembendell.com/2026/01/24/dont-forget-the-dread-deeper-healing-in-the-metacrisis/">this excellent article by Krisztina Csapó</a>, which provided the concept of moral injury:</p>

<blockquote>
  <p>the deep existential wound that arises from witnessing, participating in, or feeling complicit within systems that cause profound harm while betraying core values.</p>
</blockquote>

<p>Yup. 100%.</p>

<p>Because of course, these emotions that I’m grappling with aren’t just about AI - so much of our world is struggling right now, including from climate change, fascism, and genocide - and it all feels intertwined. AI is just one of the more prominent components occupying my brain at the moment (which is, almost certainly, a very privileged position to be in).</p>

<p>Also, as Csapó discusses in their post: when looking at the world, grief is a totally rational response. I think some of what I’m grappling with is grief for both the world that was, but also at a smaller level, at what my industry, my working life was. Part of that is because AI is everywhere right now, and especially within the tech industry. It’s an invasive weed, a virus in a largely-unvaccinated world. Even in the moments where I ponder changing careers - where would I go that isn’t already overrun by this virus?</p>

<hr />

<p>I wouldn’t blame the broader world for feeling a touch of schadenfreude towards the tech industry at the moment. Tech workers in the Global North - particularly those who lean into the Silicon Valley/venture capital mindset - are responsible for a great deal of tenuous working conditions (often known as ‘disruption’ 🙄) for so many others. And now, with mass layoffs in the tech industry, perhaps some are feeling it’s what is deserved?</p>

<p>That’s not true of course - <em>no one</em> deserves to be put in positions of financial or emotional distress, to be pushed into homelessness when a corporation decides you’re surplus to its requirements because an AI can do the work instead.</p>

<p>But for those of us who are now feeling shaken by the challenging job market and working conditions: can we take this industry hardship as a nudge to get over our exceptionalism? To embrace a class consciousness that’s long been missing in tech spaces?</p>

<hr />

<p>In Henry Desroches’ recent (excellent) essay <a href="https://henry.codes/writing/a-website-to-destroy-all-websites/">A Website to Destroy All Websites</a>, he highlights Ivan Illich’s concept of <strong>radical monopoly</strong>:</p>

<blockquote>
  <p>that point where a technological tool is so dominant that people are excluded from society unless they become its users.</p>

  <p>[…]</p>

  <p>We can map fairly directly most technological developments in the last 100 (or even 200) years to this framework: a net lift, followed by a push to extract value and subsequent insistence upon the technology’s ubiquity.</p>
</blockquote>

<p>The automobile and the Internet are both offered as examples - and while Desroches doesn’t call it out, my brain latched onto AI as another technology that is arguably following that trend, and at an accelerated rate.</p>

<p>And this eager adoption (and insistence on use) is happening despite the fact that LLMs are tools built <em>through</em> exploitation (of people, of the environment, of our digital commons), and are used <em>for further</em> exploitation.</p>

<blockquote>
  <p>Something seems to be deeply amiss in what we imagine our tools are for. […] I’ve watched as new technologies - particularly the most novel and ‘intelligent’ ones - are used to undermine and usurp human joy, security and even life itself. (<a href="https://www.jamesbridle.com/books/ways-of-being">Ways of Being - James Bridle</a>)</p>
</blockquote>

<p>There is a dream that is often offered to the altar of AI: that we can work less, for the same effort and pay, and have more spare time for actually living our lives. Perhaps this happens for some people, but the vastly more common situation from all of these AI-driven workplace changes seems to actually be: you will work <em>more</em> rather than less (if you’re lucky to have a job), and you will build more wealth and power for billionaires.</p>

<p>Of course, we live in a capitalist system - my idealism had me forgetting that this trend is nothing new. As Talking Heads have been singing for several decades: <a href="https://song.link/i/124921347">same as it ever was</a>.</p>

<hr />

<p>My friend <a href="https://mastodon.social/@paulca">Paul Campbell</a> mused a while ago on Mastodon that AI has strong parallels to plastic - they’re both incredibly convenient, and both incredibly environmentally destructive.</p>

<p>This analogy feels pretty appropriate to me, though I think it can be taken further - perhaps LLMs are the cognitive equivalent to microplastics. We’re still learning of the full, negative effects on our health courtesy of the latter - the same could be said for the impact of AI and LLMs on our memory and learning skills.</p>

<hr />

<p>I find myself saddened by how so many of my friends and peers have leant into using (and loudly promoting) AI/LLMs. Perhaps that sadness is stronger than I should be? I recognise that we all have our own boundaries, compromises and challenges to work through - as the saying goes, there’s no ethical consumption under capitalism - and we are all imperfect human beings. I am definitely no exception to that rule. And I can’t blame people for wanting shiny new technologies, for wanting things to be easier.</p>

<p>This sadness has latched onto me more tightly than others though…</p>

<p>Other systems of exploitation in our lives - such as those involving fossil fuels, or food supply chains that torture animals - these have existed for decades, if not centuries. But LLMs aren’t a long-standing technology. We’re talking a handful of years of mainstream use, maybe a decade at most - and the flaws of these technologies and the companies behind them have been clear for just as long. It’s a question that my friend Jan Lehnardt has <a href="https://writing.jan.io/2026/02/02/ai-veganism.html">considered as well</a> - we’re getting in on the ground floor here, we don’t have generational baggage and long-standing societal bad habits. And yet, despite <a href="/2026/02/13/ai-is-bad">the widely documented problems</a>, we are embracing LLMs so enthusiastically?</p>

<p>Ah, but capitalism is comfortable for the privileged if you ignore the exploitation (and for those without privilege, it’s just business as usual). I shouldn’t be surprised. Still, <a href="https://www.garfieldtech.com/blog/selfish-ai">Larry Garfield’s post</a> on hearing the constant refrain of “it is what it is” rings true - apathy from any one person is heartbreaking. To rub salt in the wound, this compounding, collective apathy erodes our individual agency.</p>

<hr />

<p>Where do we go from here? Look, I’m not sure why you were expecting a random blog post to have any meaningful answers. I’m not writing this to absolve people, nor to make peace with the situation.</p>

<p>Part of me wants to be more understanding about those who choose to use LLMs. We’ve all got to pick and choose the battles we can take on, and if this isn’t one you can grapple with right now, that’s life, and I get it. Another part of me wants to maintain the rage, particularly at anyone who’s happily cheering for a future dominated by AI and LLMs. If you talk to me about such matters, I can’t promise that I’ll be patient enough to take the ‘high’ road.</p>

<p>For me - writing this out, connecting some dots, understanding my feelings a little more - it’s provided a good reminder that AI/LLMs aren’t the root cause here. Instead, we’re facing two long-standing, entangled systems - capitalism and colonialism - that reliably offer a carrot (convenience) and stick (exploitation) approach to many of us in the Global North, with the promise that ignorance is bliss.</p>

<p>If you’re still here and looking for ideas, here’s what I plan to do that pushes back against those systems (and if this resonates with you too, that’s just a nice bonus): express gratitude more often, appreciate art and support artists, show up for my friends and family, connect meaningfully with my colleagues, contribute mutual aid for those in need, and stand in solidarity across intersections.</p>

<p>More directly on that last point: Love and support for trans folks. <a href="https://bwtribal.com/blogs/news/why-blak-the-history-behind-the-spelling">Blak</a> &amp; Black lives matter. Free Palestine. Fuck fascists.</p>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="ai" /><category term="technology" /><category term="politics" /><category term="world" /><summary type="html"><![CDATA[As generative AI and LLMs seem to take over the world, and as someone who has significant ethical concerns about these technologies, I've been feeling quite despondent lately.]]></summary></entry><entry><title type="html">AI is Bad</title><link href="https://freelancing-gods.com/2026/02/13/ai-is-bad.html" rel="alternate" type="text/html" title="AI is Bad" /><published>2026-02-13T00:00:00+00:00</published><updated>2026-02-13T00:00:00+00:00</updated><id>https://freelancing-gods.com/2026/02/13/ai-is-bad</id><content type="html" xml:base="https://freelancing-gods.com/2026/02/13/ai-is-bad.html"><![CDATA[<p>A <em>vastly</em> incomplete list of many significant issues with artificial intelligence (AI) - particularly generative AI and/or large language models (LLMs), mostly so I have a single link to share when people enquire why I find AI so unethical.</p>

<p>This is inspired by a blog post I once saw (which was much longer and better, but I didn’t keep the link and have been unable to find it again). If you think you know the post I’m talking about, please send it my way.</p>

<ul>
  <li>The considerable amounts of energy required. <a href="https://www.abc.net.au/news/2025-10-28/google-microsoft-restarting-nuclear-plants-for-ai-power/105941378">[1]</a> <a href="https://www.independent.org/article/2025/05/08/ai-power-plants-gas-coal/">[2]</a> <a href="https://www.levernews.com/biden-boosts-ai-despite-energy-dept-warning/">[3]</a> <a href="https://arstechnica.com/tech-policy/2024/09/openai-asked-us-to-approve-energy-guzzling-5gw-data-centers-report-says/">[4]</a> <a href="https://www.teenvogue.com/story/chatgpt-is-everywhere-environmental-costs-oped">[5]</a></li>
  <li>The considerable amounts of water required. <a href="https://san.com/cc/ai-tools-consume-up-to-4-times-more-water-than-estimated/">[1]</a> <a href="https://www.bbc.com/news/articles/cy8gy7lv448o">[2]</a></li>
  <li>The poor working conditions and trauma imposed on Global South workers. <a href="https://pivot-to-ai.com/2024/11/30/meet-the-underpaid-workers-in-nairobi-kenya-who-power-openai/">[1]</a> <a href="https://www.wired.com/story/millions-of-workers-are-training-ai-models-for-pennies/">[2]</a> <a href="https://futurism.com/artificial-intelligence/ai-industry-traumatizing-contractors">[3]</a></li>
  <li>The exploitation of workers generally. <a href="https://cwa-union.org/ghost-workers-ai-machine">[1]</a> <a href="https://www.noemamag.com/the-exploited-labor-behind-artificial-intelligence/">[2]</a></li>
  <li>The issues with consent and abuse. <a href="https://www.404media.co/deepfake-harassment-ohio-undress-clothoff-nudify-apps/">[1]</a> <a href="https://www.reuters.com/world/us/fbi-says-artificial-intelligence-being-used-sextortion-harassment-2023-06-07/">[2]</a></li>
  <li>The mass layoffs because the work can supposedly be done by AI instead. <a href="https://www.theverge.com/news/688679/amazon-ceo-andy-jassy-ai-efficiency">[1]</a></li>
  <li>The use of AI to increase the extraction of fossil fuels. <a href="https://globalwitness.org/en/campaigns/fossil-fuels/the-digital-drill-how-big-oil-is-using-ai-to-speed-up-fossil-fuel-extraction/">[1]</a> <a href="https://www.bloodinthemachine.com/p/ai-is-revitalizing-the-fossil-fuels">[2]</a></li>
  <li>The use of AI by genocidal governments/militaries. <a href="https://www.middleeasteye.net/opinion/israel-use-ai-gaza-terrifying-model-coming-country-near-you">[1]</a></li>
  <li>The theft of copyrighted material for training. <a href="https://jskfellows.stanford.edu/theft-is-not-fair-use-474e11f0d063">[1]</a></li>
  <li>The overloading of public-good infrastructure. <a href="https://adactio.com/journal/21831">[1]</a> <a href="https://michiel.buddingh.eu/enclosure-feedback-loop">[2]</a></li>
  <li>The supercharging of surveillance. <a href="https://www.aclu.org/news/privacy-technology/machine-surveillance-is-being-super-charged-by-large-ai-models">[1]</a></li>
  <li>The impact on education systems. <a href="https://www.currentaffairs.org/news/ai-is-destroying-the-university-and-learning-itself">[1]</a></li>
  <li>The reinforcing of fascism, racism, transphobia. <a href="https://www.npr.org/2025/07/09/nx-s1-5462609/grok-elon-musk-antisemitic-racist-content">[1]</a> <a href="https://newsocialist.org.uk/transmissions/ai-the-new-aesthetics-of-fascism/">[2]</a></li>
  <li>The enabling of disinformation and propaganda. <a href="https://www.technologyreview.com/2023/10/04/1080801/generative-ai-boosting-disinformation-and-propaganda-freedom-house/">[1]</a></li>
  <li>The impact of hallucinations, especially in medical contexts. <a href="https://apnews.com/article/ai-artificial-intelligence-health-business-90020cdf5fa16c79ca2e5b6c4c9bbb14">[1]</a> <a href="https://mastodon.social/@Thayer/116002682012736788">[2]</a></li>
  <li>The cognitive harms. <a href="https://www.bloomberg.com/news/articles/2025-08-12/ai-eroded-doctors-ability-to-spot-cancer-within-months-in-study?embedded-checkout=true">[1]</a></li>
</ul>

<p>See also:</p>

<ul>
  <li><a href="https://www.miriamsuzanne.com/2025/02/12/tech-ai-wtf/">Tech continues to be political - Miriam Eric Suzanne</a></li>
  <li><a href="https://anthonymoser.github.io/writing/ai/haterdom/2025/08/26/i-am-an-ai-hater.html">I Am An AI Hater - Anthony Moser</a></li>
  <li><a href="https://www.garfieldtech.com/blog/selfish-ai">Selfish AI - Larry Garfield</a></li>
  <li><a href="https://thecon.ai">The AI Con by Emily M. Bender and Alex Hanna</a></li>
  <li><a href="https://karendhao.com">Empire of AI by Karen Hao</a></li>
</ul>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="ai" /><category term="technology" /><category term="politics" /><category term="world" /><summary type="html"><![CDATA[A vastly incomplete list of the many significant issues with artificial intelligence.]]></summary></entry><entry><title type="html">SAML and Ruby: The Collection</title><link href="https://freelancing-gods.com/2025/05/11/saml-ruby-collection.html" rel="alternate" type="text/html" title="SAML and Ruby: The Collection" /><published>2025-05-11T00:00:00+00:00</published><updated>2025-05-11T00:00:00+00:00</updated><id>https://freelancing-gods.com/2025/05/11/saml-ruby-collection</id><content type="html" xml:base="https://freelancing-gods.com/2025/05/11/saml-ruby-collection.html"><![CDATA[<p>Over the past year, my colleagues and I at <a href="https://www.covidence.org">Covidence</a> have been rolling out SSO support for our Rails application - using SAML in particular. This has prompted a great deal of learning, as well as finding some neat solutions to a few challenges - so I’ve written up a handful of posts to share both some general concepts, and some of our solutions.</p>

<ul>
  <li><a href="/2025/05/05/saml-ruby-terminology.html">Terminology</a></li>
  <li><a href="/2025/05/06/saml-ruby-service-provider.html">Building a service provider</a></li>
  <li><a href="/2025/05/07/saml-ruby-federations.html">Parsing SAML federation data</a></li>
  <li><a href="/2025/05/08/saml-ruby-bridging.html">Testing with ephemeral (PR) apps</a></li>
  <li><a href="/2025/05/09/saml-ruby-automated-tests.html">Request and feature tests</a></li>
  <li><a href="/2025/05/10/saml-ruby-development-idp.html">A local IdP for development environments</a></li>
</ul>

<p>A lot of the content from these posts were first shared as a talk at the Melbourne Ruby Meet in October 2024 (and then again at Ruby Retreat NZ in May 2025). If taking in information via video is preferred, <a href="https://www.youtube.com/watch?v=MeQXR8ojX5c">you can watch that here</a>:</p>

<iframe width="560" height="315" src="https://www.youtube.com/embed/MeQXR8ojX5c?si=FNKy3Ly8zMT4tTSo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="ruby" /><category term="rails" /><category term="sso" /><category term="saml" /><summary type="html"><![CDATA[My colleagues and I have learnt a lot about supporting SAML in our Rails app. I've collected a lot of that into a handful of posts.]]></summary></entry><entry><title type="html">SAML and Ruby: Automated request and feature tests</title><link href="https://freelancing-gods.com/2025/05/10/saml-ruby-development-idp.html" rel="alternate" type="text/html" title="SAML and Ruby: Automated request and feature tests" /><published>2025-05-10T00:00:00+00:00</published><updated>2025-05-10T00:00:00+00:00</updated><id>https://freelancing-gods.com/2025/05/10/saml-ruby-development-idp</id><content type="html" xml:base="https://freelancing-gods.com/2025/05/10/saml-ruby-development-idp.html"><![CDATA[<p><em>This post is part of the broader series around <a href="/2025/05/11/saml-ruby-collection.html">SAML and Ruby</a>.</em></p>

<p>Most of the big pain points that have cropped up regularly for us at <a href="https://www.covidence.org">Covidence</a> while building out support for SAML requests in our app were related to testing.</p>

<p><strong>Manual testing in preview environments</strong> is something we’ve managed via <a href="/2025/05/08/saml-ruby-bridging.html">bridging SAML requests through our staging server</a> through to a legitimate (production) identity provider.</p>

<p><strong>Automated testing</strong> is something we’ve figured out through both <a href="/2025/05/09/saml-ruby-automated-tests.html">request and feature tests</a>, particularly aided by a micro IdP service which we’ve wrapped up into the open source gem <a href="https://github.com/covidence/ssolo">ssolo</a>.</p>

<p>But <strong>manual testing in local environments</strong> is something we’ve mucked around with in various ways without finding something ideal… well, until we built <code class="language-plaintext highlighter-rouge">ssolo</code>. Because if a micro IdP can be running as a server for our tests, surely it can also be running as a server for our development environments too?</p>

<p>(Yes, yes it can)</p>

<p>The gem documentation does cover this, but let’s run through it here as well! Essentially, once you have the gem installed, you can fire it up alongside a set of environment variables:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>ssolo <span class="se">\</span>
  <span class="nv">SSOLO_PERSISTENCE</span><span class="o">=</span>~/.ssolo.json <span class="se">\</span>
  <span class="nv">SSOLO_SP_CERTIFICATE</span><span class="o">=</span><span class="s2">"-----BEGIN CERTIFICATE-----</span><span class="se">\n</span><span class="s2">...</span><span class="se">\n</span><span class="s2">-----END CERTIFICATE-----"</span> <span class="se">\</span>
  <span class="nv">SSOLO_HOST</span><span class="o">=</span>127.0.0.1 <span class="se">\</span>
  <span class="nv">SSOLO_PORT</span><span class="o">=</span>9292 <span class="se">\</span>
  <span class="nv">SSOLO_SILENT</span><span class="o">=</span><span class="nb">true</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">SSOLO_PERSISTENCE</code> is important - this tells ssolo where to save the generated private key and certificate. While in tests it’s (hopefully) fine for those values to change regularly, in our local environment we want these settings to stick around - they’re likely being cached somewhere.</p>

<p><code class="language-plaintext highlighter-rouge">SSOLO_SP_CERTIFICATE</code> is also important - this is the service provider certificate that your local app is using for its SAML requests. The IdP server needs to know this, so it can both read the requests and send appropriately signed responses back.</p>

<p><code class="language-plaintext highlighter-rouge">SSOLO_HOST</code> and <code class="language-plaintext highlighter-rouge">SOLO_PORT</code> are optional - these are the underlying Puma defaults, but you can customise them if needed.</p>

<p>And <code class="language-plaintext highlighter-rouge">SSOLO_SILENT</code> can hide the logging if that’s what you’d prefer. This is more useful in a test environment - in development situations, you probably want to know if something’s gone pear-shaped!</p>

<p>You can <em>also</em> specify <code class="language-plaintext highlighter-rouge">SSOLO_NAME_ID</code> to keep the supplied name ID as a fixed value. But otherwise, you will be prompted for a value when you’re going through the SAML flow.</p>

<p>Wrap this command up into your own script, or in a Procfile, so it’s easy to have running whenever you need it.</p>

<hr />

<p>And once you’ve got it there and running, there’s two endpoints to be mindful of:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">GET /metadata</code> which returns the XML metadata</li>
  <li><code class="language-plaintext highlighter-rouge">GET /saml</code> which is the URL to initiate SAML requests</li>
</ul>

<p>So, if you’re running the server with the default environment variables, you should be able to see the metadata via <a href="http://127.0.0.1:9292/metadata">http://127.0.0.1:9292/metadata</a>, and make SAML requests to <a href="http://127.0.0.1:9292/saml">http://127.0.0.1:9292/saml</a>.</p>

<p>With all of this in place, you should be able to initiate a SAML request to this ssolo IdP, and quickly get a response back with your preferred name_id. No extra credentials required, no server wrangling with external third parties.</p>

<p>One last note: please keep in mind that <code class="language-plaintext highlighter-rouge">ssolo</code> is very minimal - we’ve built it out to be just enough for us. If you find some rough edges, we’d love to hear about them via <a href="https://github.com/covidence/ssolo">the GitHub repo</a> - and pull requests are of course welcome too!</p>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="ruby" /><category term="rails" /><category term="sso" /><category term="saml" /><summary type="html"><![CDATA[Writing tests to confirm SAML authentication in Ruby isn't too daunting at a request level - but full feature tests are also possible!]]></summary></entry><entry><title type="html">SAML and Ruby: Automated request and feature tests</title><link href="https://freelancing-gods.com/2025/05/09/saml-ruby-automated-tests.html" rel="alternate" type="text/html" title="SAML and Ruby: Automated request and feature tests" /><published>2025-05-09T00:00:00+00:00</published><updated>2025-05-09T00:00:00+00:00</updated><id>https://freelancing-gods.com/2025/05/09/saml-ruby-automated-tests</id><content type="html" xml:base="https://freelancing-gods.com/2025/05/09/saml-ruby-automated-tests.html"><![CDATA[<p><em>This post is part of the broader series around <a href="/2025/05/11/saml-ruby-collection.html">SAML and Ruby</a>.</em></p>

<p>When we started building out SAML support at <a href="https://www.covidence.org">Covidence</a>, we looked around for examples of how to best write automated tests and didn’t find anything particularly compelling. Ideally, we wanted feature tests - the full flow of starting a sign-in process on our site, via an identity provider, and then having an active session - but a path through wasn’t clear.</p>

<p>So instead, we turned to request specs, and found that worked quite well! Our testing framework of choice is RSpec, but I’m sure these tests could be adapted to other tools.</p>

<p>Taking in the approach outlined in <a href="/2025/05/06/saml-ruby-service-provider.html">this post</a> for the controller actions, we can test the endpoint which initiates a SAML request (the <code>new</code> action), where we confirm that the resulting redirect:</p>

<ul>
  <li>Has a <code class="language-plaintext highlighter-rouge">SAMLRequest</code> parameter</li>
  <li>Has a <code class="language-plaintext highlighter-rouge">RelayState</code> parameter</li>
  <li>And is going to the correct IdP URL</li>
</ul>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">get</span> <span class="s2">"/sign_in/saml"</span>
<span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">status</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span> <span class="mi">302</span>

<span class="n">redirect_uri</span> <span class="o">=</span> <span class="no">URI</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">location</span><span class="p">)</span>
<span class="n">queries</span> <span class="o">=</span> <span class="no">CGI</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">redirect_uri</span><span class="p">.</span><span class="nf">query</span><span class="p">)</span>

<span class="c1"># Confirm a SAMLRequest parameter is sent:</span>
<span class="n">expect</span><span class="p">(</span><span class="n">queries</span><span class="p">[</span><span class="s2">"SAMLRequest"</span><span class="p">].</span><span class="nf">length</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="c1"># Confirm a RelayState parameter is sent</span>
<span class="c1"># (perhaps with your preferred data):</span>
<span class="n">expect</span><span class="p">(</span><span class="n">queries</span><span class="p">[</span><span class="s2">"RelayState"</span><span class="p">].</span><span class="nf">length</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

<span class="c1"># Confirm we're redirecting to the IdP's SSO Service URL:</span>
<span class="n">redirect_uri_without_query</span> <span class="o">=</span> <span class="n">redirect_uri</span><span class="p">.</span><span class="nf">dup</span><span class="p">.</span><span class="nf">tap</span> <span class="p">{</span>
  <span class="o">|</span><span class="n">uri</span><span class="o">|</span> <span class="n">uri</span><span class="p">.</span><span class="nf">query</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="p">}.</span><span class="nf">to_s</span>
<span class="n">expect</span><span class="p">(</span><span class="n">redirect_uri_without_query</span><span class="p">).</span><span class="nf">to</span> <span class="n">eq</span><span class="p">(</span><span class="n">idp_sso_service_url</span><span class="p">)</span>
</code></pre></div></div>

<p>Testing the receiving of a SAML response (the <code>create</code> action) is a bit trickier.</p>

<p>A reasonable approach is to stub out the response object - you don’t really care how the SAML response parameter is constructed, you’re just checking what happens when a valid response is passed in.</p>

<p>The end result of what the endpoint should do is up to you and your application. Maybe it’s just a redirect (as per below), maybe it’s reviewing certain cookies, or even parsing the session cookie to confirm its state.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">saml_response</span> <span class="o">=</span> <span class="n">instance_double</span><span class="p">(</span>
  <span class="s2">"OneLogin::RubySaml::Response"</span><span class="p">,</span>
  <span class="ss">is_valid?: </span><span class="kp">true</span><span class="p">,</span>
  <span class="ss">name_id: </span><span class="s2">"test@example.com"</span><span class="p">,</span>
  <span class="ss">name_id_format: </span><span class="s2">"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"</span>
<span class="p">)</span>

<span class="n">allow</span><span class="p">(</span><span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">Response</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">to</span> <span class="n">receive</span><span class="p">(</span><span class="ss">:new</span><span class="p">)</span>
  <span class="p">.</span><span class="nf">and_return</span><span class="p">(</span><span class="n">saml_response_double</span><span class="p">)</span>

<span class="n">post</span> <span class="s2">"/sign_in/saml"</span><span class="p">,</span>
  <span class="ss">params: </span><span class="p">{</span>
    <span class="no">RelayState</span><span class="p">:</span> <span class="n">relay_state</span><span class="p">,</span>
    <span class="no">SAMLResponse</span><span class="p">:</span> <span class="s2">"saml_reponse_string"</span>
  <span class="p">}</span>

<span class="n">expect</span><span class="p">(</span><span class="n">response</span><span class="p">).</span><span class="nf">to</span> <span class="n">redirect_to</span><span class="p">(</span><span class="n">logged_in_path</span><span class="p">)</span>
</code></pre></div></div>

<p>These request tests have served us well - we’ve fleshed them out with more examples specific to our application: how failures are handled, how different customer states are managed, etc.</p>

<p>But the holy grail of a full feature test was still there, tempting us.</p>

<p>And we had a realisation, inspired by our work of managing requests from an IdP perspective with <a href="/2025/05/08/saml-ruby-bridging.html">our bridging logic</a>: what if we have our own tiny IdP server, running as a side service within our test suite? This removes any need to have an external service involved, keeping things controllable and reliable. (After all, you shouldn’t test what you can’t control!)</p>

<p>So we built a mini Rack app that operated in a separate thread, and it’s worked well. So well, in fact, that we’ve just extracted it out into a gem for others to use: <a href="https://github.com/covidence/ssolo">ssolo</a>!</p>

<p>It’s a bit more involved, so let’s break down the setup. Firstly, you’ll want to create a new ssolo controller to manage the service. When you start it, you’ll need to provide both the certificate for your service provider, and a <code class="language-plaintext highlighter-rouge">name_id</code> value. This value will be immediately returned by the IdP (rather than prompting the user for credentials).</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">controller</span> <span class="o">=</span> <span class="no">SSOlo</span><span class="o">::</span><span class="no">Controller</span><span class="p">.</span><span class="nf">new</span>
<span class="n">controller</span><span class="p">.</span><span class="nf">start</span><span class="p">(</span>
  <span class="ss">sp_certificate: </span><span class="o">&lt;&lt;~</span><span class="no">CERT</span><span class="p">,</span><span class="sh">
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----
</span><span class="no">  CERT</span>
  <span class="ss">name_id: </span><span class="s2">"test@example.com"</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Then, you can use that controller to access the IdP’s settings to configure your SAML requests appropriately:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># connect up the appropriate SAML settings via an</span>
<span class="c1"># OneLogin::RubySaml::Settings instance:</span>
<span class="n">controller</span><span class="p">.</span><span class="nf">settings</span> <span class="c1">#=&gt; OneLogin::RubySaml::Settings</span>
<span class="n">controller</span><span class="p">.</span><span class="nf">settings</span><span class="p">.</span><span class="nf">idp_entity_id</span>
<span class="n">controller</span><span class="p">.</span><span class="nf">settings</span><span class="p">.</span><span class="nf">idp_sso_service_url</span>
<span class="n">controller</span><span class="p">.</span><span class="nf">settings</span><span class="p">.</span><span class="nf">idp_cert</span>

<span class="c1"># These details are also available via a URL:</span>
<span class="n">controller</span><span class="p">.</span><span class="nf">metadata_url</span>
</code></pre></div></div>

<p>The core piece, though, is actually writing your tests to use this IdP.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Click something that takes you off to the IdP:</span>
<span class="n">click_on</span> <span class="s2">"Sign in via SSO"</span>

<span class="c1"># And then it immediately redirects you back, using the</span>
<span class="c1"># previously specified name_id:</span>
<span class="n">expect</span><span class="p">(</span><span class="n">page</span><span class="p">).</span><span class="nf">to</span> <span class="n">have_content</span><span class="p">(</span><span class="s2">"test@example.com"</span><span class="p">)</span>
</code></pre></div></div>

<p>Once you’re done, make sure you then shut the IdP process down:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">controller</span><span class="p">.</span><span class="nf">stop</span>
</code></pre></div></div>

<p>We hope it’s useful for others - please do give it a spin if you’re testing SAML in your own apps! And of course, questions and contributions are welcome via <a href="https://github.com/covidence/ssolo">the ssolo GitHub repo</a>.</p>

<p>Oh, and maybe you want to use ssolo for your development environment too? <a href="/2025/05/10/saml-ruby-development-idp.html">Onwards to the next post</a>!</p>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="ruby" /><category term="rails" /><category term="sso" /><category term="saml" /><summary type="html"><![CDATA[Writing tests to confirm SAML authentication in Ruby isn't too daunting at a request level - but full feature tests are also possible!]]></summary></entry><entry><title type="html">SAML and Ruby: Testing with ephemeral apps</title><link href="https://freelancing-gods.com/2025/05/08/saml-ruby-bridging.html" rel="alternate" type="text/html" title="SAML and Ruby: Testing with ephemeral apps" /><published>2025-05-08T00:00:00+00:00</published><updated>2025-05-08T00:00:00+00:00</updated><id>https://freelancing-gods.com/2025/05/08/saml-ruby-bridging</id><content type="html" xml:base="https://freelancing-gods.com/2025/05/08/saml-ruby-bridging.html"><![CDATA[<p><em>This post is part of the broader series around <a href="/2025/05/11/saml-ruby-collection.html">SAML and Ruby</a>.</em></p>

<p>When it comes to testing our work manually (alongside our automated test suite), we make use of Heroku’s preview apps linked to GitHub pull requests.</p>

<p>And largely, that works well for us - but when it comes to testing our SAML integration, we’ve hit a challenge: identity providers (IdPs) require service providers to be accessed by a fixed route, but our preview apps are on a range of subdomains.</p>

<p>For example: a production site may be available at <code class="language-plaintext highlighter-rouge">app.example.com</code>, and the staging site at <code class="language-plaintext highlighter-rouge">staging.example.com</code>. But each preview app will be at <code class="language-plaintext highlighter-rouge">preview-1.example.com</code>, <code class="language-plaintext highlighter-rouge">preview-2.example.com</code>, and so on - the domains are constantly changing.</p>

<p>The IdPs we’ve been testing with are resolute about the endpoints being fixed - the domain and the path. Patterns are not allowed either. And they’re an external service, not something we can control… so, we were feeling a bit stuck!</p>

<p>Then, a moment of realisation: let’s build something we <em>can</em> control - and this has ended up being a SAML bridging service via our staging site.</p>

<ul>
  <li>The preview app initiates a SAML request and sends it to the staging site (operating as an IdP).</li>
  <li>The staging site then starts a second SAML request, forwarding the user onto the true IdP.</li>
  <li>The IdP verifies the user and sends them back to the staging site (operating as a service provider), finishing the second SAML flow.</li>
  <li>The staging site then immediately passes the identity through to the preview app, to finish the initial SAML flow.</li>
</ul>

<p>Using our staging site means we don’t have to deploy a whole other app elsewhere - though we of course make sure this functionality is <em>not</em> available in production.</p>

<p>From a Rails perspective, we’ve done this in a new controller, with a pair of actions (again <code>new</code> and <code>create</code>, just like in <a href="/2025/05/06/saml-ruby-service-provider.html">our main SAML controller</a>).</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">new</span>
  <span class="c1"># Save original request details</span>
  <span class="n">save_identity_cache</span>

  <span class="n">redirect_to</span><span class="p">(</span>
    <span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">Authrequest</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
      <span class="c1"># Settings for the actual IdP:</span>
      <span class="n">service_settings</span><span class="p">,</span>
      <span class="c1"># The original request's ID:</span>
      <span class="no">RelayState</span><span class="p">:</span> <span class="n">identity_request</span><span class="p">.</span><span class="nf">request_id</span>
    <span class="p">)</span>
  <span class="p">)</span>
<span class="k">end</span>

<span class="kp">private</span>

<span class="c1"># The details of the initial SAML request (sent from the preview</span>
<span class="c1"># site to the staging site).</span>
<span class="k">def</span> <span class="nf">identity_request</span>
  <span class="vi">@identity_request</span> <span class="o">||=</span> <span class="no">SamlIdp</span><span class="o">::</span><span class="no">Request</span><span class="p">.</span><span class="nf">from_deflated_request</span><span class="p">(</span>
    <span class="n">params</span><span class="p">[</span><span class="ss">:SAMLRequest</span><span class="p">]</span>
  <span class="p">)</span>
<span class="k">end</span>

<span class="c1"># And save those initial details to the cache, to re-use on the</span>
<span class="c1"># return journey:</span>
<span class="k">def</span> <span class="nf">save_identity_cache</span>
  <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span>
    <span class="n">identity_request</span><span class="p">.</span><span class="nf">request_id</span><span class="p">,</span>
    <span class="p">{</span>
      <span class="ss">relay_state: </span><span class="n">params</span><span class="p">[</span><span class="ss">:RelayState</span><span class="p">],</span>
      <span class="ss">issuer: </span><span class="n">identity_request</span><span class="p">.</span><span class="nf">issuer</span><span class="p">,</span>
      <span class="ss">acs_url: </span><span class="n">identity_request</span><span class="p">.</span><span class="nf">acs_url</span>
    <span class="p">}</span>
  <span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This <code>new</code> action is the endpoint on our staging site that accepts the original SAML request, and initiates a <em>new</em> SAML request to the ‘true’ identity provider.</p>

<p>As part of this, it saves the essential details from the original request in the Rails cache and uses the RelayState in the <em>new</em> request to keep that identifier. Using a cache here rather than a session is important, as session cookies are not passed along when you’re redirecting between sites.</p>

<p>And then, we need to handle the request coming back from the true identity provider:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create</span>
  <span class="vi">@identity_acs_url</span> <span class="o">=</span> <span class="n">identity_cache</span><span class="p">[</span><span class="ss">:acs_url</span><span class="p">]</span>
  <span class="vi">@identity_relay_state</span> <span class="o">=</span> <span class="n">identity_cache</span><span class="p">[</span><span class="ss">:relay_state</span><span class="p">]</span>

  <span class="c1"># `encode_response` comes from the saml_idp gem</span>
  <span class="vi">@identity_response</span> <span class="o">=</span> <span class="n">encode_response</span><span class="p">(</span>
    <span class="n">service_response</span><span class="p">,</span>
    <span class="ss">audience_uri: </span><span class="n">identity_cache</span><span class="p">[</span><span class="ss">:issuer</span><span class="p">],</span>
    <span class="ss">acs_url: </span><span class="n">identity_cache</span><span class="p">[</span><span class="ss">:acs_url</span><span class="p">],</span>
    <span class="ss">encryption: </span><span class="p">{</span>
      <span class="c1"># Both SP and IdP have certificates. This should</span>
      <span class="c1"># be the certificate for the original service provider</span>
      <span class="c1"># (i.e. the preview site).</span>
      <span class="c1">#</span>
      <span class="c1"># An instance of OpenSSL::X509::Certificate is expected</span>
      <span class="ss">cert: </span><span class="n">saml_certificate</span><span class="p">,</span>
      <span class="ss">block_encryption: </span><span class="s2">"aes256-cbc"</span><span class="p">,</span>
      <span class="ss">key_transport: </span><span class="s2">"rsa-oaep-mgf1p"</span>
    <span class="p">}</span>
  <span class="p">)</span>
<span class="k">end</span>

<span class="kp">private</span>

<span class="k">def</span> <span class="nf">identity_cache</span>
  <span class="vi">@identity_cache</span> <span class="o">||=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">cache</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="n">identity_cache_key</span><span class="p">)</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">identity_cache_key</span>
  <span class="n">params</span><span class="p">[</span><span class="ss">:SAMLRequest</span><span class="p">]</span> <span class="p">?</span> <span class="n">identity_request</span><span class="p">.</span><span class="nf">request_id</span> <span class="p">:</span> <span class="n">params</span><span class="p">[</span><span class="ss">:RelayState</span><span class="p">]</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">service_response</span>
  <span class="vi">@service_response</span> <span class="o">||=</span> <span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">Response</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
    <span class="n">params</span><span class="p">[</span><span class="ss">:SAMLResponse</span><span class="p">],</span>
    <span class="ss">settings: </span><span class="n">service_settings</span>
  <span class="p">).</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">response</span><span class="o">|</span>
    <span class="k">unless</span> <span class="n">response</span><span class="p">.</span><span class="nf">is_valid?</span>
      <span class="k">raise</span> <span class="no">ArgumentError</span><span class="p">,</span> <span class="n">response</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">","</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And the corresponding view:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;</span><span class="err">%=</span> <span class="na">form_tag</span><span class="err">(@</span><span class="na">identity_acs_url</span><span class="err">,</span> <span class="na">style:</span> <span class="err">"</span><span class="na">visibility:</span> <span class="na">hidden</span><span class="err">")</span> <span class="na">do</span> <span class="err">%</span><span class="nt">&gt;</span>
  <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">hidden_field_tag</span><span class="err">("</span><span class="na">SAMLResponse</span><span class="err">",</span> <span class="err">@</span><span class="na">identity_response</span><span class="err">)</span> <span class="err">%</span><span class="nt">&gt;</span>
  <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">hidden_field_tag</span><span class="err">("</span><span class="na">RelayState</span><span class="err">",</span> <span class="err">@</span><span class="na">identity_relay_state</span><span class="err">)</span> <span class="err">%</span><span class="nt">&gt;</span>
  <span class="nt">&lt;</span><span class="err">%=</span> <span class="na">submit_tag</span> <span class="err">"</span><span class="na">Submit</span><span class="err">"</span> <span class="err">%</span><span class="nt">&gt;</span>
<span class="nt">&lt;</span><span class="err">%</span> <span class="na">end</span> <span class="err">%</span><span class="nt">&gt;</span>

<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript"</span><span class="nt">&gt;</span>
  <span class="nb">document</span><span class="p">.</span><span class="nx">forms</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">submit</span><span class="p">();</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>We need to render a form that automatically submits, because SAML responses are sent via POST requests - so we can’t rely on a standard HTTP redirect, which is sent as a GET.</p>

<p>For this action, we’re making use of <a href="https://github.com/saml-idp/saml_idp">the saml_idp gem</a>, which we configure as follows:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/saml_idp.rb</span>
<span class="no">SamlIdp</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">base_saml_location</span> <span class="o">=</span> <span class="s2">"https://staging.example.com/saml"</span>
  <span class="c1"># This is the certificate and private key for the staging site when</span>
  <span class="c1"># operating as an IdP.</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">x509_certificate</span> <span class="o">=</span> <span class="n">saml_certificate</span><span class="p">.</span><span class="nf">to_pem</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">secret_key</span> <span class="o">=</span> <span class="n">saml_private_key</span><span class="p">.</span><span class="nf">private_to_pem</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">algorithm</span> <span class="o">=</span> <span class="ss">:sha256</span>
  <span class="c1"># This block defines how we convert a 'principal' object to a name_id.</span>
  <span class="c1"># In our case, the principal is already a SAML response, so we can</span>
  <span class="c1"># just extract the name_id directly from it.</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">name_id</span><span class="p">.</span><span class="nf">formats</span> <span class="o">=</span> <span class="p">{</span>
    <span class="ss">email_address: </span><span class="o">-&gt;</span><span class="p">(</span><span class="n">principal</span><span class="p">)</span> <span class="p">{</span> <span class="n">principal</span><span class="p">.</span><span class="nf">name_id</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You may have noted in the code samples above that there’s a couple of references to certificates and private keys. These certificates are ones you can generate yourself, and this can be done within Ruby code:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">name</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">X509</span><span class="o">::</span><span class="no">Name</span><span class="p">.</span><span class="nf">parse</span> <span class="s2">"/CN=nobody/DC=example"</span>
<span class="n">private_key</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">PKey</span><span class="o">::</span><span class="no">RSA</span><span class="p">.</span><span class="nf">new</span> <span class="mi">2048</span>

<span class="n">certificate</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">X509</span><span class="o">::</span><span class="no">Certificate</span><span class="p">.</span><span class="nf">new</span>
<span class="n">certificate</span><span class="p">.</span><span class="nf">version</span> <span class="o">=</span> <span class="mi">2</span>
<span class="n">certificate</span><span class="p">.</span><span class="nf">serial</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">certificate</span><span class="p">.</span><span class="nf">not_before</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span>
<span class="n">certificate</span><span class="p">.</span><span class="nf">not_after</span> <span class="o">=</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span> <span class="o">+</span> <span class="p">(</span><span class="mi">10</span> <span class="o">*</span> <span class="mi">365</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span><span class="p">)</span>
<span class="n">certificate</span><span class="p">.</span><span class="nf">public_key</span> <span class="o">=</span> <span class="n">private_key</span><span class="p">.</span><span class="nf">public_key</span>
<span class="n">certificate</span><span class="p">.</span><span class="nf">subject</span> <span class="o">=</span> <span class="nb">name</span>
<span class="n">certificate</span><span class="p">.</span><span class="nf">issuer</span> <span class="o">=</span> <span class="nb">name</span>
<span class="n">certificate</span><span class="p">.</span><span class="nf">sign</span><span class="p">(</span><span class="n">private_key</span><span class="p">,</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">Digest</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"SHA256"</span><span class="p">))</span>

<span class="n">certificate</span>
</code></pre></div></div>

<p>There are distinct certificates for the preview sites acting as service providers, the staging site acting as an identity provider, and the staging site acting as a service provider. It’s easy to get tripped up when attempting to use the right certificate in the right moment - so you may want to use a single certificate for all of these scenarios, given this is for internal testing.</p>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="ruby" /><category term="rails" /><category term="sso" /><category term="saml" /><summary type="html"><![CDATA[Identity providers require specific domains for testing - which is a challenge for preview/PR applications. We've found a way through this by building a bridging mechanism into our staging site.]]></summary></entry><entry><title type="html">SAML and Ruby: Parsing federation metadata</title><link href="https://freelancing-gods.com/2025/05/07/saml-ruby-federations.html" rel="alternate" type="text/html" title="SAML and Ruby: Parsing federation metadata" /><published>2025-05-07T00:00:00+00:00</published><updated>2025-05-07T00:00:00+00:00</updated><id>https://freelancing-gods.com/2025/05/07/saml-ruby-federations</id><content type="html" xml:base="https://freelancing-gods.com/2025/05/07/saml-ruby-federations.html"><![CDATA[<p><em>This post is part of the broader series around <a href="/2025/05/11/saml-ruby-collection.html">SAML and Ruby</a>.</em></p>

<p>As part of rolling out SSO for our customers at <a href="https://www.covidence.org">Covidence</a>, we were quickly made aware of various federations that exist for research-related institutions (such as <a href="https://aaf.edu.au">AAF</a> and <a href="https://edugain.org">eduGAIN</a>) - and these federations collect both SAML identity and service provider metadata into central locations.</p>

<p>There’s a great advantage in this for us - instead of needing to ask each of our customers individually for their SAML IdP metadata, we can instead just refer to these aggregated files. The catch? We need to parse those files just for what’s relevant to us - and the files can get quite large!</p>

<p>Still, the <a href="https://github.com/SAML-Toolkits/ruby-saml">ruby-saml gem</a> gives us a way to do this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">saml_settings</span>
  <span class="n">parser</span> <span class="o">=</span> <span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">IdpMetadataParser</span><span class="p">.</span><span class="nf">new</span>

  <span class="n">settings</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="nf">parse_remote</span><span class="p">(</span>
    <span class="c1"># The aggregate metadata URL:</span>
    <span class="s2">"https://example.com/auth/saml2/idp/metadata"</span><span class="p">,</span>
    <span class="c1"># The specific IdP we're looking for:</span>
    <span class="ss">entity_id: </span><span class="s2">"http//example.com/target/entity"</span>
  <span class="p">)</span>

  <span class="c1"># You've got the IdP settings, but you still need to add</span>
  <span class="c1"># the content of your service provider:</span>
  <span class="n">settings</span><span class="p">.</span><span class="nf">assertion_consumer_service_url</span> <span class="o">=</span>
    <span class="s2">"http://</span><span class="si">#{</span><span class="n">request</span><span class="p">.</span><span class="nf">host</span><span class="si">}</span><span class="s2">/saml_sessions"</span>

  <span class="n">settings</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now, the above example would involve downloading the metadata file every time you’re generating settings for a SAML request - which definitely not a wise move. I recommend caching the settings for each specific identity provider you care about.</p>

<p>But also: parsing large files is very slow, and very memory hungry. We dug into the source code of the ruby-saml gem and found it’s using <a href="https://github.com/ruby/rexml">rexml</a> for the parsing. After some experimentation, we found a faster way with <a href="https://nokogiri.org">Nokogiri</a>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">saml_settings_hash</span><span class="p">(</span><span class="n">metadata_file_contents</span><span class="p">,</span> <span class="n">entity_id</span><span class="p">)</span>
  <span class="n">entire_document</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">XML</span><span class="p">(</span>
    <span class="n">metadata_file_contents</span>
  <span class="p">)</span>

  <span class="c1"># Use Nokogiri to find the appropriate XML node:</span>
  <span class="n">node</span> <span class="o">=</span> <span class="n">entire_document</span><span class="p">.</span><span class="nf">xpath</span><span class="p">(</span>
    <span class="s2">"//md:EntityDescriptor[@entityID=</span><span class="se">\"</span><span class="si">#{</span><span class="n">entity_id</span><span class="si">}</span><span class="se">\"</span><span class="s2">]/md:IDPSSODescriptor"</span><span class="p">,</span>
    <span class="s2">"md"</span> <span class="o">=&gt;</span> <span class="s2">"urn:oasis:names:tc:SAML:2.0:metadata"</span>
  <span class="p">).</span><span class="nf">first</span>
  <span class="k">return</span> <span class="kp">nil</span> <span class="k">if</span> <span class="n">node</span><span class="p">.</span><span class="nf">nil?</span>

  <span class="c1"># Convert the IdP element into a standalone XML document,</span>
  <span class="c1"># so rexml can parse it:</span>
  <span class="n">idp_document</span> <span class="o">=</span> <span class="no">Nokogiri</span><span class="o">::</span><span class="no">XML</span><span class="p">(</span><span class="n">node</span><span class="p">.</span><span class="nf">to_xml</span><span class="p">).</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">sub_document</span><span class="o">|</span>
    <span class="n">entire_document</span><span class="p">.</span><span class="nf">namespaces</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">prefix</span><span class="p">,</span> <span class="n">url</span><span class="o">|</span>
      <span class="n">sub_document</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">add_namespace</span><span class="p">(</span><span class="n">normalised_prefix</span><span class="p">(</span><span class="n">prefix</span><span class="p">),</span> <span class="n">url</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="c1"># Return a hash that can be ingested by OneLogin::RubySaml::Settings</span>
  <span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">IdpMetadataParser</span><span class="o">::</span><span class="no">IdpMetadata</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
    <span class="no">REXML</span><span class="o">::</span><span class="no">Document</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">idp_document</span><span class="p">.</span><span class="nf">to_xml</span><span class="p">).</span><span class="nf">root</span><span class="p">,</span>
    <span class="n">entity_id</span>
  <span class="p">).</span><span class="nf">to_hash</span><span class="p">(</span>
    <span class="ss">sso_binding: </span><span class="s2">"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"</span>
  <span class="p">)</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">normalised_prefix</span><span class="p">(</span><span class="n">prefix</span><span class="p">)</span>
  <span class="k">return</span> <span class="kp">nil</span> <span class="k">if</span> <span class="n">prefix</span> <span class="o">==</span> <span class="s2">"xmlns"</span>

  <span class="n">prefix</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="s2">"xmlns:"</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There’s still room for improvement here - it’d be nice to avoid parsing the entire document - but it’s not been a priority for us. The Nokogiri approach works well enough for now.</p>

<p>And I’m afraid we don’t have any benchmarks on hand, so you’ll just have to take my word for it! Granted, if you’re digging into this and do have some numbers, please send them my way.</p>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="ruby" /><category term="rails" /><category term="sso" /><category term="saml" /><summary type="html"><![CDATA[When working with multiple IdPs, parsing metadata files is not performant via the ruby-saml gem - but there is a better way with Nokogiri.]]></summary></entry><entry><title type="html">SAML and Ruby: Building a Service Provider</title><link href="https://freelancing-gods.com/2025/05/06/saml-ruby-service-provider.html" rel="alternate" type="text/html" title="SAML and Ruby: Building a Service Provider" /><published>2025-05-06T00:00:00+00:00</published><updated>2025-05-06T00:00:00+00:00</updated><id>https://freelancing-gods.com/2025/05/06/saml-ruby-service-provider</id><content type="html" xml:base="https://freelancing-gods.com/2025/05/06/saml-ruby-service-provider.html"><![CDATA[<p><em>This post is part of the broader series around <a href="/2025/05/11/saml-ruby-collection.html">SAML and Ruby</a>.</em></p>

<p>If you’re building a Rails site that needs to act as a SAML service provider, you’ve got two key options: you can use a third party service to manage the integration with identity providers, or you can build out the logic yourself.</p>

<p>There are a great many third party services to consider, including Auth0, Shibboleth, Firebase, Kinde, KeyCloak, and FusionAuth. Some of these are closed-source paid services, others are open source - often with a paid option for managed hosting. There’s value in these options, so you may want to investigate further.</p>

<p>However, building support directly in your Ruby or Rails app isn’t actually as daunting as I first feared - and as a bonus, it lets you retain control of the customer/user data, rather than being beholden to the limitations and terms of a separate service.</p>

<h3 id="ruby-saml-and-osso">ruby-saml and Osso</h3>

<p>The two key things that greatly helped us with building out service provider support into our app at Covidence are:</p>

<ul>
  <li>The <a href="https://github.com/SAML-Toolkits/ruby-saml">ruby-saml gem</a>, which has existed for many years, and so has been extensively tested against a wide variety of identity providers;</li>
  <li>And <a href="https://dev.to/sammybauch/add-saml-sso-to-a-rails-6-app-20ld">a blog post by Osso</a> on how to use that gem in a Rails application.</li>
</ul>

<p>Osso was once a third party option, and while they don’t exist as a business any more, I’m very glad for their generous spirit in sharing a solid starting point for Ruby developers diving into SAML. You’ll be well-served by reading their post, but in case a shorter summary is useful, here’s our take.</p>

<h3 id="the-way-out">The way out</h3>

<p>There are two key endpoints required to behave as a SAML service provider: one that redirects your visitors out to the identity provider, and one that accepts the resulting response when they’re sent back to your site with a verified identity.</p>

<p>For me, these work well as <code>new<code> and <code>create</code> actions in a single controller. Let's take them one at a time.</code></code></p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SAMLController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">new</span>
    <span class="c1"># Generate a new SAML request</span>
    <span class="n">saml_request</span> <span class="o">=</span> <span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">Authrequest</span><span class="p">.</span><span class="nf">new</span>

    <span class="c1"># Send the current visitor away to the IdP:</span>
    <span class="n">redirect_to</span><span class="p">(</span>
      <span class="n">saml_request</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
        <span class="c1"># These are settings for the specific IdP:</span>
        <span class="n">saml_settings</span><span class="p">,</span>
        <span class="c1"># This is your own context/state, which the IdP does not</span>
        <span class="c1"># care about but it will send it back to you:</span>
        <span class="no">RelayState</span><span class="p">:</span> <span class="s2">"new-user-request"</span>
      <span class="p">),</span>
      <span class="c1"># Ensure Rails is okay with you redirecting people away to</span>
      <span class="c1"># a different site:</span>
      <span class="ss">allow_other_host: </span><span class="kp">true</span>
    <span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>In this action, we’re generating a new SAML request, and then using it to build a redirect URL for a specific identity provider (the <code class="language-plaintext highlighter-rouge">saml_settings</code> method) and our own app’s context or state (the <code class="language-plaintext highlighter-rouge">RelayState</code> parameter). Relay states will be sent back to us by the IdP - the value of it is entirely up to you, but should be a maximum of 80 bytes. The IdP will not parse it, so it’s purely for your own app’s use.</p>

<p>The <code class="language-plaintext highlighter-rouge">saml_settings</code> method could look something like the following:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">saml_settings</span>
  <span class="n">settings</span> <span class="o">=</span> <span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">Settings</span><span class="p">.</span><span class="nf">new</span>

  <span class="c1"># Where the IdP sends users back to on our site:</span>
  <span class="n">settings</span><span class="p">.</span><span class="nf">assertion_consumer_service_url</span> <span class="o">=</span>
    <span class="s2">"http://</span><span class="si">#{</span><span class="n">request</span><span class="p">.</span><span class="nf">host</span><span class="si">}</span><span class="s2">/saml_sessions"</span>

  <span class="c1"># A unique identifier of our service, sometimes requested by</span>
  <span class="c1"># the IdP:</span>
  <span class="n">settings</span><span class="p">.</span><span class="nf">sp_entity_id</span> <span class="o">=</span>
    <span class="s2">"http://</span><span class="si">#{</span><span class="n">request</span><span class="p">.</span><span class="nf">host</span><span class="si">}</span><span class="s2">/saml/metadata"</span>

  <span class="c1"># A unique identifier of the IdP:</span>
  <span class="n">settings</span><span class="p">.</span><span class="nf">idp_entity_id</span> <span class="o">=</span>
    <span class="s2">"https://google.com/..."</span>

  <span class="c1"># The IdP URL our `new` action redirects users to:</span>
  <span class="n">settings</span><span class="p">.</span><span class="nf">idp_sso_service_url</span> <span class="o">=</span>
    <span class="s2">"https://google.com/saml/..."</span>

  <span class="c1"># The X.509 certificate used to sign requests for the IdP:</span>
  <span class="n">settings</span><span class="p">.</span><span class="nf">idp_cert</span> <span class="o">=</span> <span class="o">&lt;&lt;~</span><span class="no">CERT</span><span class="sh">
    -----BEGIN CERTIFICATE-----
    ...
    -----END CERTIFICATE-----
</span><span class="no">  CERT</span>

  <span class="n">settings</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This method is generating an object that contains all the relevant details for interacting with a given identity provider. The examples in the code are referring to Google, but you’ll want to update it for the IdP you’re actually talking to.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">assertion_consumer_service_url</code> is a URL is where visitors will be redirected back to on a successful authentication. This should point to the <code>create</code> action in this controller we’re working on.</li>
  <li><code class="language-plaintext highlighter-rouge">sp_entity_id</code> is an identifier for your service, and should be unique from the perspective of the identity provider.</li>
  <li><code class="language-plaintext highlighter-rouge">idp_entity_id</code> is an identifier for the identity provider, supplied by them, and should also be considered unique.</li>
  <li><code class="language-plaintext highlighter-rouge">idp_sso_service_url</code> is supplied by the identity provider, and is a live URL where we redirect visitors to.</li>
  <li><code class="language-plaintext highlighter-rouge">idp_cert</code> is a X509 certificate supplied by the identity provider, used for signing requests.</li>
</ul>

<p>The entity IDs for both service providers and identity providers are usually URLs. This is not a hard requirement for the SAML specification, but seems to have become a de-facto standard.</p>

<p>These URLs don’t need to be functional - it doesn’t matter if they return a 404 - but it is recommended that they return SAML metadata outlining the provider’s details as an XML document (though this is beyond the scope of this post).</p>

<p>There are other settings that your IdP may require - these can be specified as per <a href="https://github.com/SAML-Toolkits/ruby-saml?tab=readme-ov-file#the-initialization-phase">the ruby-saml documentation</a>, or parsed via their XML metadata document:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">parser</span> <span class="o">=</span> <span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">IdpMetadataParser</span><span class="p">.</span><span class="nf">new</span>
<span class="n">settings</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="nf">parse_remote</span><span class="p">(</span><span class="s2">"https://example.com/idp/metadata"</span><span class="p">)</span>
</code></pre></div></div>

<p>It is <em>very strongly</em> recommended that these settings are cached regularly, rather than requested for every new SAML request, so your site isn’t beholden to Internet connectivity glitches or failures on the IdP site.</p>

<h3 id="the-way-back-in">The way back in</h3>

<p>The above <code>new</code> action sends visitors off to the IdP - but then you’ll want an endpoint for their return. It could look something like the following:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SAMLController</span> <span class="o">&lt;</span> <span class="no">Application</span> <span class="no">Controller</span>
  <span class="c1"># Disable CSRF checks for our create action:</span>
  <span class="n">skip_before_action</span><span class="p">(</span>
    <span class="ss">:verify_authenticity_token</span><span class="p">,</span> <span class="ss">only: </span><span class="p">[</span><span class="ss">:create</span><span class="p">]</span>
  <span class="p">)</span>

  <span class="k">def</span> <span class="nf">new</span>
    <span class="c1"># ... as above</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">create</span>
    <span class="c1"># Parse the given SAML response</span>
    <span class="n">saml_response</span> <span class="o">=</span> <span class="no">OneLogin</span><span class="o">::</span><span class="no">RubySaml</span><span class="o">::</span><span class="no">Response</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
      <span class="n">params</span><span class="p">[</span><span class="ss">:SAMLResponse</span><span class="p">]</span>
    <span class="p">)</span>
    <span class="c1"># And apply the same IdP configuration settings</span>
    <span class="n">saml_response</span><span class="p">.</span><span class="nf">settings</span> <span class="o">=</span> <span class="n">saml_settings</span>

    <span class="c1"># If it's a valid response, then we have a confirmed identity</span>
    <span class="c1"># and can log the visitor in:</span>
    <span class="k">if</span> <span class="n">saml_response</span><span class="p">.</span><span class="nf">is_valid?</span>
      <span class="n">session</span><span class="p">[</span><span class="ss">:userid</span><span class="p">]</span> <span class="o">=</span> <span class="n">saml_response</span><span class="p">.</span><span class="nf">nameid</span>
    <span class="k">else</span>
      <span class="c1"># Otherwise, the response is invalid - you'll probably want</span>
      <span class="c1"># to provide some feedback and ask people to try logging in</span>
      <span class="c1"># again.</span>
      <span class="c1"># ...</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">saml_settings</span>
    <span class="c1"># ... as above</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>When the HTTP request comes in, we want to verify the SAML response with the same IdP settings as before. If the SAML response is valid, then we know we have a confirmed identity, and can use that to log them into our site.</p>

<p>The supplied <code class="language-plaintext highlighter-rouge">nameid</code> (or <code class="language-plaintext highlighter-rouge">name_id</code>) from the IdP might be an email address, or a persistent unique identifier for the user/identity, or even a more ephemeral reference. It varies for each identity provider, and sometimes can be configured - so it’s best to ensure you know ahead of time what you’re dealing with here. If you need to handle a variety of name IDs, then looking at <code class="language-plaintext highlighter-rouge">saml_response.name_id_format</code> could be helpful.</p>

<p>As for the logic of actually logging someone into your site with this identity - well, that’s going to depend on how you’ve implemented authentication, whether it’s via <a href="https://github.com/heartcombo/devise">Devise</a> or <a href="https://github.com/thoughtbot/clearance">Clearance</a> or another gem, or something you’re rolling yourself. At this point, the SAML flow is complete, so the rest is up to you.</p>

<p>But perhaps you want to read some of the <a href="/2025/05/11/saml-ruby-collection.html">other posts</a>, to get a sense of how to test all of this!</p>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="ruby" /><category term="rails" /><category term="sso" /><category term="saml" /><summary type="html"><![CDATA[Building SAML service provider logic in a Rails app isn't actually as daunting as I first feared.]]></summary></entry><entry><title type="html">SAML and Ruby: The Terminology</title><link href="https://freelancing-gods.com/2025/05/05/saml-ruby-terminology.html" rel="alternate" type="text/html" title="SAML and Ruby: The Terminology" /><published>2025-05-05T00:00:00+00:00</published><updated>2025-05-05T00:00:00+00:00</updated><id>https://freelancing-gods.com/2025/05/05/saml-ruby-terminology</id><content type="html" xml:base="https://freelancing-gods.com/2025/05/05/saml-ruby-terminology.html"><![CDATA[<p><em>This post is part of the broader series around <a href="/2025/05/11/saml-ruby-collection.html">SAML and Ruby</a>.</em></p>

<p>When working on SSO, there’s a lot of terminology that crops up, and it can get rather overwhelming at times, especially when you’re new to it all. Here’s a rough and ready list of common terms that you may come across.</p>

<h3 id="sso">SSO</h3>

<p>SSO stands for “Single Sign-On”, which is the process of delegating authentication to a third party service.</p>

<p>You’ve likely come across it at some point - you’re viewing a website, you need to sign in, and it gives you the option of authenticating via a <em>different site</em> such as Google or Facebook or (once upon a time) Twitter. You click the link, you’re taken to that third party site to sign in, and then you’re sent back to the original site and you’ve been logged in there too.</p>

<p>That process? That’s SSO.</p>

<h3 id="authentication-vs-authorisation">Authentication vs Authorisation</h3>

<p><em>Authentication</em> is the process of confirming who someone is. Commonly, this is done via a username and password.</p>

<p><em>Authorisation</em>, however, is checking whether someone has access to do certain things.</p>

<p>For example: there’s a distinction between being logged into a site (authentication), and having administrator access to manage others’ accounts (authorisation).</p>

<h3 id="saml">SAML</h3>

<p>SAML is a standardised protocol for SSO. It’s a particular way of going about asking a third party who a person is.</p>

<p>There are other SSO protocols you may have heard of. OAuth is quite common. OIDC is another. I’m sure there’s more, but thankfully I’ve not had to deal with them.</p>

<p>This series of posts are focused on SAML, though some of the concepts I’m sure apply more broadly.</p>

<h3 id="identity-providers-idps">Identity Providers (IdPs)</h3>

<p>When it comes to the back and forth between sites, we have <strong>identity providers</strong>, or IdPs. These are the services which confirm the identity of the user - i.e. where a password is entered. A very common example when you sign in to a site via Google: Google is the IdP.</p>

<h3 id="service-providers-sps">Service Providers (SPs)</h3>

<p>On the other side of fence is the Service Provider, or SP. This is the site that directs you off to the IdP - the one that has the button that says “Sign in with Google” or similar, and handles the response from the IdP when an identity has been confirmed.</p>]]></content><author><name>Pat Allan</name><uri>https://freelancing-gods.com/about.html</uri></author><category term="ruby" /><category term="rails" /><category term="sso" /><category term="saml" /><summary type="html"><![CDATA[...]]></summary></entry></feed>