<DxD/>
<DxD/>13mo ago

Can set cookie with trpc?

I tried to create a full authentication system in trpc using jwt and refresh token I find that is not quite okay to create authentication with trpc Can you suggest me a better way how I can achieve authentication in trpc project ?
21 Replies
BeBoRE
BeBoRE13mo ago
Why do you find it not okay to create auth with tRPC? The most simple solution would be to implement a route with which you can login, which then sets a JWT cookie. And then create a protected procedure that checks someone’s JWT, Most projects will be using auth frameworks like LuciaAuth, NextAuth or Clerk etc.
<DxD/>
<DxD/>OP13mo ago
I can not send cookie via trpc, I tried a lot(if I use with express) I know with clerk to do auth, I am just curious to do it with jwt
BeBoRE
BeBoRE13mo ago
You can set cookies in Express using res.cookies So as long as you give tRPC access to the res object, you can send cookies Otherwise you might want to send the JWT in the body of your response and send it as a header with every request
<DxD/>
<DxD/>OP13mo ago
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { TRPCError } from '@trpc/server';
import { db } from 'db';
import jwt from 'jsonwebtoken';
import { checkSamePassword } from 'utils/hash/checkSamePassword';
import keys from 'utils/secret/keys';
import { z } from 'zod';

import { router, publicProcedure } from '..';

export const loginRouter = router({
login: publicProcedure
.input(
z.object({
email: z.string().nonempty('Email is required').email('Invalid email address'),
password: z.string().nonempty('Password is required').min(6),
}),
)
.query(async ({ ctx, input: { email, password } }) => {
const user = await db.user.findOne({ email: email });
const validatePassword = await checkSamePassword(password, user?.password);
if (!user || !validatePassword) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Email or password is not valid, please try again.',
});
} else {
const token = jwt.sign({ _id: user._id }, keys.jwtSecret, { expiresIn: '1h' });
const expirationDate = new Date(Date.now() + 900000);

ctx.res.cookie('token', token, {
expires: expirationDate,
httpOnly: true,
sameSite: 'None', /
path: '/',
});
}
}),
});
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { TRPCError } from '@trpc/server';
import { db } from 'db';
import jwt from 'jsonwebtoken';
import { checkSamePassword } from 'utils/hash/checkSamePassword';
import keys from 'utils/secret/keys';
import { z } from 'zod';

import { router, publicProcedure } from '..';

export const loginRouter = router({
login: publicProcedure
.input(
z.object({
email: z.string().nonempty('Email is required').email('Invalid email address'),
password: z.string().nonempty('Password is required').min(6),
}),
)
.query(async ({ ctx, input: { email, password } }) => {
const user = await db.user.findOne({ email: email });
const validatePassword = await checkSamePassword(password, user?.password);
if (!user || !validatePassword) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Email or password is not valid, please try again.',
});
} else {
const token = jwt.sign({ _id: user._id }, keys.jwtSecret, { expiresIn: '1h' });
const expirationDate = new Date(Date.now() + 900000);

ctx.res.cookie('token', token, {
expires: expirationDate,
httpOnly: true,
sameSite: 'None', /
path: '/',
});
}
}),
});
in my browser in cookie i can not see token @BeBoRE
BeBoRE
BeBoRE13mo ago
You are creating a query, this should be a mutation If you changed that and it still doesn't work, see if you see the set-cookie header in your response headers
<DxD/>
<DxD/>OP13mo ago
okay, i will change it to mutation i let you know if it works
BeBoRE
BeBoRE13mo ago
What is the raw response for the happy flow?
<DxD/>
<DxD/>OP13mo ago
? @BeBoRE i modigy tu mutation
<DxD/>
<DxD/>OP13mo ago
No description
<DxD/>
<DxD/>OP13mo ago
No description
<DxD/>
<DxD/>OP13mo ago
nothing
BeBoRE
BeBoRE13mo ago
None specifies that cookies are sent on both originating and cross-site requests, but only in secure contexts (i.e., if SameSite=None then the Secure attribute must also be set). If no SameSite attribute is set, the cookie is treated as Lax.
None specifies that cookies are sent on both originating and cross-site requests, but only in secure contexts (i.e., if SameSite=None then the Secure attribute must also be set). If no SameSite attribute is set, the cookie is treated as Lax.
Using HTTP cookies SameSite=None only works in a secure context with HTTPS, you probably want to set that attribute to lax which is the default
MDN Web Docs
Using HTTP cookies - HTTP | MDN
An HTTP cookie (web cookie, browser cookie) is a small piece of data that a server sends to a user's web browser. The browser may store the cookie and send it back to the same server with later requests. Typically, an HTTP cookie is used to tell if two requests come from the same browser—keeping a user logged in, for example. It remembers st...
<DxD/>
<DxD/>OP13mo ago
Ok Still now idea how to solve it Only idea that I used is when I send token on client, I use some js cookie or smt like that and from there I save it to cookie
BeBoRE
BeBoRE13mo ago
Pretty sure if you remove sameSite: 'None' it should set the cookie correctly, also in the screenshot you send it looks like if you use my cookie as the cookie name, I don't believe you can have spaces in your cookie
<DxD/>
<DxD/>OP13mo ago
Ok, I will check it @BeBoRE
import { TRPCError } from '@trpc/server';
import { db, User } from 'db';
import jwt from 'jsonwebtoken';
import { hashPassword } from 'utils/hash/hashPassword';
import { keys } from 'utils/secret/keys';
import { z } from 'zod';

