I'm trying to pass an OAuth2 bearer token along with a Post request to my server (Using the Google Apps Script Executions API). I keep getting a 401 unauthorized error any way I try to set the Authorization token. I'm sure I'm setting it wrong somehow, but I've been unable to figure out what's wrong.
Below is my current code.
private static UnityWebRequest createRequest(string functionName, List<string> parameters)
{
PostData data = new PostData();
data.Add("function", functionName);
data.Add("parameters", string.Join(",", parameters.ToArray()));
data.Add("devMode", "true"); // TODO: remove before launch
Debug.Log(data.toJsonString());
UnityWebRequest request = new UnityWebRequest(
"https://script.googleapis.com/v1/scripts/" + SERVER_SCRIPT_ID + ":run",
UnityWebRequest.kHttpVerbPOST);
UploadHandlerRaw uploadHandler = new UploadHandlerRaw(data.toJsonBytes());
request.uploadHandler = uploadHandler;
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json; charset=utf-8");
request.SetRequestHeader("Authorization", "Bearer " + DriveAPI.getInstance().getAuthToken());
yield return request.Send();
Debug.Log(request.downloadHandler.text);
}
[Serializable]
private class PostData : Dictionary<string, string>
{
public byte[] toJsonBytes()
{
return Encoding.ASCII.GetBytes(toJsonString());
}
public string toJsonString()
{
string result = "{";
foreach (string key in this.Keys)
{
string value;
TryGetValue(key, out value);
result += "\"" + key + "\":\"" + value + "\",";
}
result = result.Substring(0, result.Length - 1) + "}";
return result;
}
}
I have tried setting the headers in the actual Data object, but didn't have luck there either.
Turns out my code was working fine.
The error message from Apps Script is misleading as the Auth token was being sent. I was able to test correctness by generating an auth token from my script (that's serving the Execution API endpoints) and hard-coding that token in my app. Doing so resulted in a successful request.
I've checked that the cloud projects used in the script and my app are the same, but am still receiving the error, but that's a problem for another question :).
Try enconding to UTF8, changing UploadHandleRaw to UploadHandle and setting request to single Post:
private static UnityWebRequest createRequest(string functionName, List<string> parameters)
{
PostData data = new PostData();
data.Add("function", functionName);
data.Add("parameters", string.Join(",", parameters.ToArray()));
data.Add("devMode", "true"); // TODO: remove before launch
Debug.Log(data.toJsonString());
UnityWebRequest request = new UnityWebRequest("https://script.googleapis.com/v1/scripts/" + SERVER_SCRIPT_ID + ":run", "POST");
UploadHandler uploadHandler =(UploadHandler) new UploadHandlerRaw(Encoding.UTF8.GetBytes(data.toJsonString()));
request.uploadHandler = uploadHandler;
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("Authorization", "Bearer " + DriveAPI.getInstance().getAuthToken());
yield return request.Send();
if (www.error != null)
{
Debug.Log("Error: " + www.error);
}
else
{
Debug.Log("Status Code: " + request.responseCode);
}
}
Related
I am a begginer and i work in a MVC project which I cant understand it well yet.
I can't understand where does the API takes data from when I try to connect in Login Screen.
It doesn't use Entity Framework and there isn't a json with the data.
When I enter Id and Pass it calls an API (GetAPIResponse) which somehow finds that is correct.
Need help to understand the code and the logic behind it.
LoginBL class contains:
public bool IsAuthenticated(LoginEntity user)
{
string url = string.Empty;
string callType = string.Empty;
string server = string.Empty;
try
{
// get URL, Call type, Server from config file
url = ConfigurationManager.AppSettings["login_url"].ToString();
callType = ConfigurationManager.AppSettings["calltype"].ToString();
server = ConfigurationManager.AppSettings["server"].ToString();
// Encrypt password
string password = Scrambler.GenerateMD5Hash(user.Password);
// Prepare content for the POST request
string content = #"calltype=" + callType + "&server=" + server + "&user=" + user.UserName + "&pass=" + password + "";
Debug.WriteLine("Callcenter login url: " + content);
HttpResponseMessage json_list = ApiCallBL.GetAPIResponse(url, content);
LoginResponseEntity obj = new LoginResponseEntity();
obj = JsonConvert.DeserializeObject<LoginResponseEntity>(json_list.Content.ReadAsStringAsync().Result);
Debug.WriteLine(callType + " Response: " + json_list.Content.ReadAsStringAsync().Result);
//if API resultCode return 0 then user details and token save in session for further use
if (obj.ResultCode == 0)
{
int restrict = obj.UserInfo.RestrictCallType.HasValue ?
obj.UserInfo.RestrictCallType.Value : 0;
HttpContext.Current.Session["user_id"] = obj.UserInfo.usr_id;
HttpContext.Current.Session["user_name"] = obj.UserInfo.usr_username;
HttpContext.Current.Session["user_group_id"] = obj.UserInfo.UserGroupID;
HttpContext.Current.Session["groupid"] = obj.UserInfo.groupid;
HttpContext.Current.Session["token"] = obj.Token;
HttpContext.Current.Session["web_server_url"] = obj.ServerInfo.web_server_url;
HttpContext.Current.Session["centerX"] = obj.ServerInfo.DefaultGeoX;
HttpContext.Current.Session["centerY"] = obj.ServerInfo.DefaultGeoY;
HttpContext.Current.Session["dateFormat"] = obj.ServerInfo.dateFormat;
HttpContext.Current.Session["currency"] = obj.ServerInfo.currency;
HttpContext.Current.Session["customer_img"] = obj.ServerInfo.customer_img;
HttpContext.Current.Session["groups"] = obj.groups;
HttpContext.Current.Session["restrict_call_type"] = restrict ;
Debug.WriteLine("obj.UserInfo.UserGroupID " + obj.UserInfo.UserGroupID);
Debug.WriteLine("obj.UserInfo.groups " + obj.groups);
//HttpContext.Current.Session["defaultLanguage"] = obj.ServerInfo.defaultLanguage;
HttpCookie cookie = new HttpCookie("Login");
// if remember me checked then user name and password stored in cookie else cookes is expired
if (user.RememberMe)
{
cookie.Values.Add("user_name", obj.UserInfo.usr_username);
cookie.Values.Add("pwd", user.Password);
cookie.Expires = DateTime.Now.AddDays(15);
HttpContext.Current.Response.Cookies.Add(cookie);
}
else
{
cookie.Expires = DateTime.Now.AddDays(-1);
HttpContext.Current.Response.Cookies.Add(cookie);
}
return true;
}
else
{
//ResultCode -5 :Invalid Login ,-1:Database Error ,-2:Server Error ,-3:Invalid Parameter specified ,-4:Invalid Token
return false;
}
}
catch
{
throw;
}
finally
{
url = string.Empty;
callType = string.Empty;
server = string.Empty;
}
}
Okay here after converts pass to MD5 creates a "string content" with the information given.
Then in next line (HttpResponseMessage json_list = ApiCallBL.GetAPIResponse(url, content);) calls the API with the url and content as parameters where it finds if the data exists.
API code:
public static HttpResponseMessage GetAPIResponse(string url, string content)
{
StringBuilder traceLog = null;
HttpContent httpContent = null;
try
{
traceLog = new StringBuilder();
traceLog.AppendLine("Start: BusinessLayer getAPIResponse() Request Data:- " + DateTime.Now + "URL = " + url + "&content = " + httpContent);
using (HttpClient client = new HttpClient())
{
httpContent = new StringContent(content);
httpContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
var resp = client.PostAsync(url, httpContent).Result;
Debug.WriteLine("resp: " + resp.Content.ReadAsStringAsync().Result);
traceLog.AppendLine("End: BusinessLayer getAPIResponse() call completed HttpResponseMessage received");
return resp;
}
}
catch
{
throw;
}
finally
{
traceLog = null;
httpContent.Dispose();
url = string.Empty;
content = string.Empty;
}
}
In the following line, console prints the result that I cant understand where it cames from (Debug.WriteLine("resp: " + resp.Content.ReadAsStringAsync().Result);)
Sorry for the confusion , I am in my first job with zero work experience and I am called to learn how this works alone without proper education on ASP.NET from them.
You will not go very far without debbugger. Learn how to debug in Visual Studio (YouTube tutorials might be fastest way). Place debug points along critical points in code (for example moment when client sends and receives response is line var resp = client.PostAsync...) and check variables.
Url for API server is actually defined in the line
url = ConfigurationManager.AppSettings["login_url"].ToString();
ConfigurationManager means Web.config file, check it's appSettings section for login_url entry, there is your url.
Btw, using (HttpClient client = new HttpClient()) is not a good way to use a HttpClient and will lead to port exhaustion. It's ok for small number of requests, but for larger ones you must reuse it, or use HttpClientFactory (for .NET Core).
Here is my API request
public IEnumerator Login(string bodyJsonString)
{
Debug.Log(bodyJsonString);
UnityWebRequest req = UnityWebRequest.Post("localhost:3000/login", bodyJsonString);
req.SetRequestHeader("content-type", "application/json");
yield return req.SendWebRequest();
if (req.isNetworkError || req.isHttpError)
{
Debug.Log(req.error);
}
else
{
Debug.Log("Form upload complete!");
}
}
It returns an error status code 500 and on the server returns an error Unexpected token % in JSON at position 0","severity
Here is my Coroutine Call
public void submitLogin()
{
_username = userInputField.GetComponent<InputField>().text;
_password = passwordInputField.GetComponent<InputField>().text;
Debug.Log("username" + _username);
Debug.Log("password" + _password);
string body = "{'username':'" + _username + "','password','" + _password + "'}";
//API Call
authChexi = new Auth();
StartCoroutine(authChexi.Login(body));
}
Let me know if you have ideas on how to deal with my form body. Thanks
So I have updated my function. I did some digging and finally solved it. My mistake was indeed manually building up a JSON. So here is my solution.
public void submitLogin()
{
_username = userInputField.GetComponent<InputField>().text;
_password = passwordInputField.GetComponent<InputField>().text;
//API Call
authChexi = new Auth();
StartCoroutine(authChexi.Login(_username, _password));
}
Created a class userdata for my json object
public class UserData
{
public string username;
public string password;
public string email;
}
And call the API
public IEnumerator Login(string username, string password)
{
//#TODO: call API login
// Store Token
// Add Token to headers
var user = new UserData();
user.username = username;
user.password = password;
string json = JsonUtility.ToJson(user);
var req = new UnityWebRequest("localhost:3000/login", "POST");
byte[] jsonToSend = new System.Text.UTF8Encoding().GetBytes(json);
req.uploadHandler = (UploadHandler)new UploadHandlerRaw(jsonToSend);
req.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
//Send the request then wait here until it returns
yield return req.SendWebRequest();
if (req.isNetworkError)
{
Debug.Log("Error While Sending: " + req.error);
}
else
{
Debug.Log("Received: " + req.downloadHandler.text);
}
}
And now it's working like a charm!
You should provide a valid JSON string in the request. You should provide double quotes instead of single quotes for each attributes with the help of the escape character ("").
Try to change the methods as follows,
public IEnumerator Login(string bodyJsonString)
{
UnityWebRequest request = new UnityWebRequest("localhost:3000/login", "POST");
byte[] data = new System.Text.UTF8Encoding().GetBytes(bodyJsonString);
request.uploadHandler = (UploadHandler) new UploadHandlerRaw(data); // important
request.downloadHandler = (DownloadHandler) new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json"); //important
yield return request.SendWebRequest();
if (request.isNetworkError) //
Debug.Log("Error While Sending: " + request.error);
else
Debug.Log("Received: " + request.downloadHandler.text);
}
public void submitLogin()
{
_username = userInputField.GetComponent<InputField>().text;
_password = passwordInputField.GetComponent<InputField>().text;
Debug.Log("username" + _username);
Debug.Log("password" + _password);
string body = "{\"username\":\"" + _username + "\",\"password\":\"" + _password + "\"}"; //important
//API Call
authChexi = new Auth();
StartCoroutine(authChexi.Login(body));
}
I'm having trouble getting a call to the Twitter API working.
I'm having to do this as the package i was using has got an issue and the fix isn't compatible with my project.
I want to retrieve a list of 'recent' tweets but I keep getting a 401. I have a feeling it is because of the way I'm creating my signature but I'm not certain.
My request URL is:
https://api.twitter.com/1.1/search/tweets.json?result_type=recent&count=1&q=#hashtag1 OR #hashtag2 OR #hashtag3
I have the following auth info:
Dictionary<string, object> _parameters = new Dictionary<string, object>();
_parameters.Add("oauth_version", "1.0");
_parameters.Add("oauth_nonce", "[VALUE]");
_parameters.Add("oauth_timestamp", [TIMESTAMP]);
_parameters.Add("oauth_signature_method", "HMAC-SHA1");
_parameters.Add("oauth_consumer_key", "[VALUE]");
_parameters.Add("oauth_consumer_secret", "[VALUE]");
_parameters.Add("oauth_token", "[VALUE]");
_parameters.Add("oauth_token_secret", "[VALUE]");
My 'create signature' method is as follows. It uses the full URL with search parameters to construct it:
private static string GenerateSignature(Dictionary<string,object> parameters, Uri requestUri, string consumerSecret, string accessTokenSecret)
{
IEnumerable<KeyValuePair<string, object>> nonSecretParameters;
nonSecretParameters = (from p in parameters
where (!SecretParameters.Contains(p.Key))
select p);
Uri urlForSigning = requestUri;
string signatureBaseString = string.Format(
CultureInfo.InvariantCulture,
"{0}&{1}&{2}",
"GET",
UrlEncode(NormalizeUrl(urlForSigning)),
UrlEncode(nonSecretParameters));
string key = string.Format(
CultureInfo.InvariantCulture,
"{0}&{1}",
UrlEncode(consumerSecret),
UrlEncode(accessTokenSecret));
HMACSHA1 hmacsha1 = new HMACSHA1(Encoding.UTF8.GetBytes(key));
byte[] signatureBytes = hmacsha1.ComputeHash(Encoding.UTF8.GetBytes(signatureBaseString));
return Convert.ToBase64String(signatureBytes);
}
private static string UrlEncode(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
value = Uri.EscapeDataString(value);
value = Regex.Replace(value, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper());
value = value
.Replace("(", "%28")
.Replace(")", "%29")
.Replace("$", "%24")
.Replace("!", "%21")
.Replace("*", "%2A")
.Replace("'", "%27");
value = value.Replace("%7E", "~");
return value;
}
private static string NormalizeUrl(Uri url)
{
string normalizedUrl = string.Format(CultureInfo.InvariantCulture, "{0}://{1}", url.Scheme, url.Host);
if (!((url.Scheme == "http" && url.Port == 80) || (url.Scheme == "https" && url.Port == 443)))
{
normalizedUrl += ":" + url.Port;
}
normalizedUrl += url.PathAndQuery + url.Fragment.Replace("%20", " ");
return normalizedUrl;
}
Once I have the signature, I generate my Authorisation header which is something like this:
OAuth oauth_consumer_key="[VALUE]",oauth_nonce="[VALUE]",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1565261231",oauth_token="[VALUE]",oauth_version="1.0",oauth_signature="[VALUE]"
Then I append the relevant auth params to the URL and make the request to the Twitter API. The final URL is something like this:
https://api.twitter.com/1.1/search/tweets.json?result_type=recent&count=1&q=%23hashtag1 OR %23hashtag2 OR %23hashtag3
I am developing an app using instagram api to bring feed to my website. I have following code but when i try to access the access_token using the code provided by Instagram it's giving me `400 Bad request error. I would be much obliged if someone could help me to overcome this problem. Many Thanks
string code="";
public ActionResult Index()
{
if (!String.IsNullOrEmpty(Request["code"]))
{
code = Request["code"].ToString();
GetDataInstagramToken();
}
return View();
}
public ActionResult Instagram()
{
var client_id = ConfigurationManager.AppSettings["instagram.clientid"].ToString();
var redirect_uri = ConfigurationManager.AppSettings["instagram.redirecturi"].ToString();
string url = "https://api.instagram.com/oauth/authorize/?client_id=" + client_id + "&redirect_uri=" + redirect_uri + "&response_type=code";
Response.Redirect(url);
return View();
}
public void GetDataInstagramToken()
{
var json = "";
var page = HttpContext.CurrentHandler as Page;
try
{
NameValueCollection parameters = new NameValueCollection();
parameters.Add("client_id", ConfigurationManager.AppSettings["instagram.clientid"].ToString());
parameters.Add("client_secret", ConfigurationManager.AppSettings["instagram.clientsecret"].ToString());
parameters.Add("grant_type", "authorization_code");
parameters.Add("redirect_uri", ConfigurationManager.AppSettings["instagram.redirecturi"].ToString());
parameters.Add("code", code);
WebClient client = new WebClient();
var result = client.UploadValues("https://api.instagram.com/oauth/access_token", "post", parameters);
var response = System.Text.Encoding.Default.GetString(result);
// deserializing nested JSON string to object
var jsResult = (JObject)JsonConvert.DeserializeObject(response);
string accessToken = (string)jsResult["access_token"];
int id = (int)jsResult["user"]["id"];
//This code register id and access token to get on client side
page.ClientScript.RegisterStartupScript(this.GetType(), "GetToken", "<script> var instagramaccessid=\"" + #"" + id + "" + "\"; var instagramaccesstoken=\"" + #"" + accessToken + "" + "\";</script>");
}
catch (Exception ex)
{
throw;
}
}
I am getting exception at
var result = client.UploadValues("https://api.instagram.com/oauth/access_token", "post", parameters);
In this line
client.UploadValues("https://api.instagram.com/oauth/access_token", "post", parameters);
You don't send any value to Instagram. If you check your parameter you can see your key but you cant see any value.
Try this:
public async void GetTokenFromCode()
{
var values = new Dictionary<string, string> {
{ "client_id","Your ChatId" },
{ "client_secret", "Your Client Secret" },
{ "grant_type", "authorization_code" },
{ "redirect_uri", "Your Redirect url"},
{ "code", "code" } };
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync("https://api.instagram.com/oauth/access_token", content);
var responseString = await response.Content.ReadAsStringAsync();
}
I am having an issue with OAuth and Facebook. I am using MVC4 standard OAuth login. I am not having the issue locally but on the server this is proving to be a problem.
If I paste the following URL into the browser it works OK:
http://localhost:46260/Account/ExternalLoginCallback?ReturnUrl=%2FDashboard&__provider__=FacebookPro&__sid__=1234somesid456 // this is autogenerated
When I change the URL for the app in facebook to the current domain and paste this url in, I get re-directed to the Unsuccessful login page:
http://freersvp.mytakeawaysite.com:80/Account/ExternalLoginCallback?ReturnUrl=%2FDashboard&__provider__=Facebook+Pro&__sid__=1234someid456 // note this is autogenerated
N.B The above two url's are the redirect uri
The below URL is what is requested and is causing the exception:
URL
https://graph.facebook.com/oauth/access_token?client_id=52*********37&redirect_uri=http%3a%2f%2ffreersvp.mytakeawaysite.com%3a80%2fAccount%2fExternalLoginCallback%3fReturnUrl%3d%252FDashboard%26__provider__%3dFacebook%2bPro%26__sid__%3d3c92eb7e84304afc931ef0ea7b62f56a&client_secret=2123***********4256&code=AQAQIJsj-ondldllVYKdpxJaZouqrlg9sjTcfUxyWhAw8MXbD2DvsOSujg2m7E3s3cvNusCI0ZZoJAuGgu_FLkPyjYMQAkTWDVyHTcAoJD-tezyXgn0vhoFzX3FmuRBHYpyJEM-dk0KgF5ugsTHo9yGjBjrcfMDUGu9IxkKQ36k3gMrwocM1_l5t342Q2kIOHdt8pPcyrs--NzgNyZv48vSq7jkZwuQ95xRjUHG5J-ptcgq0l2BlqjzHDDuvIFH23lpMWHzzqdejdj5ejukz7t_Fnhx-mrpVdcRYhP3JeZ2UOTjAyKQmUB3rInooECcjq4c
Exception
{
"error": {
"message": "Error validating verification code. Please make sure your redirect_uri is identical to the one you used in the OAuth dialog request",
"type": "OAuthException",
"code": 100
}
}
The string token does come back with null in the GetUserData function in the below code:
I am using the FacebookScopedClient:
public class FacebookScopedClient : IAuthenticationClient
{
private string appId;
private string appSecret;
private string scope;
private const string baseUrl = "https://www.facebook.com/dialog/oauth?client_id=";
public const string graphApiToken = "https://graph.facebook.com/oauth/access_token?";
public const string graphApiMe = "https://graph.facebook.com/me?";
private static string GetHTML(string URL)
{
string connectionString = URL;
try
{
System.Net.HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(connectionString);
myRequest.Credentials = CredentialCache.DefaultCredentials;
//// Get the response
WebResponse webResponse = myRequest.GetResponse();
Stream respStream = webResponse.GetResponseStream();
////
StreamReader ioStream = new StreamReader(respStream);
string pageContent = ioStream.ReadToEnd();
//// Close streams
ioStream.Close();
respStream.Close();
return pageContent;
}
catch(Exception ex)
{
}
return null;
}
private IDictionary<string, string> GetUserData(string accessCode, string redirectURI)
{
SessionControl ctl = new SessionControl();
ctl.SaveParam("redirecturi", redirectURI, -3);
ctl.Dispose();
string token = GetHTML(graphApiToken + "client_id=" + appId + "&redirect_uri=" + HttpUtility.UrlEncode(redirectURI) + "&client_secret=" + appSecret + "&code=" + accessCode);
if(token == null || token == "")
{
return null;
}
string access_token = token.Substring(token.IndexOf("access_token="), token.IndexOf("&"));
string data = GetHTML(graphApiMe + "fields=id,name,email,username,gender,link&" + access_token);
try
{
}
catch { }
// this dictionary must contains
Dictionary<string, string> userData = JsonConvert.DeserializeObject<Dictionary<string, string>>(data);
userData.Add("accesstoken", access_token);
try
{
userData.Add("id", userData["id"]);
}
catch { }
return userData;
}
public FacebookScopedClient(string appId, string appSecret, string scope)
{
this.appId = appId;
this.appSecret = appSecret;
this.scope = scope;
}
public string ProviderName
{
get { return "FacebookPro"; }
}
public void RequestAuthentication(System.Web.HttpContextBase context, Uri returnUrl)
{
string url = baseUrl + appId + "&redirect_uri=" + HttpUtility.UrlEncode(returnUrl.ToString()) + "&scope=" + scope;
context.Response.Redirect(url);
}
public AuthenticationResult VerifyAuthentication(System.Web.HttpContextBase context)
{
string code = context.Request.QueryString["code"];
string rawUrl = context.Request.Url.OriginalString;
//From this we need to remove code portion
rawUrl = Regex.Replace(rawUrl, "&code=[^&]*", "");
IDictionary<string, string> userData = GetUserData(code, rawUrl);
if(userData == null)
return new AuthenticationResult(false, ProviderName, null, null, null);
string id = userData["id"];
string username = userData["email"];
if(username == null || username == "")
{
username = userData["username"];
}
//userData.Remove("id");
userData.Remove("username");
AuthenticationResult result = new AuthenticationResult(true, ProviderName, id, username, userData);
return result;
}
}
after running your posted url that's causing the error through a url decoder the issue lies in for some reason your url encoding the entire query string and not just the url.
you will notice in that url a bunch of %26 items those are url encoded & and that's what is throwing your error. the Facebook parser is seeing %26 instead of & and treating it as one single parameter.
the & separates url query string parameters when sending to a page. Without the full code I can't tell you where to look but some where in your code your completely encoding the entire query string and need to find that piece of code and only encode the embedded urls.
ok after reading over things maybe try this theory.
I think your code is receiving this stuff from Facebook, url encoded, and then your system is re-encoding it. try taking anything received and first url decode it, manipulate it and then re-encode things as needed.
hope this helps
Try it with sandbox mode off within facebook app.
Noticing your URL's query string, I found an answer from Stackoverflow. Please see if it solves your issue:
https://stackoverflow.com/a/16699058/2005136
Steve S posted as a response:
"In our case, we were doing something unusual (so this might not be relevant to your case). Our redirect_uri was a URL with another URL embedded as an encoded path element. The URL-within-a-URL, doubly-encoded when passed to FB, had started causing problems with the Facebook API servers.
We resolved this by changing the encoding of the nested URL to a long hex number rather than % encoding, so all Facebook servers see is a simple redirect_uri containing some hex within the path, unaffected by normal URL encoding/decoding."