Skip to content

Commit

Permalink
Implement API login, logout + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
turecross321 committed Sep 18, 2024
1 parent 48c352c commit c704cec
Show file tree
Hide file tree
Showing 45 changed files with 3,151 additions and 87 deletions.
1 change: 1 addition & 0 deletions SoundShapesServer.Common/Constants/ExpiryTimes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public class ExpiryTimes
public const int CodeHours = 1;
public const int GameTokenHours = 24;
public const int ApiAccessHours = 4;
public const int RefreshTokenHours = 24 * 30;
}
27 changes: 27 additions & 0 deletions SoundShapesServer.Common/HashHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Security.Cryptography;
using System.Text;

namespace SoundShapesServer.Common;

public static class HashHelper
{
public static string ComputeSha512Hash(string input)
{
using SHA512 sha512 = SHA512.Create();

// Convert the input string to a byte array
byte[] inputBytes = Encoding.UTF8.GetBytes(input);

// Compute the hash
byte[] hashBytes = sha512.ComputeHash(inputBytes);

// Convert the byte array to a hexadecimal string
StringBuilder sb = new StringBuilder();
foreach (byte b in hashBytes)
{
sb.Append(b.ToString("x2"));
}

return sb.ToString();
}
}
1 change: 1 addition & 0 deletions SoundShapesServer.Common/SoundShapesServer.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="NPTicket" Version="3.1.0" />
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions SoundShapesServer.Common/Verification/SoundShapesSigningKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using NPTicket.Verification.Keys;

namespace SoundShapesServer.Common.Verification;

