Skip to content

Commit

Permalink
Feature/free vote (#149)
Browse files Browse the repository at this point in the history
* [Server, Client] Add Free Vote

A mechanism that allows avatars in neighborhoods without elections to
vote in an ongoing election of their choosing.

* [Server] Free Vote, Github Releases, Restore Lot Tool, Test Utils

Still need to fix an issue with the unauthenticated disconnect.

* [Server] Improvements, Account Lockout

- Too many failed password attempts can now cause an account lockout.
- More reasonable CORS policy that doesn't spam warnings.

* [UI] Add Free Vote Confirmation Text
  • Loading branch information
riperiperi authored May 20, 2019
1 parent 01aea6f commit 21cea91
Show file tree
Hide file tree
Showing 70 changed files with 1,305 additions and 136 deletions.
3 changes: 3 additions & 0 deletions TSOClient/FSO.Server.Api.Core/Api.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FSO.Server.Api.Core.Services;
using FSO.Server.Api.Core.Utils;
using FSO.Server.Common;
using FSO.Server.Common.Config;
using FSO.Server.Database.DA;
using FSO.Server.Domain;
using FSO.Server.Protocol.Gluon.Model;
Expand All @@ -27,6 +28,8 @@ public class Api : ApiAbstract
public Shards Shards;
public IGluonHostPool HostPool;
public IUpdateUploader UpdateUploader;
public IUpdateUploader UpdateUploaderClient;
public GithubConfig Github;

public Api()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
using System.Web.Http;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Cors;

namespace FSO.Server.Api.Core.Controllers.Admin
{
[EnableCors("AdminAppPolicy")]
[Route("admin/hosts")]
[ApiController]
public class AdminHostsController : ControllerBase
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FSO.Server.Api.Core.Utils;
using FSO.Server.Common;
using FSO.Server.Servers.Api.JsonWebToken;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
Expand All @@ -12,6 +13,7 @@

namespace FSO.Server.Api.Core.Controllers.Admin
{
[EnableCors("AdminAppPolicy")]
[Route("admin/oauth/token")]
[ApiController]
public class AdminOAuthController : ControllerBase
Expand All @@ -35,6 +37,17 @@ public IActionResult Post([FromForm] AuthRequest auth)
});
}

var ip = ApiUtils.GetIP(Request);
var accLock = da.Users.GetRemainingAuth(user.user_id, ip);
if (accLock != null && (accLock.active || accLock.count >= AuthLoginController.LockAttempts) && accLock.expire_time > Epoch.Now)
{
return ApiResponse.Json(System.Net.HttpStatusCode.OK, new OAuthError
{
error = "unauthorized_client",
error_description = "account_locked"
});
}

