modugnico
modugnico

Reputation: 171

HTTP Request works in Postman, but not in C# code

I want to do a simple HTTP request in C#, but something is not working and all I got is 403 Forbidden status code.

When I try to do same request in Postman, everything works fine. I tried to run Fiddler and see all headers that are being sent by Postman. I copy-pasted all of them, but i still got 403 Forbidden in the request sent by C# code.

C# Code (Using https://flurl.dev):

public static void Main(string[] args)
{
    FlurlHttp.Configure(settings => {
        settings.HttpClientFactory = new MyClientFactory();
    });

    var url = "https://example.com"
        .AppendPathSegments(new[] { "v1", "oauth", "accesstoken" })
        .SetQueryParam("grant_type", "client_credentials")
        .AllowAnyHttpStatus()
        .WithBasicAuth("username", "password")
        .WithHeaders(new {
            User_Agent = "Something/0.4.0 Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-G975F Build/NRD90M)",
            X_Secret_Header = "secret_encoded_value",
            accept_encoding = "gzip, deflate",
            Accept = "*/*"
        });

    HttpResponseMessage msg = url.GetAsync().Result;

    Console.WriteLine("StatusCodeString: " + msg.StatusCode.ToString());
    Console.WriteLine();
    Console.WriteLine(msg.Content.ReadAsStringAsync().Result);
}

class MyClientFactory : DefaultHttpClientFactory
{
    public override HttpMessageHandler CreateMessageHandler()
    {
        return new HttpClientHandler
        {
            AllowAutoRedirect = false
        };
    }
}

C# Request And Response:

CSharp Request in Fiddler CSharp Response

Postman Request And Response:

Postman Headers Postman Response Postman Response in Fiddler

Can someone explain me why is this not working? Same headers, same everything.

I replaced the url with "example.com" because i don't want to show the real API URL here.

Also sorry for so many images.. I don't know how to show the problem here in other way.

Upvotes: 17

Views: 37484

Answers (10)

Kailash Mali
Kailash Mali

Reputation: 87

Resolving the 401 Unauthorized Error for Oracle NetSuite REST API Calls with Query Parameters

When working with the Oracle NetSuite REST API, many developers encounter a frustrating issue: API requests with simple parameters work seamlessly, but filtering via query parameters often results in a 401 Unauthorized error. This blog post walks you through the problem, potential pitfalls, and the ultimate solution that resolved this issue for me.


The Problem

Here’s a summary of the behavior I observed during API integration:

Working Requests:

// These worked perfectly 
string query = "customer/10127";  
string query = "customer";  

Failing Requests:

// These consistently returned 401 Unauthorized
string query = "nonInventorySaleItem?q=itemId CONTAIN ABC123";  
string query = "nonInventoryResaleItem?q=itemId IS 32305";  
string query = "customer?q=entityId is AAMCI";  
string query = "customer?q=companyName IS AAMCI";  

These failures occurred even though all requests worked in Postman. The same query, when executed in my .NET code, would throw a 401 Unauthorized error.


The Culprit

The issue lies in how the OAuth 1.0 signature is generated and the Autorization headers passed in the request.. The query parameters need to include in the Authorization header during signature generation and need not to add in Autorization header, the API rejects the request. However, the header should ignore query parameters for header construction.


Complete C# Solution

Below is the complete implementation, including the HttpClient call and OAuth signature generation:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

public class AuthProvider
{
    private readonly string _realm = "XXXXXXX_SB1";
    private const string ConsumerKey = "YourConsumerKey";
    private const string ConsumerSecret = "YourConsumerSecret";
    private const string Token = "YourToken";
    private const string TokenSecret = "YourTokenSecret";

    public async Task<HttpResponseMessage> GetAsync(string paramterValue)
{
    using var httpClient = new HttpClient();

    //Scnerio 1:Fetch special paramters only
    string baseUrl = "https://XXXXXXX-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/Customer/747";
    string fields = "companyname,firstname,lastname,email";
    string encodedFields = Uri.EscapeDataString(fields); // Encodes "companyname,firstname,lastname,email" to "companyname%2Cfirstname%2Clastname%2Cemail"
    string fullUrl = $"{baseUrl}?fields={encodedFields}";

    //Scneior2: Filter the results with specfic fieldname only
    //string baseUrl = "https://XXXXXXX-sb1.suitetalk.api.netsuite.com/services/rest/record/v1/Customer";
    //string fields = "CompanyName IS \"Test\"";
    //string encodedFields = Uri.EscapeDataString(fields); // Encodes "companyname IS "Test""
    //string fullUrl = $"{baseUrl}?x={encodedFields}";
        // Attach OAuth authentication
        AttachAuthentication(httpClient, HttpMethod.Get, baseUrl);

        // Execute HTTP GET request
        return await httpClient.GetAsync(fullUrl);
    }

