using System;
using System.Globalization;
using System.Text;
namespace Discord
{
///
/// Provides a series of helper methods for handling Discord login tokens.
///
public static class TokenUtils
{
///
/// The minimum length of a Bot token.
///
///
/// This value was determined by comparing against the examples in the Discord
/// documentation, and pre-existing tokens.
///
internal const int MinBotTokenLength = 58;
internal const char Base64Padding = '=';
///
/// Pads a base64-encoded string with 0, 1, or 2 '=' characters,
/// if the string is not a valid multiple of 4.
/// Does not ensure that the provided string contains only valid base64 characters.
/// Strings that already contain padding will not have any more padding applied.
///
///
/// A string that would require 3 padding characters is considered to be already corrupt.
/// Some older bot tokens may require padding, as the format provided by Discord
/// does not include this padding in the token.
///
/// The base64 encoded string to pad with characters.
/// A string containing the base64 padding.
///
/// Thrown if would require an invalid number of padding characters.
///
///
/// Thrown if is null, empty, or whitespace.
///
internal static string PadBase64String(string encodedBase64)
{
if (string.IsNullOrWhiteSpace(encodedBase64))
throw new ArgumentNullException(paramName: encodedBase64,
message: "The supplied base64-encoded string was null or whitespace.");
// do not pad if already contains padding characters
if (encodedBase64.IndexOf(Base64Padding) != -1)
return encodedBase64;
// based from https://stackoverflow.com/a/1228744
var padding = (4 - (encodedBase64.Length % 4)) % 4;
if (padding == 3)
// can never have 3 characters of padding
throw new FormatException("The provided base64 string is corrupt, as it requires an invalid amount of padding.");
else if (padding == 0)
return encodedBase64;
return encodedBase64.PadRight(encodedBase64.Length + padding, Base64Padding);
}
///
/// 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
{
// re-add base64 padding if missing
encoded = PadBase64String(encoded);
// 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, NumberStyles.None, CultureInfo.InvariantCulture, 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, or by PadBase64String
}
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;
}
///
/// The set of all characters that are not allowed inside of a token.
///
internal static char[] IllegalTokenCharacters = new char[]
{
' ', '\t', '\r', '\n'
};
///
/// Checks if the given token contains a whitespace or newline character
/// that would fail to log in.
///
/// The token to validate.
///
/// True if the token contains a whitespace or newline character.
///
internal static bool CheckContainsIllegalCharacters(string token)
=> token.IndexOfAny(IllegalTokenCharacters) != -1;
///
/// 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 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(paramName: nameof(token), message: "A token cannot be null, empty, or contain only whitespace.");
// ensure that there are no whitespace or newline characters
if (CheckContainsIllegalCharacters(token))
throw new ArgumentException(message: "The token contains a whitespace or newline character. Ensure that the token has been properly trimmed.", paramName: 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 58 characters in length
// 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. " +
"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));
}
}
}
}