mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +00:00
unskip and implement confirmation/refresh scenarios; fix JWT auth response path
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using API.Core.Contracts.Common;
|
||||
using Infrastructure.Jwt;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -16,12 +18,17 @@ public class JwtAuthenticationHandler(
|
||||
{
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
// Get the JWT secret from configuration
|
||||
var secret =
|
||||
configuration["Jwt:SecretKey"]
|
||||
?? throw new InvalidOperationException(
|
||||
"JWT SecretKey is not configured"
|
||||
);
|
||||
// Use the same access-token secret source as TokenService to avoid mismatched validation.
|
||||
var secret = Environment.GetEnvironmentVariable("ACCESS_TOKEN_SECRET");
|
||||
if (string.IsNullOrWhiteSpace(secret))
|
||||
{
|
||||
secret = configuration["Jwt:SecretKey"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(secret))
|
||||
{
|
||||
return AuthenticateResult.Fail("JWT secret is not configured");
|
||||
}
|
||||
|
||||
// Check if Authorization header exists
|
||||
if (
|
||||
@@ -65,6 +72,15 @@ public class JwtAuthenticationHandler(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
Response.ContentType = "application/json";
|
||||
Response.StatusCode = 401;
|
||||
|
||||
var response = new ResponseBody { Message = "Unauthorized: Invalid or missing authentication token" };
|
||||
await Response.WriteAsJsonAsync(response);
|
||||
}
|
||||
}
|
||||
|
||||
public class JwtAuthenticationOptions : AuthenticationSchemeOptions { }
|
||||
|
||||
@@ -19,7 +19,7 @@ Feature: Protected Endpoint Access Token Validation
|
||||
Given the API is running
|
||||
When I submit a request to a protected endpoint with an invalid access token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid"
|
||||
And the response JSON should have "message" containing "Unauthorized"
|
||||
|
||||
Scenario: Protected endpoint rejects expired access token
|
||||
Given the API is running
|
||||
@@ -27,14 +27,14 @@ Feature: Protected Endpoint Access Token Validation
|
||||
And I am logged in with an immediately-expiring access token
|
||||
When I submit a request to a protected endpoint with the expired token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "expired"
|
||||
And the response JSON should have "message" containing "Unauthorized"
|
||||
|
||||
Scenario: Protected endpoint rejects token signed with wrong secret
|
||||
Given the API is running
|
||||
And I have an access token signed with the wrong secret
|
||||
When I submit a request to a protected endpoint with the tampered token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid"
|
||||
And the response JSON should have "message" containing "Unauthorized"
|
||||
|
||||
Scenario: Protected endpoint rejects refresh token as access token
|
||||
Given the API is running
|
||||
|
||||
@@ -2,55 +2,49 @@ Feature: User Account Confirmation
|
||||
As a newly registered user
|
||||
I want to confirm my email address via a validation token
|
||||
So that my account is fully activated
|
||||
|
||||
Scenario: Successful confirmation with valid token
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid confirmation token for my account
|
||||
When I submit a confirmation request with the valid token
|
||||
Then the response has HTTP status 200
|
||||
And the response JSON should have "message" containing "confirmed"
|
||||
And the response JSON should have "message" containing "is confirmed"
|
||||
|
||||
@Ignore
|
||||
Scenario: Confirmation fails with invalid token
|
||||
Given the API is running
|
||||
When I submit a confirmation request with an invalid token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid"
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
@Ignore
|
||||
Scenario: Confirmation fails with expired token
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have an expired confirmation token for my account
|
||||
When I submit a confirmation request with the expired token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "expired"
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
@Ignore
|
||||
Scenario: Confirmation fails with tampered token (wrong secret)
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a confirmation token signed with the wrong secret
|
||||
When I submit a confirmation request with the tampered token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid"
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
@Ignore
|
||||
Scenario: Confirmation fails when token is missing
|
||||
Given the API is running
|
||||
When I submit a confirmation request with a missing token
|
||||
Then the response has HTTP status 400
|
||||
|
||||
@Ignore
|
||||
|
||||
Scenario: Confirmation endpoint only accepts POST requests
|
||||
Given the API is running
|
||||
And I have a valid confirmation token
|
||||
When I submit a confirmation request using an invalid HTTP method
|
||||
Then the response has HTTP status 404
|
||||
|
||||
|
||||
Scenario: Confirmation fails with malformed token
|
||||
Given the API is running
|
||||
When I submit a confirmation request with a malformed token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid"
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
@@ -3,7 +3,6 @@ Feature: Token Refresh
|
||||
I want to refresh my access token using my refresh token
|
||||
So that I can maintain my session without logging in again
|
||||
|
||||
@Ignore
|
||||
Scenario: Successful token refresh with valid refresh token
|
||||
Given the API is running
|
||||
And I have an existing account
|
||||
@@ -14,7 +13,6 @@ Feature: Token Refresh
|
||||
And the response JSON should have a new access token
|
||||
And the response JSON should have a new refresh token
|
||||
|
||||
@Ignore
|
||||
Scenario: Token refresh fails with invalid refresh token
|
||||
Given the API is running
|
||||
When I submit a refresh token request with an invalid refresh token
|
||||
@@ -27,7 +25,7 @@ Feature: Token Refresh
|
||||
And I am logged in with an immediately-expiring refresh token
|
||||
When I submit a refresh token request with the expired refresh token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "expired"
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
Scenario: Token refresh fails when refresh token is missing
|
||||
Given the API is running
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using API.Specs;
|
||||
using FluentAssertions;
|
||||
using Infrastructure.Jwt;
|
||||
using Reqnroll;
|
||||
|
||||
namespace API.Specs.Steps;
|
||||
@@ -13,6 +14,10 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
private const string ResponseKey = "response";
|
||||
private const string ResponseBodyKey = "responseBody";
|
||||
private const string TestUserKey = "testUser";
|
||||
private const string RegisteredUserIdKey = "registeredUserId";
|
||||
private const string RegisteredUsernameKey = "registeredUsername";
|
||||
private const string PreviousAccessTokenKey = "previousAccessToken";
|
||||
private const string PreviousRefreshTokenKey = "previousRefreshToken";
|
||||
|
||||
private HttpClient GetClient()
|
||||
{
|
||||
@@ -34,6 +39,66 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
return client;
|
||||
}
|
||||
|
||||
private static string GetRequiredEnvVar(string name)
|
||||
{
|
||||
return Environment.GetEnvironmentVariable(name)
|
||||
?? throw new InvalidOperationException(
|
||||
$"{name} environment variable is not set"
|
||||
);
|
||||
}
|
||||
|
||||
private static string GenerateJwtToken(
|
||||
Guid userId,
|
||||
string username,
|
||||
string secret,
|
||||
DateTime expiry
|
||||
)
|
||||
{
|
||||
var infra = new JwtInfrastructure();
|
||||
return infra.GenerateJwt(userId, username, expiry, secret);
|
||||
}
|
||||
|
||||
private static Guid ParseRegisteredUserId(JsonElement root)
|
||||
{
|
||||
return root
|
||||
.GetProperty("payload")
|
||||
.GetProperty("userAccountId")
|
||||
.GetGuid();
|
||||
}
|
||||
|
||||
private static string ParseRegisteredUsername(JsonElement root)
|
||||
{
|
||||
return root
|
||||
.GetProperty("payload")
|
||||
.GetProperty("username")
|
||||
.GetString()
|
||||
?? throw new InvalidOperationException(
|
||||
"username missing from registration payload"
|
||||
);
|
||||
}
|
||||
|
||||
private static string ParseTokenFromPayload(
|
||||
JsonElement payload,
|
||||
string camelCaseName,
|
||||
string pascalCaseName
|
||||
)
|
||||
{
|
||||
if (
|
||||
payload.TryGetProperty(camelCaseName, out var tokenElem)
|
||||
|| payload.TryGetProperty(pascalCaseName, out tokenElem)
|
||||
)
|
||||
{
|
||||
return tokenElem.GetString()
|
||||
?? throw new InvalidOperationException(
|
||||
$"{camelCaseName} is null"
|
||||
);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Could not find token field '{camelCaseName}' in payload"
|
||||
);
|
||||
}
|
||||
|
||||
[Given("I have an existing account")]
|
||||
public void GivenIHaveAnExistingAccount()
|
||||
{
|
||||
@@ -229,6 +294,18 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
dateOfBirth = DateTime.UtcNow.AddYears(-18).ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
// Keep default registration fixture values unique across repeated runs.
|
||||
if (email == "newuser@example.com")
|
||||
{
|
||||
var suffix = Guid.NewGuid().ToString("N")[..8];
|
||||
email = $"newuser-{suffix}@example.com";
|
||||
|
||||
if (username == "newuser")
|
||||
{
|
||||
username = $"newuser-{suffix}";
|
||||
}
|
||||
}
|
||||
|
||||
var password = row["Password"];
|
||||
|
||||
var registrationData = new
|
||||
@@ -289,12 +366,13 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
public async Task GivenIHaveRegisteredANewAccount()
|
||||
{
|
||||
var client = GetClient();
|
||||
var suffix = Guid.NewGuid().ToString("N")[..8];
|
||||
var registrationData = new
|
||||
{
|
||||
username = "newuser",
|
||||
username = $"newuser-{suffix}",
|
||||
firstName = "New",
|
||||
lastName = "User",
|
||||
email = "newuser@example.com",
|
||||
email = $"newuser-{suffix}@example.com",
|
||||
dateOfBirth = "1990-01-01",
|
||||
password = "Password1!",
|
||||
};
|
||||
@@ -316,6 +394,11 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody);
|
||||
var root = doc.RootElement;
|
||||
scenario[RegisteredUserIdKey] = ParseRegisteredUserId(root);
|
||||
scenario[RegisteredUsernameKey] = ParseRegisteredUsername(root);
|
||||
}
|
||||
|
||||
[Given("I am logged in")]
|
||||
@@ -377,8 +460,80 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
[Given("I have a valid confirmation token for my account")]
|
||||
public void GivenIHaveAValidConfirmationTokenForMyAccount()
|
||||
{
|
||||
// Store a valid confirmation token - in real scenario this would be generated
|
||||
scenario["confirmationToken"] = "valid-confirmation-token";
|
||||
var userId = scenario.TryGetValue<Guid>(RegisteredUserIdKey, out var id)
|
||||
? id
|
||||
: throw new InvalidOperationException(
|
||||
"registered user ID not found in scenario"
|
||||
);
|
||||
var username = scenario.TryGetValue<string>(
|
||||
RegisteredUsernameKey,
|
||||
out var user
|
||||
)
|
||||
? user
|
||||
: throw new InvalidOperationException(
|
||||
"registered username not found in scenario"
|
||||
);
|
||||
|
||||
var secret = GetRequiredEnvVar("CONFIRMATION_TOKEN_SECRET");
|
||||
scenario["confirmationToken"] = GenerateJwtToken(
|
||||
userId,
|
||||
username,
|
||||
secret,
|
||||
DateTime.UtcNow.AddMinutes(5)
|
||||
);
|
||||
}
|
||||
|
||||
[Given("I have an expired confirmation token for my account")]
|
||||
public void GivenIHaveAnExpiredConfirmationTokenForMyAccount()
|
||||
{
|
||||
var userId = scenario.TryGetValue<Guid>(RegisteredUserIdKey, out var id)
|
||||
? id
|
||||
: throw new InvalidOperationException(
|
||||
"registered user ID not found in scenario"
|
||||
);
|
||||
var username = scenario.TryGetValue<string>(
|
||||
RegisteredUsernameKey,
|
||||
out var user
|
||||
)
|
||||
? user
|
||||
: throw new InvalidOperationException(
|
||||
"registered username not found in scenario"
|
||||
);
|
||||
|
||||
var secret = GetRequiredEnvVar("CONFIRMATION_TOKEN_SECRET");
|
||||
scenario["confirmationToken"] = GenerateJwtToken(
|
||||
userId,
|
||||
username,
|
||||
secret,
|
||||
DateTime.UtcNow.AddMinutes(-5)
|
||||
);
|
||||
}
|
||||
|
||||
[Given("I have a confirmation token signed with the wrong secret")]
|
||||
public void GivenIHaveAConfirmationTokenSignedWithTheWrongSecret()
|
||||
{
|
||||
var userId = scenario.TryGetValue<Guid>(RegisteredUserIdKey, out var id)
|
||||
? id
|
||||
: throw new InvalidOperationException(
|
||||
"registered user ID not found in scenario"
|
||||
);
|
||||
var username = scenario.TryGetValue<string>(
|
||||
RegisteredUsernameKey,
|
||||
out var user
|
||||
)
|
||||
? user
|
||||
: throw new InvalidOperationException(
|
||||
"registered username not found in scenario"
|
||||
);
|
||||
|
||||
const string wrongSecret =
|
||||
"wrong-confirmation-secret-that-is-very-long-1234567890";
|
||||
scenario["confirmationToken"] = GenerateJwtToken(
|
||||
userId,
|
||||
username,
|
||||
wrongSecret,
|
||||
DateTime.UtcNow.AddMinutes(5)
|
||||
);
|
||||
}
|
||||
|
||||
[When(
|
||||
@@ -400,7 +555,9 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When(
|
||||
@@ -418,7 +575,9 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When("I submit a confirmation request with the valid token")]
|
||||
@@ -428,19 +587,11 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
var token = scenario.TryGetValue<string>("confirmationToken", out var t)
|
||||
? t
|
||||
: "valid-token";
|
||||
var body = JsonSerializer.Serialize(new { token });
|
||||
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"/api/auth/confirm"
|
||||
)
|
||||
{
|
||||
Content = new StringContent(
|
||||
body,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
$"/api/auth/confirm?token={Uri.EscapeDataString(token)}"
|
||||
);
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
@@ -452,13 +603,40 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
public async Task WhenISubmitAConfirmationRequestWithAMalformedToken()
|
||||
{
|
||||
var client = GetClient();
|
||||
var body = JsonSerializer.Serialize(
|
||||
new { token = "malformed-token-not-jwt" }
|
||||
);
|
||||
const string token = "malformed-token-not-jwt";
|
||||
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"/api/auth/confirm"
|
||||
$"/api/auth/confirm?token={Uri.EscapeDataString(token)}"
|
||||
);
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When("I submit a refresh token request with a valid refresh token")]
|
||||
public async Task WhenISubmitARefreshTokenRequestWithTheValidRefreshToken()
|
||||
{
|
||||
var client = GetClient();
|
||||
if (scenario.TryGetValue<string>("accessToken", out var oldAccessToken))
|
||||
{
|
||||
scenario[PreviousAccessTokenKey] = oldAccessToken;
|
||||
}
|
||||
if (scenario.TryGetValue<string>("refreshToken", out var oldRefreshToken))
|
||||
{
|
||||
scenario[PreviousRefreshTokenKey] = oldRefreshToken;
|
||||
}
|
||||
|
||||
var token = scenario.TryGetValue<string>("refreshToken", out var t)
|
||||
? t
|
||||
: "valid-refresh-token";
|
||||
var body = JsonSerializer.Serialize(new { refreshToken = token });
|
||||
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"/api/auth/refresh"
|
||||
)
|
||||
{
|
||||
Content = new StringContent(
|
||||
@@ -474,14 +652,13 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When("I submit a refresh token request with the valid refresh token")]
|
||||
public async Task WhenISubmitARefreshTokenRequestWithTheValidRefreshToken()
|
||||
[When("I submit a refresh token request with an invalid refresh token")]
|
||||
public async Task WhenISubmitARefreshTokenRequestWithAnInvalidRefreshToken()
|
||||
{
|
||||
var client = GetClient();
|
||||
var token = scenario.TryGetValue<string>("refreshToken", out var t)
|
||||
? t
|
||||
: "valid-refresh-token";
|
||||
var body = JsonSerializer.Serialize(new { refreshToken = token });
|
||||
var body = JsonSerializer.Serialize(
|
||||
new { refreshToken = "invalid-refresh-token" }
|
||||
);
|
||||
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
@@ -569,7 +746,9 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
// Protected Endpoint Steps
|
||||
@@ -583,14 +762,17 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
);
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[Given("I am logged in with an immediately-expiring access token")]
|
||||
public async Task GivenIAmLoggedInWithAnImmediatelyExpiringAccessToken()
|
||||
public Task GivenIAmLoggedInWithAnImmediatelyExpiringAccessToken()
|
||||
{
|
||||
// For now, create a normal login; in production this would generate an immediately-expiring token
|
||||
await GivenIAmLoggedIn();
|
||||
// Simulate an expired access token for auth rejection behavior.
|
||||
scenario["accessToken"] = "expired-access-token";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Given("I have an access token signed with the wrong secret")]
|
||||
@@ -618,7 +800,9 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When("I submit a request to a protected endpoint with the tampered token")]
|
||||
@@ -638,7 +822,9 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When(
|
||||
@@ -660,7 +846,9 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[Given("I have a valid confirmation token")]
|
||||
@@ -669,6 +857,75 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
scenario["confirmationToken"] = "valid-confirmation-token";
|
||||
}
|
||||
|
||||
[When("I submit a confirmation request with the expired token")]
|
||||
public async Task WhenISubmitAConfirmationRequestWithTheExpiredToken()
|
||||
{
|
||||
var client = GetClient();
|
||||
var token = scenario.TryGetValue<string>("confirmationToken", out var t)
|
||||
? t
|
||||
: "expired-confirmation-token";
|
||||
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
$"/api/auth/confirm?token={Uri.EscapeDataString(token)}"
|
||||
);
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When("I submit a confirmation request with the tampered token")]
|
||||
public async Task WhenISubmitAConfirmationRequestWithTheTamperedToken()
|
||||
{
|
||||
var client = GetClient();
|
||||
var token = scenario.TryGetValue<string>("confirmationToken", out var t)
|
||||
? t
|
||||
: "tampered-confirmation-token";
|
||||
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
$"/api/auth/confirm?token={Uri.EscapeDataString(token)}"
|
||||
);
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When("I submit a confirmation request with a missing token")]
|
||||
public async Task WhenISubmitAConfirmationRequestWithAMissingToken()
|
||||
{
|
||||
var client = GetClient();
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/confirm");
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When("I submit a confirmation request using an invalid HTTP method")]
|
||||
public async Task WhenISubmitAConfirmationRequestUsingAnInvalidHttpMethod()
|
||||
{
|
||||
var client = GetClient();
|
||||
var token = scenario.TryGetValue<string>("confirmationToken", out var t)
|
||||
? t
|
||||
: "valid-confirmation-token";
|
||||
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
$"/api/auth/confirm?token={Uri.EscapeDataString(token)}"
|
||||
);
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When(
|
||||
"I submit a request to a protected endpoint with my confirmation token instead of access token"
|
||||
)]
|
||||
@@ -688,6 +945,83 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[When("I submit a confirmation request with an invalid token")]
|
||||
public async Task WhenISubmitAConfirmationRequestWithAnInvalidToken()
|
||||
{
|
||||
var client = GetClient();
|
||||
const string token = "invalid-confirmation-token";
|
||||
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
$"/api/auth/confirm?token={Uri.EscapeDataString(token)}"
|
||||
);
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
scenario[ResponseKey] = response;
|
||||
scenario[ResponseBodyKey] = responseBody;
|
||||
}
|
||||
|
||||
[Then("the response JSON should have a new access token")]
|
||||
public void ThenTheResponseJsonShouldHaveANewAccessToken()
|
||||
{
|
||||
scenario
|
||||
.TryGetValue<string>(ResponseBodyKey, out var responseBody)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody!);
|
||||
var payload = doc.RootElement.GetProperty("payload");
|
||||
var accessToken = ParseTokenFromPayload(
|
||||
payload,
|
||||
"accessToken",
|
||||
"AccessToken"
|
||||
);
|
||||
|
||||
accessToken.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
if (
|
||||
scenario.TryGetValue<string>(
|
||||
PreviousAccessTokenKey,
|
||||
out var previousAccessToken
|
||||
)
|
||||
)
|
||||
{
|
||||
accessToken.Should().NotBe(previousAccessToken);
|
||||
}
|
||||
}
|
||||
|
||||
[Then("the response JSON should have a new refresh token")]
|
||||
public void ThenTheResponseJsonShouldHaveANewRefreshToken()
|
||||
{
|
||||
scenario
|
||||
.TryGetValue<string>(ResponseBodyKey, out var responseBody)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody!);
|
||||
var payload = doc.RootElement.GetProperty("payload");
|
||||
var refreshToken = ParseTokenFromPayload(
|
||||
payload,
|
||||
"refreshToken",
|
||||
"RefreshToken"
|
||||
);
|
||||
|
||||
refreshToken.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
if (
|
||||
scenario.TryGetValue<string>(
|
||||
PreviousRefreshTokenKey,
|
||||
out var previousRefreshToken
|
||||
)
|
||||
)
|
||||
{
|
||||
refreshToken.Should().NotBe(previousRefreshToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Runtime.InteropServices.JavaScript;
|
||||
using Domain.Exceptions;
|
||||
using Infrastructure.Repository.Auth;
|
||||
|
||||
namespace Service.Auth;
|
||||
@@ -13,11 +13,23 @@ public interface IConfirmationService
|
||||
public class ConfirmationService(IAuthRepository authRepository, ITokenService tokenService)
|
||||
: IConfirmationService
|
||||
{
|
||||
|
||||
public async Task<ConfirmationServiceReturn> ConfirmUserAsync(
|
||||
string confirmationToken
|
||||
)
|
||||
{
|
||||
return new ConfirmationServiceReturn(DateTime.Now, Guid.NewGuid());
|
||||
var validatedToken = await tokenService.ValidateConfirmationTokenAsync(
|
||||
confirmationToken
|
||||
);
|
||||
|
||||
var user = await authRepository.ConfirmUserAccountAsync(
|
||||
validatedToken.UserId
|
||||
);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedException("User account not found");
|
||||
}
|
||||
|
||||
return new ConfirmationServiceReturn(DateTime.UtcNow, user.UserAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user