nuxt-auth-utils icon indicating copy to clipboard operation
nuxt-auth-utils copied to clipboard

LDAP Integration

Open Dino-Kupinic opened this issue 1 year ago • 5 comments

Are there any plans for LDAP auth? This feature is relevant for Microsoft Windows based infrastructure (Windows Domain Controller), mostly on-prem.

It is still widely used in Enterprise, though rather legacy compared to SAML and OAuth. I think this might make Nuxt more compelling for these larger organizations (even schools etc.)

thoughts?

Dino-Kupinic avatar Sep 11 '24 15:09 Dino-Kupinic

I have zero knowledge on LDAP auth actually, do you have any resources to explain it?

atinux avatar Sep 11 '24 15:09 atinux

I have zero knowledge on LDAP auth actually, do you have any resources to explain it?

I'm not an expert on this topic, but I found these articles:

https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol

https://www.okta.com/identity-101/what-is-ldap/#:~:text=Lightweight%20directory%20access%20protocol%20(LDAP,conversation%20on%20a%20new%20printer.

https://www.redhat.com/en/topics/security/what-is-ldap-authentication

https://jumpcloud.com/blog/what-is-ldap-authentication

I also found this library: https://www.npmjs.com/package/ldap-authentication

LDAP auth would allow users to use the same account they already use with windows, microsoft outlook etc. Big plus for internal apps and getting approval from sys admins

Dino-Kupinic avatar Sep 11 '24 16:09 Dino-Kupinic

I am using the ldapts library with this module

Here's a stripped down version of how I'm doing it

/server/api/auth/login.post.js

import { Client } from 'ldapts';

export default defineEventHandler(async (event) => {
  const { username, password } = await readBody(event)

  const client = new Client({
    url: 'ldap://mydomaincontroller.mydomain.local',
  });

  try {
    await client.bind(`mydomain\\${username}`, password);
    loginSuccess = true;
  }
  catch {
     throw createError({
      statusCode: 403,
      statusMessage: 'Invalid Username or Password',
    })
  }
  finally {
    await client.unbind();
  }

  await setUserSession(event, {
    user: {
      ...
    },
  })

  return sendNoContent(event)
})

jaketig avatar Sep 11 '24 20:09 jaketig

Hi there, Thank you for the solution. Could you guide me a bit more on this ? I havent been abble to make it work for now. Best

quentinglorieux avatar Jan 08 '25 13:01 quentinglorieux

I was able to get SAML working for me, using the saml2-js library. Here's what I did, in case this is helpful to anyone else. There are 2 pieces, one with the options that you get from your SAML provider (this was Microsoft Entra in my case), and the second one being a middleware that captures all of the /saml routes and directs them to the correct page. So to login, I would go to localhost:3000/saml/login, and to logout I would go to localhost:3000/saml/logout. Hopefully this is helpful to someone else as it took me a while to figure out!

npm install saml2-js

/server/helpers/saml.model.ts

import saml2 from 'saml2-js'
import fs from 'fs'

const spOptions = {
  entity_id: "ENTITY_ID",
  private_key: fs.readFileSync("MY_KEY.key").toString(),
  certificate: fs.readFileSync("MY_CERT.pem").toString(),
  assert_endpoint: "http://localhost:3000/saml/assert"
}
export const sp = new saml2.ServiceProvider(spOptions)

const idpOptions = {
  sso_login_url: "MY_LOGIN_URL",
  sso_logout_url: "MY_LOGOUT_URL",
  certificates: [fs.readFileSync("SAML_CERT.cer").toString()],
  allow_unencrypted_assertion: true
}
export const idp = new saml2.IdentityProvider(idpOptions)

/server/middleware/saml.ts

import { sp, idp } from '../helpers/saml.model'
import { H3Event } from 'h3'
import { sendRedirect } from 'h3'
import { Users } from '../api/users.model'

export default defineEventHandler(async (event: H3Event) => {
  const urlObj = getRequestURL(event)

  const temp = urlObj.pathname.substring(1).split('/')
  if (temp[0] === 'saml') {
    switch (temp[1]) {
      case 'login':
        return new Promise((resolve, reject) => {
          sp.create_login_request_url(idp, {}, function(err: any, login_url: string, request_id: string) {
            if (err != null) {
              throw createError({
                statusCode: 500,
                statusMessage: err,
              })
            }
            return resolve(sendRedirect(event, login_url))
          })
        })
        break
      case 'assert':
        const request_body = await readBody(event)
        return new Promise((resolve, reject) => {
          sp.post_assert(idp, { request_body }, async function (err: Error, saml_response: any) {
            if (err != null) {
              throw createError({
                statusCode: 500,
                statusMessage: err.message,
              })
            }

            // Save name_id and session_index for logout
            // Note:  In practice these should be saved in the user session, not globally.
            const roles = <string[]>[]
            const { name_id, attributes: samlUser } = saml_response.user

            // See if this user exists in the Users collection
            const res = await Users.find({ email: samlUser.email })
            if (res.length === 0) {
              // If not, add them
              const res = await Users.create(samlUser)
              user._id = res._id
            } else {
              // If so, update them
              user._id = res[0]._id
              await Users.updateOne({ _id: res[0]._id }, samlUser)
            }

            await setUserSession(event, {
              user,
              loggedInAt: Date.now(),
            })

            resolve(sendRedirect(event, "http://localhost:3000"))
          })
        })
        break
      case 'logout':
        break
      case 'manifest.xml':
        setResponseHeaders(event, {
          "Content-Disposition": `attachment; filename=test.xml`,
          "Content-Type": "text/xml; charset=latin1",
        })
        return sp.create_metadata()
        break
    }
  }
})

jondmoon avatar Feb 24 '25 19:02 jondmoon