    private void AttachAuthentication(HttpClient httpClient, HttpMethod httpMethod, string baseUrl)
    {
        // Generate OAuth 1.0 Authentication header
        var oauthHeader = GenerateOAuthHeader(httpMethod.Method, baseUrl, _realm);

        // Set request headers
        httpClient.DefaultRequestHeaders.Clear();
        httpClient.DefaultRequestHeaders.Add("Authorization", oauthHeader);
        httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

    private static string GenerateOAuthHeader(string httpMethod, string url, string realm = "XXXXXXX_SB1")
{
    var oauthParameters = new SortedDictionary<string, string>
    {
        { "oauth_signature_method", "HMAC-SHA256" },
        { "oauth_consumer_key", ConsumerKey },
        { "oauth_token", Token },
        { "oauth_timestamp", ((int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds).ToString() },
        { "oauth_nonce", Guid.NewGuid().ToString("N") },
        { "oauth_version", "1.0" }
    };

    // Extract query parameters from URL
    var uri = new Uri(url);
    var queryParameters = uri.Query.TrimStart('?').Split('&')
                          .Where(q => !string.IsNullOrWhiteSpace(q))
                          .Select(q => q.Split('='))
                          .ToDictionary(kvp => Uri.UnescapeDataString(kvp[0]), kvp => Uri.UnescapeDataString(kvp[1]));

    foreach (var queryParam in queryParameters)
    {
        oauthParameters.Add(queryParam.Key, queryParam.Value);
    }

    // Rebuild URL without query string
    var normalizedUrl = $"{uri.Scheme}://{uri.Host}{uri.AbsolutePath}";

    // Create the base string
    var baseString = $"{httpMethod}&{Uri.EscapeDataString(normalizedUrl)}&{Uri.EscapeDataString(string.Join("&", oauthParameters.Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}")))}";

    // Generate the signature
    var signingKey = $"{Uri.EscapeDataString(ConsumerSecret)}&{Uri.EscapeDataString(TokenSecret)}";
    using var hasher = new HMACSHA256(Encoding.ASCII.GetBytes(signingKey));
    var signature = Convert.ToBase64String(hasher.ComputeHash(Encoding.ASCII.GetBytes(baseString)));

    oauthParameters.Add("oauth_signature", signature);

    // Build the OAuth Authorization header, excluding query parameters
    var header = $"OAuth realm=\"{realm}\", " + string.Join(", ", oauthParameters
        .Where(kvp => !queryParameters.ContainsKey(kvp.Key)) // Exclude query parameters
        .Select(kvp => $"{kvp.Key}=\"{Uri.EscapeDataString(kvp.Value)}\""));
    return header;
}

Key Takeaways:

  1. Base URL Signature Generation: Always use the required parameter in the autorization headers
  2. Escape Query Parameters: Ensure query parameters are URL-encoded to avoid malformed requests.
  3. Postman vs. Code: Postman automatically handles some signature quirks; ensure your code matches its behavior.

By fixing the signature generation process and excluding query parameters, I successfully resolved the 401 Unauthorized error. I hope this helps anyone facing a similar issue with the Oracle NetSuite REST API or OAuth 1.0 authentication!

Feel free to share your experiences or ask questions in the comments! 😊

Upvotes: 0

FredMartinez
FredMartinez

Reputation: 31

In my case it was that Postman used HTTP/2 to send the requests while my code used HTTP/1.1 that was unsupported by the server.

Upvotes: 1

elfico
elfico

Reputation: 550

I had similar issues and all answers here didn't work. I was getting a 403 on in my C# client but it was working well in Postman. After a few hours, and following @JsAndDotNet advice to use fiddler, I found out the server was bouncing all requests without User-Agent.

