diff --git a/src/Discord.Net.Core/Utils/TokenUtils.cs b/src/Discord.Net.Core/Utils/TokenUtils.cs index 8fa846267..68aad5d96 100644 --- a/src/Discord.Net.Core/Utils/TokenUtils.cs +++ b/src/Discord.Net.Core/Utils/TokenUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Text; namespace Discord { @@ -16,6 +17,65 @@ namespace Discord /// internal const int MinBotTokenLength = 58; + /// + /// Decodes a base 64 encoded string into a ulong value. + /// + /// A base 64 encoded string containing a User Id. + /// A ulong containing the decoded value of the string, or null if the value was invalid. + internal static ulong? DecodeBase64UserId(string encoded) + { + if (string.IsNullOrWhiteSpace(encoded)) + return null; + + try + { + // decode the base64 string + var bytes = Convert.FromBase64String(encoded); + var idStr = Encoding.UTF8.GetString(bytes); + // try to parse a ulong from the resulting string + if (ulong.TryParse(idStr, out var id)) + return id; + } + catch (DecoderFallbackException) + { + // ignore exception, can be thrown by GetString + } + catch (FormatException) + { + // ignore exception, can be thrown if base64 string is invalid + } + catch (ArgumentException) + { + // ignore exception, can be thrown by BitConverter + } + return null; + } + + /// + /// Checks the validity of a bot token by attempting to decode a ulong userid + /// from the bot token. + /// + /// + /// The bot token to validate. + /// + /// + /// True if the bot token was valid, false if it was not. + /// + internal static bool CheckBotTokenValidity(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return false; + + // split each component of the JWT + var segments = message.Split('.'); + + // ensure that there are three parts + if (segments.Length != 3) + return false; + // return true if the user id could be determined + return DecodeBase64UserId(segments[0]).HasValue; + } + /// /// Checks the validity of the supplied token of a specific type. /// @@ -42,13 +102,17 @@ namespace Discord // this value was determined by referencing examples in the discord documentation, and by comparing with // pre-existing tokens if (token.Length < MinBotTokenLength) - throw new ArgumentException(message: $"A Bot token must be at least {MinBotTokenLength} characters in length.", paramName: nameof(token)); + throw new ArgumentException(message: $"A Bot token must be at least {MinBotTokenLength} characters in length. " + + "Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token)); + // check the validity of the bot token by decoding the ulong userid from the jwt + if (!CheckBotTokenValidity(token)) + throw new ArgumentException(message: "The Bot token was invalid. " + + "Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token)); break; default: // All unrecognized TokenTypes (including User tokens) are considered to be invalid. throw new ArgumentException(message: "Unrecognized TokenType.", paramName: nameof(token)); } } - } } diff --git a/test/Discord.Net.Tests/Tests.TokenUtils.cs b/test/Discord.Net.Tests/Tests.TokenUtils.cs index 8cebc649d..9a1102ec5 100644 --- a/test/Discord.Net.Tests/Tests.TokenUtils.cs +++ b/test/Discord.Net.Tests/Tests.TokenUtils.cs @@ -76,9 +76,7 @@ namespace Discord [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] // 59 char token [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 @@ -99,6 +97,9 @@ namespace Discord [InlineData("937it3ow87i4ery69876wqire")] // 57 char bot token [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK")] + [InlineData("This is an invalid token, but it passes the check for string length.")] + // valid token, but passed in twice + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] public void TestBotTokenInvalidThrowsArgumentException(string token) { Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Bot, token)); @@ -124,5 +125,44 @@ namespace Discord Assert.Throws(() => TokenUtils.ValidateToken((TokenType)type, "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")); } + + /// + /// Checks the method for expected output. + /// + /// The Bot Token to test. + /// The expected result. + [Theory] + // this method only checks the first part of the JWT + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4..", true)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK", true)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4. this part is invalid. this part is also invalid", true)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.", false)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4", false)] + [InlineData("NDI4NDc3OTQ0MDA5MTk1NTIw.xxxx.xxxxx", true)] + // should not throw an unexpected exception + [InlineData("", false)] + [InlineData(null, false)] + public void TestCheckBotTokenValidity(string token, bool expected) + { + Assert.Equal(expected, TokenUtils.CheckBotTokenValidity(token)); + } + + [Theory] + // cannot pass a ulong? as a param in InlineData, so have to have a separate param + // indicating if a value is null + [InlineData("NDI4NDc3OTQ0MDA5MTk1NTIw", false, 428477944009195520)] + // should return null w/o throwing other exceptions + [InlineData("", true, 0)] + [InlineData(" ", true, 0)] + [InlineData(null, true, 0)] + [InlineData("these chars aren't allowed @U#)*@#!)*", true, 0)] + public void TestDecodeBase64UserId(string encodedUserId, bool isNull, ulong expectedUserId) + { + var result = TokenUtils.DecodeBase64UserId(encodedUserId); + if (isNull) + Assert.Null(result); + else + Assert.Equal(expectedUserId, result); + } } }