Having role on session, is this security issue and/or is there better way?

Hey quick question on t3 (more security and sessions) if i have this role property on the user and want to check it, everytime a call is made. Is it find to store it on the ctx.session.user object or is there a better way? This role will obviously determine what privileges the user has and whether certain checks needs to be skip in case of the user is an admin etc. Basically what i want to achieve is that when an admin checks a partners org it should skip the checkUserOwnsOrganization check Code: schema.prisma:
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
role Role @default(USER)
accounts Account[]
sessions Session[]
ethWallets EthWallet[]
organizations Organization[]
ownedOrganizations Organization[] @relation("OrganizationOwner")
}

enum Role {
USER
ADMIN
PARTNER
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
role Role @default(USER)
accounts Account[]
sessions Session[]
ethWallets EthWallet[]
organizations Organization[]
ownedOrganizations Organization[] @relation("OrganizationOwner")
}

enum Role {
USER
ADMIN
PARTNER
}
File: [...nextauth].ts
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
session.user.role = user.role;
}
return session;
},
},
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
session.user.role = user.role;
}
return session;
},
},
router > organization.ts
getById: protectedProcedure
.input(z.string())
.query(async ({ ctx, input }) => {
console.log('=====================================')
console.log('test', ctx.session.user.role)

await checkUserOwnsOrganization({ id: input, prisma: ctx.prisma, userId: ctx.session.user.id })

const organization = await ctx.prisma.organization.findUnique({
where: { id: input },
});

return organization;
}),
getById: protectedProcedure
.input(z.string())
.query(async ({ ctx, input }) => {
console.log('=====================================')
console.log('test', ctx.session.user.role)

await checkUserOwnsOrganization({ id: input, prisma: ctx.prisma, userId: ctx.session.user.id })

const organization = await ctx.prisma.organization.findUnique({
where: { id: input },
});

return organization;
}),
19 Replies
djcmurphy
djcmurphy14mo ago
I've never used nextauth but the trpc context(ctx) is meant for that type of thing. I'm pretty sure they even detail roles auth using ctx in the trpc docs. It's all good as long as it all stays on the server and can't be messed with.
mbuxmann
mbuxmann14mo ago
ahh so i should be able to achieve this without next auth part? @MonobrainChris
djcmurphy
djcmurphy14mo ago
I use a different solution but I'm assuming next auth returns a user object from the db. so thats fine to use for auth. Not sure of Next auth implementation but it's all kinda the same. Get user object from DB based on successful login. nextauth will store a cookie or something to keep the user logged in. NextAuth appears quite opinionated I would read the docs thoroughly to get a good understanding
mbuxmann
mbuxmann14mo ago
Ahh okay i'll do that. I was checking and wondering also this could theoretically be achieved like this right?
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
import superjson from "superjson";

const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
},
});

export const router = t.router;

/**
* Unprotected procedure
**/
export const publicProcedure = t.procedure;

/**
* Reusable middleware to ensure
* users are logged in
*/
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

const userRole = getUserRole(ctx.session.user.id)//get role from db

return next({
ctx: {
// infers the `session` as non-nullable
session: {
...ctx.session, user: { ...ctx.session.user, role: userRole },
},
},
});
});

/**
* Protected procedure
**/
export const protectedProcedure = t.procedure.use(isAuthed);
import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
import superjson from "superjson";

const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
},
});

export const router = t.router;

/**
* Unprotected procedure
**/
export const publicProcedure = t.procedure;

/**
* Reusable middleware to ensure
* users are logged in
*/
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

const userRole = getUserRole(ctx.session.user.id)//get role from db

return next({
ctx: {
// infers the `session` as non-nullable
session: {
...ctx.session, user: { ...ctx.session.user, role: userRole },
},
},
});
});

