Digest authentication - 2 requests needed?

I need to do a HTTPS POST request with digest authentication.

Found this on Github - DigestAuthorization.ino

So I incorporated it in my script and it works.
However, in the example script two HTTP requests are made which is one too many IMO - it makes it sluggish at times:

  // configure traged server and url
  http.begin(client, String(server) + String(uri)); // ONE HERE


  const char* keys[] = { "WWW-Authenticate" };
  http.collectHeaders(keys, 1);

  Serial.print("[HTTP] GET...\n");
  // start connection and send HTTP header
  int httpCode = http.GET();

  if (httpCode > 0) {
    String authReq = http.header("WWW-Authenticate");
    Serial.println(authReq);

    String authorization = getDigestAuth(authReq, String(username), String(password), "GET", String(uri), 1);

    http.end();
    http.begin(client, String(server) + String(uri)); // ONE HERE

    http.addHeader("Authorization", authorization);

    int httpCode = http.GET();
    if (httpCode > 0) {
      String payload = http.getString();
      Serial.println(payload);
    } else {
      Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    }
  } else {
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
  }

  http.end();

The first request returns 401 and the second one returns 200 which looks odd but apparently is to be expected, as per the wiki:

This typical transaction consists of the following steps:

  1. The client asks for a page that requires authentication but does not provide a username and password.[note 2] Typically this is because the user simply entered the address or followed a link to the page.
  2. The server responds with the 401 "Unauthorized" response code, providing the authentication realm and a randomly generated, single-use value called a nonce.
  3. At this point, the browser will present the authentication realm (typically a description of the computer or system being accessed) to the user and prompt for a username and password. The user may decide to cancel at this point.
  4. Once a username and password have been supplied, the client re-sends the same request but adds an authentication header that includes the response code.
  5. In this example, the server accepts the authentication and the page is returned. If the username is invalid and/or the password is incorrect, the server might return the "401" response code and the client would prompt the user again.

What concerns me is this:

"The client asks for a page that requires authentication but does not provide a username and password."

I do want to provide username and password right away. To me it sounds like if I did I would have to only make a single request instead of sending one without auth and right away a second with auth.

Do I really have to keep it split into two HTTP requests or is there a way to authenticate with a single request? Or is there a way to authenticate once (eg in setup()) and then only send a single request until the board is reset? (this seems to be the way... at least as per ChatGPT - more below)

The concern here is speed.

Asked ChatGPT, here's its response:

If you want to minimize the number of HTTP requests for better performance, you can consider caching the authentication information once it's obtained. For example, you can perform the authentication process in the setup() function and store the authentication information for subsequent use.

Here's a modified version of your code to demonstrate this:

//deleted code provided by ChatGPT, refer to my code below

In this example, the authentication header is obtained in the setup() function and stored in the authHeader variable. Subsequent HTTP requests in the loop() function then use this stored authentication header, reducing the need for the initial 401 Unauthorized request.


Will test in the following hours and report back.

If ChatGPT's answer is bullshit, please, do let me know :wink: .

yes. for the first request you get a 401. then for all request in a session use then the same nonce and response value until you get a 401 with a new value

1 Like

Absolutely understand.
I can read the code, I do write code for a living, but whether a block code works or not is another matter completely.
I had to pop off to spend some time with the youngling, so I put the comment there just in case someone knew for a fact that digest cannot be done in a single request ever so that I might potentially save myself some testing/debugging time when I come back.

Yup, so my hunch was correct, which @Juraj confirmed and ChatGPT as well.
I disregarded the GPT provided code (deleted it), stitched sth together quick. Now it works instantly!

Here's the code for anyone looking for HTTPS with digest authentication.

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <ESP8266HTTPClient.h>

const char* ssid = "your_ssid";
const char* ssidPassword = "your_ssid_pass";

const char* username = "server_username";
const char* password = "server_pass";

const char* server = "https://192.168.1.127:%PORT%";
const char* uri = "the_rest_of_the_url";

const int button = D4; // push button digital
int button_state = 0;  // temporary variable for reading the button pin status
bool api_call_success = false;

String global_auth_header = "";
WiFiClientSecure *client = new WiFiClientSecure;
HTTPClient https;  // must be declared after WiFiClient for correct destruction order, because used by http.begin(client,...)

