[SalesForce] Why Am I getting invalid_client_credentials with Web Server OAuth Flow

I'm trying to create a web service that will provide an OData API to my managed package. As part of that I need to handle authorization so the service can access the user's Org data where they have my managed package installed. My managed package is configured with a connected app. I am following the web server OAuth flow for this. I do not have a digital certificate for my connected app, as I understand that is not needed for web server OAuth flow. my web service is hosted in Azure and SSL is set up properly for it. The callback URL to my service is added to the connected app callback URLs. my testing process goes like this:

  1. Navigate to https://myservice/api/mycontroller/Authorize?redirectUri=http%3A%2F%2Fwww.google.com
  2. The Authorize method then redirects to the salesforce authorize endpoint: https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id={ClientId}&redirect_uri=https%3A%2F%myservice%2Fapi%2Fmycontroller%2FCallback&state={state}";
  3. I log into my org and approve access.
  4. I am now redirected back to the callback uri of my service with an authorization code and the state.
  5. I then initiate a request for a token with the auth code.

When I make a request for the token, i am receiving the following response:

{"error":"invalid_client","error_description":"invalid client credentials"}

I don't understand why I get this error. In my service code, i have the callback method stubbed to just return some details on the token request. here's my code (sensitive data trimmed):

Controller

[Route("api/[controller]/[action]/{id?}")]
public class MyController : Controller
{
    private static AuthService Service = new AuthService();


    [HttpGet]
    public IActionResult Authorize(string redirectUri)
    {
        if (string.IsNullOrEmpty(redirectUri))
        {
            return new BadRequestResult();
        }

        var authUrl = Service.GetAuthUrl(redirectUri);
        return new RedirectResult(authUrl);
    }

    [HttpGet]
    public async Task<IActionResult> Callback(string state, string code)
    {
        try
        {
            if (string.IsNullOrEmpty(code))
            {
                return new OkResult();
            }
            var tokenResponse = await Service.RequestTokenAsync(code);
            var redirectUri = Service.ResolveRequest(state, tokenResponse);
            if (string.IsNullOrEmpty(redirectUri))
            {
                return new BadRequestResult();
            }

            return Content($"redirectUrl: {redirectUri} | response: {JsonConvert.SerializeObject(tokenResponse)}");
        }
        catch (Exception ex)
        {
            return Content($"error: {ex.ToString()}");
        }
    }
}

AuthService.cs

using RestSharp;
using System.Net;
using System.Threading.Tasks;

namespace MyService.Auth
{
    public class AuthService
    {
        private static string ClientId = "MyClientId";
        private static string Secret = "MySecret";
        private static string SalesForceAuthBaseUrl = "https://login.salesforce.com/services/oauth2";
        private static string AuthCallback = "https%3A%2F%myservice%2Fapi%2Fauth%2FCallback";

        public string GetAuthUrl(string redirectUri)
        {
            var state = WebUtility.UrlEncode(redirectUri);
            return $"{SalesForceAuthBaseUrl}/authorize?response_type=code&client_id={ClientId}&redirect_uri={AuthCallback}&state={state}";
        }

        public async Task<string> RequestTokenAsync(string code)
        {
            var client = new RestClient(SalesForceAuthBaseUrl);
            var request = new RestRequest("token", 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&code={code}&client_id={ClientId}&secret={Secret}&redirect_uri={AuthCallback}", 
                ParameterType.RequestBody);

            var taskCompletion = new TaskCompletionSource<IRestResponse>();
            client.ExecuteAsync(request, r => taskCompletion.SetResult(r));
            RestResponse response = (RestResponse)(await taskCompletion.Task);

            return response.Content;
        }

        public string ResolveRequest(string redirectUri, string tokenResponse)
        {
            var data = WebUtility.UrlEncode(Base64Encode(tokenResponse));
            return Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(redirectUri, "data", data);
        }

        public static string Base64Encode(string plainText)
        {
            var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
            return System.Convert.ToBase64String(plainTextBytes);
        }

        public static string Base64Decode(string base64EncodedData)
        {
            var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData);
            return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
        }
    }
}

Why Would I get "invalid client credentials" on the token request?

Best Answer

Found the problem! I had a typo in my token request. It should have been "client_secret" instead of "secret". so it should be:

request.AddParameter("application/x-www-form-urlencoded", 
            $"grant_type=authorization_code&code={code}&client_id={ClientId}&client_secret={Secret}&redirect_uri={AuthCallback}", 
            ParameterType.RequestBody);
Related Topic