import { router, publicProcedure } from '..';

export const registerRouter = router({
register: publicProcedure
.input(
z.object({
email: z.string().nonempty('Email is required').email('Invalid email address'),
password: z.string().refine((value) => value.length >= 6, {
message: 'Password must contain at least 6 characters',
}),
confrimPassword: z.string().refine((value) => value.length >= 6, {
message: 'Password must contain at least 6 characters',
}),
}),
)
.mutation(async ({ ctx, input: { email, password, confrimPassword } }) => {
const checkUserExist = await db.user.findOne({ email: email });
if (checkUserExist) {
throw new TRPCError({
code: 'CONFLICT',
message: `Already exists an account with this ${email}, please try to log in`,
});
}
if (!password || password.length < 6 || password.length !== confrimPassword.length) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `Password must contains at least 6 characters`,
});
}
const user = await db.user.findOne({ email: email });
const token = jwt.sign({ _id: user?._id }, keys.jwtSecret, { expiresIn: '5min' });
ctx.res.cookie('session_token', token, { httpOnly: true });
const hashedPassword = await hashPassword(password);
const newUser: User = { email, password: hashedPassword };
await db.user.insertOne(newUser);
return newUser;
}),
});
import { TRPCError } from '@trpc/server';
import { db, User } from 'db';
import jwt from 'jsonwebtoken';
import { hashPassword } from 'utils/hash/hashPassword';
import { keys } from 'utils/secret/keys';
import { z } from 'zod';

import { router, publicProcedure } from '..';

export const registerRouter = router({
register: publicProcedure
.input(
z.object({
email: z.string().nonempty('Email is required').email('Invalid email address'),
password: z.string().refine((value) => value.length >= 6, {
message: 'Password must contain at least 6 characters',
}),
confrimPassword: z.string().refine((value) => value.length >= 6, {
message: 'Password must contain at least 6 characters',
}),
}),
)
.mutation(async ({ ctx, input: { email, password, confrimPassword } }) => {
const checkUserExist = await db.user.findOne({ email: email });
if (checkUserExist) {
throw new TRPCError({
code: 'CONFLICT',
message: `Already exists an account with this ${email}, please try to log in`,
});
}
if (!password || password.length < 6 || password.length !== confrimPassword.length) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `Password must contains at least 6 characters`,
});
}
const user = await db.user.findOne({ email: email });
const token = jwt.sign({ _id: user?._id }, keys.jwtSecret, { expiresIn: '5min' });
ctx.res.cookie('session_token', token, { httpOnly: true });
const hashedPassword = await hashPassword(password);
const newUser: User = { email, password: hashedPassword };
await db.user.insertOne(newUser);
return newUser;
}),
});
<DxD/>
<DxD/>OP13mo ago
No description
No description
<DxD/>
<DxD/>OP13mo ago
i do not understand where i do wrong
import * as trpcExpress from '@trpc/server/adapters/express';
import cors from 'cors';
import * as dotenv from 'dotenv';
import express from 'express';
import { envCheck } from 'utils/check/env';

dotenv.config();
envCheck();

import { db } from './db/connection';
import { createContext } from './trpc/context';
import { appRouter } from './trpc/router';

const app = express();

app.use(cors());

app.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}),
);

const PORT = process.env.PORT;

db.connect()
.then(() => {
console.log('Successfully connected to MongoDB.');
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}.`);
});
})
.catch(() => {
console.log('Failed to connect to MongoDB. Exiting...');
process.exit(1);
});
import * as trpcExpress from '@trpc/server/adapters/express';
import cors from 'cors';
import * as dotenv from 'dotenv';
import express from 'express';
import { envCheck } from 'utils/check/env';

dotenv.config();
envCheck();

import { db } from './db/connection';
import { createContext } from './trpc/context';
import { appRouter } from './trpc/router';

const app = express();

app.use(cors());

app.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}),
);

const PORT = process.env.PORT;

db.connect()
.then(() => {
console.log('Successfully connected to MongoDB.');
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}.`);
});
})
.catch(() => {
console.log('Failed to connect to MongoDB. Exiting...');
process.exit(1);
});
nvm, i solve it
BeBoRE
BeBoRE13mo ago
What was the issue?
<DxD/>
<DxD/>OP13mo ago
i forgot to add
app.use(
cors({
origin: 'http://localhost:3000',
credentials: true,
}),
);
app.use(
cors({
origin: 'http://localhost:3000',
credentials: true,
}),
);
when i create express be 🙂 i add that and now works fine
BeBoRE
BeBoRE13mo ago
Ah the empty cors made it so that the cookie wasn’t being send with, ngl I have never understood CORS
<DxD/>
<DxD/>OP13mo ago
:))) 3 days for a fu….ing CORS