1
\$\begingroup\$

I have created a REST auth layer in front of my GraphQL API, I would like to know if it is secure enough or if I can improve anything, I will be adding more auth providers, in the end this API will serve a web app using the demonstrated Google log in and a React Native app with email, SMS, Facebook and Apple auth

// index.ts
server
  .register(fastifyCors, {
    origin: process.env.CORS_ORIGIN,
    credentials: true,
  })
  .register(fastifyCookie)
  .register(fastifySession, {
    cookieName: "fid",
    store: new RedisStore({
      client: new Redis(process.env.REDIS_URI),
      ttl: SESSION_TTL,
    }),
    cookie: {
      maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
      httpOnly: true,
      sameSite: "lax",
      secure: true,
    },
    saveUninitialized: false,
    secret: process.env.SESSION_SECRET,
  })
  .register(fastifyPassport.initialize())
  .register(apolloServer.createHandler({ cors: false }))
  .register(authRoutes)
// setupPassport.ts
export const setupPassport = (prisma: PrismaClient) => {
  fastifyPassport.registerUserSerializer<User, User["id"]>(
    async (user) => user.id
  )

  fastifyPassport.registerUserDeserializer<User["id"], User>(async (id) => {
    const user = await prisma.user.findUnique({
      where: {
        id,
      },
    })

    if (!user) {
      throw new Error("No user")
    }

    return user
  })

  fastifyPassport.use(
    new Strategy(
      {
        clientID: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
        callbackURL: "http://localhost:4000/auth/google/callback",
      },
      async (_accessToken, _refreshToken, profile, done) => {
        const existingUser = await prisma.identity.findFirst({
          where: {
            provider: "GOOGLE",
            providerId: profile.id,
          },
          include: {
            user: true,
          },
        })

        if (existingUser) {
          return done(undefined, existingUser)
        }

        if (!profile.emails?.length || !profile.name) {
          throw new Error("No email or name")
        }

        const newUser = await prisma.user.create({
          data: {
            ...profile.name,
            email: profile.emails[0].value,
            identities: {
              create: [{ provider: "GOOGLE", providerId: profile.id }],
            },
          },
        })

        return done(undefined, newUser)
      }
    )
  )
}
// authRoutes.ts
export const authRoutes: FastifyPluginCallback = (fastify, _opts, done) => {
  fastify.get(
    "/auth/google",
    {
      preValidation: fastifyPassport.authenticate("google", {
        scope: ["profile", "email"],
      }),
    },
    async (request, reply) => {}
  )

  fastify.get(
    "/auth/google/callback",
    {
      preValidation: fastifyPassport.authorize("google", { session: false }),
    },
    async (request, reply) => {}
  )

  done()
}
// permissions.ts
import { shield } from "graphql-shield"

export const permissions = shield({
  Query: {},
  Mutation: {},
  ShopLocation: rules.isAuthenticatedUser,
})

// rules.ts
import { rule } from "graphql-shield"

export const rules = {
  isAuthenticatedUser: rule()(async (_parent, _args, ctx: Context) => {
    return ctx.request.isAuthenticated()
  }),
  isAdmin: rule()(async (_parent, _args, ctx: Context) => {
    return ctx.request.user.role === Role.ADMIN
  }),
  isOperator: rule()(async (_parent, _args, ctx: Context) => {
    return ctx.request.user.role === Role.OPERATOR
  }),
  isUser: rule()(async (_parent, _args, ctx: Context) => {
    return ctx.request.user.role === Role.USER
  }),
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy