listUsers returns status 401 UNAUTHORIZED even though the current user is an admin.

I am using the admin() and adminClient() plugins while testing out better-auth. I am trying to list out the users using the auth client, but I am getting a 401 unauthorized. I've registered a user and manually changed the role to admin in the database. I am using nextjs15 Here's the code:
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { authClient } from "@/lib/auth-client";
import { verifySession } from "@/lib/verify-session";

export default async function UsersPage() {
const { user } = await verifySession();

console.log("CURRENT USER: ", user);

const { data } = await authClient.admin.listUsers({
query: {
limit: 10,
offset: 0,
},
});

const users = data?.users;

return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
</CardHeader>
<CardContent>
<ul>
{users && users.map((user) => <li key={user.id}>{user.email}</li>)}
</ul>
</CardContent>
</Card>
</div>
);
}
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { authClient } from "@/lib/auth-client";
import { verifySession } from "@/lib/verify-session";

export default async function UsersPage() {
const { user } = await verifySession();

console.log("CURRENT USER: ", user);

const { data } = await authClient.admin.listUsers({
query: {
limit: 10,
offset: 0,
},
});

const users = data?.users;

return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
</CardHeader>
<CardContent>
<ul>
{users && users.map((user) => <li key={user.id}>{user.email}</li>)}
</ul>
</CardContent>
</Card>
</div>
);
}
Here are the console logs:
CURRENT USER: {
id: 'ZjLohn28IT3OHklqxwAdspRV1Om2X0Ss',
name: 'xxxx',
emailVerified: true,
image: null,
createdAt: 2025-02-07T10:24:09.645Z,
updatedAt: 2025-02-07T10:24:09.645Z,
role: 'admin',
banned: null,
banReason: null,
banExpires: null
}
✓ Compiled /api/auth/[...all] in 180ms (1197 modules)
GET /api/auth/admin/list-users?limit=10&offset=0 401 in 345ms
CURRENT USER: {
id: 'ZjLohn28IT3OHklqxwAdspRV1Om2X0Ss',
name: 'xxxx',
emailVerified: true,
image: null,
createdAt: 2025-02-07T10:24:09.645Z,
updatedAt: 2025-02-07T10:24:09.645Z,
role: 'admin',
banned: null,
banReason: null,
banExpires: null
}
✓ Compiled /api/auth/[...all] in 180ms (1197 modules)
GET /api/auth/admin/list-users?limit=10&offset=0 401 in 345ms
What am I missing? 😅
8 Replies
invocation97
invocation97OP3mo ago
Okay, so I realized I was using the authClient on the server side. I've changed it since to use the server side auth. However, the issue persists. This is what I changed it to:
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function UsersPage() {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session) {
return redirect("/auth/sign-in");
} else {
console.log("SESSION: ", session);
}
const response = await auth.api.listUsers({
query: {
limit: 10,
offset: 0,
},
});
console.log("RESPONSE: ", response);

const users = response.users;

if (!users || users.length === 0) {
return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<p>No users found.</p>
</div>
);
}

return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
</CardHeader>
<CardContent>
<ul>
{users && users.map((user) => <li key={user.id}>{user.email}</li>)}
</ul>
</CardContent>
</Card>
</div>
);
}
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function UsersPage() {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session) {
return redirect("/auth/sign-in");
} else {
console.log("SESSION: ", session);
}
const response = await auth.api.listUsers({
query: {
limit: 10,
offset: 0,
},
});
console.log("RESPONSE: ", response);

const users = response.users;

if (!users || users.length === 0) {
return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<p>No users found.</p>
</div>
);
}

return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
</CardHeader>
<CardContent>
<ul>
{users && users.map((user) => <li key={user.id}>{user.email}</li>)}
</ul>
</CardContent>
</Card>
</div>
);
}
I added the manual auth check, thinking that it might be the culprit. The session logs correctly and the role is still set to admin, however I get the same error as before
⨯ [Error [BetterCallAPIError]: API Error: UNAUTHORIZED ] {
status: 'UNAUTHORIZED',
headers: Headers {},
body: [Object],
digest: '3161027971'
}
⨯ [Error [BetterCallAPIError]: API Error: UNAUTHORIZED ] {
status: 'UNAUTHORIZED',
headers: Headers {},
body: [Object],
digest: '3161027971'
}
It turns out you need to pass in the headers the same way you would with any other auth form, so this is the final solution to the conundrum
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function UsersPage() {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session) {
return redirect("/auth/sign-in");
} else {
console.log("SESSION: ", session);
}
const response = await auth.api.listUsers({
headers: await headers(),
query: {
limit: 10,
offset: 0,
},
next: {
tags: ["users"],
},
});
console.log("RESPONSE: ", response);

