<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet href="/rss/stylesheet/" type="text/xsl"?>
<rss xmlns:content='http://purl.org/rss/1.0/modules/content/' xmlns:taxo='http://purl.org/rss/1.0/modules/taxonomy/' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:itunes='http://www.itunes.com/dtds/podcast-1.0.dtd' xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0" xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:atom='http://www.w3.org/2005/Atom' xmlns:podbridge='http://www.podbridge.com/podbridge-ad.dtd' version='2.0'>
<channel>
  <title>Jaryl Chng&apos;s Knowledge Base</title>
  <language>en-us</language>
  <generator>microfeed.org</generator>
  <itunes:type>episodic</itunes:type>
  <itunes:explicit>false</itunes:explicit>
  <atom:link rel="self" href="https://kb-jarylchng-com.pages.dev/rss/" type="application/rss+xml"/>
  <link>https://kb.jarylchng.com</link>
  <description>
    <![CDATA[<p>Welcome to the index page of my knowledge base, if you haven't done so, do visit my website at <a href="https://jarylchng.com" rel="noopener noreferrer" target="_blank">https://jarylchng.com</a>.</p><p>I will mainly use this site to document stuff, most of which will likely be in the public domain.</p>]]>
  </description>
  <itunes:author>Jaryl Chng</itunes:author>
  <itunes:image href="https://kb-static.jarylchng.com/kb-jarylchng-com/production/images/channel-c68f1f55f856ab833b4365991609dbec.png"/>
  <image>
    <title>Jaryl Chng&apos;s Knowledge Base</title>
    <url>https://kb-static.jarylchng.com/kb-jarylchng-com/production/images/channel-c68f1f55f856ab833b4365991609dbec.png</url>
    <link>https://kb.jarylchng.com</link>
  </image>
  <copyright>©2024</copyright>
  <itunes:category text="Technology"/>
  <item>
    <title>n8n &amp; Authelia - Bypass n8n native login page using Trusted Header Single Sign-On and custom hooks configuration</title>
    <guid>sNRmS-7j5u1</guid>
    <pubDate>Thu, 18 Apr 2024 03:55:34 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<h2>From v1.110.1 onwards, please use this snippet instead</h2><pre class="ql-syntax" spellcheck="false">const { dirname, resolve } = require('path')
const Layer = require('router/lib/layer')
const { issueCookie } = require(resolve(dirname(require.resolve('n8n')), 'auth/jwt'))
const ignoreAuthRegexp = /^\/(assets|healthz|webhook|rest\/oauth2-credential)/
module.exports = {
    n8n: {
        ready: [
            async function ({ app }, config) {
                const { stack } = app.router
                const index = stack.findIndex((l) =&gt; l.name === 'cookieParser')
                stack.splice(index + 1, 0, new Layer('/', {
                    strict: false,
                    end: false
                }, async (req, res, next) =&gt; {
                    // skip if URL is ignored
                    if (ignoreAuthRegexp.test(req.url)) return next()

                    // skip if user management is not set up yet
                    if (!config.get('userManagement.isInstanceOwnerSetUp', false)) return next()

                    // skip if cookie already exists
                    if (req.cookies?.['n8n-auth']) return next()

                    // if N8N_FORWARD_AUTH_HEADER is not set, skip
                    if (!process.env.N8N_FORWARD_AUTH_HEADER) return next()

                    // if N8N_FORWARD_AUTH_HEADER header is not found, skip
                    const email = req.headers[process.env.N8N_FORWARD_AUTH_HEADER.toLowerCase()]
                    if (!email) return next()

                    // search for user with email
                    const user = await this.dbCollections.User.findOneBy({email})
                    if (!user) {
                        res.statusCode = 401
                        res.end(`User ${email} not found, please have an admin invite the user first.`)
                        return
                    }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (!user.role) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; user.role = {}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

                    // issue cookie if all is OK
                    issueCookie(res, user)
                    return next()
                }))
            },
        ],
    },
}
</pre><h2>From v1.87.0 onwards, please use this snippet instead</h2><pre class="ql-syntax" spellcheck="false">const { dirname, resolve } = require('path')
const Layer = require('router/lib/layer')
const { issueCookie } = require(resolve(dirname(require.resolve('n8n')), 'auth/jwt'))
const ignoreAuthRegexp = /^\/(assets|healthz|webhook|rest\/oauth2-credential)/
module.exports = {
    n8n: {
        ready: [
            async function ({ app }, config) {
                const { stack } = app.router
                const index = stack.findIndex((l) =&gt; l.name === 'cookieParser')
                stack.splice(index + 1, 0, new Layer('/', {
                    strict: false,
                    end: false
                }, async (req, res, next) =&gt; {
                    // skip if URL is ignored
                    if (ignoreAuthRegexp.test(req.url)) return next()

                    // skip if user management is not set up yet
                    if (!config.get('userManagement.isInstanceOwnerSetUp', false)) return next()

                    // skip if cookie already exists
                    if (req.cookies?.['n8n-auth']) return next()

                    // if N8N_FORWARD_AUTH_HEADER is not set, skip
                    if (!process.env.N8N_FORWARD_AUTH_HEADER) return next()

                    // if N8N_FORWARD_AUTH_HEADER header is not found, skip
                    const email = req.headers[process.env.N8N_FORWARD_AUTH_HEADER.toLowerCase()]
                    if (!email) return next()

                    // search for user with email
                    const user = await this.dbCollections.User.findOneBy({email})
                    if (!user) {
                        res.statusCode = 401
                        res.end(`User ${email} not found, please have an admin invite the user first.`)
                        return
                    }

                    // issue cookie if all is OK
                    issueCookie(res, user)
                    return next()
                }))
            },
        ],
    },
}
</pre><p><br></p><p>I use Authelia as my SSO solution for my home services, however after n8n released v1.0, they seem to have removed Basic Authentication and locked SAML and OIDC behind an enterprise license. They seem to have removed the options to disable user management completely as well. I do believe they have never supported <a href="https://www.authelia.com/integration/trusted-header-sso/introduction/" rel="noopener noreferrer" target="_blank">Trusted Header SSO</a> even before v1.0.</p><p>My main goal was to avoid the need to key in 2 sets of credentials every time (Authelia and n8n), and to have a more seamless SSO experience by bypassing the n8n login page.</p><h2>Solution</h2><blockquote>I would not have easily found this solution without the help of <a href="https://community.n8n.io/t/self-hosted-user-management/30520/4" rel="noopener noreferrer" target="_blank">@MutedJam and @netroy on the N8N community forum</a>.</blockquote><p>With the help of the `n8n.ready` <a href="https://docs.n8n.io/embed/configuration/#backend-hooks" rel="noopener noreferrer" target="_blank">backend external hook</a>, we are able to intercept and add another middleware to issue a JWT authentication token when the header is detected.</p><h2>Assumptions</h2><ul><li>n8n is hosted using Docker</li><li><a href="https://www.authelia.com/integration/trusted-header-sso/introduction/#forwarding-the-response-headers" rel="noopener noreferrer" target="_blank">Your reverse proxy is setting and forwarding the Remote-Email header from Authelia</a></li><li>n8n is already secured by Authelia on a reverse proxy</li></ul><blockquote>Remote-Email is used by majority of the reverse proxy guides on Authelia documentation, if you did not customize much, it should be the same.</blockquote><h2>Instructions</h2><h3>Ensure your user's e-mail matches Authelia</h3><p>Make sure to change it to match your LDAP store or <a href="https://github.com/authelia/authelia/blob/master/examples/compose/lite/authelia/users_database.yml#L15" rel="noopener noreferrer" target="_blank">flat file</a> depending on your Authelia configuration.</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/sNRmS-7j5u1/image-65fff4e06bf50ca4cf8cff012be341ec.png"></p><h3>Creating the External Hook file</h3><p>Create a file named `hooks.js` and place this file somewhere your n8n instance can reach.</p><p>In the case of the official Docker image, it is where you mounted `/home/node/` to, and for the sake of this guide, I will place it at where it would be effectively mounted at `/home/node/.n8n/hooks.js`.</p><p>Copy and paste the following into the file:</p><pre class="ql-syntax" spellcheck="false">const { dirname, resolve } = require('path')
const Layer = require('express/lib/router/layer')
const { issueCookie } = require(resolve(dirname(require.resolve('n8n')), 'auth/jwt'))
const ignoreAuthRegexp = /^\/(assets|healthz|webhook|rest\/oauth2-credential)/
module.exports = {
    n8n: {
        ready: [
            async function ({ app }, config) {
                const { stack } = app._router
                const index = stack.findIndex((l) =&gt; l.name === 'cookieParser')
                stack.splice(index + 1, 0, new Layer('/', {
                    strict: false,
                    end: false
                }, async (req, res, next) =&gt; {
                    // skip if URL is ignored
                    if (ignoreAuthRegexp.test(req.url)) return next()

                    // skip if user management is not set up yet
                    if (!config.get('userManagement.isInstanceOwnerSetUp', false)) return next()

                    // skip if cookie already exists
                    if (req.cookies?.['n8n-auth']) return next()

                    // if N8N_FORWARD_AUTH_HEADER is not set, skip
                    if (!process.env.N8N_FORWARD_AUTH_HEADER) return next()

                    // if N8N_FORWARD_AUTH_HEADER header is not found, skip
                    const email = req.headers[process.env.N8N_FORWARD_AUTH_HEADER.toLowerCase()]
                    if (!email) return next()

                    // search for user with email
                    const user = await this.dbCollections.User.findOneBy({email})
                    if (!user) {
                        res.statusCode = 401
                        res.end(`User ${email} not found, please have an admin invite the user first.`)
                        return
                    }

                    // issue cookie if all is OK
                    issueCookie(res, user)
                    return next()
                }))
            },
        ],
    },
}
</pre><h3>Configure extra Docker container environment variables</h3><p>Ensure to append a 2 new environment variable to your n8n instance and re-create the container depending on how you deployed it:</p><pre class="ql-syntax" spellcheck="false">EXTERNAL_HOOK_FILES=/home/node/.n8n/hooks.js
N8N_FORWARD_AUTH_HEADER=Remote-Email
</pre><p>Reference: <a href="https://docs.n8n.io/embed/configuration/#registering-hooks" rel="noopener noreferrer" target="_blank">Registering hooks with EXTERNAL_HOOK_FILES</a></p><h2>Precautions</h2><p>Make sure to secure n8n properly by making it only accessible via your reverse proxy. Do not allow direct access as any user would be able to impersonate someone by sending a custom `Remote-Email` header if so.</p><h2>Afterword</h2><p>It seems that it is likely possible to re-create a whole SAML/OIDC set-up just by implementing it in the hook, but it is definitely a project on its own and definitely out of scope for this post. However, you are free to customize the script however you wish according to your needs. There is likely a way to automatically create users as well, but I have yet to really dig into the n8n codes.</p><p>I've really only tested this on Nginx, but it should be reverse proxy agnostic.</p><p>If you would like another workflow automation software that does not gate OIDC (at least for the first 10 users) with a license, you could look at <a href="https://www.windmill.dev/" rel="noopener noreferrer" target="_blank">Windmill</a>.</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/n8n-and-authelia-bypass-n8n-native-login-page-usin-sNRmS-7j5u1/</link>
    <itunes:episodeType>full</itunes:episodeType>
    <enclosure url="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/video-b978a4a52c0ab6c33e8a8a75f1851add.mp4" type="video/mp4" length="174006"/>
    <itunes:duration>00:00:04</itunes:duration>
  </item>
</channel>
</rss>