Xamarin Essentials Unable to exchange Okta authorization code for token - single-sign-on

I was using OpenID and we have to switch to Xamarin.Essentials.WebAuthenticator.
I can get an authorization code from Okta using WebAuthenticator.AuthenticateAsync().
But, everything I try to then translate that code into an access token returns 400 Bad Request.
Okta's API error is "E0000021: HTTP media type not supported exception" and it goes on to say, "Bad request. Accept and/or Content-Type headers likely do not match supported values."
I have tried to follow https://developer.okta.com/blog/2020/07/31/xamarin-essentials-webauthenticator as much as possible, but we are not using the hybrid grant type like he is.
We are using only Authorization Code, which means I have to make a secondary call, and I have spent two days trying to figure out how.
private async Task LoginOktaAsync()
{
try
{
var loginUrl = new Uri(BuildAuthenticationUrl()); // that method is down below
var callbackUrl = new Uri("com.oktapreview.dev-999999:/callback"); // it's not really 999999
var authenticationResult = await Xamarin.Essentials.WebAuthenticator.AuthenticateAsync(loginUrl, callbackUrl);
string authCode;
authenticationResult.Properties.TryGetValue("code",out authCode);
// Everything works fine up to this point. I get the authorization code.
var url = $"https://dev-999999.oktapreview.com/oauth2/default/v1/token"
+"?grant_type=authorization_code"
+$"&code={authCode}&client_id={OktaConfiguration.ClientId}&code_verifier={codeVerifier}";
var request = new HttpRequestMessage(HttpMethod.Post, url);
var client = new HttpClient();
var response = await client.SendAsync(request); // this generates the 400 error.
}
catch(Exception e)
{
Debug.WriteLine($"Error: {e.Message}");
}
}
Here are the methods that produce the login url and a couple of other things:
public string BuildAuthenticationUrl()
{
var state = CreateCryptoGuid();
var nonce = CreateCryptoGuid();
CreateCodeChallenge();
var url = $"https://dev-999999.oktapreview.com/oauth2/default/v1/authorize?response_type=code"
+ "&response_mode=fragment"
+ "&scope=openid%20profile%20email"
+ "&redirect_uri=com.oktapreview.dev-999999:/callback"
+$"&client_id={OktaConfiguration.ClientId}"
+$"&state={state}"
+$"&code_challenge={codeChallenge}"
+ "&code_challenge_method=S256"
+$"&nonce={nonce}";
return url;
}
private string CreateCryptoGuid()
{
using (var generator = RandomNumberGenerator.Create())
{
var bytes = new byte[16];
generator.GetBytes(bytes);
return new Guid(bytes).ToString("N");
}
}
private string CreateCodeChallenge()
{
codeChallenge = GenerateCodeToVerify();
codeVerifier = codeChallenge;
using (var sha256 = SHA256.Create())
{
var codeChallengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeChallenge));
return Convert.ToBase64String(codeChallengeBytes);
}
}
private string GenerateCodeToVerify()
{
var str = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
Random rnd = new Random();
for (var i = 0; i < 100; i++)
{
str += possible.Substring(rnd.Next(0,possible.Length-1),1);
}
return str;
}
'''

After much online research, I discovered the issue was with how I was doing my post to get the token. This is how I made it work:
public static Dictionary<string, string> JsonDecode(string encodedString)
{
var inputs = new Dictionary<string, string>();
var json = JValue.Parse(encodedString) as JObject;
foreach (KeyValuePair<string, JToken> kv in json)
{
if (kv.Value is JValue v)
{
if (v.Type != JTokenType.String)
inputs[kv.Key] = v.ToString();
else
inputs[kv.Key] = (string)v;
}
}
return inputs;
}
private async Task<string> ExchangeAuthCodeForToken(string authCode)
{
string accessToken = string.Empty;
List<KeyValuePair<string, string>> kvdata = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("code", authCode),
new KeyValuePair<string, string>("redirect_uri", OktaConfiguration.Callback),
new KeyValuePair<string, string>("client_id", OktaConfiguration.ClientId),
new KeyValuePair<string, string>("code_verifier", codeVerifier)
};
var content = new FormUrlEncodedContent(kvdata);
var request = new HttpRequestMessage(HttpMethod.Post, OktaConfiguration.TokenUrl)
{Content = content, Method = HttpMethod.Post};
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.SendAsync(request);
string text = await response.Content.ReadAsStringAsync();
Dictionary<string, string> data = JsonDecode(text);
data.TryGetValue("access_token", out accessToken);
return accessToken;
}

Related

How to call PUT on Moodle with PutAsync?

I am trying to modernize some calls to Moodle REST APIs.
Everything is doing OK as long as I use querystring, but I can't pinpoint why using a PutAsync doesn't work in this case (tested with Postman, it works):
using (var httpClient = _httpClientFactory.CreateClient())
{
var url = "http://127.0.0.1/moodle/webservice/rest/server.php";
var urlFunction = UrlFunction.core_user_create_users.ToString();
var urlFormat = UrlFormat.json.ToString();
var requestModel = new GetCoursesRequestModel()
{
wstoken = [token],
wsfunction = urlFunction,
moodlewsrestformat = urlFormat
};
var jsonRequest = JsonConvert.SerializeObject(requestModel);
var stringContentRequest = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
var response = await httpClient.PutAsync(url, stringContentRequest);
response.EnsureSuccessStatusCode();
var courseDtos = await response.Content.ReadFromJsonAsync<List<MoodleAPIDtosCourseDto>>();
if (courseDtos is null)
throw new InputFormatterException(
$"Pb with response format.");
}
´´´´
The error is generated when I try to read my response at await response.Content.ReadFromJsonAsync()

Download mp3 file at browser through API .net core

Was trying to download an mp3 file in a browser through the API that I created. But instead of receiving an mp3 file. I keep getting JSON format response. I had referred from answer in return-file-in-asp-net-core-web-api, but still, I can't download the mp3 file.
Is there any mistake that I've overlooked, please kindly help?
This is my downloading method from UI
void DownloadRecording(RecordingHistory voicehistory)
{
try
{
using (var client = new WebClient())
{
client.DownloadFile("https://2d489fd863a2.ngrok.io/api/download/" + voicehistory.RecordingId + ".mp3", voicehistory.RecordingId + ".mp3");
}
}
catch { }
}
This is my api function for downloading mp3 from server
[HttpGet("download/{recordingFile}")]
public async Task<IActionResult> DownloadVoiceRecording(string recordingFile)
{
string filePath = Directory.GetCurrentDirectory() + #"\audio\Processed\" + recordingFile;
var memory = new MemoryStream();
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
var types = GetMimeTypes();
var ext = Path.GetExtension(filePath).ToLowerInvariant();
return File(filePath, types[ext], recordingFile);
}
private Dictionary<string, string> GetMimeTypes()
{
return new Dictionary<string, string>
{
{".mp3", "audio/mpeg"},
{".wav","audio/wav" }
};
}
This is the response I get from browser and Postman
{
"Version": "2.0.0.0",
"StatusCode": 200,
"Message": "Status 200 OK",
"Result":"��#� ... ... /// A lot of random symbol here
}
Because the first parameter of the return value File is a type of Stream, memory needs to be passed in.
[HttpGet("download/{recordingFile}")]
public async Task<IActionResult> DownloadVoiceRecording(string recordingFile)
{
string filePath = Directory.GetCurrentDirectory() + #"\audio\Processed\" + recordingFile;
var memory = new MemoryStream();
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
var types = GetMimeTypes();
var ext = Path.GetExtension(filePath).ToLowerInvariant();
return File(memory, types[ext], recordingFile);
}
I'm using Blazor for this. It turns out that there was an API response wrapper in Blazor APIReponse middleware. I had to put my API into an exception so it won't turn into JSON when I access it. It works finally.
Below is the APIReponse wrapper in Blazor.
var formattedRequest = await FormatRequest(request);
var originalBodyStream = httpContext.Response.Body;
using (var responseBody = new MemoryStream())
{
try
{
string responseBodyContent = null;
var response = httpContext.Response;
if (new string[] { "/api/localization", "/api/data", "/api/externalauth", "/api/download" }.Any(e => request.Path.StartsWithSegments(new PathString(e.ToLower()))))
await _next.Invoke(httpContext);
else
{
response.Body = responseBody;
await _next.Invoke(httpContext);
//wrap response in ApiResponse
if (httpContext.Response.StatusCode == Status200OK)
{
responseBodyContent = await FormatResponse(response);
await HandleSuccessRequestAsync(httpContext, responseBodyContent, Status200OK);
}
else
await HandleNotSuccessRequestAsync(httpContext, httpContext.Response.StatusCode);
}

How to post JSON data to Pardot API via Httpclient

I am trying to post the JSON data to Pardot. I have used the info from here to call the Pardot API and currently using Pardot form handler to post the data. I want to know if i could the data via Pardot API call by using CREATE or UPSERT instead of using a form handler.
Below is my code
class SendingDataToPardot
{
public string Login()
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
var url = "https://pi.pardot.com/api/login/version/3";
string apiKey = null;
var loginInfo = new Dictionary<string, string>
{
{"email", "xx"},
{"password", "xxx"},
{"user_key", "xxx"}
};
var httpContent = new FormUrlEncodedContent(loginInfo);
using (var client = new HttpClient())
{
HttpResponseMessage response = client.PostAsync(url, httpContent).Result;
if (response.IsSuccessStatusCode)
{
string resultValue = response.Content.ReadAsStringAsync().Result;
apiKey = XDocument.Parse(resultValue).Element("rsp").Element("api_key").Value;
return apiKey;
}
else
{
return null;
}
}
}
public string POST()
{
string Api_Key = Login();
var url = "form handler url";
var contactFormData = new Dictionary<string, string>
{
{"email", "test#test.com"},
{"FirstName", "xxx"},
{"LastName", "xxxxx"},
{"Comments", "this is a test"}
};
var data= new FormUrlEncodedContent(contactFormData);
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Api_Key);
HttpResponseMessage response = client.PostAsync(url, data).Result;
string result = response.Content.ReadAsStringAsync().Result;
return result;
}
}
}
}
For most of the APIs Pardot exposes, you need to do XML work with it.
Looks like you are using Java, so you might have luck using a public library, even if just for understanding communication patterns (we had to rewrite it for our purposes, but it did serve as a great blueprint).
Have a look at the https://github.com/Crim/pardot-java-client project and see if it helps you out.

Unable to get token from tableau server

I am following below link to integrate tableau report on web application(asp .net mvc),
https://onlinehelp.tableau.com/current/server/en-us/trusted_auth.htm
https://onlinehelp.tableau.com/current/server/en-us/trusted_auth_webrequ.htm
Code :
var uri = "http://<server ip>:8000/trusted";
Dictionary<string, string> parameters = new Dictionary<string, string>();
parameters.Add("username ", "<user>");
parameters.Add("target_site", "<site>");
var bodyParameters = new ArrayList();
foreach (var parameter in parameters)
{
bodyParameters.Add(string.Format("{0}={1}", HttpUtility.UrlEncode(parameter.Key), HttpUtility.UrlEncode(Convert.ToString(parameter.Value))));
}
string requestBody = String.Join("&", bodyParameters.ToArray());
var request = WebRequest.CreateHttp(uri);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
try
{
using (var writer = new StreamWriter(request.GetRequestStream()))
{
writer.Write(requestBody);
}
var response = (HttpWebResponse)request.GetResponse();
using (var reader = new StreamReader(response.GetResponseStream()))
{
String body = reader.ReadToEnd();
}
}
catch (Exception ex)
{
string str = ex.Message.ToString();
}
My machine is registered as trusted host on tableau server, still i am getting -1 when i request for token.

Get id of last Rest API POST using Entity Framework

I need to be able to access the id of a new Post. I will be using this id to populate another field called LocationId like this: "L" + id = LocationId (example L22) where 22 is the id of the new Post. Here is the code for my Post request:
private async void BtnSubmit_Clicked(object sender, EventArgs e)
{
var imageArray = FilesHelper.ReadFully(file.GetStream());
file.Dispose();
var location = new Models.Location()
{
LocationName = EntName.Text,
ImageArray = imageArray,
};
ApiServices apiServices = new ApiServices();
bool response = await apiServices.PostLocation(location);
bool response2 = await apiServices.InputLocationId(id, location);
if (!response || !response2)
{
await DisplayAlert("Alert", "Something wrong", "Cancel");
}
else
{
await DisplayAlert("Hi", "Your record has beed added successfully", "Alright");
}
await Navigation.PushAsync(new SetupPage());
This is on the client side. I have all the APIs created (such as PostLocation and InputLocationId)on Azure SQL Server. This is for a mobile inventory app built using Xamarin.
public async Task<bool> PostLocation(Location location)
{
var json = JsonConvert.SerializeObject(location);
var httpClient = new HttpClient();
var content = new StringContent(json, Encoding.UTF8, "application/json");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", Settings.AccessToken);
var wimsApiUrl = "http://xxxxxxx.azurewebsites.net/api/Locations";
//Get the Body of the Post
var body = await httpClient.PostAsync(wimsApiUrl, content);
//Convert it to a string
var jString = await body.Content.ReadAsStringAsync();
//Place it in a JSON Object
JObject joResponse = JObject.Parse(jString);
//Parse the JSON Object into an Int from a String
var id = int.Parse(joResponse["Id"].ToString());
//This is used in my other script to Put the LocationId of Lxx
AddNewLocationPage.NewLocationId = id;
return body.IsSuccessStatusCode;
}
My Post Location API:
// POST: api/Locations
[ResponseType(typeof(Location))]
public IHttpActionResult PostLocation([FromBody] Location location)
{
string userId = User.Identity.GetUserId();
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var stream = new MemoryStream(location.ImageArray);
var guid = Guid.NewGuid().ToString();
var file = String.Format("{0}.jpg", guid);
var folder = "~/Content/Images";
var fullPath = String.Format("{0}/{1}", folder, file);
var response = FilesHelper.UploadPhoto(stream, folder, file);
if (response)
{
location.ImagePath = fullPath;
}
var newLocation = new Location()
{
LocationName = location.LocationName,
User = userId,
ImagePath = location.ImagePath
};
db.Locations.Add(newLocation);
db.SaveChanges();
return Ok(new { newLocation.Id});
}
I will then take the id and put it in this Put Request to create the LocationId:
public async Task<bool> InputLocationId(int id, Location location)
{
var json = JsonConvert.SerializeObject(location);
var httpClient = new HttpClient();
var content = new StringContent(json, Encoding.UTF8, "application/json");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", Settings.AccessToken);
var wimsApiUrl = "http://xxxxxxx.azurewebsites.net/api/Locations/InputLocationId/";
var completeUrl = String.Format("{0}{1}", wimsApiUrl, id);
var response = await httpClient.PutAsync(completeUrl, content);
return response.IsSuccessStatusCode;
}
The InputLocationId API will automatically create the LocationId. Here is my API:
// PUT: api/Locations/5
[HttpPut]
[ResponseType(typeof(void))]
[Route("api/Locations/InputLocationId/{id}")]
public IHttpActionResult InputLocationId(int id, [FromBody] Location location)
{
//string userId = User.Identity.GetUserId();
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var result = db.Locations.FirstOrDefault(locationId => locationId.Id == id);
var resultant = String.Format("L{0}", id);
location.LocationName = location.LocationName;
result.LocationId = resultant;
db.SaveChanges();
return Ok("The record has been updated");
}
I am simply stuck on how to access that id!
// get the response body
var body = await httpClient.PostAsync(wimsApiUrl, content);
// load it into a JSON object using Newtonsoft
JObject data = JObject.Parse(body);
// get the id
var id = int.Parse(data["id"]);
The returns need to be converted into a string from the HttpResponseMessage.
var body = await httpClient.PostAsync(wimsApiUrl, content);
var jString = await body.Content.ReadAsStringAsync();
Then we can place it into a JSON Object:
JObject joResponse = JObject.Parse(jString);
Now this JSON Object can be parsed into an Int. Note it needs to be converted to a string.
var id = int.Parse(joResponse["Id"].ToString());