Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug?]: Authentication Error After Upgrading from Version 6.6.4 to 7.4.3 #10543

Open
1 task done
furkanhalkan opened this issue May 6, 2024 · 3 comments
Open
1 task done
Labels
bug/needs-info More information is needed for reproduction topic/auth

Comments

@furkanhalkan
Copy link

What's not working?

Hello,

After updating from version 6.6.4 to 7.4.3, I am encountering an error with WebAuthn stating:

Could not start authentication: Cannot retrieve user details without being logged in.

I have not made any changes to the auth-related code, which was working flawlessly in version 6.6.4. However, my project does not function in version 7.4.3, and it does not provide any error messages. Below, I am sharing my schema.prisma and all codes related to auth. Thank you in advance for your support.

schema.prisma:

model User {
  id                  Int              @id @default(autoincrement())
  user_id             String           @unique
  email               String           @unique
  hashedPassword      String
  salt                String
  resetToken          String?
  resetTokenExpiresAt DateTime?
  webAuthnChallenge   String?          @unique
  Clinic_id           String?
  phone_number        String?
  createdAt           DateTime         @default(now())
  updatedAt           DateTime         @updatedAt
  credentials         UserCredential[]
  userdetails         UserDetails[]
}

model UserCredential {
  id         String  @id
  userId     String
  user       User    @relation(fields: [userId], references: [user_id])
  publicKey  Bytes
  transports String?
  counter    BigInt
}

api/src/functions/auth.ts:

import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
import axios from 'axios'
import { v4 as uuidv4 } from 'uuid'

import { DbAuthHandler, DbAuthHandlerOptions } from '@redwoodjs/auth-dbauth-api'

import { db } from 'src/lib/db'

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {

    handler: (user) => {
      return user
    },

    expires: 60 * 60 * 24,

    errors: {
      usernameNotFound: 'Username not found',
      usernameRequired: 'Username is required',
    },
  }

  const loginOptions: DbAuthHandlerOptions['login'] = {
    handler: (user) => {
      return user
    },

    errors: {
      usernameOrPasswordMissing: 'Both username and password are required',
      usernameNotFound: 'Kullanıcı Adı veya Şifre Yanlış',
      incorrectPassword: 'Kullanıcı Adı veya Şifre Yanlış',
    },

    expires: 60 * 60 * 24 * 365 * 10,
  }

  const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
    handler: (_user) => {
      return true
    },

    allowReusedPassword: true,

    errors: {
      resetTokenExpired: 'resetToken is expired',
      resetTokenInvalid: 'resetToken is invalid',
      resetTokenRequired: 'resetToken is required',
      reusedPassword: 'Must choose a new password',
    },
  }

  const signupOptions: DbAuthHandlerOptions['signup'] = {

    handler: async ({ username, hashedPassword, salt, userAttributes }) => {
      try {
        const userid = uuidv4()
        const clinicID = uuidv4()
        const doctorId = uuidv4()

        if (!userAttributes.googlecapth) {
          throw new Error('ReCAPTCHA response is missing.')
        }

        const response = await axios.post(
          `https://www.google.com/recaptcha/api/siteverify?secret=${process.env.GOOGLE_SECRET_KEY}&response=${userAttributes.googlecapth}`
        )

        if (!response.data.success) {
          throw new Error('ReCAPTCHA verification failed.')
        }

        const transactionResult = await db.$transaction([
          db.user.create({
            data: {
              user_id: userid,
              email: username,
              hashedPassword: hashedPassword,
              salt: salt,
              Clinic_id: clinicID,
              phone_number: userAttributes.phonenumber,
            },
          }),
          db.userDetails.create({
            data: {
              user_id: userid,
              Clinic_id: clinicID,
              name_surname: userAttributes.namesurname,
              Clinic_name: userAttributes.clinicname,
              email: username,
              phone_number: userAttributes.phonenumber,
            },
          }),
          db.clinic.create({
            data: {
              Clinic_id: clinicID,
              Clinic_name: userAttributes.clinicname,
              Kurucu_ID: userid,
            },
          }),
          db.doctor.create({
            data:{
              doctor_id:doctorId,
              clinic_id:clinicID,
              name_surname:userAttributes.namesurname,
              phone_number:userAttributes.phonenumber,
              creater_id:userid
            }
          })
        ])

        return transactionResult
      } catch (error) {
        console.error('Signup error:', error)
        throw error // veya belki de kullanıcıya uygun bir hata mesajı dön.
      }
    },

    passwordValidation: (_password) => {
      return true
    },

    errors: {
      fieldMissing: '${field} is required',
      usernameTaken: 'Username `${username}` already in use',
    },
  }

  const authHandler = new DbAuthHandler(event, context, {
    db: db,

    authModelAccessor: 'user',

    credentialModelAccessor: 'userCredential',

    authFields: {
      id: 'user_id',
      username: 'email',
      hashedPassword: 'hashedPassword',
      salt: 'salt',
      resetToken: 'resetToken',
      resetTokenExpiresAt: 'resetTokenExpiresAt',
      challenge: 'webAuthnChallenge',
    },

    cookie: {
      HttpOnly: true,
      Path: '/',
      SameSite: 'Strict',
      Secure: process.env.NODE_ENV !== 'development' ? true : false,
    },

    forgotPassword: forgotPasswordOptions,
    login: loginOptions,
    resetPassword: resetPasswordOptions,
    signup: signupOptions,

    webAuthn: {
      enabled: true,
      expires: 60 * 60 * 24 * 365 * 10,
      name: 'Cube Dental',
      domain:
        process.env.NODE_ENV === 'development' ? 'localhost' : 'server.com',
      origin:
        process.env.NODE_ENV === 'development'
          ? 'http://localhost:8910'
          : 'https://server.com',
      type: 'platform',
      timeout: 60000,
      credentialFields: {
        id: 'id',
        userId: 'userId',
        publicKey: 'publicKey',
        transports: 'transports',
        counter: 'counter',
      },
    },
  })

  return await authHandler.invoke()
}

