jlguenego / node-expose-sspi

Expose Microsoft Windows SSPI to Node for SSO authentication.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Fasitify + CORS = no authorization key in header

gjovanov opened this issue · comments

Describe the bug
In this fastify setup:

const corOptions = {
origin: '*',
methods: ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: ['content-type', 'authorization', 'access-control-allow-origin', 'access-control-allow-credentials'],
credentials: true
}

await fastify
.register(require('middie'))
.register(require('fastify-cookie'))
.register(require('fastify-cors'), corsOptions)

await fastify
.use('/api/auth/login', require('cors')(corsOptions))

await fastify
.use('/api/auth/login', sso.auth({ useGroups: false, useOwner: false, useActiveDirectory: false }))

with this route:

const routes = {
authenticate: true,
method: 'GET',
url: '/api/auth/login',
schema: {
response: {
200: authSchema.login.response[200],
409: errorSchema.response[409],
500: errorSchema.response[500]
}
},
handler: authController.login
}

authController being an instance of:

class AuthController {
login (request, reply) {
reply.send(request.raw.sso ? request.raw.sso.user : { error: 'Unknown' })
}
}

await fastify
.decorate('authenticate', async (request, reply, done) => {
const handler = sso.auth({ useGroups: false, useOwner: false, useActiveDirectory: false })
await handler(request, reply, done)
})
.after(() => {
routes.forEach((route) => {
fastify.route(route)
})
})

fastify being started on localhost:3001.

When Accessed without cors:

http://localhost:3001/api/auth/login

Everything works fine.

However, when accessed from another app listening on port 3000, these trace logs can be found:

node-expose-sspi:schManager initCookie +0ms
node-expose-sspi:schManager cookie not found, so generating one +11ms
node-expose-sspi:auth cookieToken: NEGOTIATE_8946007805 +0ms
node-expose-sspi:auth no authorization key in header +0ms

To Reproduce

Fastify + CORs setup as described from above, doesn't work

Trace
node-expose-sspi:schManager initCookie +0ms
node-expose-sspi:schManager cookie not found, so generating one +11ms
node-expose-sspi:auth cookieToken: NEGOTIATE_8946007805 +0ms
node-expose-sspi:auth no authorization key in header +0ms

Expected behavior

Same as without CORS

Screenshots
N/A

Environment version:

  • OS: Windows 10 Pro version
  • Browser version: Chrome 86.0.4240
  • Node version and architecture: v15.1.0

Please indicates also:

  • Are you on a Windows domain ? yes
  • Can you reach the domain controller ? yes
  • Do your session have admin privileges ? no
  • Which authentication protocol ? NTLM
  • Active Directory, or local window policies that could impact the authentication. N/A

Additional context
N/A

I am going to investigate.

The problem is that you register middie before the fastify-cors. And you should not be using cors and fastify-cors at same time.

The hook registered by middie (sso.auth, etc.) should be registered AFTER the cors. Because the browser always needs the cors headers, even if the answer is 401.

If ok with my answer, please close the defect.

I have investigated a little bit more.

TL;DR : look at the working tested solution at https://github.com/jlguenego/angular-sso-example

First you cannot use origin with '*' when you want to do CORS with XHR, using also cookies. You need to send the origin with the domain of the client (stored in the Origin request header).
see for instance https://stackoverflow.com/questions/46288437/set-cookies-for-cross-origin-requests

Also CORS credentials must be set (and you have done it)

The snippet:

    fastify.use(
      cors((req, callback) => {
        const options = {
          credentials: true,
          origin: req.headers.origin,
        };
        callback(null, options);
      }) as NextHandleFunction
    );

But... also on client side, you need to set "withCredentials" EVERYWHERE (again for the cookies).
Example with Angular:

       const { sso } = await this.http
        .get<ConnectWithSSOResponse>(domainUrl + '/ws/is-connected', {
          withCredentials: true,
        })
        .toPromise();

There is also a problem with Fastify : this framework retains the header setting. So it is not because we do reply.header('thing') = 'something' that the header will be really sent, specially if another middleware decides to send back the HTTP response (like node-expose-sspi).

So please do not use fastify-cors but cors and middie.

Please look at this code.
https://github.com/jlguenego/angular-sso-example/blob/master/back-fastify/src/server.ts

I think now this defect can be closed 😄

Thanks for clarifying @jlguenego . I will give a try today and let you know if it works and close it accordingly.

Hey @jlguenego ,
I tried the above proposal and it worked.

Here is the end reduced changes I've made:

  1. added the { withCredentials: true } to all ajax requests from the CORs domain
  2. Replaced the order of middie and fastify-cors plugin registration as follows:
await fastify
.register(require('fastify-cookie'))
.register(require('fastify-cors'), corsOptions)
.register(require('middie'))
  1. Removed the usage of cors plugin
  2. Replaced the asterisk * with the exact CORS origin http://localhost:3000
const options = {
  origin: 'http://localhost:3000',   // instead of '*'
  methods: ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
  credentials: true
}

Pls note that after this, the cors is not needed anymore. fastify-cors does the CORs handling just fine, even though sso.auth({ ... }) is registered via middie.

Anyways, I'm closing as after the steps from above it works just fine.

Many thanks for your help @jlguenego and awesome work with this repo!

I have to retest with fastify-cors, may be I did not see something... but anyway, you have your solution and that is what matters.