String exractParam(String& authReq, const String& param, const char delimit) {
  int _begin = authReq.indexOf(param);
  if (_begin == -1) { return ""; }
  return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length()));
}

String getCNonce(const int len) {
  static const char alphanum[] = "0123456789"
                                 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                                 "abcdefghijklmnopqrstuvwxyz";
  String s = "";

  for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; }

  return s;
}

String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) {
  // extracting required parameters for RFC 2069 simpler Digest
  String realm = exractParam(authReq, "realm=\"", '"');
  String nonce = exractParam(authReq, "nonce=\"", '"');
  String cNonce = getCNonce(8);

  char nc[9];
  snprintf(nc, sizeof(nc), "%08x", counter);

  // parameters for the RFC 2617 newer Digest
  MD5Builder md5;
  md5.begin();
  md5.add(username + ":" + realm + ":" + password);  // md5 of the user:realm:user
  md5.calculate();
  String h1 = md5.toString();

  md5.begin();
  md5.add(method + ":" + uri);
  md5.calculate();
  String h2 = md5.toString();

  md5.begin();
  md5.add(h1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + h2);
  md5.calculate();
  String response = md5.toString();

  String authorization = "Digest username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + uri + "\", algorithm=\"MD5\", qop=auth, nc=" + String(nc) + ", cnonce=\"" + cNonce + "\", response=\"" + response + "\"";
  Serial.println(authorization);

  return authorization;
}

void connect_to_wifi() {
  digitalWrite(LED_BUILTIN, LOW); // turn on built-in LED to indicate connecting to wifi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, ssidPassword);
  Serial.print("Coneccting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.print("\n");
  Serial.println("Connected");
  digitalWrite(LED_BUILTIN, HIGH); // turn off built-in LED to indicate we are connected to wifi
}

String make_initial_request() {
  Serial.print("[HTTP] begin...\n");

  // configure traged server and url
  https.begin(*client, String(server) + String(uri));
  https.addHeader("Content-Type", "application/json");

  const char* keys[] = { "WWW-Authenticate" };
  https.collectHeaders(keys, 1);

  Serial.print("[HTTP] POST...\n");
  // start connection and send HTTP header
  int httpCode = https.POST("{\"key\":\"value\"}");
  Serial.println("Initial http status code: " + String(httpCode));

  String authReq = https.header("WWW-Authenticate");
  String authorization = getDigestAuth(authReq, String(username), String(password), "POST", String(uri), 1);

  https.end();

  return authorization;
}

void setup() {
  randomSeed(RANDOM_REG32);
  Serial.begin(115200);
  
  pinMode(button, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);

  client->setInsecure(); // this needs to be straightened out

  connect_to_wifi();

  global_auth_header = make_initial_request();
}

void loop() {
  button_state = digitalRead(button);
  // high = not pushed
  if (button_state == LOW) {
    api_call_success = switch_video(global_auth_header);
    if (api_call_success != true) {
      Serial.println("API fail");
    }
    delay(1000);
  }
}

bool switch_video(String authorization) {
  digitalWrite(LED_BUILTIN, LOW);
  Serial.println("Button pushed - sending request to TV");
  Serial.print("[HTTP] begin...\n");

  // configure traged server and url
  https.begin(*client, String(server) + String(uri));

  https.addHeader("Authorization", authorization);
  https.addHeader("Content-Type", "application/json");

  int httpCode = https.POST("{\"key\":\"value\"}");
  Serial.println("Secondary http status code: " + String(httpCode));
  if (httpCode > 0) {
    if (httpCode == 200) {
      Serial.println("API call success");
      digitalWrite(LED_BUILTIN, HIGH);
      https.end();
      return true;
    } else {
      Serial.printf("[HTTP] POST... failed, error: %s\n", https.errorToString(httpCode).c_str());
    }
  } else {
    Serial.printf("[HTTP] POST... failed, error: %s\n", https.errorToString(httpCode).c_str());
  }
  digitalWrite(LED_BUILTIN, HIGH);
  https.end();
  return false;
}

Little bit of a background on this code. My young one and me watch youtube videos in the evening. Remote would still be a little bit of a challenge (pointing towards the tv, not pressing random buttons, navigating to the appropriate button in the yt gui etc) so I'm building this little single button remote which when pressed plays next video in the queue (using SmartTube).

Note that I don't program in C and this was stitched together real quick :wink:

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.