I'm attempting to add Discord OAuth to a mobile app using PKCE. I'm able to successfully authenticate with DiscordAPI, and I receive a code
back from the server.
However, when I try to call the https://discordapp.com/api/oauth2/token
endpoint I continually get a Bad Request
result.
I've tried a few approaches to attaining my token, but all have failed.
Attempt (1) using HttpClient:
var nvc = new List<KeyValuePair<string, string>>();
nvc.Add(new KeyValuePair<string, string>("client_id", Constants.ClientId));
nvc.Add(new KeyValuePair<string, string>("client_secret", Constants.Secret));
nvc.Add(new KeyValuePair<string, string>("code_verifier", codeVerifier)); // method param
nvc.Add(new KeyValuePair<string, string>("grant_type", "authorization_code"));
nvc.Add(new KeyValuePair<string, string>("code", authCode)); // method param
nvc.Add(new KeyValuePair<string, string>("redirect_uri", Constants.Redirect));
nvc.Add(new KeyValuePair<string, string>("scope", "identify email connections"));
var req = new HttpRequestMessage(HttpMethod.Post, "https://discordapp.com/api/oauth2/token")
{
Content = new FormUrlEncodedContent(nvc)
};
var authTokenResponse = await this.Client.SendAsync(req).ConfigureAwait(false);
// results in BadRequest
I've also tried using a StringContent
object like this:
var stringContent = new StringContent($"grant_type=authorization_code&client_id={Constants.ClientId}&client_secret={Constants.Secret}&code_verifier={codeVerifier}&code={authCode}&redirect_uri={Constants.Redirect}&scopes=identify%20email%20connections", Encoding.UTF8, "application/x-www-form-urlencoded");
Both of these result in Bad Request
Attempt (2) using RestSharp
var client = new RestSharp.RestClient($"{this.Client.BaseAddress}/oauth2/token");
var request = new RestRequest(Method.POST);
request.AddHeader("cache-control", "no-cache");
request.AddHeader("content-type", "application/x-www-form-urlencoded");
request.AddParameter("application/x-www-form-urlencoded", $"grant_type=authorization_code&client_id={Constants.ClientId}&client_secret={Constants.Secret}&code_verifier={code}&code={authCode}&redirect_uri={Constants.Redirect}&scopes=identify%20email%20connections", ParameterType.RequestBody);
var response = await client.ExecuteAsync(request);
I've carefully double-checked all of my values and made sure my Client Id and Secret are correct. I've tried sending the request with and without the code_verifier
. Without the code_verifier
I get Unauthorized
.
How should I modify my HttpClient request to successfully attain my auth token?
More info:
To comply with PKCE, I need a code_verifier
. This is how I'm generating the code. (Note, I am not trying to use this as an auth code.)
Random rng = new Random();
byte[] code = new byte[32];
rng.NextBytes(code);
CodeVerifier = Base64UrlEncoder.Encode(rng.ToString());
Then I use a hashing algorithm borrowed from c-sharpcorner.com, which looks like this:
using (SHA256 sha256Hash = SHA256.Create())
{
// ComputeHash - returns byte array
var bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData));
// Convert byte array to a string
var builder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
builder.Append(bytes[i].ToString("x2"));
}
return builder.ToString();
}
I pass the result as my code_challenge
while attaining my auth code.
Here's is where I learned that PKCE was required.
https://github.com/discordapp/discord-api-docs/issues/450
PKCE discussion continues here:
https://github.com/discordapp/discord-api-docs/issues/1296
Update:
Alright, so based iwokal's answer, I re-read the posts linked above, so PKCE is only "required" if you are using a Custom Scheme Redirect
, if you are just using an http
redirect just leave the code_verifier
out and you are good to go.
That said, this PKCE workflow is undocumented. Since I spent 500 points, I'll award the points to whoever is able to resolve the PKCE OAuth workflow.
You're making your request to the wrong url. The Code flow is created to prevent leaking the client token. The flow is as the following
Note: there are 2 redirect uri, 1, which resolves the code and another which resolves the token.
You're trying to make your request directly to the token endpoint. You need to make a request to the Authorize endpoint. To obtain a token
var state = Guid.NewGuid().ToString();
var codeParams = new List<KeyValuePair<string, string>>();
codeParams.Add(new KeyValuePair<string, string>("client_id", Constants.ClientId));
codeParams.Add(new KeyValuePair<string, string>("redirect_uri", Constants.Redirect));
codeParams.Add(new KeyValuePair<string, string>("scope", "identify email connections"));
codeParams.Add(new KeyValuePair<string, string>("response_type", "code"));
codeParams.Add(new KeyValuePair<string, string>("state", state))
var req = new HttpRequestMessage(HttpMethod.Post,
"https://discordapp.com/api/oauth2/authorize")
{
Content = new FormUrlEncodedContent(nvc)
};
var authTokenResponse = await this.Client.SendAsync(req).ConfigureAwait(false);
Discord will then respond your redirect_uri
with the code and the state
https://mywebsite.com/?code=NhhvTDYsFcdgNLnnLijcl7Ku7bEEeee&state=15773059ghq9183habn
Note: The original request to the Authorize doesn't contain your client secret, the client secret is only used from your server. This way only the server can request AuthTokens
Note: most places that support oauth require you to setup you're redirect url with the server.
Note: the state will also be returned with the code verify the state you sent is the one returned.
Take the code the returned to you (NhhvTDYsFcdgNLnnLijcl7Ku7bEEeee) and use the to request a token using your original example, with the proper code:
var nvc = new List<KeyValuePair<string, string>>();
nvc.Add(new KeyValuePair<string, string>("client_id", Constants.ClientId));
nvc.Add(new KeyValuePair<string, string>("client_secret", Constants.Secret));
nvc.Add(new KeyValuePair<string, string>("code_verifier", codeVerifier)); // method param
nvc.Add(new KeyValuePair<string, string>("grant_type", "authorization_code"));
nvc.Add(new KeyValuePair<string, string>("code", authCode)); // method param
nvc.Add(new KeyValuePair<string, string>("redirect_uri", Constants.Redirect));
nvc.Add(new KeyValuePair<string, string>("scope", "identify email connections"));
var req = new HttpRequestMessage(HttpMethod.Post,
"https://discordapp.com/api/oauth2/token")
{
Content = new FormUrlEncodedContent(nvc)
};
var authTokenResponse = await this.Client.SendAsync(req).ConfigureAwait(false);
Note make sure when you make a request to the Token endpoint your using
'Content-Type': 'application/x-www-form-urlencoded'
If your still having trouble with the flow, Use Postman it has built in OAuth support and obtain the token, watch what they send with fiddler, and emulate it.