Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions Crypter.API/Controllers/UserAuthenticationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
using Crypter.API.Methods;
using Crypter.Common.Contracts;
using Crypter.Common.Contracts.Features.UserAuthentication;
using Crypter.Common.Contracts.Features.UserAuthentication.PasswordChange;
using Crypter.Core.Features.UserAuthentication.Commands;
using Crypter.Core.Features.UserAuthentication.Queries;
using EasyMonads;
Expand Down Expand Up @@ -101,6 +100,7 @@ or RegistrationError.InvalidEmailAddress
/// <returns></returns>
[HttpPost("login")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(LoginResponse))]
[ProducesResponseType(StatusCodes.Status307TemporaryRedirect, Type = typeof(ChallengeResponse))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ErrorResponse))]
public async Task<IActionResult> LoginAsync([FromBody] LoginRequest request)
Expand All @@ -116,7 +116,8 @@ IActionResult MakeErrorResponse(LoginError error)
or LoginError.InvalidPassword
or LoginError.InvalidTokenTypeRequested
or LoginError.ExcessiveFailedLoginAttempts
or LoginError.InvalidPasswordVersion => MakeErrorResponseBase(HttpStatusCode.BadRequest, error)
or LoginError.InvalidPasswordVersion
or LoginError.InvalidMultiFactorChallenge => MakeErrorResponseBase(HttpStatusCode.BadRequest, error),
};
#pragma warning restore CS8524
}
Expand All @@ -126,7 +127,9 @@ or LoginError.ExcessiveFailedLoginAttempts
return await _sender.Send(command)
.MatchAsync(
MakeErrorResponse,
Ok,
x => x.Match<IActionResult>(
y => RedirectPreserveMethod(y.ChallengeHash), // TODO figure out the real redirect url?
Ok),
MakeErrorResponse(LoginError.UnknownError));
}

Expand Down Expand Up @@ -161,7 +164,6 @@ IActionResult MakeErrorResponse(RefreshError error)
#pragma warning restore CS8524
}


string requestUserAgent = HeadersParser.GetUserAgent(HttpContext.Request.Headers);
RefreshUserSessionCommand request = new RefreshUserSessionCommand(User, requestUserAgent);
return await _sender.Send(request)
Expand All @@ -182,8 +184,7 @@ IActionResult MakeErrorResponse(RefreshError error)
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(void))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ErrorResponse))]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(void))]
public async Task<IActionResult> PasswordChallengeAsync([FromBody] PasswordChallengeRequest request,
CancellationToken cancellationToken)
public async Task<IActionResult> PasswordChallengeAsync([FromBody] PasswordChallengeRequest request, CancellationToken cancellationToken)
{
IActionResult MakeErrorResponse(PasswordChallengeError error)
{
Expand Down
6 changes: 6 additions & 0 deletions Crypter.Common.Client/Crypter.Common.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
</ItemGroup>

<ItemGroup>
<Reference Include="OneOf">
<HintPath>..\..\..\.nuget\packages\oneof\3.0.271\lib\netstandard2.0\OneOf.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Crypter File Transfer
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
Expand Down Expand Up @@ -40,6 +40,7 @@
using Crypter.Common.Contracts;
using Crypter.Common.Contracts.Features.UserAuthentication;
using EasyMonads;
using OneOf;

namespace Crypter.Common.Client.HttpClients;

Expand Down Expand Up @@ -154,6 +155,16 @@ public async Task<Either<ErrorResponse, TResponse>> PostEitherAsync<TRequest, TR
return await DeserializeResponseAsync<TResponse>(response);
}

public async Task<Either<ErrorResponse, OneOf<T0, T1>>> PostEitherAsync<TRequest, T0, T1>(string uri, TRequest body, HttpStatusCode t0StatusCode, HttpStatusCode t1StatusCode)
where T0 : class
where T1 : class
where TRequest : class
{
Func<HttpRequestMessage> requestFactory = MakeRequestMessageFactory(HttpMethod.Post, uri, body);
using HttpResponseMessage response = await SendWithAuthenticationAsync(requestFactory, false);
return await DeserializeResponseAsync<T0, T1>(response, t0StatusCode, t1StatusCode);
}

public async Task<Maybe<Unit>> PostMaybeUnitResponseAsync<TRequest>(string uri, TRequest body)
where TRequest : class
{
Expand Down Expand Up @@ -325,8 +336,7 @@ private async Task<Either<ErrorResponse, Unit>> DeserializeEitherUnitResponseAsy
return Unit.Default;
}

private async Task<Either<ErrorResponse, TResponse>> DeserializeResponseAsync<TResponse>(
HttpResponseMessage response)
private async Task<Either<ErrorResponse, TResponse>> DeserializeResponseAsync<TResponse>(HttpResponseMessage response)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Expand All @@ -339,9 +349,38 @@ private async Task<Either<ErrorResponse, TResponse>> DeserializeResponseAsync<TR
: await JsonSerializer.DeserializeAsync<ErrorResponse>(stream, _jsonSerializerOptions)
.ConfigureAwait(false);
}

