mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(auth): reject a weak claim password before sending the code (#1648)
This commit is contained in:
1 parent
ae11ee86aa
commit
e0876d719c
6 files changed
+30
-8
No files matched your search
@@ -35114,7 +35114,8 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"ticket": {"type": "string", "description": "Email change ticket identifier"},
|
"ticket": {"type": "string", "description": "Email change ticket identifier"},
|
||||||
"new_email": {"$ref": "#/components/schemas/EmailType"},
|
"new_email": {"$ref": "#/components/schemas/EmailType"},
|
||||||
"original_proof": {"type": "string", "description": "Proof token obtained from verifying the original email"}
|
"original_proof": {"type": "string", "description": "Proof token obtained from verifying the original email"},
|
||||||
|
"new_password": {"$ref": "#/components/schemas/PasswordType"}
|
||||||
},
|
},
|
||||||
"required": ["ticket", "new_email", "original_proof"]
|
"required": ["ticket", "new_email", "original_proof"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export function UserAccountController(app: HonoApp) {
|
|||||||
const body = ctx.req.valid('json');
|
const body = ctx.req.valid('json');
|
||||||
const result = await ctx
|
const result = await ctx
|
||||||
.get('emailChangeService')
|
.get('emailChangeService')
|
||||||
.requestNewEmail(user, body.ticket, body.new_email, body.original_proof);
|
.requestNewEmail(user, body.ticket, body.new_email, body.original_proof, body.new_password);
|
||||||
return ctx.json(result);
|
return ctx.json(result);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidat
|
|||||||
import {ms} from 'itty-time';
|
import {ms} from 'itty-time';
|
||||||
import type {ApiContext} from '../../ApiContext';
|
import type {ApiContext} from '../../ApiContext';
|
||||||
import {EMAIL_CLEARABLE_SUSPICIOUS_ACTIVITY_FLAGS} from '../../auth/AuthEmail';
|
import {EMAIL_CLEARABLE_SUSPICIOUS_ACTIVITY_FLAGS} from '../../auth/AuthEmail';
|
||||||
|
import * as AuthPassword from '../../auth/AuthPassword';
|
||||||
import type {User} from '../../models/User';
|
import type {User} from '../../models/User';
|
||||||
import type {EmailChangeRepository} from '../repositories/auth/EmailChangeRepository';
|
import type {EmailChangeRepository} from '../repositories/auth/EmailChangeRepository';
|
||||||
|
|
||||||
@@ -158,6 +159,7 @@ export class EmailChangeService {
|
|||||||
ticket: string,
|
ticket: string,
|
||||||
newEmail: string,
|
newEmail: string,
|
||||||
originalProof: string,
|
originalProof: string,
|
||||||
|
newPassword?: string,
|
||||||
): Promise<RequestNewEmailResult> {
|
): Promise<RequestNewEmailResult> {
|
||||||
const {email, emailDnsValidation, users, rateLimit} = this.apiContext.services;
|
const {email, emailDnsValidation, users, rateLimit} = this.apiContext.services;
|
||||||
const row = await getActiveChangeTicketForUser(this.repo, ticket, user.id);
|
const row = await getActiveChangeTicketForUser(this.repo, ticket, user.id);
|
||||||
@@ -182,6 +184,9 @@ export class EmailChangeService {
|
|||||||
if (existing && existing.id !== user.id) {
|
if (existing && existing.id !== user.id) {
|
||||||
throw InputValidationError.fromCode('new_email', ValidationErrorCodes.EMAIL_ALREADY_IN_USE);
|
throw InputValidationError.fromCode('new_email', ValidationErrorCodes.EMAIL_ALREADY_IN_USE);
|
||||||
}
|
}
|
||||||
|
if (newPassword != null && (await AuthPassword.isPasswordPwned(this.apiContext, newPassword))) {
|
||||||
|
throw InputValidationError.fromCode('new_password', ValidationErrorCodes.PASSWORD_IS_TOO_COMMON);
|
||||||
|
}
|
||||||
assertChangeCooldown(row.new_code_sent_at, this.RESEND_COOLDOWN_MS);
|
assertChangeCooldown(row.new_code_sent_at, this.RESEND_COOLDOWN_MS);
|
||||||
await checkChangeRateLimit(rateLimit, {
|
await checkChangeRateLimit(rateLimit, {
|
||||||
identifier: `email_change:new:${user.id}`,
|
identifier: `email_change:new:${user.id}`,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const ClaimAccountModal = observer(() => {
|
|||||||
const [stage, setStage] = useState<Stage>('collect');
|
const [stage, setStage] = useState<Stage>('collect');
|
||||||
const [ticket, setTicket] = useState<string | null>(null);
|
const [ticket, setTicket] = useState<string | null>(null);
|
||||||
const [originalProof, setOriginalProof] = useState<string | null>(null);
|
const [originalProof, setOriginalProof] = useState<string | null>(null);
|
||||||
|
const [emailToken, setEmailToken] = useState<string | null>(null);
|
||||||
const [resendNewAt, setResendNewAt] = useState<Date | null>(null);
|
const [resendNewAt, setResendNewAt] = useState<Date | null>(null);
|
||||||
const [submittingAction, setSubmittingAction] = useState<boolean>(false);
|
const [submittingAction, setSubmittingAction] = useState<boolean>(false);
|
||||||
const [now, setNow] = useState<number>(Date.now());
|
const [now, setNow] = useState<number>(Date.now());
|
||||||
@@ -88,7 +89,7 @@ export const ClaimAccountModal = observer(() => {
|
|||||||
if (!activeProof) {
|
if (!activeProof) {
|
||||||
throw new Error('Missing original proof token');
|
throw new Error('Missing original proof token');
|
||||||
}
|
}
|
||||||
const result = await UserCommands.requestEmailChangeNew(activeTicket!, data.email, activeProof);
|
const result = await UserCommands.requestEmailChangeNew(activeTicket!, data.email, activeProof, data.newPassword);
|
||||||
setResendNewAt(result.resend_available_at ? new Date(result.resend_available_at) : null);
|
setResendNewAt(result.resend_available_at ? new Date(result.resend_available_at) : null);
|
||||||
form.setValue('verificationCode', '');
|
form.setValue('verificationCode', '');
|
||||||
form.clearErrors('verificationCode');
|
form.clearErrors('verificationCode');
|
||||||
@@ -99,9 +100,14 @@ export const ClaimAccountModal = observer(() => {
|
|||||||
if (!ticket || !originalProof) return;
|
if (!ticket || !originalProof) return;
|
||||||
setSubmittingAction(true);
|
setSubmittingAction(true);
|
||||||
try {
|
try {
|
||||||
const {email_token} = await UserCommands.verifyEmailChangeNew(ticket, data.verificationCode, originalProof);
|
let activeEmailToken = emailToken;
|
||||||
|
if (!activeEmailToken) {
|
||||||
|
const verified = await UserCommands.verifyEmailChangeNew(ticket, data.verificationCode, originalProof);
|
||||||
|
activeEmailToken = verified.email_token;
|
||||||
|
setEmailToken(activeEmailToken);
|
||||||
|
}
|
||||||
await UserCommands.update({
|
await UserCommands.update({
|
||||||
email_token,
|
email_token: activeEmailToken,
|
||||||
new_password: data.newPassword,
|
new_password: data.newPassword,
|
||||||
});
|
});
|
||||||
ToastCommands.createToast({type: 'success', children: i18n._(ACCOUNT_CLAIMED_SUCCESSFULLY_DESCRIPTOR)});
|
ToastCommands.createToast({type: 'success', children: i18n._(ACCOUNT_CLAIMED_SUCCESSFULLY_DESCRIPTOR)});
|
||||||
|
|||||||
@@ -158,8 +158,14 @@ function requestNewEmailBody(
|
|||||||
ticket: string,
|
ticket: string,
|
||||||
newEmail: string,
|
newEmail: string,
|
||||||
originalProof: string,
|
originalProof: string,
|
||||||
): {ticket: string; new_email: string; original_proof: string} {
|
newPassword?: string,
|
||||||
return {ticket, new_email: newEmail, original_proof: originalProof};
|
): {ticket: string; new_email: string; original_proof: string; new_password?: string} {
|
||||||
|
return {
|
||||||
|
ticket,
|
||||||
|
new_email: newEmail,
|
||||||
|
original_proof: originalProof,
|
||||||
|
...(newPassword != null ? {new_password: newPassword} : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyNewEmailBody(
|
function verifyNewEmailBody(
|
||||||
@@ -365,11 +371,12 @@ export async function requestEmailChangeNew(
|
|||||||
ticket: string,
|
ticket: string,
|
||||||
newEmail: string,
|
newEmail: string,
|
||||||
originalProof: string,
|
originalProof: string,
|
||||||
|
newPassword?: string,
|
||||||
): Promise<EmailChangeRequestNewResponse> {
|
): Promise<EmailChangeRequestNewResponse> {
|
||||||
try {
|
try {
|
||||||
logger.debug('Requesting new email code');
|
logger.debug('Requesting new email code');
|
||||||
const response = await http.post<EmailChangeRequestNewResponse>(Endpoints.USER_EMAIL_CHANGE_REQUEST_NEW, {
|
const response = await http.post<EmailChangeRequestNewResponse>(Endpoints.USER_EMAIL_CHANGE_REQUEST_NEW, {
|
||||||
body: requestNewEmailBody(ticket, newEmail, originalProof),
|
body: requestNewEmailBody(ticket, newEmail, originalProof, newPassword),
|
||||||
});
|
});
|
||||||
return response.body;
|
return response.body;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -136,6 +136,9 @@ export type EmailChangeVerifyOriginalRequest = z.infer<typeof EmailChangeVerifyO
|
|||||||
export const EmailChangeRequestNewRequest = EmailChangeTicketRequest.extend({
|
export const EmailChangeRequestNewRequest = EmailChangeTicketRequest.extend({
|
||||||
new_email: EmailType.describe('New email address to switch to'),
|
new_email: EmailType.describe('New email address to switch to'),
|
||||||
original_proof: createStringType().describe('Proof token obtained from verifying the original email'),
|
original_proof: createStringType().describe('Proof token obtained from verifying the original email'),
|
||||||
|
new_password: PasswordType.optional().describe(
|
||||||
|
'Password the caller intends to set, rejected here instead of after the code is sent',
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type EmailChangeRequestNewRequest = z.infer<typeof EmailChangeRequestNewRequest>;
|
export type EmailChangeRequestNewRequest = z.infer<typeof EmailChangeRequestNewRequest>;
|
||||||
|
|||||||
Reference in new issue
Block a user