mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
[Fix] Token Store Overrides are now Respected (#1156)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migrations are backwards-compatible / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E API Tests with external source of truth / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Dev Environment Test With Custom Base Port / restart-dev-and-test-with-custom-base-port (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests with custom base port / setup-tests-with-custom-base-port (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migrations are backwards-compatible / Test migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migrations are backwards-compatible / No migration changes (skipped) (push) Has been cancelled
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migrations are backwards-compatible / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E API Tests with external source of truth / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Dev Environment Test With Custom Base Port / restart-dev-and-test-with-custom-base-port (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests with custom base port / setup-tests-with-custom-base-port (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migrations are backwards-compatible / Test migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migrations are backwards-compatible / No migration changes (skipped) (push) Has been cancelled
### Context Recently, a user raised [this issue](https://github.com/stack-auth/stack-auth/issues/1144), which indicated that `tokenOverrides` were not being respected/used in the `getUser()` function. If we trace the flow through this function, we see `this._getSession -> this._getOrCreateTokenStore -> _createCookieHelper -> createCookieHelper -> createNextCookieHelper -> await rscHeaders()`. What this means is that even when a `requestLike tokenOverride` was passed, we would not end up using it because the `createCookieHelper` call occurs before the extant override checking logic in `getOrCreateTokenStore`, and the `createCookieHelper` didn't check the override but only the default `tokenStoreInit`. This caused the error to propagate up. ### Summary of Changes We check the `tokenStoreOverride` in the `createCookieHelper` function now, preventing this issue from happening. We also add extra test coverage to verify that overrides are respected, and don't overwrite the default token store. ### Out of Scope Discussion The original issue was raised with a `bun` runtime running `next.js` code. There seems to be some incompatibility between `bun 1.3.8` and `nextjs 15+`, not just with our backend but with fetching and working with responses from any `nextjs` server.
This commit is contained in:
parent
7a35751f8e
commit
bb69ee4230
@ -359,3 +359,151 @@ it("clientApp auth methods should match user auth methods", async ({ expect }) =
|
||||
const userAuthHeaders = await user.getAuthHeaders();
|
||||
expect(appAuthHeaders["x-stack-auth"]).toBe(userAuthHeaders["x-stack-auth"]);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// Request-like tokenStore override tests
|
||||
// (Critical for Bun middleware compatibility - GitHub issue #1144)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Helper to build a cookie string for a request-like object.
|
||||
*/
|
||||
function buildCookieHeader(cookies: Record<string, string>): string {
|
||||
return Object.entries(cookies)
|
||||
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
|
||||
.join("; ");
|
||||
}
|
||||
|
||||
it("getUser should work with request-like tokenStore containing auth cookies", async ({ expect }) => {
|
||||
// Use nextjs-cookie as default to simulate real middleware scenario.
|
||||
// This ensures the fix prevents rscHeaders() from being called when an override is provided.
|
||||
const { serverApp, clientApp } = await createApp({}, {
|
||||
server: { tokenStore: "nextjs-cookie" },
|
||||
});
|
||||
|
||||
// Create two different users
|
||||
const userAEmail = `${crypto.randomUUID()}@user-a.test`;
|
||||
const userBEmail = `${crypto.randomUUID()}@user-b.test`;
|
||||
const password = "test-password-123";
|
||||
|
||||
// Sign up User A
|
||||
await clientApp.signUpWithCredential({
|
||||
email: userAEmail,
|
||||
password,
|
||||
verificationCallbackUrl: "http://localhost:3000",
|
||||
});
|
||||
await clientApp.signInWithCredential({ email: userAEmail, password });
|
||||
const userA = await clientApp.getUser({ or: "throw" });
|
||||
const userATokens = await userA.currentSession.getTokens();
|
||||
await clientApp.signOut();
|
||||
|
||||
// Sign up User B and keep them signed in on clientApp
|
||||
await clientApp.signUpWithCredential({
|
||||
email: userBEmail,
|
||||
password,
|
||||
verificationCallbackUrl: "http://localhost:3000",
|
||||
});
|
||||
await clientApp.signInWithCredential({ email: userBEmail, password });
|
||||
const userB = await clientApp.getUser({ or: "throw" });
|
||||
|
||||
// Verify the two users are different
|
||||
expect(userA.id).not.toBe(userB.id);
|
||||
|
||||
// Verify serverApp's default nextjs-cookie store would fail outside Next.js context.
|
||||
// without passing tokenStore override rscHeaders() would be called and fail.
|
||||
await expect(serverApp.getUser()).rejects.toThrow();
|
||||
|
||||
// Build cookies with User A's tokens (Option B - use different user's tokens)
|
||||
const refreshCookieName = `stack-refresh-${serverApp.projectId}--default`;
|
||||
const refreshCookieValue = JSON.stringify({
|
||||
refresh_token: userATokens.refreshToken,
|
||||
updated_at_millis: Date.now(),
|
||||
});
|
||||
const accessCookieValue = JSON.stringify([userATokens.refreshToken, userATokens.accessToken]);
|
||||
|
||||
const cookieHeader = buildCookieHeader({
|
||||
[refreshCookieName]: refreshCookieValue,
|
||||
"stack-access": accessCookieValue,
|
||||
});
|
||||
|
||||
// Create a request-like object with User A's cookies
|
||||
const requestLike = {
|
||||
headers: new Headers({
|
||||
cookie: cookieHeader,
|
||||
}),
|
||||
};
|
||||
|
||||
// Call getUser with the request-like tokenStore
|
||||
// This MUST read from requestLike because:
|
||||
// 1. serverApp's default store is empty
|
||||
// 2. clientApp has User B signed in, not User A
|
||||
// 3. Only requestLike contains User A's tokens
|
||||
const serverUser = await serverApp.getUser({ tokenStore: requestLike });
|
||||
|
||||
expect(serverUser).not.toBeNull();
|
||||
expect(serverUser!.id).toBe(userA.id); // Must be User A, not User B
|
||||
expect(serverUser!.primaryEmail).toBe(userAEmail);
|
||||
});
|
||||
|
||||
it("getUser should return null for request-like tokenStore with no auth cookies", async ({ expect }) => {
|
||||
// Use nextjs-cookie as default to simulate real middleware scenario
|
||||
const { serverApp } = await createApp({}, {
|
||||
server: { tokenStore: "nextjs-cookie" },
|
||||
});
|
||||
|
||||
// Create a request-like object with no auth cookies
|
||||
const requestLike = {
|
||||
headers: new Headers({
|
||||
cookie: "",
|
||||
}),
|
||||
};
|
||||
|
||||
// Should return null, not throw
|
||||
const serverUser = await serverApp.getUser({ tokenStore: requestLike });
|
||||
|
||||
expect(serverUser).toBeNull();
|
||||
});
|
||||
|
||||
it("getUser should work with x-stack-auth header in request-like tokenStore", async ({ expect }) => {
|
||||
const { serverApp, clientApp } = await createApp({});
|
||||
await signIn(clientApp);
|
||||
|
||||
// Get the auth headers from the signed-in user
|
||||
const authHeaders = await clientApp.getAuthHeaders();
|
||||
|
||||
// Create a request-like object with x-stack-auth header
|
||||
const requestLike = {
|
||||
headers: new Headers({
|
||||
"x-stack-auth": authHeaders["x-stack-auth"],
|
||||
}),
|
||||
};
|
||||
|
||||
// Call getUser with the request-like tokenStore
|
||||
const serverUser = await serverApp.getUser({ tokenStore: requestLike });
|
||||
const clientUser = await clientApp.getUser({ or: "throw" });
|
||||
|
||||
expect(serverUser).not.toBeNull();
|
||||
expect(serverUser!.primaryEmail).toBe("test@test.com");
|
||||
expect(serverUser!.id).toBe(clientUser.id);
|
||||
});
|
||||
|
||||
it("getUser with tokenStore override should not affect the app's default token store", async ({ expect }) => {
|
||||
const { serverApp, clientApp } = await createApp({});
|
||||
await signIn(clientApp);
|
||||
|
||||
const clientUser = await clientApp.getUser({ or: "throw" });
|
||||
|
||||
// Get user via serverApp with explicit tokenStore override
|
||||
const tokens = await clientUser.currentSession.getTokens();
|
||||
const serverUserWithOverride = await serverApp.getUser({
|
||||
tokenStore: { accessToken: tokens.accessToken!, refreshToken: tokens.refreshToken! },
|
||||
});
|
||||
|
||||
expect(serverUserWithOverride).not.toBeNull();
|
||||
expect(serverUserWithOverride!.id).toBe(clientUser.id);
|
||||
|
||||
// serverApp's default token store (memory) should still be empty
|
||||
// since we used an override, not the default
|
||||
const serverUserDefault = await serverApp.getUser();
|
||||
expect(serverUserDefault).toBeNull();
|
||||
});
|
||||
|
||||
@ -229,7 +229,6 @@ export class StackClientInterface {
|
||||
refreshToken: null,
|
||||
});
|
||||
|
||||
|
||||
return await this._networkRetry(
|
||||
() => this.sendClientRequestInner(path, requestOptions, session!, requestType),
|
||||
session,
|
||||
|
||||
@ -317,8 +317,9 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
|
||||
private _anonymousSignUpInProgress: Promise<{ accessToken: string, refreshToken: string }> | null = null;
|
||||
|
||||
protected async _createCookieHelper(): Promise<CookieHelper> {
|
||||
if (this._tokenStoreInit === 'nextjs-cookie' || this._tokenStoreInit === 'cookie') {
|
||||
protected async _createCookieHelper(overrideTokenStoreInit?: TokenStoreInit): Promise<CookieHelper> {
|
||||
const tokenStoreInit = overrideTokenStoreInit === undefined ? this._tokenStoreInit : overrideTokenStoreInit;
|
||||
if (tokenStoreInit === 'nextjs-cookie' || tokenStoreInit === 'cookie') {
|
||||
return await createCookieHelper();
|
||||
} else {
|
||||
return await createPlaceholderCookieHelper();
|
||||
@ -877,7 +878,7 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
|
||||
}
|
||||
|
||||
protected async _getSession(overrideTokenStoreInit?: TokenStoreInit): Promise<InternalSession> {
|
||||
const tokenStore = this._getOrCreateTokenStore(await this._createCookieHelper(), overrideTokenStoreInit);
|
||||
const tokenStore = this._getOrCreateTokenStore(await this._createCookieHelper(overrideTokenStoreInit), overrideTokenStoreInit);
|
||||
const session = this._getSessionFromTokenStore(tokenStore);
|
||||
return session;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user