private async Task<Either<ErrorResponse, OneOf<T0, T1>>> DeserializeResponseAsync<T0, T1>(HttpResponseMessage response, HttpStatusCode t0StatusCode, HttpStatusCode t1StatusCode)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
return Either<ErrorResponse, OneOf<T0, T1>>.Neither;
}

await using Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

OneOf<T0, T1>? result = null;
if (response.StatusCode == t0StatusCode)
{
T0? t0 = await JsonSerializer.DeserializeAsync<T0>(stream, _jsonSerializerOptions).ConfigureAwait(false);
result = t0 ?? (OneOf<T0, T1>?)null;
}

if (response.StatusCode == t1StatusCode)
{
T1? t1 = await JsonSerializer.DeserializeAsync<T1>(stream, _jsonSerializerOptions).ConfigureAwait(false);
result = t1 ?? (OneOf<T0, T1>?)null;
}

if (result is null)
{
return await JsonSerializer.DeserializeAsync<ErrorResponse>(stream, _jsonSerializerOptions).ConfigureAwait(false);
}

return result.Value;
}

private async Task<Either<ErrorResponse, StreamDownloadResponse>> GetStreamResponseAsync(
HttpResponseMessage response)
private async Task<Either<ErrorResponse, StreamDownloadResponse>> GetStreamResponseAsync(HttpResponseMessage response)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Expand Down
45 changes: 40 additions & 5 deletions Crypter.Common.Client/HttpClients/CrypterHttpClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Crypter File Transfer
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
Expand All @@ -26,13 +26,15 @@

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using Crypter.Common.Client.Interfaces.HttpClients;
using Crypter.Common.Contracts;
using EasyMonads;
using OneOf;

namespace Crypter.Common.Client.HttpClients;

Expand Down Expand Up @@ -84,6 +86,15 @@ public async Task<Either<ErrorResponse, TResponse>> PostEitherAsync<TRequest, TR
return await SendRequestEitherResponseAsync<TResponse>(request);
}

public async Task<Either<ErrorResponse, OneOf<T0, T1>>> PostEitherAsync<TRequest, T0, T1>(string uri, TRequest body, HttpStatusCode t0, HttpStatusCode t1)
where T0 : class
where T1 : class
where TRequest : class
{
using HttpRequestMessage request = MakeRequestMessage(HttpMethod.Post, uri, body);
return await SendRequestEitherResponseAsync<T0, T1>(request, t0, t1);
}

