basicauthentication.inc (10117B)
1 With the small exception of IP address based access control, 2 requests from all connecting clients where served equally until now. 3 This chapter discusses a first method of client's authentication and 4 its limits. 5 6 A very simple approach feasible with the means already discussed would 7 be to expect the password in the @emph{URI} string before granting access to 8 the secured areas. The password could be separated from the actual resource identifier 9 by a certain character, thus the request line might look like 10 @verbatim 11 GET /picture.png?mypassword 12 @end verbatim 13 @noindent 14 15 In the rare situation where the client is customized enough and the connection 16 occurs through secured lines (e.g., a embedded device directly attached to 17 another via wire) and where the ability to embed a password in the URI or to 18 pass on a URI with a password are desired, this can be a reasonable choice. 19 20 But when it is assumed that the user connecting does so with an ordinary 21 Internet browser, this implementation brings some problems about. For example, 22 the URI including the password stays in the address field or at least in the 23 history of the browser for anybody near enough to see. It will also be 24 inconvenient to add the password manually to any new URI when the browser does 25 not know how to compose this automatically. 26 27 At least the convenience issue can be addressed by employing the simplest 28 built-in password facilities of HTTP compliant browsers, hence we want to 29 start there. It will, however, turn out to have still severe weaknesses in 30 terms of security which need consideration. 31 32 Before we will start implementing @emph{Basic Authentication} as described in 33 @emph{RFC 7617}, we will also abandon the simplistic and generally 34 problematic practice of responding every request the first time our callback 35 is called for a given connection. Queuing a response upon the first request 36 is akin to generating an error response (even if it is a "200 OK" reply!). 37 The reason is that MHD usually calls the callback in three phases: 38 39 @enumerate 40 @item 41 First, to initially tell the application about the connection and inquire whether 42 it is OK to proceed. This call typically happens before the client could upload 43 the request body, and can be used to tell the client to not proceed with the 44 upload (if the client requested "Expect: 100 Continue"). Applications may queue 45 a reply at this point, but it will force the connection to be closed and thus 46 prevent keep-alive / pipelining, which is generally a bad idea. Applications 47 wanting to proceed with the request throughout the other phases should just return 48 "MHD_YES" and not queue any response. Note that when an application suspends 49 a connection in this callback, the phase does not advance and the application 50 will be called again in this first phase. 51 @item 52 Next, to tell the application about upload data provided by the client. 53 In this phase, the application may not queue replies, and trying to do so 54 will result in MHD returning an error code from @code{MHD_queue_response}. 55 If there is no upload data, this phase is skipped. 56 @item 57 Finally, to obtain a regular response from the application. This can be 58 almost any type of response, including ones indicating failures. The 59 one exception is a "100 Continue" response, which applications must never 60 generate: MHD generates that response automatically when necessary in the 61 first phase. If the application does not queue a response, MHD may call 62 the callback repeatedly (depending a bit on the threading model, the 63 application should suspend the connection). 64 @end enumerate 65 66 But how can we tell whether the callback has been called before for the 67 particular request? This is what the @code{req_cls} parameter of the callback 68 is for: initially, the pointer it references is set to NULL by @emph{MHD}. 69 But whatever we store there will be "remembered" on the 70 next call (for the same request). Thus, we can use the @code{req_cls} 71 location to keep track of the request state. For now, we will simply 72 generate no response until the parameter is non-null---implying the callback 73 was called before at least once. We do not need to share information between 74 different calls of the callback, so we can set the parameter to any address 75 that is assured to be not null. The pointer to the @code{connection} structure 76 will be pointing to a legal address, so we take this. 77 78 The first time @code{answer_to_connection} is called, we will not even look at the headers. 79 80 @verbatim 81 static enum MHD_Result 82 answer_to_connection (void *cls, struct MHD_Connection *connection, 83 const char *url, const char *method, const char *version, 84 const char *upload_data, size_t *upload_data_size, 85 void **req_cls) 86 { 87 if (0 != strcmp(method, "GET")) return MHD_NO; 88 if (NULL == *req_cls) {*req_cls = connection; return MHD_YES;} 89 90 ... 91 /* else respond accordingly */ 92 ... 93 } 94 @end verbatim 95 @noindent 96 97 Note how we lop off the connection on the first condition (no "GET" request), 98 but return asking for more on the other one with @code{MHD_YES}. With this 99 minor change, we can proceed to implement the actual authentication process. 100 101 @heading Request for authentication 102 103 Let us assume we had only files not intended to be handed out without the 104 correct username/password, so every "GET" request will be challenged. 105 @emph{RFC 7617} describes how the server shall ask for authentication by 106 adding a @emph{WWW-Authenticate} response header with the name of the 107 @emph{realm} protected. MHD can generate and queue such a failure response 108 for you using the @code{MHD_queue_basic_auth_required_response3} API. The only 109 thing you need to do is construct a response with the error page to be shown 110 to the user if he aborts basic authentication. But first, you should check if 111 the proper credentials were already supplied using the 112 @code{MHD_basic_auth_get_username_password3} call. (The older 113 @code{MHD_queue_basic_auth_fail_response} and 114 @code{MHD_basic_auth_get_username_password} calls are deprecated.) 115 116 Your code would then look like this: 117 @verbatim 118 static enum MHD_Result 119 answer_to_connection (void *cls, struct MHD_Connection *connection, 120 const char *url, const char *method, 121 const char *version, const char *upload_data, 122 size_t *upload_data_size, void **req_cls) 123 { 124 struct MHD_BasicAuthInfo *auth_info; 125 enum MHD_Result ret; 126 struct MHD_Response *response; 127 128 if (0 != strcmp (method, "GET")) 129 return MHD_NO; 130 if (NULL == *req_cls) 131 { 132 *req_cls = connection; 133 return MHD_YES; 134 } 135 auth_info = MHD_basic_auth_get_username_password3 (connection); 136 if (NULL == auth_info) 137 { 138 static const char *page = 139 "<html><body>Authorization required</body></html>"; 140 response = MHD_create_response_from_buffer_static (strlen (page), page); 141 ret = MHD_queue_basic_auth_required_response3 (connection, 142 "admins", 143 MHD_YES, 144 response); 145 } 146 else if ((strlen ("root") != auth_info->username_len) || 147 (0 != memcmp (auth_info->username, "root", 148 auth_info->username_len)) || 149 /* The next check against NULL is optional, 150 * if 'password' is NULL then 'password_len' is always zero. */ 151 (NULL == auth_info->password) || 152 (strlen ("pa$$w0rd") != auth_info->password_len) || 153 (0 != memcmp (auth_info->password, "pa$$w0rd", 154 auth_info->password_len))) 155 { 156 static const char *page = 157 "<html><body>Wrong username or password</body></html>"; 158 response = MHD_create_response_from_buffer_static (strlen (page), page); 159 ret = MHD_queue_basic_auth_required_response3 (connection, 160 "admins", 161 MHD_YES, 162 response); 163 } 164 else 165 { 166 static const char *page = "<html><body>A secret.</body></html>"; 167 response = MHD_create_response_from_buffer_static (strlen (page), page); 168 ret = MHD_queue_response (connection, MHD_HTTP_OK, response); 169 } 170 if (NULL != auth_info) 171 MHD_free (auth_info); 172 MHD_destroy_response (response); 173 return ret; 174 } 175 @end verbatim 176 177 See the @code{examples} directory for the complete example file. 178 179 @heading Remarks 180 For a proper server, the conditional statements leading to a return of @code{MHD_NO} should yield a 181 response with a more precise status code instead of silently closing the connection. For example, 182 failures of memory allocation are best reported as @emph{internal server error} and unexpected 183 authentication methods as @emph{400 bad request}. 184 185 @heading Exercises 186 @itemize @bullet 187 @item 188 Make the server respond to wrong credentials (but otherwise well-formed requests) with the recommended 189 @emph{401 unauthorized} status code. If the client still does not authenticate correctly within the 190 same connection, close it and store the client's IP address for a certain time. (It is OK to check for 191 expiration not until the main thread wakes up again on the next connection.) If the client fails 192 authenticating three times during this period, add it to another list for which the 193 @code{AcceptPolicyCallback} function denies connection (temporally). 194 195 @item 196 With the network utility @code{netcat} connect and log the response of a "GET" request as you 197 did in the exercise of the first example, this time to a file. Now stop the server and let @emph{netcat} 198 listen on the same port the server used to listen on and have it fake being the proper server by giving 199 the file's content as the response (e.g. @code{cat log | nc -l -p 8888}). Pretending to think your were 200 connecting to the actual server, browse to the eavesdropper and give the correct credentials. 201 202 Copy and paste the encoded string you see in @code{netcat}'s output to some of the Base64 decode tools available online 203 and see how both the user's name and password could be completely restored. 204 205 @end itemize