api/src/lib/auth.ts:

import type { Decoded } from '@redwoodjs/api'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'

import { db } from './db'

/**
 * The session object sent in as the first argument to getCurrentUser() will
 * have a single key `id` containing the unique ID of the logged in user
 * (whatever field you set as `authFields.id` in your auth function config).
 * You'll need to update the call to `db` below if you use a different model
 * name or unique field name, for example:
 *
 *   return await db.profile.findUnique({ where: { email: session.id } })
 *                   ───┬───                       ──┬──
 *      model accessor ─┘      unique id field name ─┘
 *
 * !! BEWARE !! Anything returned from this function will be available to the
 * client--it becomes the content of `currentUser` on the web side (as well as
 * `context.currentUser` on the api side). You should carefully add additional
 * fields to the `select` object below once you've decided they are safe to be
 * seen if someone were to open the Web Inspector in their browser.
 */
export const getCurrentUser = async (session: Decoded) => {
  if (!session || typeof session.id !== 'string') {
    throw new Error('Invalid session')
  }

  return await db.user.findUnique({
    where: { user_id: session.id },
    select: { user_id: true, Clinic_id:true },
  })
}

/**
 * The user is authenticated if there is a currentUser in the context
 *
 * @returns {boolean} - If the currentUser is authenticated
 */
export const isAuthenticated = (): boolean => {
  console.log(context.currentUser)
  return !!context.currentUser
}

/**
 * When checking role membership, roles can be a single value, a list, or none.
 * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client`
 */
type AllowedRoles = string | string[] | undefined

/**
 * Checks if the currentUser is authenticated (and assigned one of the given roles)
 *
 * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles
 *
 * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles,
 * or when no roles are provided to check against. Otherwise returns false.
 */
