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);
+ }
}
}