Files
multica/packages/core/auth/index.ts
Bohan Jiang 2317533da4 fix(auth): validate next= redirect target to prevent open redirect (#1309)
* refactor(auth): add sanitizeNextUrl helper in @multica/core/auth

Extracts a reusable helper that returns a post-login redirect URL only
when it's a safe single-slash relative path, and null otherwise. Rejects
absolute URLs, protocol-relative URLs, backslashes, and control
characters so call sites can safely pass the result to router.push().

Keeping the rule in a single helper (with direct unit tests) avoids
each consumer re-implementing the validation and drifting.

* fix(auth): validate next= redirect target to prevent open redirect

Closes #1116

Next.js router.push accepts absolute URLs, so a crafted
`/login?next=https://evil.example` would send the user off-origin
after a successful login. The Google OAuth callback has the same
vector via the `state=next:<url>` payload.

Sanitize both entry points through `sanitizeNextUrl` from
`@multica/core/auth` so only safe single-slash relative paths survive;
null results fall through to the existing workspace-list-based default
without any hard-coded path.

---------

Co-authored-by: JunghwanNA <70629228+shaun0927@users.noreply.github.com>
2026-04-18 13:24:01 +08:00

41 lines
1.3 KiB
TypeScript

export { createAuthStore } from "./store";
export type { AuthStoreOptions, AuthState } from "./store";
export { sanitizeNextUrl } from "./utils";
import type { createAuthStore as CreateAuthStoreFn } from "./store";
type AuthStoreInstance = ReturnType<typeof CreateAuthStoreFn>;
/** Module-level singleton — set once at app boot via `registerAuthStore()`. */
let _store: AuthStoreInstance | null = null;
/**
* Register the auth store instance created by the app.
* Must be called at boot before any component renders.
*/
export function registerAuthStore(store: AuthStoreInstance) {
_store = store;
}
/**
* Singleton accessor — a Zustand hook backed by the registered instance.
* Supports `useAuthStore(selector)` and `useAuthStore.getState()`.
*/
export const useAuthStore: AuthStoreInstance = new Proxy(
(() => {}) as unknown as AuthStoreInstance,
{
apply(_target, _thisArg, args) {
if (!_store)
throw new Error(
"Auth store not initialised — call registerAuthStore() first",
);
return (_store as unknown as (...a: unknown[]) => unknown)(...args);
},
get(_target, prop) {
// Allow property inspection (HMR/React Refresh) before registration
if (!_store) return undefined;
return Reflect.get(_store, prop);
},
},
);