[Bug?]: Authentication Error After Upgrading from Version 6.6.4 to 7.4.3
furkanhalkan opened this issue · comments
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
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?
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.
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({
where: { [this.options.authFields.id]: this.session?.id },
select,
})
7.4.3.
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.