Skip to content

Commit

Permalink
Merge pull request #136 from PinguApps/103-create-oauth2-session
Browse files Browse the repository at this point in the history
Implemented create oauth2 session
  • Loading branch information
pingu2k4 authored Aug 11, 2024
2 parents 601e2e3 + 4baee14 commit 60cf521
Show file tree
Hide file tree
Showing 18 changed files with 634 additions and 13 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
<!-- ![34 / 288](https://progress-bar.dev/34/?scale=288&suffix=%20/%20288&width=500) -->
![Server & Client - 34 / 288](https://img.shields.io/badge/Server_&_Client-34%20%2F%20288-red?style=for-the-badge)
<!-- ![35 / 288](https://progress-bar.dev/35/?scale=288&suffix=%20/%20288&width=500) -->
![Server & Client - 35 / 288](https://img.shields.io/badge/Server_&_Client-35%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)

<!-- ![32 / 93](https://progress-bar.dev/32/?scale=93&suffix=%20/%2093&width=300) -->
![Client - 32 / 93](https://img.shields.io/badge/Client-32%20%2F%2093-red?style=for-the-badge)
<!-- ![33 / 93](https://progress-bar.dev/33/?scale=93&suffix=%20/%2093&width=300) -->
![Client - 33 / 93](https://img.shields.io/badge/Client-33%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
<!-- ![34 / 52](https://progress-bar.dev/34/?scale=52&suffix=%20/%2052&width=120) -->
![Account - 34 / 52](https://img.shields.io/badge/Account-34%20%2F%2052-yellow?style=for-the-badge)
<!-- ![35 / 52](https://progress-bar.dev/35/?scale=52&suffix=%20/%2052&width=120) -->
![Account - 35 / 52](https://img.shields.io/badge/Account-35%20%2F%2052-0af0?style=for-the-badge)

| Endpoint | Client | Server |
|:-:|:-:|:-:|
Expand Down Expand Up @@ -189,7 +189,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
| [Create Anonymous Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createAnonymousSession) |||
| [Create Email Password Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createEmailPasswordSession) |||
| [Update Magic URL Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMagicURLSession) |||
| [Create OAuth2 Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createOAuth2Session) | ||
| [Create OAuth2 Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createOAuth2Session) | ||
| [Update Phone Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updatePhoneSession) |||
| [Create Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createSession) |||
| [Get Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#getSession) |||
Expand Down
21 changes: 20 additions & 1 deletion src/PinguApps.Appwrite.Client/Clients/AccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ namespace PinguApps.Appwrite.Client;
public class AccountClient : IAccountClient, ISessionAware
{
private readonly IAccountApi _accountApi;
private readonly Config _config;

public AccountClient(IServiceProvider services)
public AccountClient(IServiceProvider services, Config config)
{
_accountApi = services.GetRequiredService<IAccountApi>();
_config = config;
}

string? ISessionAware.Session { get; set; }
Expand Down Expand Up @@ -561,4 +563,21 @@ public async Task<AppwriteResult<Session>> CreateEmailPasswordSession(CreateEmai
return e.GetExceptionResponse<Session>();
}
}

/// <inheritdoc/>
public AppwriteResult<CreateOauth2Session> CreateOauth2Session(CreateOauth2SessionRequest request)
{
try
{
request.Validate(true);

var uri = request.BuildUri(_config.Endpoint, _config.ProjectId);

return new AppwriteResult<CreateOauth2Session>(new CreateOauth2Session(uri.AbsoluteUri));
}
catch (Exception e)
{
return e.GetExceptionResponse<CreateOauth2Session>();
}
}
}
12 changes: 11 additions & 1 deletion src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,17 @@ public interface IAccountClient
/// <para>A user is limited to 10 active sessions at a time by default. <see href="https://appwrite.io/docs/authentication-security#limits">Learn more about session limits</see>.</para>
/// <para><see href="https://appwrite.io/docs/references/1.5.x/client-rest/account#createEmailPasswordSession">Appwrite Docs</see></para>
/// </summary>
/// <param name="request">The request</param>
/// <param name="request">The request content</param>
/// <returns>The Session</returns>
Task<AppwriteResult<Session>> CreateEmailPasswordSession(CreateEmailPasswordSessionRequest request);

/// <summary>
/// <para>Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed.</para>
/// <para>If there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.</para>
/// <para>A user is limited to 10 active sessions at a time by default. <see href="https://appwrite.io/docs/authentication-security#limits">Learn more about session limits</see></para>
/// <para><see href="https://appwrite.io/docs/references/1.5.x/client-rest/account#createOAuth2Session">Appwrite Docs</see></para>
/// </summary>
/// <param name="request">The request content</param>
/// <returns>The CreateOauth2Session object</returns>
AppwriteResult<CreateOauth2Session> CreateOauth2Session(CreateOauth2SessionRequest request);
}
5 changes: 5 additions & 0 deletions src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.Extensions.DependencyInjection;
using PinguApps.Appwrite.Client.Handlers;
using PinguApps.Appwrite.Client.Internals;
using PinguApps.Appwrite.Shared;
using Refit;

namespace PinguApps.Appwrite.Client;
Expand Down Expand Up @@ -30,6 +31,8 @@ public static IServiceCollection AddAppwriteClient(this IServiceCollection servi
.AddHttpMessageHandler<HeaderHandler>()
.AddHttpMessageHandler<ClientCookieSessionHandler>();

services.AddSingleton(new Config(endpoint, projectId));

services.AddSingleton<IAccountClient, AccountClient>();
services.AddSingleton<IAppwriteClient, AppwriteClient>();
services.AddSingleton(x => new Lazy<IAppwriteClient>(() => x.GetRequiredService<IAppwriteClient>()));
Expand Down Expand Up @@ -60,6 +63,8 @@ public static IServiceCollection AddAppwriteClientForServer(this IServiceCollect
}
});

services.AddSingleton(new Config(endpoint, projectId));

services.AddSingleton<IAccountClient, AccountClient>();
services.AddSingleton<IAppwriteClient, AppwriteClient>();

Expand Down
8 changes: 5 additions & 3 deletions src/PinguApps.Appwrite.Playground/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ public async Task Run(string[] args)

Console.WriteLine(_client.Session);

var response = await _client.Account.CreateEmailPasswordSession(new Shared.Requests.CreateEmailPasswordSessionRequest
var response = _client.Account.CreateOauth2Session(new Shared.Requests.CreateOauth2SessionRequest
{
Email = "[email protected]",
Password = "password"
Provider = "google",
SuccessUri = "https://localhost:5001/success",
FailureUri = "https://localhost:5001/fail",
Scopes = ["scope1", "scope2"]
});

Console.WriteLine(response.Result.Match(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;

namespace PinguApps.Appwrite.Shared.Attributes;
internal class QueryParameterAttribute : Attribute
{
public QueryParameterAttribute(string key)
{
Key = key;
}

public string Key { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace PinguApps.Appwrite.Shared.Attributes;

internal class UrlReplacementAttribute : Attribute
{
public UrlReplacementAttribute(string pattern)
{
Pattern = pattern;
}

public string Pattern { get; }
}
5 changes: 5 additions & 0 deletions src/PinguApps.Appwrite.Shared/Config.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace PinguApps.Appwrite.Shared;
public record Config(
string Endpoint,
string ProjectId
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using PinguApps.Appwrite.Shared.Attributes;
using PinguApps.Appwrite.Shared.Requests.Validators;

namespace PinguApps.Appwrite.Shared.Requests;

/// <summary>
/// The request for creating an Oauth2 Session
/// </summary>
public class CreateOauth2SessionRequest : QueryParamBaseRequest<CreateOauth2SessionRequest, CreateOauth2SessionRequestValidator>
{
/// <summary>
/// OAuth2 Provider. Currently, supported providers are:
/// <para>amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, github, gitlab, google, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoho, zoom</para>
/// </summary>
[UrlReplacement("{provider}")]
public string Provider { get; set; } = string.Empty;

/// <summary>
/// URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an <see href="https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html">open redirect</see> attack against your project API
/// </summary>
[QueryParameter("success")]
public string? SuccessUri { get; set; }

/// <summary>
/// URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an <see href="https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html">open redirect</see> attack against your project API
/// </summary>
[QueryParameter("failure")]
public string? FailureUri { get; set; }

/// <summary>
/// A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long
/// </summary>
[QueryParameter("scopes[]")]
public List<string>? Scopes { get; set; }

protected override string Path => "/account/tokens/oauth2/{provider}";
}
105 changes: 105 additions & 0 deletions src/PinguApps.Appwrite.Shared/Requests/QueryParamBaseRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FluentValidation;
using PinguApps.Appwrite.Shared.Attributes;

namespace PinguApps.Appwrite.Shared.Requests;
public abstract class QueryParamBaseRequest<TRequest, TValidator> : BaseRequest<TRequest, TValidator>
where TRequest : class
where TValidator : IValidator<TRequest>, new()
{
public Uri BuildUri(string endpoint, string? projectId)
{
var endpointUri = new Uri(endpoint);
var endpointPath = endpointUri.AbsolutePath;

var path = Path;

var urlReplacementProperties = GetType()
.GetProperties()
.Where(x => Attribute.IsDefined(x, typeof(UrlReplacementAttribute)));

foreach (var property in urlReplacementProperties)
{
var value = property.GetValue(this);

if (value is string strValue)
{
var attribute = property.GetCustomAttribute<UrlReplacementAttribute>();

path = path.Replace(attribute.Pattern, strValue);
}
}

var combinedPath = CombinePaths(endpointPath, path);

var builder = new UriBuilder(endpoint)
{
Path = combinedPath
};

var queryProperties = GetType()
.GetProperties()
.Where(x => Attribute.IsDefined(x, typeof(QueryParameterAttribute)));

var queries = new List<string>();

if (projectId is not null)
{
queries.Add($"project={projectId}");
}

foreach (var property in queryProperties)
{
var value = property.GetValue(this);

if (value is null)
continue;

var attribute = property.GetCustomAttribute<QueryParameterAttribute>();

if (value is IEnumerable<object> values && value is not string)
{
foreach (var item in values)
{
queries.Add($"{attribute.Key}={Uri.EscapeDataString(item.ToString())}");
}
}
else
{
queries.Add($"{attribute.Key}={Uri.EscapeDataString(value.ToString())}");
}
}

if (queries.Count > 0)
{
builder.Query = string.Join("&", queries);
}

return builder.Uri;
}

static string CombinePaths(string urlPath, string existingPath)
{
if (urlPath == "/")
{
return existingPath;
}

if (!urlPath.EndsWith('/'))
{
urlPath += "/";
}

if (existingPath.StartsWith('/'))
{
existingPath = existingPath.TrimStart('/');
}

return urlPath + existingPath;
}

protected abstract string Path { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using FluentValidation;

namespace PinguApps.Appwrite.Shared.Requests.Validators;
public class CreateOauth2SessionRequestValidator : AbstractValidator<CreateOauth2SessionRequest>
{
public CreateOauth2SessionRequestValidator()
{
RuleFor(x => x.Provider).NotEmpty().Must(x => string.Equals(x, x.ToLower(), StringComparison.Ordinal)).WithMessage("Provider must be all lower case.");
RuleFor(x => x.SuccessUri).Must(uri => Uri.TryCreate(uri, UriKind.Absolute, out _)).When(x => x.SuccessUri is not null);
RuleFor(x => x.FailureUri).Must(uri => Uri.TryCreate(uri, UriKind.Absolute, out _)).When(x => x.FailureUri is not null);
RuleFor(x => x.Scopes)
.Must(x => x!.Count <= 100).WithMessage("You can have a maximum of 100 scopes")
.Must(x => x!.Count > 0).WithMessage("You must have more than 0 scopes if passing a non null value")
.When(x => x.Scopes is not null);
RuleForEach(x => x.Scopes).MaximumLength(4096);
}
}
11 changes: 11 additions & 0 deletions src/PinguApps.Appwrite.Shared/Responses/CreateOauth2Session.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;

namespace PinguApps.Appwrite.Shared.Responses;

/// <summary>
/// A Create Oauth2 Session object
/// </summary>
/// <param name="Uri">The Uri that you should redirect the user towards to begin Oauth2 authentication</param>
public record CreateOauth2Session(
[property: JsonPropertyName("uri")] string Uri
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using PinguApps.Appwrite.Shared.Requests;

namespace PinguApps.Appwrite.Client.Tests.Clients.Account;
public partial class AccountClientTests
{
[Fact]
public void CreateOauth2Session_ShouldReturnSuccess_WhenApiCallSucceeds()
{
// Arrange
var request = new CreateOauth2SessionRequest()
{
Provider = "google"
};

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

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

[Fact]
public void CreateOauth2Session_ShouldReturnErrorResponse_WhenExceptionOccurs()
{
// Arrange
var request = new CreateOauth2SessionRequest()
{
Provider = ""
};

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

// Assert
Assert.False(result.Success);
Assert.True(result.IsInternalError);
}
}
Loading

0 comments on commit 60cf521

Please sign in to comment.