const users = response.users;

if (!users || users.length === 0) {
return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<p>No users found.</p>
</div>
);
}

return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
</CardHeader>
<CardContent>
<ul>
{users && users.map((user) => <li key={user.id}>{user.email}</li>)}
</ul>
</CardContent>
</Card>
</div>
);
}
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function UsersPage() {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session) {
return redirect("/auth/sign-in");
} else {
console.log("SESSION: ", session);
}
const response = await auth.api.listUsers({
headers: await headers(),
query: {
limit: 10,
offset: 0,
},
next: {
tags: ["users"],
},
});
console.log("RESPONSE: ", response);

const users = response.users;

if (!users || users.length === 0) {
return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<p>No users found.</p>
</div>
);
}

return (
<div className="space-y-4">
<h1 className="text-3xl font-bold">Users</h1>
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
</CardHeader>
<CardContent>
<ul>
{users && users.map((user) => <li key={user.id}>{user.email}</li>)}
</ul>
</CardContent>
</Card>
</div>
);
}
belikebee
belikebee3mo ago
i have the same issue🥲
invocation97
invocation97OP3mo ago
@belikebee Have you seen my latest message? It turns out you just need to pass in the headers: await headers() to the object because it is a server function and can't know if there's a session on the request time without including the headers.
belikebee
belikebee3mo ago
yeah. it helped me...thx😍
joseph013318
joseph0133182mo ago
@invocation97 Thankyou soo much 🙏 Was facing the same issue. They should include this in the docs
roque
roque2w ago
Hi! I did something like this: "use client"; import { authClient } from "@/lib/auth-client"; import React, { useEffect, useState } from "react"; import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; interface User { id: string; name: string; email: string; emailVerified: boolean; createdAt: string; updatedAt: string; role: string; } export default function ListAdm() { const [users, setUsers] = useState<User[]>([]); useEffect(() => { async function fetchUsers() { const response: any = await authClient.admin.listUsers({ query: { limit: 100 }, }); setUsers(response.data.users); } fetchUsers(); }, []); if (!users || users.length === 0) { return ( <div className="space-y-4"> <p>No users found.</p> </div> ); } return ( <> <Table> <TableCaption>Lista de usuários</TableCaption> <TableHeader> <TableRow> <TableHead className="w-[100px]">Nome</TableHead> <TableHead className="w-[100px]">E-mail</TableHead> <TableHead className="text-right">Ação</TableHead> </TableRow> </TableHeader> <TableBody> {users.map((user, index) => ( <TableRow key={index}> <TableCell className="font-medium">{user.name}</TableCell> <TableCell className="font-medium">{user.email}</TableCell> <TableCell className="text-right">BOTAO</TableCell> </TableRow> ))} </TableBody> </Table> </> ); } I'm using authClient.admin.listUsers instead of using auth.api.listUsers. Is this a good way to do it? And I'm thinking how to a get a instant refresh when my auth component creates an user. @invocation97
invocation97
invocation97OP2w ago
Hey 😉 That's more of a "react" way to do it, which is completely fine, but you're loosing the benefits of nextjs server rendering. It works because the client already has the headers containing the information about the current users session (I'm assuming you're logged in as the admin). The idea is that the HTML is rendered ahead of time in my example, and in yours the <script> tag containing your javaScript is in the initial HTML, then it runs and gets the data, and lastly it appends it to the DOM. That process is obviously a bit slower and has more requests. The way you can improve it, if you want to keep it client-side, is you could use a fetching library like (tanstack query)[https://tanstack.com/query/latest/docs/framework/react/overview]. There you can set a key for each query, which you can invalidate whenever you do a mutation, which in your case would be doing any CRUD operations with the users. Just make sure that the keys match, and the data would get re-fetched everywhere. However, I'd still recommend my approach for Nextjs specifically since it is inherently faster.
roque
roque2w ago
Got it, thanks so much for sharing with me! 😃 Your approach fits much better, taking advantage of the speed of Next.js I have an admin authentication form that creates the user and is displayed in that list. For the authentication, I'm using authClient.admin.createUser and router.fresh which refreshes the page. And for the list, I'm using your approach with auth.api.listUsers. I thought I could use authClient.admin for everything just for the actions and admin pages. But by using that, I'd have to use ‘use client’ and wouldn't take advantage of the power of Next.js, right?

Did you find this page helpful?