public class SoundShapesSigningKey : PsnSigningKey
{
public static readonly SoundShapesSigningKey Instance = new();
private SoundShapesSigningKey() {}

public override string CurveX => "39c62d061d4ee35c5f3f7531de0af3cf918346526edac727";
public override string CurveY => "a5d578b55113e612bf1878d4cc939d61a41318403b5bdf86";
}
23 changes: 13 additions & 10 deletions SoundShapesServer.Tests/Server/SSTestContext.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using Bunkum.Core.Storage;
using Bunkum.Protocols.Http.Direct;
using Bunkum.Protocols.Http.Direct;
using SoundShapesServer.Database;
using SoundShapesServer.Tests.Database;
using SoundShapesServer.Types;
using SoundShapesServer.Types.Database;
using Testcontainers.PostgreSql;
Expand Down Expand Up @@ -33,21 +31,27 @@ public SSTestContext(Lazy<TestSSServer> server, GameDatabaseContext database, Ht

private int UserIncrement => this._users++;

public DbUser CreateUser(string? username = null)
public DbUser CreateUser(string? username = null, string email = "user@email.yep", UserRole role = UserRole.Default)
{
username ??= this.UserIncrement.ToString();
return this.Database.CreateUser(username);
DbUser user = this.Database.CreateRegisteredUser(username, email, role);
return user;
}

public HttpClient GetAuthenticatedClient(TokenType type, PlatformType platform = PlatformType.PS3, DbUser? user = null)
{
user ??= this.CreateUser();

DbToken token = Database.CreateToken(user, type, platform);

DbToken token = Database.CreateToken(user, type, platform, null, null, null);

return GetAuthenticatedClient(token);
}

public HttpClient GetAuthenticatedClient(DbToken token)
{
HttpClient client = this.Listener.GetClient();

if (type is TokenType.GameEula or TokenType.GameAccess)
if (token.TokenType is TokenType.GameEula or TokenType.GameAccess)
{
client.DefaultRequestHeaders.Add("X-OTG-Identity-SessionId", token.Id.ToString());
}
Expand All @@ -58,7 +62,6 @@ public HttpClient GetAuthenticatedClient(TokenType type, PlatformType platform =

return client;
}

public void Dispose()
{
this.Database.Dispose();
Expand Down
12 changes: 0 additions & 12 deletions SoundShapesServer.Tests/Server/TestServer.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using Bunkum.Core.Storage;
using Bunkum.EntityFrameworkDatabase;
using Bunkum.Protocols.Http;
using NotEnoughLogs;
using NotEnoughLogs.Behaviour;
using NotEnoughLogs.Sinks;
using SoundShapesServer.Common.Time;
using SoundShapesServer.Database;
using SoundShapesServer.Tests.Database;
using SoundShapesServer.Types.Config;

Expand Down Expand Up @@ -37,14 +35,4 @@ protected override (LoggerConfiguration logConfig, List<ILoggerSink>? sinks) Get

return (logConfig, sinks);
}

protected override void SetupServices()
{

}

protected override void SetupMiddlewares()
{

}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System.Net.Http.Json;
using SoundShapesServer.Common;
using SoundShapesServer.Endpoints.Api;
using SoundShapesServer.Tests.Server;
using SoundShapesServer.Types;
using SoundShapesServer.Types.Database;
using SoundShapesServer.Types.Requests.Api;
using TestContext = NUnit.Framework.TestContext;
using SoundShapesServer.Types.Responses.Api.ApiTypes;
using SoundShapesServer.Types.Responses.Api.DataTypes;

namespace SoundShapesServer.Tests.Tests;
namespace SoundShapesServer.Tests.Tests.Authentication;

public class AuthenticationTests : ServerTest
{
Expand All @@ -14,7 +17,7 @@ public void AuthenticationEnforcementWorks()
{
using SSTestContext context = this.GetServer();

string apiEndpoint = "/api/v1/me";
string apiEndpoint = "/api/v1/users/me";
string gameEulaEndpoint = "/otg/ps3/SCEA/en/~eula.get";
//string gameEndpoint = ""; // Todo: add any authenticated endpoint to this when there is one implemented

Expand All @@ -35,6 +38,49 @@ public void AuthenticationEnforcementWorks()
}
}

[Test]
public void ApiLoginAndRefreshTokenWork()
{
using SSTestContext context = this.GetServer();

string email = "bigBoss@mail.com";
string password = "password";

string passwordSha512 = HashHelper.ComputeSha512Hash(password);
string passwordBcrypt = BCrypt.Net.BCrypt.HashPassword(passwordSha512, ApiAuthenticationEndpoints.WorkFactor);
DbUser user = context.CreateUser(email: email);
user = context.Database.SetUserPassword(user, passwordBcrypt);

using HttpClient client = context.Http;

HttpResponseMessage response = client.GetAsync("/api/v1/users/me").Result;
Assert.That(!response.IsSuccessStatusCode);

response = client.PostAsJsonAsync("/api/v1/logIn", new ApiLogInRequest
{
Email = email,
PasswordSha512 = passwordSha512
}).Result;

Assert.That(response.IsSuccessStatusCode);

ApiLoginResponse? deSerialized =
response.Content.ReadFromJsonAsync<ApiResponse<ApiLoginResponse>>().Result!.Data;
Assert.That(deSerialized != null);

Assert.That(context.Database.GetTokenWithId(deSerialized!.AccessToken.Id) != null);

response = client.PostAsJsonAsync("/api/v1/refreshToken", new ApiRefreshTokenRequest
{
RefreshTokenId = deSerialized!.RefreshToken.Id
}).Result;

deSerialized = response.Content.ReadFromJsonAsync<ApiResponse<ApiLoginResponse>>().Result!.Data;
Assert.That(deSerialized != null);

Assert.That(context.Database.GetTokenWithId(deSerialized!.AccessToken.Id) != null);
}

// todo: fix the email service so that this test works
/*
[Test]
Expand Down
95 changes: 95 additions & 0 deletions SoundShapesServer.Tests/Tests/Authentication/TokenTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Net.Http.Json;
using SoundShapesServer.Common.Constants;
using SoundShapesServer.Tests.Server;
using SoundShapesServer.Types;
using SoundShapesServer.Types.Database;
using SoundShapesServer.Types.Requests.Api;
using SoundShapesServer.Types.Responses.Api.ApiTypes;
using SoundShapesServer.Types.Responses.Api.DataTypes;

namespace SoundShapesServer.Tests.Tests.Authentication;

public class TokenTests : ServerTest
{
[Test]
public void RevokeTokenWorks()
{
using SSTestContext context = this.GetServer();

DbUser user = context.CreateUser();
DbRefreshToken refreshToken = context.Database.CreateRefreshToken(user);

List<DbToken> tokens = new();
for (int i = 0; i < 5; i++)
{
tokens.Add(context.Database.CreateApiTokenWithRefreshToken(refreshToken));
}

HttpClient client = context.GetAuthenticatedClient(tokens.First());

HttpResponseMessage response = client.PostAsync("/api/v1/revokeToken", null).Result;
Assert.That(response.IsSuccessStatusCode);
foreach (DbToken token in tokens)
{
Assert.That(context.Database.GetTokenWithId(token.Id), Is.EqualTo(null));
}
Assert.That(context.Database.GetRefreshTokenWithId(refreshToken.Id), Is.EqualTo(null));
}

[Test]
public void TokenExpiryWorks()
{
using SSTestContext context = this.GetServer();

HttpClient client = context.GetAuthenticatedClient(TokenType.ApiAccess);

HttpResponseMessage response = client.GetAsync("/api/v1/users/me").Result;
Assert.That(response.IsSuccessStatusCode);

context.Time.Now = context.Time.Now.AddYears(1);

response = client.GetAsync("/api/v1/users/me").Result;
Assert.That(!response.IsSuccessStatusCode);
}

[Test]
public void RefreshTokenExpiryWorks()
{
using SSTestContext context = this.GetServer();
using HttpClient client = context.Http;

DbUser user = context.CreateUser();
DbRefreshToken refreshToken = context.Database.CreateRefreshToken(user);

// get right before refresh token expires
context.Time.Now = context.Time.Now.AddHours(ExpiryTimes.RefreshTokenHours).AddMinutes(-1);
HttpResponseMessage response = client.PostAsJsonAsync("/api/v1/refreshToken", new ApiRefreshTokenRequest
{
RefreshTokenId = refreshToken.Id
}).Result;
Assert.That(response.IsSuccessStatusCode);

// assure that the generated access token works
ApiLoginResponse? deSerialized =
response.Content.ReadFromJsonAsync<ApiResponse<ApiLoginResponse>>().Result!.Data;
Assert.That(deSerialized != null);
DbToken? accessToken = context.Database.GetTokenWithId(deSerialized!.AccessToken.Id);
Assert.That(accessToken != null);

// get right after refresh token expires
context.Time.Now = context.Time.Now.AddMinutes(2);

response = client.PostAsJsonAsync("/api/v1/refreshToken", new ApiRefreshTokenRequest
{
RefreshTokenId = refreshToken.Id
}).Result;

// check that the refresh token no longer works after the expiry date
Assert.That(!response.IsSuccessStatusCode);

// check that the access token was generated with the refresh token has been revoked
using HttpClient authClient = context.GetAuthenticatedClient(accessToken!);
response = client.GetAsync("/api/v1/users/me").Result;
Assert.That(!response.IsSuccessStatusCode);
}
}
28 changes: 28 additions & 0 deletions SoundShapesServer/Database/GameDatabaseContext.Ips.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using SoundShapesServer.Types.Database;

namespace SoundShapesServer.Database;

public partial class GameDatabaseContext
{
public DbIp GetOrCreateIp(DbUser user, string ipAddress)
{
DbIp? existingIp = Ips.Include(i => i.User)
.FirstOrDefault(i => i.UserId == user.Id && i.IpAddress == ipAddress);

if (existingIp != null)
return existingIp;

EntityEntry<DbIp> ip = Ips.Add(new DbIp
{
IpAddress = ipAddress,
CreationDate = Time.Now,
UserId = user.Id
});

SaveChanges();

return ip.Entity;
}
}
54 changes: 54 additions & 0 deletions SoundShapesServer/Database/GameDatabaseContext.RefreshTokens.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using SoundShapesServer.Common.Constants;
using SoundShapesServer.Types;
using SoundShapesServer.Types.Database;

namespace SoundShapesServer.Database;

public partial class GameDatabaseContext
{
public DbToken CreateApiTokenWithRefreshToken(DbRefreshToken refresh)
{
return CreateToken(refresh.User, TokenType.ApiAccess, null, null, refresh, null);
}

public DbRefreshToken? GetRefreshTokenWithId(Guid guid)
{
DbRefreshToken? token = RefreshTokens
.Include(t => t.User)
.FirstOrDefault(t => t.Id == guid);

if (_time.Now >= token?.ExpiryDate)
{
RemoveRefreshToken(token);
return null;
}

return token;
}

private void RemoveRefreshToken(DbRefreshToken token)
{
RefreshTokens.Remove(token);

SaveChanges();
}

public DbRefreshToken CreateRefreshToken(DbUser user)
{
EntityEntry<DbRefreshToken> token = RefreshTokens.Add(new DbRefreshToken
{
UserId = user.Id,
CreationDate = _time.Now,
ExpiryDate = _time.Now.AddHours(ExpiryTimes.RefreshTokenHours)
});

SaveChanges();

// Reload to load the ID
token.Reload();

return token.Entity;
}
}
Loading

0 comments on commit c704cec

Please sign in to comment.