Custom auth strategy breaks admin login page
Closed this issue · 2 comments
Describe the Bug
I want to replace disable Payload local strategy entirely to add the functionality to login using email and mobile number simultaneously. But when I login to admin, I get a blank page of only Payload logo:
User Collection
import { authStrategy } from '@/collection/authStrategy'
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: {
disableLocalStrategy: true,
strategies: [authStrategy],
},
fields: [
{
name: 'email',
type: 'text',
required: true,
unique: true,
},
{
name: 'hash',
type: 'text',
required: false,
},
],
}
@/collection/authStrategy
import crypto from 'crypto'
import jwt from 'jsonwebtoken'
import {
AuthStrategy,
AuthStrategyFunctionArgs,
AuthStrategyResult,
parseCookies,
type Payload,
User,
} from 'payload'
export async function getMe({
headers,
payload,
}: {
headers: Request['headers']
payload: Payload
}): Promise<User | null> {
const cookie = parseCookies(headers)
const token = cookie.get(`${payload.config.cookiePrefix}-token`)
if (!token) return null
let jwtUser: jwt.JwtPayload | string
try {
jwtUser = jwt.verify(
token,
crypto.createHash('sha256').update(payload.config.secret).digest('hex').slice(0, 32),
{
algorithms: ['HS256'],
},
)
} catch (e) {
if (e instanceof jwt.TokenExpiredError) return null
throw e
}
if (typeof jwtUser === 'string' || typeof jwtUser.email !== 'string') return null
const usersQuery = await payload.find({
collection: 'users',
where: { email: { equals: jwtUser.email } },
depth: 10,
})
return {
collection: 'User',
...usersQuery.docs[0],
}
}
async function authenticate({
headers,
payload,
}: AuthStrategyFunctionArgs): Promise<AuthStrategyResult> {
const me = await getMe({ headers, payload })
if (!me) {
return { user: null }
}
return {
user: me,
}
}
export const authStrategy: AuthStrategy = {
name: 'risaman',
authenticate,
}
/app/(payload)/api/users/login/route.ts
import { handleLogin } from '@/app/(payload)/api/users/login/handleLogin'
import { respond } from '@/respond'
import { PayloadRequest } from 'payload'
import { z } from 'zod'
export async function handleLogin({ email, password }: { email: string; password: string }) {
const payload = await getPayloadHMR({ config })
const data = await payload.find({
collection: 'users',
where: {
email: { equals: email },
},
depth: 10,
})
const user = data.docs[0]
const isValid = await bcrypt.compare(password, user.hash || '')
if (!isValid) {
return [
null,
Response.json(
{
message: 'Not passed',
},
{
status: 401,
},
),
]
}
const collectionConfig = payload.collections['users'].config
const payloadConfig = payload.config
const fieldsToSign = getFieldsToSign({
collectionConfig,
email: user.email,
user: {
collection: 'users',
...user,
},
})
const token = jwt.sign(fieldsToSign, payload.secret, {
expiresIn: collectionConfig.auth.tokenExpiration,
})
const cookie = generatePayloadCookie({
collectionAuthConfig: collectionConfig.auth,
cookiePrefix: payloadConfig.cookiePrefix,
token,
})
return [
user,
Response.json(
{
message: 'Passed',
},
{
headers: {
'set-cookie': cookie,
},
},
),
]
}
const schema = z.object({
email: z.string().email(),
password: z.string().min(7),
})
type LoginPayload = z.infer<typeof schema>
async function readData(request: PayloadRequest): Promise<LoginPayload> {
if (!request.formData) {
throw new Error('Insufficient data')
}
const payload = (await request.formData()).get('_payload')
const fields = typeof payload === 'string' ? JSON.parse(payload) : null
return schema.parse(fields)
}
export async function POST(request: PayloadRequest) {
let payload: LoginPayload
try {
payload = await readData(request)
} catch (error) {
const response = {
message: 'Validation failed',
...(error instanceof z.ZodError ? { errors: error.format() } : {}),
}
return respond(response, 400)
}
const [user, response] = await handleLogin(payload)
return response
}
Link to the code that reproduces this issue
https://github.com/bmamouri/payload-auth-replacement
Reproduction Steps
I have a minimum reproduction repo
Which area(s) are affected? (Select all that apply)
area: core
Environment Info
Node.js v18.20.4
Binaries:
Node: 18.20.4
npm: 10.7.0
Yarn: N/A
pnpm: 9.12.1
Relevant Packages:
payload: 3.0.0-beta.123
next: 15.0.0
@payloadcms/db-postgres: 3.0.0-beta.123
@payloadcms/graphql: 3.0.0-beta.123
@payloadcms/next/utilities: 3.0.0-beta.123
@payloadcms/ui/shared: 3.0.0-beta.123
react: 19.0.0-rc-603e6108-20241029
react-dom: 19.0.0-rc-603e6108-20241029
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 24.1.0: Thu Oct 10 21:05:14 PDT 2024; root:xnu-11215.41.3~2/RELEASE_ARM64_T8103
Available memory (MB): 16384
Available CPU cores: 8
I have just realised that this is by design: https://github.com/payloadcms/payload/blob/v3.0.0-beta.123/packages/next/src/views/Login/index.tsx#L86
It seems when you set disableLocalStrategy, you would need to create your own login form.
This issue has been automatically locked.
Please open a new issue if this issue persists with any additional detail.