One way to be sure is to check the response. In my case, the response was supposed to be in json, but I was getting a text/html which indicates that the server was seeing my request as emanating from a browser and hence blocking it.

Specifying my own User-Agent (any random string really) fixed the problem

Upvotes: 6

JJJCoder
JJJCoder

Reputation: 16936

Just to help anyone searching - there can be multiple reasons for such an error.

Steps to help debug such a problem.

  1. Download Fiddler (or an equivalent like Wireshark) to record the network traffic.
  2. Make the working request through postman (recording the traffic)
  3. Make the failing request through C# (recording the traffic)
  4. Look at what's different between the two requests.

In my case, I was getting quotation marks in my authentication. It was a subtle difference that I didn't realise until I compared the two

Working Call (200 OK)

working call viewed with fiddler

Failing Call (401 unauthorized)

failing call viewed with fiddler

Upvotes: 7

Van Nostril Boy
Van Nostril Boy

Reputation: 33

in case this helps anyone. I had the same problem with an api I was calling and was at a bit of a loss because everything including the authorization header seemed exactly the same between postman and visual studio. But when I really looked carefully at fiddler i noticed that when using postman, the api that was being called was https://api.someplace.com/properties/?pagesize=20 and in visual studio i had https://api.someplace.com/properties?pagesize=20

Note the lack of the slash between "properties" and the querystring. "...properties/?..." worked fine and "...properties?..." resulted in a 403.

I think (obviously correct me if I'm wrong here) that what actually happened was that in the first http call, where the slash was missing, the server made a HTTP 301 redirect to the api with the slash. When the redirect happened the authorisation header was not carried along for the ride and this resulted in a 403 authorization error.

Now why this redirect was necessary I dont know, any apis I build allow for both scenarios and no redirect should be necessary. But in the end, I simply changed the url to be "../properties/?etc..." and the problem went away.

Upvotes: 1

Ocean Airdrop
Ocean Airdrop

Reputation: 2921

For me the problem was the TLS settings in C#. Try adding this line at the start of your app or just before your HTTP request code:

ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

Upvotes: 9

Mostafa Hassan
Mostafa Hassan

Reputation: 298

I know this old, but to get the same C# code as postman sent, let the postman generate the code for, but first you must get RestSharp lib from nuget or from PM console type this:

Install-Package RestRequest -Version 1.2.0

Steps:

1- Call your rest api from postman

2- Press the code button enter image description here 3-A pop-up window will open, then choose whatever language you want, in your case it's C# enter image description here

Upvotes: 3

Brian Olson
Brian Olson

Reputation: 271

From Postman there should be a link on the right side called code. Click that and then select C# to get the code generated by Postman. Paste that in and try it out.

Upvotes: 18

ErBu
ErBu

Reputation: 383

My suggestion would be retrieving raw request strings from postman and C# application and using something like https://text-compare.com/ to look for differences. The guess is there's some extremely minor difference like extra slash that is very difficult to notice with plain eye.

Upvotes: 2

Duck Ling
Duck Ling

Reputation: 2130

The next step would be to compare the Raw requests and responses, from your C# code and Postman, place them side by side and compare the differences - I assure you there would be at least one. :-)

403 is an authorization problem so the token would be the first suspect, since the bad structure of your request is more likely to throw a 400 "Bad request" error.

In this particular case though, I've run your code in VS2019 on my machine using Flurl and it seems to be working fine. It returns an example HTML page:

<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style type="text/css">
    body {
        background-color: #f0f0f2;
        margin: 0;
        padding: 0;
        font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

    }
    div {
        width: 600px;
        margin: 5em auto;
        padding: 50px;
        background-color: #fff;
        border-radius: 1em;
    }
    a:link, a:visited {
        color: #38488f;
        text-decoration: none;
    }
    @media (max-width: 700px) {
        body {
            background-color: #fff;
        }
        div {
            width: auto;
            margin: 0 auto;
            border-radius: 0;
            padding: 1em;
        }
    }
    </style>
</head>

<body>
<div>
    <h1>Example Domain</h1>
    <p>This domain is established to be used for illustrative examples in documents. You may use this
    domain in examples without prior coordination or asking for permission.</p>
    <p><a href="http://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

Upvotes: 1

Related Questions