Skip to content

Commit 3aea4bd

Browse files
committed
chore: retire deprecated password-reset aliases with 410 Gone
The deprecated /reset-password and /recover password-reset aliases are unused by the new frontend (verified: no call sites outside the generated SDK), so retire them alongside the v1 auth endpoints. - POST /2/account/reset-password, POST /1/account/recover/{id}/{secret}, and HEAD /1/account/recover/{id}/{secret} now return 410 Gone pointing at their canonical replacements, and are hidden from the OpenAPI doc. - Replace the "legacy route still works" tests with 410 Gone assertions and add coverage for the retired /reset-password alias.
1 parent 81c162c commit 3aea4bd

4 files changed

Lines changed: 37 additions & 76 deletions

File tree

API.IntegrationTests/Tests/MailTests.cs

Lines changed: 22 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ public async Task V1PasswordReset_Retired_Returns410Gone()
9494
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Gone);
9595
}
9696

97+
[Test]
98+
public async Task ResetPasswordAlias_Retired_Returns410Gone()
99+
{
100+
using var client = WebApplicationFactory.CreateClient();
101+
var response = await client.PostAsync("/2/account/reset-password", TestHelper.JsonContent(new
102+
{
103+
email = "whatever@test.org",
104+
turnstileResponse = "valid-token"
105+
}));
106+
107+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Gone);
108+
}
109+
97110
[Test]
98111
public async Task V2PasswordReset_SendsPasswordResetEmail()
99112
{
@@ -311,62 +324,26 @@ public async Task ChangeEmail_Unchanged_Returns400_AndSendsNoEmail()
311324
}
312325

