lucia-auth / lucia

Authentication, simple and clean
https://lucia-auth.com
MIT License
8.32k stars 447 forks source link

[Feature Request]: Fastify guide? #1406

Open Sn0wye opened 4 months ago

Sn0wye commented 4 months ago

Package

lucia

Description

Fastify guide?

Seen a lot of other frameworks, and Fastify, as it is one of the most popular alternatives of express should have a guide.

Sn0wye commented 4 months ago

I'm implementing it myself, and if you want the code, I will share it soon :D

pilcrowOnPaper commented 4 months ago

Yeah, we should probably start off by adding one in "get started" and "validate session cookies"

Sn0wye commented 4 months ago

Hey, apologies for the delay, but here's what I've done!

// plugins/csrf.ts
import { fastifyPlugin } from 'fastify-plugin';
import { verifyRequestOrigin } from 'lucia';
import { type App } from '@/app';

type Options = {
  enabled: boolean;
};

export const csrfPlugin = fastifyPlugin(
  async (app: App, opts: Options) => {
    if (!opts.enabled) {
      return;
    }

    app.addHook('preHandler', (req, res, done) => {
      if (req.method === 'GET') {
        return done();
      }

      const originHeader = req.headers.origin ?? null;
      // NOTE: You may need to use `X-Forwarded-Host` instead
      const hostHeader = req.headers.host ?? null;
      if (
        !originHeader ||
        !hostHeader ||
        !verifyRequestOrigin(originHeader, [hostHeader])
      ) {
        // TODO: verify this
        console.error('Invalid origin', { originHeader, hostHeader });
        return res.status(403);
      }
    });
  },
  {
    name: 'csrf',
    fastify: '4.x'
  }
);
Sn0wye commented 4 months ago
//plugins/auth.ts
import { fastifyPlugin } from 'fastify-plugin';
import { type Session, type User } from 'lucia';
import { type App } from '@/app';
import { auth } from '@/auth/lucia';

export const authPlugin = fastifyPlugin(
  async (app: App) => {
    app.addHook('preHandler', async (req, res) => {
      const sessionId = auth.readSessionCookie(req.headers.cookie ?? '');

      if (!sessionId) {
        req.user = null;
        req.session = null;
        return;
      }

      const { session, user } = await auth.validateSession(sessionId);
      if (session && session.fresh) {
        const cookie = auth.createSessionCookie(session.id);
        res.setCookie(cookie.name, cookie.value, cookie.attributes);
      }

      if (!session) {
        const cookie = auth.createBlankSessionCookie();
        res.setCookie(cookie.name, cookie.value, cookie.attributes);
      }

      req.user = user;
      req.session = session;
      return;
    });
  },
  {
    name: 'auth',
    fastify: '4.x'
  }
);

declare module 'fastify' {
  interface FastifyRequest {
    user: User | null;
    session: Session | null;
  }
}
Sn0wye commented 4 months ago
// app.ts

export const app = fastify();

app.register(csrfPlugin, {
  enabled: env.NODE_ENV === 'production'
});
app.register(authPlugin);
Sn0wye commented 4 months ago

Side note: the code might have some things that are not needed, and I took it from my own application, but the core remains the same, feel free to simplify it!

StepanMynarik commented 3 months ago

Thank you @Sn0wye , I will try it soon.

Sn0wye commented 3 months ago

Thank you @Sn0wye , I will try it soon.

Thanks, but the csrf is not working, if you manage to do so, please share what did I miss!

heethjain21 commented 3 months ago

I implemented this a few days ago, and wasn't able to check until today. Had assumed the code was correct, but found issues today along with your comment of the error.

I have a slightly different version of auth.plugin, so that works but csrf has a very minor missing code:

There is a missing done() once the csrf validation if correct. Because of this, the API call doesn't moves forward and is also stuck without throwing any error too Along with this, there is also a missing send() in case of Forbidden error

Adding the done() in the else block

if (
    !originHeader ||
    !hostHeader ||
    !verifyRequestOrigin(originHeader, [hostHeader])
) {
      console.error('Invalid origin', { originHeader, hostHeader })
      return res.status(403).send({ message: 'Forbidden' })
} else {
      done()
}

@Sn0wye this should fix the issue here

Sn0wye commented 3 months ago

I implemented this a few days ago, and wasn't able to check until today. Had assumed the code was correct, but found issues today along with your comment of the error.

I have a slightly different version of auth.plugin, so that works but csrf has a very minor missing code:

There is a missing done() once the csrf validation if correct. Because of this, the API call doesn't moves forward and is also stuck without throwing any error too Along with this, there is also a missing send() in case of Forbidden error

Adding the done() in the else block

if (
    !originHeader ||
    !hostHeader ||
    !verifyRequestOrigin(originHeader, [hostHeader])
) {
      console.error('Invalid origin', { originHeader, hostHeader })
      return res.status(403).send({ message: 'Forbidden' })
} else {
      done()
}

@Sn0wye this should fix the issue here

nice catch! but here the plugin was running fine, since it has two definitions, one using the done, and the other being async (no need for done, just return). My problem was in production with the host and origin headers that never matched

heethjain21 commented 3 months ago

My problem was in production with the host and origin headers that never matched

Was it related to not calling the send() on returning res.status(403) ?