0

After a lot of browsing and reading docs, I came across this answer, which worked until I upgraded flutter.

My current issue is that the credentials do not reach my server. IntelliJ error:

I/FlutterActivityDelegate( 6944): onResume setting current activity to this
D/EGL_emulation( 6944): eglMakeCurrent: 0xa7f852a0: ver 2 0 (tinfo 0xa7f83250)
I/flutter ( 6944): doing login with credentials:: W and T
I/flutter ( 6944): my_response is:: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
I/flutter ( 6944): <title>429 Too Many Requests</title>
I/flutter ( 6944): <h1>Too Many Requests</h1>
I/flutter ( 6944): <p>1 per 1 minute</p>
E/flutter ( 6944): [ERROR:topaz/lib/tonic/logging/dart_error.cc(16)] Unhandled exception:
E/flutter ( 6944): type '(Exception) => void' is not a subtype of type '(Object) => FutureOr<dynamic>'

My server output for the request is:

 credentials::  - 
 192.168.1.175 - - [12/May/2018 10:56:23] "GET /users/login HTTP/1.1" 401 -
 192.168.1.175 - - [12/May/2018 10:56:23] "GET /users/login HTTP/1.1" 429 -

LE: the "credentials:: - " is a print done in the authentication level of the server. LLE: a curl request is working just fine

curl -u 123456:password http://localhost:5000/users/login 
{
 "is_logged_in": true, 
 "last_login": 1525980360
}

Response is:

credentials:: 123456 - password
127.0.0.1 - - [12/May/2018 13:00:50] "GET /users/login HTTP/1.1" 200 -

I am using the exact same code as in the link provided above.

Teach3r
  • 13
  • 1
  • 9
  • My guess is that you credentials *are* reaching the server (as you see the 401/429 pair) but as you see from the body of the reply, 429 means `Too Many Requests`. Your server is throttling you. Is there evidence in your server log of repeated requests? – Richard Heap May 12 '18 at 11:50
  • The 401 is because the request is being made with empty credentials. If you see the line before the 401, "credentials::" the "-" is separating the username and password, hence the empty credentials. The server has no proxy, it's just a simple flask rest api, in dev mode. – Teach3r May 12 '18 at 11:54
  • That's correct. The way you have it coded, the client tries first without credentials, and when it gets back `401 Unauthorized` it then tries a second time with credentials (which it gets through the callback). In your `curl` example, curl is preemptively sending the credentials. You can try that in Dart if you like by using https://stackoverflow.com/questions/50244416/how-to-pass-basic-auth-credentials-in-api-call-for-a-flutter-mobile-application/50244616#50244616 – Richard Heap May 12 '18 at 12:41
  • Right, that's correct! I've changed the throttle on the API, and now the authentication works, indeed I can see there's a first initial call without the credentials. Is there a way of suppressing that first request? Seems ominous, since it did work yesterday, and few hours later, it stopped working. I made another console project, in which I've added just that method, and only one request, to test it outside the initial project. I have looked into the implementation of the http library, and I can't seem to understand the need to make the first request with empty credentials. – Teach3r May 12 '18 at 12:51
  • The reason for the initial empty request is because the client isn't expecting to have to provide authentication. However, the server requires it, so it sends back a 401, with some additional information (realm - and in the case of Digest authentication, nonce). Without realm (and nonce, if applicable) the client isn't able to construct the Authentication header. But... since you are using Basic not Digest, and you know the realm in advance in this example, you can preemptively send the Authentication header yourself, as shown in the link above. – Richard Heap May 12 '18 at 13:18
  • I understand your point, but seems counter intuitive. The reason I wanted that implementation, is because it saves me from typing and not do: `final requests = createBasicAuthenticationIoHttpClient(userName, password); final _encoded_auth = base64.encode(utf8.encode("${userName}:${password}")); final basic_auth = 'Basic ' + _encoded_auth; return requests.get(LOGIN_URL, headers: {'authorization': basic_auth}) .then((dynamic res) {` I don't see the point in making custom class just to send the headers. – Teach3r May 12 '18 at 14:14
  • You can't have it both ways. The client will, correctly, only call the `authenticate` callback if it receives 401 - so you are going to roundtrip the server twice. If you want to short circuit this, you have to add the header yourself preemptively. (Note that this only works if using Basic auth where you know the realm. Doesn't work for Digest auth.) – Richard Heap May 12 '18 at 14:34

1 Answers1

4

Here's a summary answer wrapping up the discussion in the comments.

It seems like the Too Many Requests error may have been triggered by the typical behaviour of HTTP client talking to server that requires authorization.

GET ->
    <- 401 Unauthorized + scheme + realm (and nonce if applicable)
GET with Authorization header ->
    <- 200 OK (if credentials are valid)

It seems like the server may have counted the first GET/401 towards some sort of limit of requests per minute.

However, a request from curl was successful, probably because curl preemptively sent the Authorization header with the first GET, instead of waiting to be asked with a 401. It can do this as long as the authorization scheme is Basic because it doesn't need any information from the server. (It wouldn't work with Digest because it cannot calculate the Authorization header without the nonce sent with the 401.)

So the question arises, is there a way to preemptively send Basic auth in package:http?

Normal way - only send in response to a 401

// create an IOClient with authentication
HttpClient inner = new HttpClient();
inner.authenticate = (uri, scheme, realm) {
  inner.addCredentials(
      uri, realm, new HttpClientBasicCredentials(username, password));
};
http.IOClient client = new http.IOClient(inner);

// and use it like this
http.Response response = await client.get('https://api.somewhere.io');
// todo - handle the response

Preemptive way - only works with Basic authentication

// use the normal http.get method, but add the header manually, with every request
http.Response response = await http.get(
  'https://api.somewhere.io',
  headers: {'authorization': basicAuthenticationHeader(username, password)},
);
// todo - handle the response

with the utility method

String basicAuthenticationHeader(String username, String password) {
  return 'Basic ' + base64Encode(utf8.encode('$username:$password'));
}
Richard Heap
  • 48,344
  • 9
  • 130
  • 112
  • Richard, I have accepted your answer, as this helped me to better understand the mechanism but also provided me with a workaround for my problem. Currently I am weighting if I want to implement Digest or not, which seems to circumvent the workaround altogether. Thank you for your help, much appreciated! – Teach3r May 14 '18 at 16:14
  • @Teach3r It sounds like you have control of the server end too. If your server does (or can) provide a cookie-based session tracking scheme you could combine it. On any request to the server, check whether the session is already authenticated - and if not send 401. However, this means another change at the client end to return the cookie, covered in this answer https://stackoverflow.com/questions/50299253/flutter-http-maintain-php-session/50299669#50299669 – Richard Heap May 14 '18 at 16:38
  • can I PM you in some way, should you be ok with it? – Teach3r May 14 '18 at 17:04
  • We can continue in a chat room https://chat.stackoverflow.com/rooms/info/171023/discussion-on-dart-basic-auth-sessions?tab=general – Richard Heap May 14 '18 at 17:52