feat: Send reset password recordId as query parameter

This is a revert of a previous change
but is now possible due to the use of JSON bodies.
This does mean JavaScript is required in the HTML page,
but that will be required for future changes anyway.
This commit is contained in:
Joachim Van Herwegen
2021-11-15 14:36:57 +01:00
parent c216efd62f
commit 8f8e8e6df4
7 changed files with 21 additions and 11 deletions

View File

@@ -6,7 +6,7 @@
"comment": "Handles the reset password page submission",
"@id": "urn:solid-server:auth:password:ResetPasswordRoute",
"@type": "BasicInteractionRoute",
"route": "^/resetpassword/[^/]*$",
"route": "^/resetpassword/$",
"viewTemplates": {
"BasicInteractionRoute:_viewTemplates_key": "text/html",
"BasicInteractionRoute:_viewTemplates_value": "@css:templates/identity/email-password/reset-password.html.ejs"

View File

@@ -68,7 +68,8 @@ export class ForgotPasswordHandler extends InteractionHandler {
*/
private async sendResetMail(recordId: string, email: string): Promise<void> {
this.logger.info(`Sending password reset to ${email}`);
const resetLink = joinUrl(this.baseUrl, this.idpPath, `resetpassword/${recordId}`);
// `joinUrl` strips trailing slash when query parameter gets added
const resetLink = `${joinUrl(this.baseUrl, this.idpPath, 'resetpassword/')}?rid=${recordId}`;
const renderedEmail = await this.templateEngine.render({ resetLink });
await this.emailSender.handleSafe({
recipient: email,

View File

@@ -21,10 +21,8 @@ export class ResetPasswordHandler extends InteractionHandler {
}
public async handle({ operation }: InteractionHandlerInput): Promise<InteractionResponseResult> {
// Extract record ID from request URL
const recordId = /\/([^/]+)$/u.exec(operation.target.path)?.[1];
// Validate input data
const { password, confirmPassword } = await readJsonStream(operation.body.data);
const { password, confirmPassword, recordId } = await readJsonStream(operation.body.data);
assert(
typeof recordId === 'string' && recordId.length > 0,
'Invalid request. Open the link from your email again',

View File

@@ -15,7 +15,14 @@
<input id="confirmPassword" type="password" name="confirmPassword" placeholder="">
</li>
</ol>
<input type="hidden" id="recordId" name="recordId" value="">
</fieldset>
<p class="actions"><button type="submit" name="submit">Reset password</button></p>
</form>
<script>
const hidden = document.getElementById('recordId');
const recordId = new URLSearchParams(window.location.search).get('rid');
hidden.value = recordId;
</script>

View File

@@ -206,8 +206,12 @@ describe('A Solid server with IDP', (): void => {
// Reset password form has no action causing the current URL to be used
expect(relative).toBeUndefined();
// Extract recordId from URL since JS is used to add it
const recordId = /\?rid=([^/]+)$/u.exec(nextUrl)?.[1];
expect(typeof recordId).toBe('string');
// POST the new password to the same URL
const formData = stringify({ password: password2, confirmPassword: password2 });
const formData = stringify({ password: password2, confirmPassword: password2, recordId });
res = await fetch(nextUrl, {
method: 'POST',
headers: { 'content-type': APPLICATION_X_WWW_FORM_URLENCODED },

View File

@@ -64,7 +64,7 @@ describe('A ForgotPasswordHandler', (): void => {
expect(emailSender.handleSafe).toHaveBeenLastCalledWith({
recipient: email,
subject: 'Reset your password',
text: `To reset your password, go to this link: http://test.com/base/idp/resetpassword/${recordId}`,
text: `To reset your password, go to this link: http://test.com/base/idp/resetpassword/?rid=${recordId}`,
html,
});
});

View File

@@ -27,25 +27,25 @@ describe('A ResetPasswordHandler', (): void => {
const errorMessage = 'Invalid request. Open the link from your email again';
operation = createPostJsonOperation({});
await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
operation = createPostJsonOperation({}, '');
operation = createPostJsonOperation({ recordId: 5 });
await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
});
it('errors for invalid passwords.', async(): Promise<void> => {
const errorMessage = 'Your password and confirmation did not match.';
operation = createPostJsonOperation({ password: 'password!', confirmPassword: 'otherPassword!' }, url);
operation = createPostJsonOperation({ password: 'password!', confirmPassword: 'otherPassword!', recordId }, url);
await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
});
it('errors for invalid emails.', async(): Promise<void> => {
const errorMessage = 'This reset password link is no longer valid.';
operation = createPostJsonOperation({ password: 'password!', confirmPassword: 'password!' }, url);
operation = createPostJsonOperation({ password: 'password!', confirmPassword: 'password!', recordId }, url);
(accountStore.getForgotPasswordRecord as jest.Mock).mockResolvedValueOnce(undefined);
await expect(handler.handle({ operation })).rejects.toThrow(errorMessage);
});
it('renders a message on success.', async(): Promise<void> => {
operation = createPostJsonOperation({ password: 'password!', confirmPassword: 'password!' }, url);
operation = createPostJsonOperation({ password: 'password!', confirmPassword: 'password!', recordId }, url);
await expect(handler.handle({ operation })).resolves.toEqual({ type: 'response' });
expect(accountStore.getForgotPasswordRecord).toHaveBeenCalledTimes(1);
expect(accountStore.getForgotPasswordRecord).toHaveBeenLastCalledWith(recordId);