Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
.AsNoTracking()
.FirstOrDefaultAsync(u => !string.IsNullOrWhiteSpace(u.Token) &&
u.Token == token &&
u.TokenExpires > DateTime.UtcNow &&
!u.IsBanned)
.ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace Nullinside.Api.Common.Twitch;
namespace Nullinside.Api.Common.Auth;

/// <summary>
/// Represents an OAuth token in the Twitch workflow.
/// </summary>
public class TwitchAccessToken {
public class OAuthToken {
/// <summary>
/// The Twitch access token.
/// </summary>
Expand Down
9 changes: 5 additions & 4 deletions src/Nullinside.Api.Common/Twitch/ITwitchApiProxy.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Nullinside.Api.Common.Twitch.Json;
using Nullinside.Api.Common.Auth;
using Nullinside.Api.Common.Twitch.Json;

using TwitchLib.Api.Helix.Models.Chat.GetChatters;
using TwitchLib.Api.Helix.Models.Moderation.BanUser;
Expand All @@ -14,7 +15,7 @@ public interface ITwitchApiProxy {
/// <summary>
/// The Twitch access token. These are the credentials used for all requests.
/// </summary>
TwitchAccessToken? OAuth { get; set; }
OAuthToken? OAuth { get; set; }

/// <summary>
/// The Twitch app configuration. These are used for all requests.
Expand All @@ -28,15 +29,15 @@ public interface ITwitchApiProxy {
/// <param name="token">The cancellation token.</param>
/// <remarks>The object will have its <see cref="OAuth" /> updated with the new settings for the token.</remarks>
/// <returns>The OAuth details if successful, null otherwise.</returns>
Task<TwitchAccessToken?> CreateAccessToken(string code, CancellationToken token = new());
Task<OAuthToken?> CreateAccessToken(string code, CancellationToken token = new());

/// <summary>
/// Refreshes the access token.
/// </summary>
/// <param name="token">The cancellation token.</param>
/// <remarks>The object will have its <see cref="OAuth" /> updated with the new settings for the token.</remarks>
/// <returns>The OAuth details if successful, null otherwise.</returns>
Task<TwitchAccessToken?> RefreshAccessToken(CancellationToken token = new());
Task<OAuthToken?> RefreshAccessToken(CancellationToken token = new());

/// <summary>
/// Determines if the <see cref="OAuth" /> is valid.
Expand Down
13 changes: 7 additions & 6 deletions src/Nullinside.Api.Common/Twitch/TwitchApiProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using Newtonsoft.Json;

using Nullinside.Api.Common.Auth;
using Nullinside.Api.Common.Twitch.Json;

using TwitchLib.Api;
Expand Down Expand Up @@ -60,7 +61,7 @@ public TwitchApiProxy() {
/// </param>
public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires, string? clientId = null,
string? clientSecret = null, string? clientRedirect = null) {
OAuth = new TwitchAccessToken {
OAuth = new OAuthToken {
AccessToken = token,
RefreshToken = refreshToken,
ExpiresUtc = tokenExpires
Expand All @@ -79,21 +80,21 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires,
public int Retries { get; set; } = 3;

/// <inheritdoc />
public virtual TwitchAccessToken? OAuth { get; set; }
public virtual OAuthToken? OAuth { get; set; }

/// <inheritdoc />
public virtual TwitchAppConfig? TwitchAppConfig { get; set; }

/// <inheritdoc />
public virtual async Task<TwitchAccessToken?> CreateAccessToken(string code, CancellationToken token = new()) {
public virtual async Task<OAuthToken?> CreateAccessToken(string code, CancellationToken token = new()) {
ITwitchAPI api = GetApi();
AuthCodeResponse? response = await api.Auth.GetAccessTokenFromCodeAsync(code, TwitchAppConfig?.ClientSecret,
TwitchAppConfig?.ClientRedirect).ConfigureAwait(false);
if (null == response) {
return null;
}

OAuth = new TwitchAccessToken {
OAuth = new OAuthToken {
AccessToken = response.AccessToken,
RefreshToken = response.RefreshToken,
ExpiresUtc = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn)
Expand All @@ -102,7 +103,7 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires,
}

/// <inheritdoc />
public virtual async Task<TwitchAccessToken?> RefreshAccessToken(CancellationToken token = new()) {
public virtual async Task<OAuthToken?> RefreshAccessToken(CancellationToken token = new()) {
try {
if (string.IsNullOrWhiteSpace(TwitchAppConfig?.ClientSecret) || string.IsNullOrWhiteSpace(TwitchAppConfig?.ClientId)) {
return null;
Expand All @@ -114,7 +115,7 @@ public TwitchApiProxy(string token, string refreshToken, DateTime tokenExpires,
return null;
}

OAuth = new TwitchAccessToken {
OAuth = new OAuthToken {
AccessToken = response.AccessToken,
RefreshToken = response.RefreshToken,
ExpiresUtc = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn)
Expand Down
9 changes: 7 additions & 2 deletions src/Nullinside.Api.Model/Shared/UserHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ public static class UserHelpers {
/// <param name="twitchUsername">The username of the user on twitch.</param>
/// <param name="twitchId">The id of the user on twitch.</param>
/// <returns>The bearer token if successful, null otherwise.</returns>
public static async Task<string?> GenerateTokenAndSaveToDatabase(INullinsideContext dbContext, string email,
public static async Task<OAuthToken?> GenerateTokenAndSaveToDatabase(INullinsideContext dbContext, string email,
TimeSpan tokenExpires, string? authToken = null, string? refreshToken = null, DateTime? expires = null,
string? twitchUsername = null, string? twitchId = null, CancellationToken cancellationToken = new()) {
string bearerToken = AuthUtils.GenerateToken();
string bearerRefreshToken = AuthUtils.GenerateToken();
DateTime expiresUtc = DateTime.UtcNow + tokenExpires;
try {
User? existing = await dbContext.Users.FirstOrDefaultAsync(u => u.Email == email && !u.IsBanned, cancellationToken).ConfigureAwait(false);
if (null == existing && !string.IsNullOrWhiteSpace(twitchUsername)) {
Expand Down Expand Up @@ -75,7 +76,11 @@ public static class UserHelpers {
}

await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return bearerToken;
return new() {
AccessToken = bearerToken,
RefreshToken = bearerRefreshToken,
ExpiresUtc = expiresUtc
};
}
catch {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ public async Task GenerateTokenForExistingUser() {
Assert.That(_db.Users.Count(), Is.EqualTo(1));

// Generate a new token
string? token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false);
var token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false);
Assert.That(token, Is.Not.Null);

// Verify we still only have one user
Assert.That(_db.Users.Count(), Is.EqualTo(1));
Assert.That(_db.Users.First().Token, Is.EqualTo(token));
Assert.That(_db.Users.First().Token, Is.EqualTo(token.AccessToken));
}

/// <summary>
Expand All @@ -48,12 +48,12 @@ public async Task GenerateTokenForNewUser() {
Assert.That(_db.Users.Count(), Is.EqualTo(1));

// Generate a new token
string? token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false);
var token = await UserHelpers.GenerateTokenAndSaveToDatabase(_db, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false);
Assert.That(token, Is.Not.Null);

// Verify we have a new user
Assert.That(_db.Users.Count(), Is.EqualTo(2));
Assert.That(_db.Users.FirstOrDefault(u => u.Email == "email")?.Token, Is.EqualTo(token));
Assert.That(_db.Users.FirstOrDefault(u => u.Email == "email")?.Token, Is.EqualTo(token.AccessToken));

// Verfy the old user is untouched
Assert.That(_db.Users.FirstOrDefault(u => u.Email == "email2")?.Token, Is.Null);
Expand All @@ -65,7 +65,7 @@ public async Task GenerateTokenForNewUser() {
[Test]
public async Task HandleUnexpectedErrors() {
// Force an error to occur.
string? token = await UserHelpers.GenerateTokenAndSaveToDatabase(null!, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false);
var token = await UserHelpers.GenerateTokenAndSaveToDatabase(null!, "email", Constants.OAUTH_TOKEN_TIME_LIMIT).ConfigureAwait(false);
Assert.That(token, Is.Null);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Security.Claims;
using System.Text;
using System.Text.Unicode;

using Google.Apis.Auth;

Expand All @@ -9,13 +11,18 @@

using Moq;

using Newtonsoft.Json;

using Nullinside.Api.Common.Auth;
using Nullinside.Api.Common.Twitch;
using Nullinside.Api.Controllers;
using Nullinside.Api.Model;
using Nullinside.Api.Model.Ddl;
using Nullinside.Api.Shared;
using Nullinside.Api.Shared.Json;

using Org.BouncyCastle.Utilities.Encoders;

namespace Nullinside.Api.Tests.Nullinside.Api.Controllers;

/// <summary>
Expand Down Expand Up @@ -74,12 +81,15 @@ public async Task PerformGoogleLoginExisting() {

// We should have been redirected to the successful route.
Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True);
var queryParam = obj.Url["/user/login?token=".Length..];

// No additional users should have been created.
Assert.That(_db.Users.Count(), Is.EqualTo(1));

// We should have saved the token in the existing user's database.
Assert.That(obj.Url.EndsWith(_db.Users.First().Token!), Is.True);
// We should have saved the token in the existing user's database.
var json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam));
var oauth = JsonConvert.DeserializeObject<OAuthToken>(json);
Assert.That(oauth?.AccessToken!, Is.EqualTo(_db.Users.First().Token!));
}

/// <summary>
Expand All @@ -94,12 +104,15 @@ public async Task PerformGoogleLoginNewUser() {

// We should have been redirected to the successful route.
Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True);
var queryParam = obj.Url["/user/login?token=".Length..];

// No additional users should have been created.
Assert.That(_db.Users.Count(), Is.EqualTo(1));

// We should have saved the token in the existing user's database.
Assert.That(obj.Url.EndsWith(_db.Users.First().Token!), Is.True);
var json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam));
var oauth = JsonConvert.DeserializeObject<OAuthToken>(json);
Assert.That(oauth?.AccessToken!, Is.EqualTo(_db.Users.First().Token!));
}

/// <summary>
Expand Down Expand Up @@ -139,7 +152,7 @@ public async Task GoToErrorOnBadGmailResponse() {
public async Task PerformTwitchLoginExisting() {
// Tells us twitch parsed the code successfully.
_twitchApi.Setup(a => a.CreateAccessToken(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(() => Task.FromResult<TwitchAccessToken?>(new TwitchAccessToken()));
.Returns(() => Task.FromResult<OAuthToken?>(new OAuthToken()));

// Gets a matching email address from our database
_twitchApi.Setup(a => a.GetUserEmail(It.IsAny<CancellationToken>()))
Expand All @@ -159,12 +172,15 @@ public async Task PerformTwitchLoginExisting() {

// We should have been redirected to the successful route.
Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True);
var queryParam = obj.Url["/user/login?token=".Length..];

// No additional users should have been created.
Assert.That(_db.Users.Count(), Is.EqualTo(1));

// We should have saved the token in the existing user's database.
Assert.That(obj.Url.EndsWith(_db.Users.First().Token!), Is.True);
var json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam));
var oauth = JsonConvert.DeserializeObject<OAuthToken>(json);
Assert.That(oauth?.AccessToken!, Is.EqualTo(_db.Users.First().Token!));
}

/// <summary>
Expand All @@ -174,7 +190,7 @@ public async Task PerformTwitchLoginExisting() {
public async Task PerformTwitchLoginNewUser() {
// Tells us twitch parsed the code successfully.
_twitchApi.Setup(a => a.CreateAccessToken(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(() => Task.FromResult<TwitchAccessToken?>(new TwitchAccessToken()));
.Returns(() => Task.FromResult<OAuthToken?>(new OAuthToken()));

// Gets a matching email address from our database
_twitchApi.Setup(a => a.GetUserEmail(It.IsAny<CancellationToken>()))
Expand All @@ -186,12 +202,15 @@ public async Task PerformTwitchLoginNewUser() {

// We should have been redirected to the successful route.
Assert.That(obj.Url.StartsWith("/user/login?token="), Is.True);
var queryParam = obj.Url["/user/login?token=".Length..];

// No additional users should have been created.
Assert.That(_db.Users.Count(), Is.EqualTo(1));

// We should have saved the token in the existing user's database.
Assert.That(obj.Url.EndsWith(_db.Users.First().Token!), Is.True);
var json = Encoding.UTF8.GetString(Convert.FromBase64String(queryParam));
var oauth = JsonConvert.DeserializeObject<OAuthToken>(json);
Assert.That(oauth?.AccessToken!, Is.EqualTo(_db.Users.First().Token!));
}

/// <summary>
Expand All @@ -201,7 +220,7 @@ public async Task PerformTwitchLoginNewUser() {
public async Task PerformTwitchLoginBadTwitchResponse() {
// Tells us twitch thinks it was a bad code.
_twitchApi.Setup(a => a.CreateAccessToken(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(() => Task.FromResult<TwitchAccessToken?>(null));
.Returns(() => Task.FromResult<OAuthToken?>(null));

// Make the call and ensure it's successful.
var controller = new TestableUserController(_configuration, _db, _webSocketPersister.Object);
Expand All @@ -218,7 +237,7 @@ public async Task PerformTwitchLoginBadTwitchResponse() {
public async Task PerformTwitchLoginWithNoEmailAccount() {
// Tells us twitch parsed the code successfully.
_twitchApi.Setup(a => a.CreateAccessToken(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(() => Task.FromResult<TwitchAccessToken?>(new TwitchAccessToken()));
.Returns(() => Task.FromResult<OAuthToken?>(new OAuthToken()));

// Make the call and ensure it's successful.
var controller = new TestableUserController(_configuration, _db, _webSocketPersister.Object);
Expand All @@ -237,7 +256,7 @@ public async Task PerformTwitchLoginDbFailure() {

// Tells us twitch parsed the code successfully.
_twitchApi.Setup(a => a.CreateAccessToken(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(() => Task.FromResult<TwitchAccessToken?>(new TwitchAccessToken()));
.Returns(() => Task.FromResult<OAuthToken?>(new OAuthToken()));

// Gets an email address from twitch
_twitchApi.Setup(a => a.GetUserEmail(It.IsAny<CancellationToken>()))
Expand Down
Loading