Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/core/src/server/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ const testApp = {
signIn,
revokeOtherSessions,
revokeAllSessions,
// The core's own registered functions, exercised as an app would export them.
isAuthenticated: core.isAuthenticated,
signOut: core.signOut,
};

/**
Expand Down Expand Up @@ -149,6 +152,18 @@ async function sessionIdsOf(t: ReturnType<typeof setup>, userId: string) {
return sessions.map((s) => s.sessionId);
}

const isAuthenticatedRef = makeFunctionReference<
"query",
Record<string, never>,
boolean
>("testApp:isAuthenticated");

const signOutRef = makeFunctionReference<
"mutation",
{ refreshToken: string },
null
>("testApp:signOut");

describe("ctx.convexAuth.revokeOtherSessions", () => {
test("revokes every session of the user but the caller's", async () => {
const t = setup();
Expand Down Expand Up @@ -288,3 +303,31 @@ describe("ctx.convexAuth.revokeAllSessions", () => {
);
});
});

describe("isAuthenticated", () => {
test("is false for a caller with no identity", async () => {
const t = setup();
expect(await t.query(isAuthenticatedRef, {})).toBe(false);
});

test("is false for a subject that is no session", async () => {
const t = setup();
expect(
await t
.withIdentity({ subject: "not-a-session-id" })
.query(isAuthenticatedRef, {}),
).toBe(false);
});

test("is true while the session is live, and false once it is revoked", async () => {
const t = setup();
const bundle = await t.mutation(signUpRef, { accountId: "alice" });
const caller = t.withIdentity(sessionIdentity(bundle));
expect(await caller.query(isAuthenticatedRef, {})).toBe(true);

await t.mutation(signOutRef, { refreshToken: bundle.refreshToken });

// The JWT is untouched and still verifies; the session is what died.
expect(await caller.query(isAuthenticatedRef, {})).toBe(false);
});
});
9 changes: 7 additions & 2 deletions packages/core/src/server/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type ConvexAuthCtx,
type UserCallbacks,
} from "../lib/types.ts";
import { getAuthUserId } from "./userId.ts";

/**
* A type for a factory that returns a `RegisteredMutation` with a handler that
Expand Down Expand Up @@ -105,13 +106,17 @@ export type AuthCore<UsersTable extends string = string> = {
Promise<RefreshResult>
>;
/**
* Reports whether the caller's access token identifies a signed-in user.
* Reports whether the caller's access token identifies a live session.
*
* Convex verifies the token's signature (against the deployment's JWKS)
* before this query runs, so the result is a trustworthy authentication
* verdict. SSR hosts call it with the token from the auth cookie to decide
* authentication state, rather than trusting the (client-controlled) cookie
* by merely decoding it.
*
* The query reads the session, so it returns `false` once the session is
* gone (signed out elsewhere, revoked, or expired) even while the token
* itself is still valid, and a subscription re-runs when that happens.
*/
isAuthenticated: RegisteredQuery<
"public",
Expand Down Expand Up @@ -247,7 +252,7 @@ export function setupConvexAuth<UsersTable extends string = "users">(options: {
args: {},
returns: v.boolean(),
handler: async (ctx): Promise<boolean> => {
return (await ctx.auth.getUserIdentity()) !== null;
return (await getAuthUserId(ctx, component)) !== null;
},
});

Expand Down
Loading