| ⚑ | In this context, by HTTP Authentication we mean authentication with the Basic scheme. The procedure is straightforward and relies on setting the response headers and reading/decoding the requests headers. This is how it works: |
❶ when the servers requires the client's credential (usually, username and password, but it could be any other information as well), the server respond with HTTP/1.1 401 Unauthorized. It also includes the expected scheme (which in this case is WWW-Authenticate: Basic) and could also include the realm (e.g. realm="Protected. Enter your password"). Then the server waits for the client's reply;
❷ the client sends a reply, containing the header Authorization: Basic *****, where "*****" are the base64-encoded client's credentials (say, his name and password, separated by ":", like in "user:pssw");
❸ the server decode the string and check if the client is accepted, routinely looking it up in a database;
❹ if the credentials are OK, the servers replies with HTTP/1.1 200 OK and set the Cookie header to include the client's name (e.g. Set-Cookie: user=newUser);
❺ if the credentials are not OK, the server responds with HTTP/1.1 403 Forbidden, or gives the client another chance to offer his/her credentials by re-sending the 401 message;
❻ once the Cookie are set (i.e. the login is successful), in the next requests the server can only check if the client is included in the list of authorised persons. Put bluntly, you just embed the protected routes (which require restricted access) in the isLogged function, as shown below;
❼ when the client logs out, the server may restart the procedure by setting the HTTP/1.1 401 Unauthorized header, or simply remove the client name from the list of the accepted names by Set-Cookie: user=?.
| ✎ | Although it is titled "HTTP Authentication in C", this is the standard procedure in HTTP authentication, which applies regardless of whether the application is in C, C++, Java, Node JS or your other favourite language. Note that the base64-encoded string (client's credentials) are readily decoded in case the communication is intercepted, therefore it has to be used only within encrypted channels (HTTPS). |
Now let's go through the code, which comprises 3 basic functions: login, logout and isLogged. Then you also may slightly modify your router in order to protect the restricted access routes.
1. Login
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | #define MAX_ATTEMPTS 3 /* attempts to login */ #define MAX_LOG_MIN 1 /* max login period in minutes */ time_t loginTime; void login(struct _request *H) { char ratio[] = " / \0"; ratio[2] = '0'+MAX_ATTEMPTS; for(int i = 0; i < MAX_ATTEMPTS; i++) { respond(H->conn, "HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"Protected. Attempt "); ratio[0] = '0' + (i+1); respond(H->conn, ratio); respond(H->conn, "\"\r\n\r\n"); close(H->conn); listenOn(0, H); const char *find = "Authorization: Basic "; char *buf = readSocket(H); buf = strstr(buf, find) + strlen(find); char *cr = strchr(buf, '\r'); *cr = '\0'; if(strcmp(base64_decode(buf), "user:pass") == 0) { time(&loginTime); respond(H->conn, "HTTP/1.1 200 OK\r\nSet-Cookie: user=user,Today,Tomorrow\r\nContent-Type: text/plain\r\n\r\nLogged in succesfully."); return; } } respond(H->conn, "HTTP/1.1 200 OK\r\nSet-Cookie: user=?\r\nContent-Type: text/plain\r\n\r\nUnauthorised."); //respond(H->conn, "HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain\r\n\r\nYou have no access!"); } |
Instead of responding with HTTP/1.1 403 Forbidden, the function responds with HTTP/1.1 200 OK and Set-Cookie: user=?. It offers the client several attempts to enter his/her credentials and be accepted. MAX_ATTEMPTS sets the limit of those attempts.
2. Logout
1 2 3 4 | void logout(struct _request *H) { respond(H->conn, "HTTP/1.1 200 OK\r\nSet-Cookie: user=?\r\nContent-Type: text/plain\r\n\r\nYou have just logged out."); printf("Logged out.\n"); } |
On logout, we just remove the client from the list of the accepted entities, by setting Set-Cookie: user=?. You may also respond with HTTP/1.1 401 Unauthorized and thus require re-entering the client's credentials.
3. isLogged
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | char isLogged(struct _request *H, const char *userName) { char *coo = getHeader(H, "Cookie"); if(coo && (coo = strstr(coo, "user=")) && strstr(coo+5, userName)) { time_t curr; time(&curr); unsigned period = (unsigned int)difftime(curr, loginTime); if((unsigned int)difftime(curr, loginTime) < MAX_LOG_MIN*60) { printf("\n> Logged %d secs ago...\n", (unsigned int)difftime(curr, loginTime)); return '\1'; } else printf("> Time of login (%d vs. %d) [in secs] expired.\n", period, MAX_LOG_MIN*60); } respond(H->conn, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nUnauthorised."); return '\0'; } |
If the server cannot find the user in the Cookies, it responds with Unauthorized. It also does so if the access time expires (time elapsed from the client's last successful login). MAX_LOG_MIN sets the period length in minutes. On its expires, the client needs to re-login.
4. Protected routes
Now that you have the isLogged() checker, you can protected the routes of sensitive information (in the example below, the route /info):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void router(struct _request *H) { if( strcmp(H->URI,"/")==0 ) serveFile(H, "res/index.html"); else if( strcmp(H->URI,"/info")==0) { if(isLogged(H, "user")) dynamicHTML(H); } else if( strcmp(H->URI,"/logout")==0) logout(H); else if( strcmp(H->URI,"/login")==0) login(H); else if( strcmp(H->URI,"/exit")==0 ) { respond(H->conn, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nServer stopped."); close(H->conn); close(H->socket); printf("> Server on port %d stopped.\n", H->port); exit(0); } else serveFile(H, H->URI+1); } |
The modified (adapted) router now also provides login and logout routes.
* * *
Last remarks
| ⚑ | As you noticed, the basic authentication routine boils down to Cookies. That is, you may simplify the client-server communication and omit messages 401 and 403, leaving just the normal 200. The isLogged function is just looking for the access token, be it name, email, sophisticatedly encrypted pass-ticket or dynamically changeable token which can be deciphered and understood only between your client's application and the server. You may set and remove this token when you deem appropriate, according to the logics of your applications, without necessarily resorting to the 401 and 403 messages. It is to say that the security depends heavily on your philosophy - the server will allow or deny access in accordance of what you set in the Cookies and the procedures that read and interpret those Cookies. |
*You can see the code source here.
No comments:
Post a Comment