313326
[Test]
314-
public async Task PasswordResetComplete_LegacyRecoverRoute_StillWorks()
327+
public async Task PasswordResetComplete_LegacyRecoverRoute_Returns410Gone()
315328
{
316-
var email = TestHelper.UniqueEmail("mail-pwreset-legacy");
317-
var username = TestHelper.UniqueUsername("mailpwresetlegacy");
318-
const string newPassword = "LegacyNewPassword456#";
319-
using var mailpit = WebApplicationFactory.CreateMailpitHelper();
320-
321-
await TestHelper.CreateUserInDb(WebApplicationFactory, username, email, "OldPassword123#");
322-
323329
using var client = WebApplicationFactory.CreateClient();
324330

325-
var resetResponse = await client.PostAsync("/2/account/password-reset", TestHelper.JsonContent(new { email, turnstileResponse = "valid-token" }));
326-
await Assert.That(resetResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
331+
var response = await client.PostAsync(
332+
$"/1/account/recover/{Guid.CreateVersion7()}/somesecret",
333+
TestHelper.JsonContent(new { password = "LegacyNewPassword456#" }));
327334

328-
var message = await mailpit.WaitForMessageAsync(email);
329-
await Assert.That(message).IsNotNull();
330-
var fullMessage = await mailpit.GetMessageAsync(message!.Id);
331-
var (resetId, secret) = ExtractPasswordResetParams(fullMessage!.Html);
332-
await Assert.That(resetId).IsNotNull().And.IsNotEmpty();
333-
334-
// Hit the deprecated route directly — must still complete the reset.
335-
var completeResponse = await client.PostAsync(
336-
$"/1/account/recover/{resetId}/{secret}",
337-
TestHelper.JsonContent(new { password = newPassword }));
338-
await Assert.That(completeResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
339-
340-
var loginResponse = await client.PostAsync("/2/account/login", TestHelper.JsonContent(new
341-
{
342-
usernameOrEmail = email,
343-
password = newPassword,
344-
turnstileResponse = "valid-token"
345-
}));
346-
await Assert.That(loginResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
335+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Gone);
347336
}
348337

349338
[Test]
350-
public async Task PasswordResetCheck_LegacyHeadRecoverRoute_StillWorks()
339+
public async Task PasswordResetCheck_LegacyHeadRecoverRoute_Returns410Gone()
351340
{
352-
var email = TestHelper.UniqueEmail("mail-pwreset-check-legacy");
353-
var username = TestHelper.UniqueUsername("mailpwresetchecklegacy");
354-
using var mailpit = WebApplicationFactory.CreateMailpitHelper();
355-
356-
await TestHelper.CreateUserInDb(WebApplicationFactory, username, email, "OldPassword123#");
357-
358341
using var client = WebApplicationFactory.CreateClient();
359-
var resetResponse = await client.PostAsync("/2/account/password-reset", TestHelper.JsonContent(new { email, turnstileResponse = "valid-token" }));
360-
await Assert.That(resetResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
361342

362-
var message = await mailpit.WaitForMessageAsync(email);
363-
await Assert.That(message).IsNotNull();
364-
var fullMessage = await mailpit.GetMessageAsync(message!.Id);
365-
var (resetId, secret) = ExtractPasswordResetParams(fullMessage!.Html);
343+
var response = await client.SendAsync(new HttpRequestMessage(
344+
HttpMethod.Head, $"/1/account/recover/{Guid.CreateVersion7()}/somesecret"));
366345

367-
var legacyCheck = await client.SendAsync(new HttpRequestMessage(
368-
HttpMethod.Head, $"/1/account/recover/{resetId}/{secret}"));
369-
await Assert.That(legacyCheck.StatusCode).IsEqualTo(HttpStatusCode.OK);
346+
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Gone);
370347
}
371348

372349
[Test]

API/Controller/Account/PasswordResetCheckValid.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,16 @@ public Task<IActionResult> PasswordResetCheckValid([FromRoute] Guid passwordRese
2828
=> CheckPasswordReset(passwordResetId, secret, cancellationToken);
2929

3030
/// <summary>
31-
/// Check if a password reset is in progress. Deprecated: use GET /password-reset/{id}/{secret} instead.
31+
/// Check if a password reset is in progress. Retired: use GET /1/account/password-reset/{passwordResetId}/{secret} instead.
3232
/// </summary>
3333
/// <param name="passwordResetId">The id of the password reset</param>
3434
/// <param name="secret">The secret of the password reset</param>
35-
/// <param name="cancellationToken"></param>
36-
/// <response code="200">Valid password reset process</response>
37-
/// <response code="404">Password reset process not found</response>
38-
[Obsolete("Use GET /password-reset/{passwordResetId}/{secret} instead.")]
35+
[Obsolete("Retired. Use GET /1/account/password-reset/{passwordResetId}/{secret} instead.")]
3936
[HttpHead("recover/{passwordResetId}/{secret}")]
40-
[EnableRateLimiting("auth")]
41-
[ProducesResponseType<LegacyEmptyResponse>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
42-
[ProducesResponseType<OpenShockProblem>(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // PasswordResetNotFound
37+
[ApiExplorerSettings(IgnoreApi = true)]
4338
[MapToApiVersion("1")]
44-
public Task<IActionResult> PasswordResetCheckValidLegacy([FromRoute] Guid passwordResetId, [FromRoute] string secret, CancellationToken cancellationToken)
45-
=> CheckPasswordReset(passwordResetId, secret, cancellationToken);
39+
public IActionResult PasswordResetCheckValidLegacy([FromRoute] Guid passwordResetId, [FromRoute] string secret)
40+
=> Problem(GoneError.EndpointRetired("GET /1/account/password-reset/{passwordResetId}/{secret}"));
4641

4742
private async Task<IActionResult> CheckPasswordReset(Guid passwordResetId, string secret, CancellationToken cancellationToken)
4843
{

API/Controller/Account/PasswordResetComplete.cs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,16 @@ public Task<IActionResult> PasswordResetComplete([FromRoute] Guid passwordResetI
3030
=> CompletePasswordReset(passwordResetId, secret, body);
3131

3232
/// <summary>
33-
/// Complete a password reset process. Deprecated: use POST /password-reset/{id}/{secret}/complete instead.
33+
/// Complete a password reset process. Retired: use POST /1/account/password-reset/{passwordResetId}/{secret}/complete instead.
3434
/// </summary>
3535
/// <param name="passwordResetId">The id of the password reset</param>
3636
/// <param name="secret">The secret of the password reset</param>
37-
/// <param name="body"></param>
38-
/// <response code="200">Password successfully changed</response>
39-
/// <response code="404">Password reset process not found</response>
40-
[Obsolete("Use POST /password-reset/{passwordResetId}/{secret}/complete instead.")]
37+
[Obsolete("Retired. Use POST /1/account/password-reset/{passwordResetId}/{secret}/complete instead.")]
4138
[HttpPost("recover/{passwordResetId}/{secret}")]
42-
[EnableRateLimiting("auth")]
43-
[Consumes(MediaTypeNames.Application.Json)]
44-
[ProducesResponseType<LegacyEmptyResponse>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
45-
[ProducesResponseType<OpenShockProblem>(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // PasswordResetNotFound
39+
[ApiExplorerSettings(IgnoreApi = true)]
4640
[MapToApiVersion("1")]
47-
public Task<IActionResult> PasswordResetCompleteLegacy([FromRoute] Guid passwordResetId,
48-
[FromRoute] string secret, [FromBody] PasswordResetProcessData body)
49-
=> CompletePasswordReset(passwordResetId, secret, body);
41+
public IActionResult PasswordResetCompleteLegacy([FromRoute] Guid passwordResetId, [FromRoute] string secret)
42+
=> Problem(GoneError.EndpointRetired("POST /1/account/password-reset/{passwordResetId}/{secret}/complete"));
5043

5144
private async Task<IActionResult> CompletePasswordReset(Guid passwordResetId, string secret, PasswordResetProcessData body)
5245
{

API/Controller/Account/PasswordResetInitiateV2.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.AspNetCore.RateLimiting;
55
using OpenShock.API.Models.Requests;
66
using OpenShock.API.Services.Turnstile;
7+
using OpenShock.Common.Errors;
78
using OpenShock.Common.Problems;
89

910
namespace OpenShock.API.Controller.Account;
@@ -24,18 +25,13 @@ public Task<IActionResult> PasswordResetInitiateV2([FromBody] PasswordResetReque
2425
=> PasswordResetInitiate(body, turnstileService, cancellationToken);
2526

2627
/// <summary>
27-
/// Initiate a password reset. Deprecated: use POST /password-reset instead.
28+
/// Initiate a password reset. Retired: use POST /2/account/password-reset instead.
2829
/// </summary>
29-
/// <response code="200">Password reset email sent if the email is associated to an registered account</response>
30-
[Obsolete("Use POST /password-reset instead.")]
30+
[Obsolete("Retired. Use POST /2/account/password-reset instead.")]
3131
[HttpPost("reset-password")]
32-
[EnableRateLimiting("auth")]
33-
[Consumes(MediaTypeNames.Application.Json)]
34-
[ProducesResponseType(StatusCodes.Status200OK)]
35-
[ProducesResponseType<OpenShockProblem>(StatusCodes.Status403Forbidden, MediaTypeNames.Application.ProblemJson)]
32+
[ApiExplorerSettings(IgnoreApi = true)]
3633
[MapToApiVersion("2")]
37-
public Task<IActionResult> PasswordResetInitiateV2Legacy([FromBody] PasswordResetRequestV2 body, [FromServices] ICloudflareTurnstileService turnstileService, CancellationToken cancellationToken)
38-
=> PasswordResetInitiate(body, turnstileService, cancellationToken);
34+
public IActionResult PasswordResetInitiateV2Legacy() => Problem(GoneError.EndpointRetired("POST /2/account/password-reset"));
3935

4036
private async Task<IActionResult> PasswordResetInitiate(PasswordResetRequestV2 body, ICloudflareTurnstileService turnstileService, CancellationToken cancellationToken)
4137
{

0 commit comments

Comments
 (0)