Skip to content

Commit

Permalink
Merge pull request #123 from PinguApps/90-feat-account-create-2fa-cha…
Browse files Browse the repository at this point in the history
…llenge

Implemented create 2fa challenge
  • Loading branch information
pingu2k4 authored Aug 10, 2024
2 parents df9813f + 770abf8 commit 6f1791b
Show file tree
Hide file tree
Showing 13 changed files with 341 additions and 10 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,14 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
```

## ⌛ Progress
<!-- ![22 / 288](https://progress-bar.dev/22/?scale=288&suffix=%20/%20288&width=500) -->
![Server & Client - 22 / 288](https://img.shields.io/badge/Server_&_Client-22%20%2F%20288-red?style=for-the-badge)
<!-- ![23 / 288](https://progress-bar.dev/23/?scale=288&suffix=%20/%20288&width=500) -->
![Server & Client - 23 / 288](https://img.shields.io/badge/Server_&_Client-23%20%2F%20288-red?style=for-the-badge)

<!-- ![2 / 195](https://progress-bar.dev/2/?scale=195&suffix=%20/%20195&width=300) -->
![Server - 2 / 195](https://img.shields.io/badge/Server-2%20%2F%20195-red?style=for-the-badge)

<!-- ![20 / 93](https://progress-bar.dev/20/?scale=93&suffix=%20/%2093&width=300) -->
![Client - 20 / 93](https://img.shields.io/badge/Client-20%20%2F%2093-red?style=for-the-badge)
<!-- ![21 / 93](https://progress-bar.dev/21/?scale=93&suffix=%20/%2093&width=300) -->
![Client - 21 / 93](https://img.shields.io/badge/Client-21%20%2F%2093-red?style=for-the-badge)

### 🔑 Key
| Icon | Definition |
Expand All @@ -155,8 +155,8 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
|| There is currently no intention to implement the endpoint for the given SDK type (client or server) |

### Account
<!-- ![22 / 52](https://progress-bar.dev/22/?scale=52&suffix=%20/%2052&width=120) -->
![Account - 22 / 52](https://img.shields.io/badge/Account-22%20%2F%2052-yellow?style=for-the-badge)
<!-- ![23 / 52](https://progress-bar.dev/23/?scale=52&suffix=%20/%2052&width=120) -->
![Account - 23 / 52](https://img.shields.io/badge/Account-23%20%2F%2052-yellow?style=for-the-badge)

| Endpoint | Client | Server |
|:-:|:-:|:-:|
Expand All @@ -171,7 +171,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
| [Add Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaAuthenticator) |||
| [Verify Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMfaAuthenticator) |||
| [Delete Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#deleteMfaAuthenticator) |||
| [Create 2FA Challenge](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaChallenge) | ||
| [Create 2FA Challenge](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaChallenge) | ||
| [Create MFA Challenge (confirmation)](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMfaChallenge) |||
| [List Factors](https://appwrite.io/docs/references/1.5.x/client-rest/account#listMfaFactors) |||
| [Get MFA Recovery Codes](https://appwrite.io/docs/references/1.5.x/client-rest/account#getMfaRecoveryCodes) |||
Expand Down
17 changes: 17 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/AccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,21 @@ public async Task<AppwriteResult> DeleteAuthenticator(DeleteAuthenticatorRequest
return e.GetExceptionResponse();
}
}

/// <inheritdoc/>
public async Task<AppwriteResult<MfaChallenge>> Create2faChallenge(Create2faChallengeRequest request)
{
try
{
request.Validate(true);

var result = await _accountApi.Create2faChallenge(Session, request);

return result.GetApiResponse();
}
catch (Exception e)
{
return e.GetExceptionResponse<MfaChallenge>();
}
}
}
8 changes: 8 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,12 @@ public interface IAccountClient
/// <param name="request">The request content</param>
/// <returns>The result</returns>
Task<AppwriteResult> DeleteAuthenticator(DeleteAuthenticatorRequest request);

/// <summary>
/// Begin the process of MFA verification after sign-in. Finish the flow with updateMfaChallenge method
/// <para><see href="https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaChallenge">Appwrite Docs</see></para>
/// </summary>
/// <param name="request">The request content</param>
/// <returns>The Mfa Challenge</returns>
Task<AppwriteResult<MfaChallenge>> Create2faChallenge(Create2faChallengeRequest request);
}
3 changes: 3 additions & 0 deletions src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,7 @@ internal interface IAccountApi : IBaseApi

[Delete("/account/mfa/authenticators/{type}")]
Task<IApiResponse> DeleteAuthenticator([Header("x-appwrite-session")] string? session, string type, [Body] DeleteAuthenticatorRequest request);

[Post("/account/mfa/challenge")]
Task<IApiResponse<MfaChallenge>> Create2faChallenge([Header("x-appwrite-session")] string? session, Create2faChallengeRequest request);
}
5 changes: 2 additions & 3 deletions src/PinguApps.Appwrite.Playground/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ public async Task Run(string[] args)
_client.SetSession(_session);

//var response = await _client.Account.AddAuthenticator();
var response = await _client.Account.DeleteAuthenticator(new Shared.Requests.DeleteAuthenticatorRequest
var response = await _client.Account.Create2faChallenge(new Shared.Requests.Create2faChallengeRequest
{
Type = "totp",
Otp = "413526"
Factor = Shared.Enums.SecondFactor.Email
});

Console.WriteLine(response.Result.Match(
Expand Down
30 changes: 30 additions & 0 deletions src/PinguApps.Appwrite.Shared/Enums/SecondFactor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Runtime.Serialization;

namespace PinguApps.Appwrite.Shared.Enums;

/// <summary>
/// The type of second factor
/// </summary>
public enum SecondFactor
{
/// <summary>
/// Email 2fa
/// </summary>
[EnumMember(Value = "email")]
Email,
/// <summary>
/// Phone 2fa
/// </summary>
[EnumMember(Value = "phone")]
Phone,
/// <summary>
/// Authenticator App 2fa
/// </summary>
[EnumMember(Value = "totp")]
Totp,
/// <summary>
/// 2fa Recovery Code
/// </summary>
[EnumMember(Value = "recoveryCode")]
RecoveryCode
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Enums;
using PinguApps.Appwrite.Shared.Requests.Validators;

namespace PinguApps.Appwrite.Shared.Requests;
public class Create2faChallengeRequest : BaseRequest<Create2faChallengeRequest, Create2faChallengeRequestValidator>
{
/// <summary>
/// Factor used for verification. Must be one of following: <list type="bullet">
/// <item><description>email</description></item>
/// <item><description>phone</description></item>
/// <item><description>totp</description></item>
/// <item><description>recoveryCode</description></item>
/// </list>
/// </summary>
[JsonPropertyName("factor")]
public SecondFactor Factor { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using FluentValidation;

namespace PinguApps.Appwrite.Shared.Requests.Validators;
public class Create2faChallengeRequestValidator : AbstractValidator<Create2faChallengeRequest>
{
public Create2faChallengeRequestValidator()
{
RuleFor(x => x.Factor).IsInEnum();
}
}
18 changes: 18 additions & 0 deletions src/PinguApps.Appwrite.Shared/Responses/MfaChallenge.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Text.Json.Serialization;

namespace PinguApps.Appwrite.Shared.Responses;

/// <summary>
/// An Appwrite Mfa Challenge object
/// </summary>
/// <param name="Id">Token ID</param>
/// <param name="CreatedAt">Token creation date in ISO 8601 format</param>
/// <param name="UserId">User ID</param>
/// <param name="Expire">Token expiration date in ISO 8601 format</param>
public record MfaChallenge(
[property: JsonPropertyName("$id")] string Id,
[property: JsonPropertyName("$createdAt")] DateTime CreatedAt,
[property: JsonPropertyName("userId")] string UserId,
[property: JsonPropertyName("expire")] DateTime Expire
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Requests;
using PinguApps.Appwrite.Shared.Tests;
using RichardSzalay.MockHttp;

namespace PinguApps.Appwrite.Client.Tests.Clients.Account;
public partial class AccountClientTests
{
[Fact]
public async Task Create2faChallenge_ShouldReturnSuccess_WhenApiCallSucceeds()
{
// Arrange
var request = new Create2faChallengeRequest();

var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false));

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/mfa/challenge")
.ExpectedHeaders(true)
.WithJsonContent(request, options)
.Respond(Constants.AppJson, Constants.MfaTypeResponse);

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.Create2faChallenge(request);

// Assert
Assert.True(result.Success);
}

[Fact]
public async Task Create2faChallenge_ShouldHandleException_WhenApiCallFails()
{
// Arrange
var request = new Create2faChallengeRequest();

var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false));

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/mfa/challenge")
.ExpectedHeaders(true)
.WithJsonContent(request, options)
.Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError);

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.Create2faChallenge(request);

// Assert
Assert.True(result.IsError);
Assert.True(result.IsAppwriteError);
}

[Fact]
public async Task Create2faChallenge_ShouldReturnErrorResponse_WhenExceptionOccurs()
{
// Arrange
var request = new Create2faChallengeRequest();

var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false));

_mockHttp.Expect(HttpMethod.Post, $"{Constants.Endpoint}/account/mfa/challenge")
.ExpectedHeaders(true)
.WithJsonContent(request, options)
.Throw(new HttpRequestException("An error occurred"));

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.Create2faChallenge(request);

// Assert
Assert.False(result.Success);
Assert.True(result.IsInternalError);
Assert.Equal("An error occurred", result.Result.AsT2.Message);
}
}
9 changes: 9 additions & 0 deletions tests/PinguApps.Appwrite.Shared.Tests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,13 @@ public static class Constants
"uri": "otpauth://totp/Appwrite%20Test%3Apingu%40appwrite.com?issuer=Appwrite%20Test&secret=The_Secret"
}
""";

public const string MfaChallengeResponse = """
{
"$id": "bb8ea5c16897e",
"$createdAt": "2020-10-15T06:38:00.000+00:00",
"userId": "5e5ea5c168bb8",
"expire": "2020-10-15T06:38:00.000+00:00"
}
""";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using FluentValidation;
using PinguApps.Appwrite.Shared.Enums;
using PinguApps.Appwrite.Shared.Requests;

namespace PinguApps.Appwrite.Shared.Tests.Requests;
public class Create2faChallengeRequestTests
{
[Fact]
public void Constructor_InitializesWithExpectedValues()
{
// Arrange & Act
var request = new Create2faChallengeRequest();

// Assert
Assert.Equal(SecondFactor.Email, request.Factor);
}

[Fact]
public void Properties_CanBeSet()
{
// Arrange
var request = new Create2faChallengeRequest();

// Act
request.Factor = SecondFactor.Phone;

// Assert
Assert.Equal(SecondFactor.Phone, request.Factor);
}

[Theory]
[InlineData(SecondFactor.Email)]
[InlineData(SecondFactor.Phone)]
[InlineData(SecondFactor.Totp)]
[InlineData(SecondFactor.RecoveryCode)]
public void IsValid_WithValidInputs_ReturnsTrue(SecondFactor factor)
{
// Arrange
var request = new Create2faChallengeRequest
{
Factor = factor
};

// Act
var isValid = request.IsValid();

// Assert
Assert.True(isValid);
}

[Theory]
[InlineData((SecondFactor)999)]
public void IsValid_WithInvalidInputs_ReturnsFalse(SecondFactor factor)
{
// Arrange
var request = new Create2faChallengeRequest
{
Factor = factor
};

var ggg = (int)factor;

// Act
var isValid = request.IsValid();

// Assert
Assert.False(isValid);
}

[Fact]
public void Validate_WithThrowOnFailuresTrue_ThrowsValidationExceptionOnFailure()
{
// Arrange
var request = new Create2faChallengeRequest
{
Factor = (SecondFactor)999
};

// Assert
Assert.Throws<ValidationException>(() => request.Validate(true));
}

[Fact]
public void Validate_WithThrowOnFailuresFalse_ReturnsInvalidResultOnFailure()
{
// Arrange
var request = new Create2faChallengeRequest
{
Factor = (SecondFactor)999
};

// Act
var result = request.Validate(false);

// Assert
Assert.False(result.IsValid);
}
}
Loading

0 comments on commit 6f1791b

Please sign in to comment.