var authSettings = da.Users.GetAuthenticationSettings(user.user_id);
var isPasswordCorrect = PasswordHasher.Verify(auth.password, new PasswordHash
{
Expand All @@ -44,13 +57,26 @@ public IActionResult Post([FromForm] AuthRequest auth)

if (!isPasswordCorrect)
{
var durations = AuthLoginController.LockDuration;
var failDelay = 60 * durations[Math.Min(durations.Length - 1, da.Users.FailedConsecutive(user.user_id, ip))];
if (accLock == null)
{
da.Users.NewFailedAuth(user.user_id, ip, (uint)failDelay);
}
else
{
var remaining = da.Users.FailedAuth(accLock.attempt_id, (uint)failDelay, AuthLoginController.LockAttempts);
}

return ApiResponse.Json(System.Net.HttpStatusCode.OK, new OAuthError
{
error = "unauthorized_client",
error_description = "user_credentials_invalid"
});
}

da.Users.SuccessfulAuth(user.user_id, ip);

JWTUser identity = new JWTUser();
identity.UserName = user.username;
var claims = new List<string>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FSO.Server.Api.Core.Utils;
using FSO.Server.Common;
using FSO.Server.Protocol.Gluon.Model;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Net;
Expand All @@ -10,6 +11,7 @@

namespace FSO.Server.Api.Core.Controllers.Admin
{
[EnableCors("AdminAppPolicy")]
[Route("admin/shards")]
[ApiController]
public class AdminShardsController : ControllerBase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
using System.Linq;
using System.Web.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Cors;

namespace FSO.Server.Api.Core.Controllers.Admin
{
[EnableCors("AdminAppPolicy")]
[Route("admin/tasks")]
[ApiController]
public class AdminTasksController : ControllerBase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using FSO.Server.Api.Core.Services;
using FSO.Server.Api.Core.Utils;
using FSO.Server.Database.DA.Updates;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
Expand All @@ -18,6 +19,7 @@

namespace FSO.Server.Api.Core.Controllers.Admin
{
[EnableCors("AdminAppPolicy")]
[Route("admin/updates")]
public class AdminUpdatesController : ControllerBase
{
Expand Down Expand Up @@ -129,7 +131,7 @@ public async Task<IActionResult> UploadAddon(AddonUploadModel upload)
{
await upload.clientAddon.CopyToAsync(file);
}
info.addon_zip_url = await api.UpdateUploader.UploadFile($"addons/client{addonID}.zip", $"updateTemp/addons/client{reqID}.zip");
info.addon_zip_url = await api.UpdateUploader.UploadFile($"addons/client{addonID}.zip", $"updateTemp/addons/client{reqID}.zip", $"addon-{addonID}");
System.IO.File.Delete($"updateTemp/addons/client{reqID}.zip");
}

Expand All @@ -139,7 +141,7 @@ public async Task<IActionResult> UploadAddon(AddonUploadModel upload)
{
await upload.serverAddon.CopyToAsync(file);
}
info.server_zip_url = await api.UpdateUploader.UploadFile($"addons/server{addonID}.zip", $"updateTemp/addons/server{reqID}.zip");
info.server_zip_url = await api.UpdateUploader.UploadFile($"addons/server{addonID}.zip", $"updateTemp/addons/server{reqID}.zip", $"addon-{addonID}");
System.IO.File.Delete($"updateTemp/addons/server{reqID}.zip");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using FSO.Server.Common;
using FSO.Server.Database.DA.Inbox;
using FSO.Server.Database.DA.Users;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
Expand All @@ -13,6 +14,7 @@

namespace FSO.Server.Api.Core.Controllers.Admin
{
[EnableCors("AdminAppPolicy")]
[Route("admin/users")]
[ApiController]
public class AdminUsersController : ControllerBase
Expand Down
37 changes: 34 additions & 3 deletions TSOClient/FSO.Server.Api.Core/Controllers/AuthLoginController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ public class AuthLoginController : ControllerBase
private static Func<IActionResult> ERROR_302 = printError("INV-302", "The game has experienced an internal error. Please try again.");
private static Func<IActionResult> ERROR_160 = printError("INV-160", "The server is currently down for maintainance. Please try again later.");
private static Func<IActionResult> ERROR_150 = printError("INV-150", "We're sorry, but your account has been suspended or cancelled.");
private static string LOCK_MESSAGE = "Your account has been locked due to too many incorrect login attempts. " +
"If you cannot remember your password, it can be reset at https://beta.freeso.org/forgot. Locked for: ";

public static int LockAttempts = 5;

public static int[] LockDuration = new int[] {
5,
15,
30,
60,
120,
720,
1440
};

// GET api/<controller>
[HttpGet]
Expand Down Expand Up @@ -54,6 +68,14 @@ public IActionResult Get(string username, string password, string version, strin
return ERROR_160();
}

var ip = ApiUtils.GetIP(Request);

var accLock = db.Users.GetRemainingAuth(user.user_id, ip);
if (accLock != null && (accLock.active || accLock.count >= LockAttempts) && accLock.expire_time > Epoch.Now)
{
return printError("INV-170", LOCK_MESSAGE + Epoch.HMSRemaining(accLock.expire_time))();
}

var authSettings = db.Users.GetAuthenticationSettings(user.user_id);
var isPasswordCorrect = PasswordHasher.Verify(password, new PasswordHash
{
Expand All @@ -63,17 +85,26 @@ public IActionResult Get(string username, string password, string version, strin

if (!isPasswordCorrect)
{
var failDelay = 60 * LockDuration[Math.Min(LockDuration.Length - 1, db.Users.FailedConsecutive(user.user_id, ip))];
if (accLock == null)
{
db.Users.NewFailedAuth(user.user_id, ip, (uint)failDelay);
} else
{
var remaining = db.Users.FailedAuth(accLock.attempt_id, (uint)failDelay, LockAttempts);
if (remaining == 0)
return printError("INV-170", LOCK_MESSAGE + Epoch.HMSRemaining(Epoch.Now + (uint)failDelay))();
}
return ERROR_110();
}

var ip = ApiUtils.GetIP(Request);


var ban = db.Bans.GetByIP(ip);
if (ban != null)
{
return ERROR_110();
}

db.Users.SuccessfulAuth(user.user_id, ip);
db.Users.UpdateClientID(user.user_id, clientid ?? "0");

/** Make a ticket **/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using FSO.Server.Api.Core.Utils;
using FSO.Server.Protocol.CitySelector;
using FSO.Server.Servers.Api.JsonWebToken;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
Expand All @@ -12,6 +13,7 @@

namespace FSO.Server.Api.Core.Controllers
{
[EnableCors]
[Route("cityselector/app/AvatarDataServlet")]
[ApiController]
public class AvatarDataController : ControllerBase
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using FSO.Server.Api.Core.Utils;
using FSO.Server.Common;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
Expand All @@ -10,6 +11,7 @@

namespace FSO.Server.Api.Core.Controllers
{
[EnableCors]
[ApiController]
public class CityJSONController : ControllerBase
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace FSO.Server.Api.Core.Controllers.GameAPI
{
[EnableCors]
[Route("userapi/update")]
public class UpdateController : ControllerBase
{
Expand Down
79 changes: 79 additions & 0 deletions TSOClient/FSO.Server.Api.Core/Controllers/GithubController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Octokit;

namespace FSO.Server.Api.Core.Controllers
{
[EnableCors]
[ApiController]
public class GithubController : ControllerBase
{
readonly GitHubClient client =
new GitHubClient(new ProductHeaderValue(Api.INSTANCE.Github.AppName), new Uri("https://github.com/"));

private string StoredToken;
private static string CSRF;

// GET: /<controller>/
[HttpGet]
[Route("github/")]
public IActionResult Index()
{
if (Api.INSTANCE.Github == null) return NotFound();
if (Api.INSTANCE.Github.AccessToken != null) return NotFound();

return Redirect(GetOauthLoginUrl());
}

[HttpGet]
[Route("github/callback")]
public async Task<IActionResult> Callback(string code, string state)
{
if (Api.INSTANCE.Github == null) return NotFound();
if (Api.INSTANCE.Github.AccessToken != null) return NotFound();

if (!String.IsNullOrEmpty(code))
{
var expectedState = CSRF;
if (state != expectedState) throw new InvalidOperationException("SECURITY FAIL!");
//CSRF = null;

var token = await client.Oauth.CreateAccessToken(
new OauthTokenRequest(Api.INSTANCE.Github.ClientID, Api.INSTANCE.Github.ClientSecret, code)
{
RedirectUri = new Uri("http://localhost:80/github/callback")
});
StoredToken = token.AccessToken;
}

return Ok(StoredToken);
}

private string GetOauthLoginUrl()
{
var rngCsp = new RNGCryptoServiceProvider();
string csrf = "";
var random = new byte[24];
rngCsp.GetBytes(random);
for (int i=0; i<24; i++)
{
csrf += (char)('?' + random[i]/4);
}
CSRF = csrf;

// 1. Redirect users to request GitHub access
var request = new OauthLoginRequest(Api.INSTANCE.Github.ClientID)
{
Scopes = { "admin:org", "repo" },
State = csrf
};
var oauthLoginUrl = client.Oauth.GetGitHubLoginUrl(request);
return oauthLoginUrl.ToString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Cors;

namespace FSO.Server.Api.Core.Controllers
{
Expand All @@ -38,7 +39,8 @@ public static void Delete(string key)
memoryCache.Remove(key);
}
}


[EnableCors]
[ApiController]
public class LotInfoController : ControllerBase
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using FSO.Server.Api.Core.Utils;
using FSO.Server.Common;
using FSO.Server.Database.DA.EmailConfirmation;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Net;
Expand All @@ -14,6 +15,8 @@ namespace FSO.Server.Api.Core.Controllers
/// Controller for user registrations.
/// Supports email confirmation if enabled in config.json.
/// </summary>

[EnableCors]
[Route("userapi/registration")]
[ApiController]
public class RegistrationController : ControllerBase
Expand Down
Loading

0 comments on commit 21cea91

Please sign in to comment.