public async Task<Maybe<Unit>> PostMaybeUnitResponseAsync<TRequest>(string uri, TRequest body)
where TRequest : class
{
Expand Down Expand Up @@ -171,15 +182,39 @@ private async Task<Either<ErrorResponse, TResponse>> SendRequestEitherResponseAs

private async Task<Either<ErrorResponse, Unit>> SendRequestEitherUnitResponseAsync(HttpRequestMessage request)
{
using HttpResponseMessage response =
await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
using HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
await using Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return response.IsSuccessStatusCode
? Unit.Default
: await JsonSerializer.DeserializeAsync<ErrorResponse>(stream, _jsonSerializerOptions)
.ConfigureAwait(false);
: await JsonSerializer.DeserializeAsync<ErrorResponse>(stream, _jsonSerializerOptions).ConfigureAwait(false);
}

private async Task<Either<ErrorResponse, OneOf<T0, T1>>> SendRequestEitherResponseAsync<T0, T1>(HttpRequestMessage request, HttpStatusCode t0StatusCode, HttpStatusCode t1StatusCode)
{
using HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
await using Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

OneOf<T0, T1>? result = null;
if (response.StatusCode == t0StatusCode)
{
T0? t0 = await JsonSerializer.DeserializeAsync<T0>(stream, _jsonSerializerOptions).ConfigureAwait(false);
result = t0 ?? (OneOf<T0, T1>?)null;
}

if (response.StatusCode == t1StatusCode)
{
T1? t1 = await JsonSerializer.DeserializeAsync<T1>(stream, _jsonSerializerOptions).ConfigureAwait(false);
result = t1 ?? (OneOf<T0, T1>?)null;
}

if (result is null)
{
return await JsonSerializer.DeserializeAsync<ErrorResponse>(stream, _jsonSerializerOptions).ConfigureAwait(false);
}

return result.Value;
}

private async Task<Either<ErrorResponse, StreamDownloadResponse>> GetStreamAsync(HttpRequestMessage request)
{
HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Crypter File Transfer
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
Expand All @@ -25,12 +25,13 @@
*/

using System;
using System.Net;
using System.Threading.Tasks;
using Crypter.Common.Client.Interfaces.HttpClients;
using Crypter.Common.Client.Interfaces.Requests;
using Crypter.Common.Contracts.Features.UserAuthentication;
using Crypter.Common.Contracts.Features.UserAuthentication.PasswordChange;
using EasyMonads;
using OneOf;

namespace Crypter.Common.Client.HttpClients.Requests;

Expand Down Expand Up @@ -60,11 +61,11 @@ public Task<Either<RegistrationError, Unit>> RegisterAsync(RegistrationRequest r
.ExtractErrorCode<RegistrationError, Unit>();
}

public Task<Either<LoginError, LoginResponse>> LoginAsync(LoginRequest loginRequest)
public Task<Either<LoginError, OneOf<ChallengeResponse, LoginResponse>>> LoginAsync(LoginRequest loginRequest)
{
const string url = "api/user/authentication/login";
return _crypterHttpClient.PostEitherAsync<LoginRequest, LoginResponse>(url, loginRequest)
.ExtractErrorCode<LoginError, LoginResponse>();
return _crypterHttpClient.PostEitherAsync<LoginRequest, ChallengeResponse, LoginResponse>(url, loginRequest, HttpStatusCode.OK, HttpStatusCode.TemporaryRedirect)
.ExtractErrorCode<LoginError, OneOf<ChallengeResponse, LoginResponse>>();
}

public async Task<Either<RefreshError, RefreshResponse>> RefreshSessionAsync()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 Crypter File Transfer
* Copyright (C) 2025 Crypter File Transfer
*
* This file is part of the Crypter file transfer project.
*
Expand All @@ -25,10 +25,12 @@
*/

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Crypter.Common.Contracts;
using EasyMonads;
using OneOf;

namespace Crypter.Common.Client.Interfaces.HttpClients;

Expand All @@ -48,6 +50,11 @@ Task<Either<ErrorResponse, TResponse>> PostEitherAsync<TRequest, TResponse>(stri
where TRequest : class
where TResponse : class;

Task<Either<ErrorResponse, OneOf<T0, T1>>> PostEitherAsync<TRequest, T0, T1>(string uri, TRequest body, HttpStatusCode t0, HttpStatusCode t1)
where TRequest : class
where T0 : class
where T1 : class;

Task<Maybe<Unit>> PostMaybeUnitResponseAsync(string uri);

Task<Maybe<Unit>> PostMaybeUnitResponseAsync<TRequest>(string uri, TRequest body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@
using System;
using System.Threading.Tasks;
using Crypter.Common.Contracts.Features.UserAuthentication;
using Crypter.Common.Contracts.Features.UserAuthentication.PasswordChange;
using EasyMonads;
using OneOf;

namespace Crypter.Common.Client.Interfaces.Requests;

public interface IUserAuthenticationRequests
{
EventHandler? RefreshTokenRejectedHandler { set; }
Task<Either<RegistrationError, Unit>> RegisterAsync(RegistrationRequest registerRequest);
Task<Either<LoginError, LoginResponse>> LoginAsync(LoginRequest loginRequest);
Task<Either<LoginError, OneOf<ChallengeResponse, LoginResponse>>> LoginAsync(LoginRequest loginRequest);
Task<Either<RefreshError, RefreshResponse>> RefreshSessionAsync();
Task<Either<PasswordChallengeError, Unit>> PasswordChallengeAsync(PasswordChallengeRequest testPasswordRequest);
Task<Either<PasswordChangeError, Unit>> ChangePasswordAsync(PasswordChangeRequest passwordChangeRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
using Crypter.Common.Contracts.Features.UserAuthentication;
using Crypter.Common.Primitives;
using EasyMonads;
using OneOf;

namespace Crypter.Common.Client.Interfaces.Services;

Expand All @@ -39,7 +40,7 @@ public interface IUserSessionService
Maybe<UserSession> Session { get; }

Task<bool> IsLoggedInAsync();
Task<Either<LoginError, Unit>> LoginAsync(Username username, Password password, bool rememberUser);
Task<Either<LoginError, Unit>> LoginAsync(Username username, Password password, bool rememberUser, Func<string, Task<MultiFactorVerification>> multifactorVerificationDelegate);
Task<bool> TestPasswordAsync(Password password);
Task<Unit> LogoutAsync();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/

using System.Threading.Tasks;
using Crypter.Common.Contracts.Features.UserAuthentication.PasswordChange;
using Crypter.Common.Contracts.Features.UserAuthentication;
using Crypter.Common.Primitives;
using EasyMonads;

Expand Down
Loading