* improve bot token validation by trying to decode user id from token Try to decode the user id from the supplied bot token as a way of validating the token. If this should fail, indicate that the token is invalid. * Update the tokenutils tests to pass the new validation checks * Add test case for CheckBotTokenValidity method * lint: clean up whitespace * Add check for null or whitespace string, lint whitespace * fix userid conversion * Add hint to user to check that token is not an oauth client secret * Catch exception that can be thrown by GetString * Refactor token conversion logic into it's own testable methodpull/1214/head
@@ -1,4 +1,5 @@ | |||||
using System; | using System; | ||||
using System.Text; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
@@ -17,6 +18,65 @@ namespace Discord | |||||
internal const int MinBotTokenLength = 58; | internal const int MinBotTokenLength = 58; | ||||
/// <summary> | /// <summary> | ||||
/// Decodes a base 64 encoded string into a ulong value. | |||||
/// </summary> | |||||
/// <param name="encoded"> A base 64 encoded string containing a User Id.</param> | |||||
/// <returns> A ulong containing the decoded value of the string, or null if the value was invalid. </returns> | |||||
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; | |||||
} | |||||
/// <summary> | |||||
/// Checks the validity of a bot token by attempting to decode a ulong userid | |||||
/// from the bot token. | |||||
/// </summary> | |||||
/// <param name="message"> | |||||
/// The bot token to validate. | |||||
/// </param> | |||||
/// <returns> | |||||
/// True if the bot token was valid, false if it was not. | |||||
/// </returns> | |||||
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; | |||||
} | |||||
/// <summary> | |||||
/// Checks the validity of the supplied token of a specific type. | /// Checks the validity of the supplied token of a specific type. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="tokenType"> The type of token to validate. </param> | /// <param name="tokenType"> The type of token to validate. </param> | ||||
@@ -42,13 +102,17 @@ namespace Discord | |||||
// this value was determined by referencing examples in the discord documentation, and by comparing with | // this value was determined by referencing examples in the discord documentation, and by comparing with | ||||
// pre-existing tokens | // pre-existing tokens | ||||
if (token.Length < MinBotTokenLength) | 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; | break; | ||||
default: | default: | ||||
// All unrecognized TokenTypes (including User tokens) are considered to be invalid. | // All unrecognized TokenTypes (including User tokens) are considered to be invalid. | ||||
throw new ArgumentException(message: "Unrecognized TokenType.", paramName: nameof(token)); | throw new ArgumentException(message: "Unrecognized TokenType.", paramName: nameof(token)); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } |
@@ -76,9 +76,7 @@ namespace Discord | |||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] | [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] | ||||
// 59 char token | // 59 char token | ||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] | [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.ZnCjm1XVW7vRze4b7Cq4se7kKWss")] | ||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] | |||||
public void TestBotTokenDoesNotThrowExceptions(string token) | public void TestBotTokenDoesNotThrowExceptions(string token) | ||||
{ | { | ||||
// This example token is pulled from the Discord Docs | // This example token is pulled from the Discord Docs | ||||
@@ -99,6 +97,9 @@ namespace Discord | |||||
[InlineData("937it3ow87i4ery69876wqire")] | [InlineData("937it3ow87i4ery69876wqire")] | ||||
// 57 char bot token | // 57 char bot token | ||||
[InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK")] | [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) | public void TestBotTokenInvalidThrowsArgumentException(string token) | ||||
{ | { | ||||
Assert.Throws<ArgumentException>(() => TokenUtils.ValidateToken(TokenType.Bot, token)); | Assert.Throws<ArgumentException>(() => TokenUtils.ValidateToken(TokenType.Bot, token)); | ||||
@@ -124,5 +125,44 @@ namespace Discord | |||||
Assert.Throws<ArgumentException>(() => | Assert.Throws<ArgumentException>(() => | ||||
TokenUtils.ValidateToken((TokenType)type, "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")); | TokenUtils.ValidateToken((TokenType)type, "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")); | ||||
} | } | ||||
/// <summary> | |||||
/// Checks the <see cref="TokenUtils.CheckBotTokenValidity(string)"/> method for expected output. | |||||
/// </summary> | |||||
/// <param name="token"> The Bot Token to test.</param> | |||||
/// <param name="expected"> The expected result. </param> | |||||
[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); | |||||
} | |||||
} | } | ||||
} | } |