/**
* Protected procedure
**/
export const protectedProcedure = t.procedure.use(isAuthed);
obviously a db check will be needed i assume, but i guess in this case it will always fetch the latest role from the user while the prior one will use the value at sign in?
djcmurphy
djcmurphy14mo ago
if protectedProcedure is imported into your routes then yeah it checks if no session or user publicProcedure can be used for the login procedures
mbuxmann
mbuxmann14mo ago
yea but i mean the extension i made to protectedProcedure should attach the role to the session right and will be more up to date then the previous example where that value is just fetched at log in?
cje
cje14mo ago
role on session doesn't really make sense just conceptually from what a session is
djcmurphy
djcmurphy14mo ago
The role should be in session alreasy if the user object is in it
cje
cje14mo ago
oh yea on the user object its fine but if you need this role on pretty much every procedure, id recommend using the next-auth session callback instead of trpc middleware
cje
cje14mo ago
Callbacks | NextAuth.js
Callbacks are asynchronous functions you can use to control what happens when an action is performed.
mbuxmann
mbuxmann14mo ago
ahh like the first example I gave? @cje . Yea that makes sense @MonobrainChris thanks!
djcmurphy
djcmurphy14mo ago
Just to be clear. When you auth it grabs the user object if that includes the role you can just use that inside session to do more fine grained auth. I'm assuming when you auth the whole user including role gets put in session?
mbuxmann
mbuxmann14mo ago
yea, but from my exmaple i am only placing the user id and role on the session.
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
session.user.role = user.role;
}
return session;
},
},
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
session.user.role = user.role;
}
return session;
},
},
but i assume you talking about this above
callbacks: {
session({ session, user }) {
if (session.user) {
session.user = user
}
return session;
},
},
callbacks: {
session({ session, user }) {
if (session.user) {
session.user = user
}
return session;
},
},
which should place the whole user object into session
import { DefaultSession } from "next-auth";

declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
user?: {
id: string;
role: Prisma.Role;
} & DefaultSession["user"];
}
}
import { DefaultSession } from "next-auth";

declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
user?: {
id: string;
role: Prisma.Role;
} & DefaultSession["user"];
}
}
export interface DefaultSession {
user?: {
name?: string | null;
email?: string | null;
image?: string | null;
};
expires: ISODateString;
}
export interface DefaultSession {
user?: {
name?: string | null;
email?: string | null;
image?: string | null;
};
expires: ISODateString;
}
djcmurphy
djcmurphy14mo ago
Oh yeah i saw that earlier sorry. I personally pass the user object all over the place in client using an auth context component. as long as password is gone and auth is good you can pass it about It's the user details after all
mbuxmann
mbuxmann14mo ago
yea i was just scared that a user could change the role client side and access different things but i assume since this is using sessions on the server its fine? from what i understand is when user logs in the user properties are added to the session and then stored on the backend i assume if it was JWT it would be otherwise
djcmurphy
djcmurphy14mo ago
not sure with how it works under hood honestly. They use stateless cookies save the session data and refer back to it for auth when needed. when you log out you probably call a function that deletes the cookie session data is often user data in this context at least that is how iron-session does it I think nextauth is the same but with more functionality.
mbuxmann
mbuxmann14mo ago
Ahh okay thanks for the help! I appreciate it. WIll do some more reading.
Casal0x
Casal0x13mo ago
Hi @MartinB I'm struggling with same issue how do you fixed it ? if you fixed
mbuxmann
mbuxmann13mo ago
I got it working just havent worked on the project in a while so forgot a bit but busy taking a look Found this in the callbacks section in the nextAuth
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
session.user.role = user.role;
}
return session;
},
},
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
session.user.role = user.role;
}
return session;
},
},
User modal
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
role Role @default(USER)
opted Boolean @default(false)
accounts Account[]
sessions Session[]
ethWallets EthWallet[]
organizations Organization[]
ownedOrganizations Organization[] @relation("OrganizationOwner")
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
role Role @default(USER)
opted Boolean @default(false)
accounts Account[]
sessions Session[]
ethWallets EthWallet[]
organizations Organization[]
ownedOrganizations Organization[] @relation("OrganizationOwner")
}
just note you need to logout and back in after assigning a role to a user via prisma studio or api to make the changes reflect