basicauthentication.inc (9880B)
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 2617}, 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? Initially, the pointer this parameter references is 68 set by @emph{MHD} in the callback. But it will also be "remembered" on the 69 next call (for the same request). Thus, we can use the @code{req_cls} 70 location to keep track of the request state. For now, we will simply 71 generate no response until the parameter is non-null---implying the callback 72 was called before at least once. We do not need to share information between 73 different calls of the callback, so we can set the parameter to any address 74 that is assured to be not null. The pointer to the @code{connection} structure 75 will be pointing to a legal address, so we take this. 76 77 The first time @code{answer_to_connection} is called, we will not even look at the headers. 78 79 @verbatim 80 static int 81 answer_to_connection (void *cls, struct MHD_Connection *connection, 82 const char *url, const char *method, const char *version, 83 const char *upload_data, size_t *upload_data_size, 84 void **req_cls) 85 { 86 if (0 != strcmp(method, "GET")) return MHD_NO; 87 if (NULL == *req_cls) {*req_cls = connection; return MHD_YES;} 88 89 ... 90 /* else respond accordingly */ 91 ... 92 } 93 @end verbatim 94 @noindent 95 96 Note how we lop off the connection on the first condition (no "GET" request), 97 but return asking for more on the other one with @code{MHD_YES}. With this 98 minor change, we can proceed to implement the actual authentication process. 99 100 @heading Request for authentication 101 102 Let us assume we had only files not intended to be handed out without the 103 correct username/password, so every "GET" request will be challenged. 104 @emph{RFC 7617} describes how the server shall ask for authentication by 105 adding a @emph{WWW-Authenticate} response header with the name of the 106 @emph{realm} protected. MHD can generate and queue such a failure response 107 for you using the @code{MHD_queue_basic_auth_fail_response} API. The only 108 thing you need to do is construct a response with the error page to be shown 109 to the user if he aborts basic authentication. But first, you should check if 110 the proper credentials were already supplied using the 111 @code{MHD_basic_auth_get_username_password} call. 112 113 Your code would then look like this: 114 @verbatim 115 static enum MHD_Result 116 answer_to_connection (void *cls, struct MHD_Connection *connection, 117 const char *url, const char *method, 118 const char *version, const char *upload_data, 119 size_t *upload_data_size, void **req_cls) 120 { 121 struct MHD_BasicAuthInfo *auth_info; 122 enum MHD_Result ret; 123 struct MHD_Response *response; 124 125 if (0 != strcmp (method, "GET")) 126 return MHD_NO; 127 if (NULL == *req_cls) 128 { 129 *req_cls = connection; 130 return MHD_YES; 131 } 132 auth_info = MHD_basic_auth_get_username_password3 (connection); 133 if (NULL == auth_info) 134 { 135 static const char *page = 136 "<html><body>Authorization required</body></html>"; 137 response = MHD_create_response_from_buffer_static (strlen (page), page); 138 ret = MHD_queue_basic_auth_fail_response3 (connection, 139 "admins", 140 MHD_YES, 141 response); 142 } 143 else if ((strlen ("root") != auth_info->username_len) || 144 (0 != memcmp (auth_info->username, "root", 145 auth_info->username_len)) || 146 /* The next check against NULL is optional, 147 * if 'password' is NULL then 'password_len' is always zero. */ 148 (NULL == auth_info->password) || 149 (strlen ("pa$$w0rd") != auth_info->password_len) || 150 (0 != memcmp (auth_info->password, "pa$$w0rd", 151 auth_info->password_len))) 152 { 153 static const char *page = 154 "<html><body>Wrong username or password</body></html>"; 155 response = MHD_create_response_from_buffer_static (strlen (page), page); 156 ret = MHD_queue_basic_auth_fail_response3 (connection, 157 "admins", 158 MHD_YES, 159 response); 160 } 161 else 162 { 163 static const char *page = "<html><body>A secret.</body></html>"; 164 response = MHD_create_response_from_buffer_static (strlen (page), page); 165 ret = MHD_queue_response (connection, MHD_HTTP_OK, response); 166 } 167 if (NULL != auth_info) 168 MHD_free (auth_info); 169 MHD_destroy_response (response); 170 return ret; 171 } 172 @end verbatim 173 174 See the @code{examples} directory for the complete example file. 175 176 @heading Remarks 177 For a proper server, the conditional statements leading to a return of @code{MHD_NO} should yield a 178 response with a more precise status code instead of silently closing the connection. For example, 179 failures of memory allocation are best reported as @emph{internal server error} and unexpected 180 authentication methods as @emph{400 bad request}. 181 182 @heading Exercises 183 @itemize @bullet 184 @item 185 Make the server respond to wrong credentials (but otherwise well-formed requests) with the recommended 186 @emph{401 unauthorized} status code. If the client still does not authenticate correctly within the 187 same connection, close it and store the client's IP address for a certain time. (It is OK to check for 188 expiration not until the main thread wakes up again on the next connection.) If the client fails 189 authenticating three times during this period, add it to another list for which the 190 @code{AcceptPolicyCallback} function denies connection (temporally). 191 192 @item 193 With the network utility @code{netcat} connect and log the response of a "GET" request as you 194 did in the exercise of the first example, this time to a file. Now stop the server and let @emph{netcat} 195 listen on the same port the server used to listen on and have it fake being the proper server by giving 196 the file's content as the response (e.g. @code{cat log | nc -l -p 8888}). Pretending to think your were 197 connecting to the actual server, browse to the eavesdropper and give the correct credentials. 198 199 Copy and paste the encoded string you see in @code{netcat}'s output to some of the Base64 decode tools available online 200 and see how both the user's name and password could be completely restored. 201 202 @end itemize