diff --git a/src/Discord.Net.Core/Utils/TokenUtils.cs b/src/Discord.Net.Core/Utils/TokenUtils.cs new file mode 100644 index 000000000..2cc0f1041 --- /dev/null +++ b/src/Discord.Net.Core/Utils/TokenUtils.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public static class TokenUtils + { + /// + /// Checks the validity of the supplied token of a specific type. + /// + /// The type of token to validate. + /// The token value to validate. + /// Thrown when the supplied token string is null, empty, or contains only whitespace. + /// Thrown when the supplied TokenType or token value is invalid. + public static void ValidateToken(TokenType tokenType, string token) + { + // A Null or WhiteSpace token of any type is invalid. + if (string.IsNullOrWhiteSpace(token)) + throw new ArgumentNullException("A token cannot be null, empty, or contain only whitespace.", nameof(token)); + + switch (tokenType) + { + case TokenType.Webhook: + // no validation is performed on Webhook tokens + break; + case TokenType.Bearer: + // no validation is performed on Bearer tokens + break; + case TokenType.Bot: + // bot tokens are assumed to be at least 59 characters in length + // this value was determined by referencing examples in the discord documentation, and by comparing with + // pre-existing tokens + if (token.Length < 59) + throw new ArgumentException("A Bot token must be at least 59 characters in length.", nameof(token)); + break; + default: + // All unrecognized TokenTypes (including User tokens) are considered to be invalid. + throw new ArgumentException("Unrecognized TokenType.", nameof(token)); + } + } + + } +} diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index f8642b96c..8ac4e9f98 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -55,11 +55,11 @@ namespace Discord.Rest await _stateLock.WaitAsync().ConfigureAwait(false); try { - await LoginInternalAsync(tokenType, token).ConfigureAwait(false); + await LoginInternalAsync(tokenType, token, validateToken).ConfigureAwait(false); } finally { _stateLock.Release(); } } - private async Task LoginInternalAsync(TokenType tokenType, string token) + private async Task LoginInternalAsync(TokenType tokenType, string token, bool validateToken) { if (_isFirstLogin) { @@ -73,6 +73,21 @@ namespace Discord.Rest try { + // If token validation is enabled, validate the token and let it throw any ArgumentExceptions + // that result from invalid parameters + if (validateToken) + { + try + { + TokenUtils.ValidateToken(tokenType, token); + } + catch (ArgumentException ex) + { + // log these ArgumentExceptions and allow for the client to attempt to log in anyways + await LogManager.WarningAsync("Discord", "A supplied token was invalid", ex).ConfigureAwait(false); + } + } + await ApiClient.LoginAsync(tokenType, token).ConfigureAwait(false); await OnLoginAsync(tokenType, token).ConfigureAwait(false); LoginState = LoginState.LoggedIn; diff --git a/test/Discord.Net.Tests/Tests.TokenUtils.cs b/test/Discord.Net.Tests/Tests.TokenUtils.cs new file mode 100644 index 000000000..dc5a93e34 --- /dev/null +++ b/test/Discord.Net.Tests/Tests.TokenUtils.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Discord +{ + public class TokenUtilsTests + { + /// + /// Tests the usage of + /// to see that when a null, empty or whitespace-only string is passed as the token, + /// it will throw an ArgumentNullException. + /// + [Theory] + [InlineData(null)] + [InlineData("")] // string.Empty isn't a constant type + [InlineData(" ")] + [InlineData(" ")] + [InlineData("\t")] + public void TestNullOrWhitespaceToken(string token) + { + // an ArgumentNullException should be thrown, regardless of the TokenType + Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Bearer, token)); + Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Bot, token)); + Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Webhook, token)); + } + + /// + /// Tests the behavior of + /// to see that valid Webhook tokens do not throw Exceptions. + /// + /// + [Theory] + [InlineData("123123123")] + // bot token + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] + // bearer token taken from discord docs + [InlineData("6qrZcUqja7812RVdnEKjpzOL4CvHBFG")] + // client secret + [InlineData("937it3ow87i4ery69876wqire")] + public void TestWebhookTokenDoesNotThrowExceptions(string token) + { + TokenUtils.ValidateToken(TokenType.Webhook, token); + } + + // No tests for invalid webhook token behavior, because there is nothing there yet. + + /// + /// Tests the behavior of + /// to see that valid Webhook tokens do not throw Exceptions. + /// + /// + [Theory] + [InlineData("123123123")] + // bot token + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] + // bearer token taken from discord docs + [InlineData("6qrZcUqja7812RVdnEKjpzOL4CvHBFG")] + // client secret + [InlineData("937it3ow87i4ery69876wqire")] + public void TestBearerTokenDoesNotThrowExceptions(string token) + { + TokenUtils.ValidateToken(TokenType.Bearer, token); + } + + // No tests for invalid bearer token behavior, because there is nothing there yet. + + /// + /// Tests the behavior of + /// to see that valid Bot tokens do not throw Exceptions. + /// Valid Bot tokens can be strings of length 59 or above. + /// + [Theory] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] + [InlineData("This appears to be completely invalid, however the current validation rules are not very strict.")] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWss")] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] + public void TestBotTokenDoesNotThrowExceptions(string token) + { + // This example token is pulled from the Discord Docs + // https://discordapp.com/developers/docs/reference#authentication-example-bot-token-authorization-header + // should not throw any exception + TokenUtils.ValidateToken(TokenType.Bot, token); + } + + /// + /// Tests the usage of with + /// a Bot token that is invalid. + /// + [Theory] + [InlineData("This is invalid")] + // missing a single character from the end + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] + // bearer token + [InlineData("6qrZcUqja7812RVdnEKjpzOL4CvHBFG")] + // client secret + [InlineData("937it3ow87i4ery69876wqire")] + public void TestBotTokenInvalidThrowsArgumentException(string token) + { + Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Bot, token)); + } + + /// + /// Tests the behavior of + /// to see that an is thrown when an invalid + /// is supplied as a parameter. + /// + /// + /// The type is treated as an invalid . + /// + [Theory] + // TokenType.User + [InlineData(0)] + // out of range TokenType + [InlineData(4)] + [InlineData(7)] + public void TestUnrecognizedTokenType(int type) + { + Assert.Throws(() => + TokenUtils.ValidateToken((TokenType)type, "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")); + } + } +}