export const hasRole = (roles: AllowedRoles): boolean => {
  if (!isAuthenticated()) {
    return false
  }

  const currentUserRoles = context.currentUser?.roles

  if (typeof roles === 'string') {
    if (typeof currentUserRoles === 'string') {
      // roles to check is a string, currentUser.roles is a string
      return currentUserRoles === roles
    } else if (Array.isArray(currentUserRoles)) {
      // roles to check is a string, currentUser.roles is an array
      return currentUserRoles?.some((allowedRole) => roles === allowedRole)
    }
  }

  if (Array.isArray(roles)) {
    if (Array.isArray(currentUserRoles)) {
      // roles to check is an array, currentUser.roles is an array
      return currentUserRoles?.some((allowedRole) =>
        roles.includes(allowedRole)
      )
    } else if (typeof currentUserRoles === 'string') {
      // roles to check is an array, currentUser.roles is a string
      return roles.some((allowedRole) => currentUserRoles === allowedRole)
    }
  }

  // roles not found
  return false
}

/**
 * Use requireAuth in your services to check that a user is logged in,
 * whether or not they are assigned a role, and optionally raise an
 * error if they're not.
 *
 * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access.
 *
 * @returns - If the currentUser is authenticated (and assigned one of the given roles)
 *
 * @throws {@link AuthenticationError} - If the currentUser is not authenticated
 * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions
 *
 * @see https://github.com/redwoodjs/redwood/tree/main/packages/auth for examples
 */
export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
  if (!isAuthenticated()) {
    throw new AuthenticationError("You don't have permission to do that.")
  }

  if (roles && !hasRole(roles)) {
    throw new ForbiddenError("You don't have access to do that.")
  }
}

I appreciate any help or guidance on resolving this issue.

How do we reproduce the bug?

No response

What's your environment? (If it applies)

System:
    OS: Windows 11 10.0.22631
  Binaries:
    Node: 20.10.0 - ~\AppData\Local\Temp\xfs-fc6bc9b1\node.CMD
    Yarn: 3.6.3 - ~\AppData\Local\Temp\xfs-fc6bc9b1\yarn.CMD
  Browsers:
    Edge: Chromium (123.0.2420.97)
  npmPackages:
    @redwoodjs/api: 7.4.3 => 7.4.3
    @redwoodjs/auth-dbauth-setup: 7.4.3 => 7.4.3
    @redwoodjs/cli-storybook: 7.4.3 => 7.4.3
    @redwoodjs/core: 7.4.3 => 7.4.3

Are you interested in working on this?

  • I'm interested in working on this
@furkanhalkan furkanhalkan added the bug/needs-info More information is needed for reproduction label May 6, 2024
@Josh-Walker-GM
Copy link
Collaborator

Thanks for opening this issue and providing so much detail!

I'm going to be debugging a potentially related bug to do with our request context which has been reported to be broken between the v6->v7 upgrade. I'll follow up to this issue after checking the context one. I aim to get back to you in a day or two - if that sounds okay?

@furkanhalkan
Copy link
Author

Thank you for the quick response and for looking into the issue! I am happy to wait for the updates you mentioned. Your assistance is greatly appreciated.

@dthyresson
Copy link
Contributor

Hi @furkanhalkan I'm having a look at this and helping @Josh-Walker-GM and as I investigated, I did notice a change in the way getCurrentUser is implemented in 6.6.4 and 7.4.3:

Compare

6.6.4

user = await this.dbAccessor.findUnique({

      user = await this.dbAccessor.findUnique({
        where: { [this.options.authFields.id]: this.session?.id },
        select,
      })

7.4.3.

user = await this.dbAccessor.findUnique({

      user = await this.dbAccessor.findUnique({
        where: {
          [this.options.authFields.id]:
            this.session?.[this.options.authFields.id],
        },
        select,
      })

The value for the session is a little different.

Note sure that is your issue, but the error Cannot retrieve user details without being logged in. happens when

  async _getCurrentUser() {
    if (!this.session?.[this.options.authFields.id]) {
      throw new DbAuthError.NotLoggedInError()
    }

Or actually it could be that, too.

Either that or the this.session?.[this.options.authFields.id isn't set properly.

Let me know if that helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug/needs-info More information is needed for reproduction topic/auth
Projects
None yet
Development

No branches or pull requests

4 participants