fix(login): split passwordLogin / devLogin so SIGN IN cannot bypass
Oracle round-3 finding: passing password=undefined to api.signIn silently
hit /api/v1/auth/dev-login. With dev-login disabled in a production
build, a buyer typing only their email and pressing SIGN IN would still
have been authenticated as that user.
- api.passwordLogin(email, password) explicitly requires a password and
always hits /api/v1/auth/login. Throws on missing password.
- api.devLogin(email) explicitly hits /api/v1/auth/dev-login and is the
only entry point to that endpoint.
- store.loginAs(email, password, { method: 'password' | 'dev' }) routes
to the right call. Default is password (requires password).
- Login.handleSubmit rejects empty password with 'Password required'
before any network call.
- Login.handleDevLogin bails when devLoginEnabled is false.
- SSO button is disabled while probe state is 'unknown'.
- Login.test.tsx: regression test 'blocks empty-password SIGN IN —
must NOT silently call dev-login'. Plus SSO test asserts the button
starts disabled.
29/29 unit tests green.
This commit is contained in:
+21
-5
@@ -191,16 +191,15 @@ export const api = {
|
||||
return authedRequest<AuthMe>(this.config, "GET", "/api/v1/auth/me", undefined, signal);
|
||||
},
|
||||
|
||||
async signIn(email: string, password?: string, signal?: AbortSignal): Promise<{ access_token: string; refresh_token?: string }> {
|
||||
const payload = password ? { email, password } : { email };
|
||||
const path = password ? "/api/v1/auth/login" : "/api/v1/auth/dev-login";
|
||||
async passwordLogin(email: string, password: string, signal?: AbortSignal): Promise<{ access_token: string; refresh_token?: string }> {
|
||||
if (!password) throw new Error("Password required");
|
||||
const init: RequestInit = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({ email, password }),
|
||||
signal,
|
||||
};
|
||||
const r = await instrumentedFetch("POST", `${this.config.baseUrl}${path}`, init, payload);
|
||||
const r = await instrumentedFetch("POST", `${this.config.baseUrl}/api/v1/auth/login`, init, { email, password });
|
||||
if (!r.ok) {
|
||||
const text = await r.text().catch(() => "");
|
||||
throw new Error(`Login failed: ${r.status} ${text.slice(0, 100)}`);
|
||||
@@ -210,6 +209,23 @@ export const api = {
|
||||
return data;
|
||||
},
|
||||
|
||||
async devLogin(email: string, signal?: AbortSignal): Promise<{ access_token: string; refresh_token?: string }> {
|
||||
const init: RequestInit = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
signal,
|
||||
};
|
||||
const r = await instrumentedFetch("POST", `${this.config.baseUrl}/api/v1/auth/dev-login`, init, { email });
|
||||
if (!r.ok) {
|
||||
const text = await r.text().catch(() => "");
|
||||
throw new Error(`Dev-login failed: ${r.status} ${text.slice(0, 100)}`);
|
||||
}
|
||||
const data = await r.json() as { access_token: string; refresh_token?: string };
|
||||
this.setBearer(data.access_token);
|
||||
return data;
|
||||
},
|
||||
|
||||
async devLoginConfig(signal?: AbortSignal): Promise<{ enabled: boolean | null }> {
|
||||
try {
|
||||
const init: RequestInit = { method: "GET", headers: { "Accept": "application/json" }, signal };
|
||||
|
||||
Reference in New Issue
Block a user