简体   繁体   中英

401 Unauthorized when calling Stormpath REST API with WebRequest object?

I am using Stormpath for my authentication service. I call the RestAPI of Stormpath by using HttpWebRequest.

I am also using HttpWebRequest to call the RestAPI but it does not work.

private void BtnGetResetApiClick(object sender, EventArgs e)
{

var username = "aaaa";
var password = "bbbb";
ServicePointManager.ServerCertificateValidationCallback = Callback;
var request = WebRequest.Create("https://api.stormpath.com/v1/tenants/current") as HttpWebRequest;

request.UserAgent = ".NET SDK";
request.Method = "GET";
request.Accept = "*/*";

var data = string.Format("{0}:{1}", username, HttpUtility.HtmlEncode(password));

var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(data));
string authHeader = string.Format("Basic {0}", token);

request.Headers.Add("Authorization", authHeader);
request.ServerCertificateValidationCallback = Callback;

using (var response = request.GetResponse())
{
    var stream = response.GetResponseStream();
    if (stream != null)
    {
        var streamReader = new StreamReader(stream);
        var str = streamReader.ReadToEnd();
        streamReader.Close();
        stream.Close();
    }
  }
}

private bool Callback(object obj, X509Certificate certificate, X509Chain  chain, SslPolicyErrors errors)
{
 return true;
}

When calling:

var response = request.GetResponse()

I got an exception:

An unhandled exception of type 'System.Net.WebException' occurred in System.dll The remote server returned an error: (401) Unauthorized.

Can you help me to see if my code has something wrong?

Update - use the SDK, it's much easier!

If you're calling the Stormpath API from C# frequently, don't bother with writing requests by hand. Use the Stormpath .NET SDK instead. I'm the author. :)

Install it using install-package Stormpath.SDK from the Package Manager Console. Then, create an IClient object:

// In a production environment, you'll want to load these from
// environment variables or a secure file, instead of hardcoding!
var apiKey = ClientApiKeys.Builder()
                 .SetId("Your_Stormpath_API_key_ID")
                 .SetSecret("Your_Stormpath_API_key_secret")
                 .Build();
var client = Clients.Builder()
                 .SetApiKey(apiKey)
                 .Build();

Getting the tenant info is now just a simple call:

var tenant = await client.GetCurrentTenantAsync();
Console.WriteLine($"Current tenant is: {tenant.Name}");

If you really want to make raw requests, you can still do that! I'll explain below.


Constructing the Authorization header

A 401 Unauthorized response means that the API was not able to find a valid Authorization header in your request. To authenticate correctly, you need two things:

  • An authorization payload in the format apiKeyID:apiKeySecret
  • An Authorization header with value: Basic base64(payload)

Here's how to construct the complete header:

// API_KEY_ID and API_KEY_SECRET populated elsewhere
var authPayload = string.Format("{0}:{1}", API_KEY_ID, API_KEY_SECRET);
var authPayloadEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(authPayload));
request.Headers.Add(HttpRequestHeader.Authorization, "Basic " + authPayloadEncoded);

You don't need the ServerCertificateValidationCallback = Callback stuff. With the above header, the request will be seen by the API as a valid request (assuming your API Key ID and Secret are correct, of course).


Redirection handling

One thing to watch out for (this tripped me up at first!) is that WebRequest will follow HTTP 302 redirects automatically, but will not apply the existing headers to the new request.

The solution is to disable redirect following:

request.AllowAutoRedirect = false;

This means you'll have to handle 302 responses yourself, but it's the only way to correctly apply the Authorization header to each request.


Working example

I created a simple working example in this gist . Since I'll be creating requests multiple times, I wrote a helper function:

private static HttpWebRequest BuildRequest(string method, string uri)
{
    var request = WebRequest.Create(uri) as HttpWebRequest;
    request.UserAgent = "dotnet/csharp web-request";
    request.Method = method;
    request.ContentType = "application/json";

    // Important, otherwise the WebRequest will try to auto-follow
    // 302 redirects without applying the authorization header to the
    // subsequent requests.
    request.AllowAutoRedirect = false;

    // Construct HTTP Basic authorization header
    var authPayload = string.Format("{0}:{1}", API_KEY_ID, API_KEY_SECRET);
    var authPayloadEncoded = Convert.ToBase64String(Encoding.UTF8.GetBytes(authPayload));
    request.Headers.Add(HttpRequestHeader.Authorization, "Basic " + authPayloadEncoded);

    return request;
}

And a simple console app to demonstrate getting the current tenant URL and name:

// Get these from the Stormpath admin console
private static string API_KEY_ID = "Your_Stormpath_API_key_ID";
private static string API_KEY_SECRET = "Your_Stormpath_API_key_secret";

static void Main(string[] args)
{
    // First, we need to get the current tenant's actual URL
    string tenantUrl = null;
    var getCurrentTenantRequest = BuildRequest("GET", "https://api.stormpath.com/v1/tenants/current");

    try
    {
        using (var response = getCurrentTenantRequest.GetResponse())
        {
            tenantUrl = response.Headers["Location"];
        }
    }
    catch (WebException wex)
    {
        Console.WriteLine("Request failed. {0}", wex.Message);
        throw;
    }

    // Now that we have the real tenant URL, get the tenant info
    string tenantData = null;
    var getTenantInfoRequest = BuildRequest("GET", tenantUrl);

    try
    {
        using (var response = getTenantInfoRequest.GetResponse())
        using (var responseStream = response.GetResponseStream())
        using (var reader = new StreamReader(responseStream))
        {
            tenantData = reader.ReadToEnd();
        }
    }
    catch (WebException wex)
    {
        Console.WriteLine("Request failed. {0}", wex.Message);
        throw;
    }

    // Use JSON.NET to parse the data and get the tenant name
    var parsedData = JsonConvert.DeserializeObject<Dictionary<string, object>>(tenantData);
    Console.WriteLine("Current tenant is: {0}", parsedData["name"]);

    // Wait for user input
    Console.ReadKey(false);
}

The code is pretty verbose because we're making raw requests to the API. Again, if you're making requests frequently, use the SDK instead!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM