Authentication
Ecewo offers some session management APIs for authentication and authorization:
set_cookie()
to set cookiecreate_session()
to create a sessionfind_session()
to find a session in memoryget_session()
to get the session from requestfree_session()
to delete the session from memory
With the power of these APIs and cJSON, we can easily manage the authentication and authorization.
Let’s make an authentication example and see how it works.
We have two test users:
[ { "id": 1, "name": "John", "username": "johndoe" "password": "123123123" }, { "id": 2, "name": "Jane", "username": "janedoe" "password": "123123123" } ]
Let’s write a login
handler:
#include "router.h"#include "jansson.h"#include "session.h"#include "../db/db.h"
extern sqlite3 *db;
void handle_login(Req *req, Res *res){ const char *body = req->body;
if (body == NULL) { reply(res, "400 Bad Request", "text/plain", "Missing request body"); return; }
json_error_t err; json_t *json = json_loads(body, 0, &err); if (!json) { reply(res, "400 Bad Request", "text/plain", "Invalid JSON"); return; }
// username ve password alanlarını al json_t *j_username = json_object_get(json, "username"); json_t *j_password = json_object_get(json, "password");
if (!json_is_string(j_username) || !json_is_string(j_password)) { json_decref(json); reply(res, "400 Bad Request", "text/plain", "Missing or invalid fields"); return; }
const char *username = json_string_value(j_username); const char *password = json_string_value(j_password);
const char *sql = "SELECT * FROM users WHERE username = ? AND password = ?"; sqlite3_stmt *stmt; int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, 0); if (rc != SQLITE_OK) { json_decref(json); reply(res, "500 Internal Server Error", "text/plain", "DB prepare failed"); return; }
sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, password, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char *db_name = (const char *)sqlite3_column_text(stmt, 1);
char *sid = create_session(); Session *sess = find_session(sid);
set_session(sess, "name", db_name); set_session(sess, "username", username); set_session(sess, "theme", "dark");
set_cookie(res, "session_id", sid, 3600);
printf("Session ID: %s\n", sid); printf("Session JSON: %s\n", sess->data);
reply(res, "200 OK", "text/plain", "Login successful!"); } else { reply(res, "401 Unauthorized", "text/plain", "Invalid username or password"); }
sqlite3_finalize(stmt); json_decref(json);}
#ifndef HANDLERS_H#define HANDLERS_H
#include "router.h"
void handle_login(Req *req, Res *res);
#endif
#include "ecewo.h"#include "router.h"#include "handlers/handlers.h"#include "db/db.h"
int main(){ init_db(); post("/login", handle_login); ecewo(4000); sqlite3_close(db); return 0;}
Let’s send a request to http://localhost:4000/login
with that body:
{ "username": "janedoe", "password": "321321"}
If login is successful, we’ll see a “Login successful!” response and a header like "Cookie": "session_id=VKdbMRbqMhh_40F6ef2FreEba6JqkH16"
will be added to the headers.
Logout
Section titled “Logout”We also write a logout handler to use after login. Let’s add these parts:
// Add this handler:
void handle_logout(Req *req, Res *res){ // First, check if the user has session Session *sess = get_session(&req->headers);
if (!sess) { reply(res, "400", "text/plain", "You have to login first"); } else { free_session(sess); // Delete session from the memory set_cookie(res, "session_id", "", 0); // Time out cookie, the browser will delete it reply(res, "302 Found", "text/plain", "Logged out"); }}
Declare the logout handler too:
#ifndef HANDLERS_H#define HANDLERS_H
#include "router.h"
void handle_login(Req *req, Res *res);void handle_logout(Req *req, Res *res); // We added now
#endif
And also add to entry point:
#include "ecewo.h"#include "router.h"#include "handlers/handlers.h"#include "db/db.h"
int main(){ init_db(); post("/login", handle_login); get("/logout", handle_logout); // We added it now ecewo(4000); sqlite3_close(db); return 0;}
Now let’s send a request to http://localhost:4000/logout
after login. Cookie
header will be deleted and we’ll see that response:
Logged out
If we send one more request, we’ll see:
You have to login first
Getting Session Data
Section titled “Getting Session Data”We added 3 data to the session in the Login
handler: name
, username
and theme
. Let’s write another function that sends the session data:
#include "router.h"#include "cjson.h"#include "session.h"
void handle_session_data(Req *req, Res *res){ Session *user_session = get_session(&req->headers);
if (!user_session) { reply(res, "401 Unauthorized", "application/json", "{\"error\":\"Authentication required\"}"); return; }
/* Parse the JSON string stored in session->data */ json_error_t err; json_t *session_data = json_loads(user_session->data, 0, &err); if (!session_data) { /* If parsing fails, return an error */ reply(res, "500 Internal Server Error", "application/json", "{\"error\":\"Failed to parse session data\"}"); return; }
/* Serialize back to a compact JSON string */ char *session_str = json_dumps(session_data, JSON_COMPACT); if (!session_str) { json_decref(session_data); reply(res, "500 Internal Server Error", "application/json", "{\"error\":\"Failed to serialize session data\"}"); return; }
/* Send the session JSON back to the client */ reply(res, "200 OK", "application/json", session_str);
/* Clean up */ free(session_str); json_decref(session_data);}
#ifndef HANDLERS_H#define HANDLERS_H
#include "router.h"
void handle_login(Req *req, Res *res);void handle_logout(Req *req, Res *res);void handle_session_data(Req *req, Res *res); // We added now
#endif
#include "ecewo.h"#include "router.h"#include "handlers.h"#include "db.h"
int main(){ init_db(); get("/session", handle_session_data); // We added it now post("/login", handle_login); post("/logout", handle_logout); ecewo(4000); sqlite3_close(db); return 0;}
First, we need to login. Rebuild the program and send a POST
request to the http://localhost:4000/login
and get the session.
After that, send another request to the http://localhost:4000/session
address to see the session data.
The output will this:
{ "name": "John", "username": "johndoe", "theme": "dark"}
Here are the session data, which we have added while the user is logging in.
If you don’t want the whole session data, but just one or two, you can do it as well:
void handle_session_data(Req *req, Res *res){ Session *user_session = get_session(&req->headers);
if (!user_session) { reply(res, "401 Unauthorized", "application/json", "{\"error\":\"Authentication required\"}"); return; }
/* Parse stored session JSON */ json_error_t err; json_t *session_data = json_loads(user_session->data, 0, &err); if (!session_data) { reply(res, "500 Internal Server Error", "application/json", "{\"error\":\"Invalid session data\"}"); return; }
/* Extract \"name\" field */ json_t *j_name = json_object_get(session_data, "name");
/* Build response object */ json_t *response = json_object(); if (json_is_string(j_name)) { json_object_set_new(response, "name", json_string(json_string_value(j_name))); } else { json_object_set_new(response, "name", json_string("Unknown")); }
/* Serialize response */ char *json_str = json_dumps(response, JSON_COMPACT); if (!json_str) { json_decref(session_data); json_decref(response); reply(res, "500 Internal Server Error", "application/json", "{\"error\":\"Failed to serialize response\"}"); return; }
/* Send it */ reply(res, "200 OK", "application/json", json_str);
/* Cleanup */ free(json_str); json_decref(session_data); json_decref(response);}
The output will be:
{ "name": "John"}
Protected Routes
Section titled “Protected Routes”Let’s say that we want some pages to be available for authenticated users only. In this situation, we can use get_session()
function to check if the user has a session.
void handle_protected(Req *req, Res *res){ // Check if the user has session Session *sess = get_session(&req->headers);
// If the user hasn't, return an error with 401 status code if (!sess) { reply(res, "401 Unauthorized", "text/plain", "You must be logged in."); return; }
// If has, let the user in reply(res, "200 OK", "text/plain", "Welcome to the protected area!");}
void handle_protected(Req *req, Res *res);
get("/protected", handle_protected);
Let’s send a request to http://localhost:4000/protected
. If we authenticated, we’ll see:
Welcome to the protected area!
If we did not, we’ll see:
You must be logged in.
Well, some routes should be for the user’s himself only such as Edit Profile page. In that situation, we need to think deeper.
For this example, we’ll define a route with slug
and we’ll check first if the username in session data and slug is the same.
If they are, then we’ll run a sql query and send a response.
int main(){ // <-- Some other codes -->
get("/edit/:slug", edit_profile);
// <-- Some other codes -->}
void edit_profile(Req *req, Res *res);
void edit_profile(Req *req, Res *res){ // First, check the user's session Session *sess = get_session(&req->headers); if (!sess) { reply(res, "401 Unauthorized", "text/plain", "Authentication required"); return; }
// Parse session data as JSON with Jansson json_error_t err; json_t *session_data = json_loads(sess->data, 0, &err); if (!session_data) { reply(res, "500 Internal Server Error", "text/plain", "Invalid session data"); return; }
/* Extract \"username\" field from session */ json_t *j_username = json_object_get(session_data, "username"); if (!json_is_string(j_username)) { json_decref(session_data); reply(res, "500 Internal Server Error", "text/plain", "Session missing username"); return; } const char *session_username = json_string_value(j_username);
/* Compare slug param vs session username */ const char *slug = get_req(&req->params, "slug"); if (!slug || strcmp(slug, session_username) != 0) { json_decref(session_data); reply(res, "403 Forbidden", "text/plain", "Unauthorized: Slug does not match session username"); return; }
/* Prepare and run SQL */ const char *sql = "SELECT id, name FROM users WHERE username = ?;"; sqlite3_stmt *stmt; int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { const char *errmsg = sqlite3_errmsg(db); char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "{\"error\":\"DB error: %s\"}", errmsg); json_decref(session_data); reply(res, "500 Internal Server Error", "application/json", error_msg); return; } sqlite3_bind_text(stmt, 1, slug, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { int id = sqlite3_column_int(stmt, 0); const char *name = (const char *)sqlite3_column_text(stmt, 1);
/* Build JSON response with Jansson */ json_t *user_json = json_object(); json_object_set_new(user_json, "id", json_integer(id)); json_object_set_new(user_json, "name", json_string(name));
char *json_string = json_dumps(user_json, JSON_COMPACT); reply(res, "200 OK", "application/json", json_string);
free(json_string); json_decref(user_json); } else { reply(res, "404 Not Found", "text/plain", "User not found"); }
/* Cleanup */ sqlite3_finalize(stmt); json_decref(session_data); free_req(&req->params);}
Let’s send 3 different request to the http://localhost:4000/edit/johndoe
route.
If we try without any authorization, we’ll get that response:
{ Authentication required}
If we try to reach that page as someone who is not johndoe, we’ll receive:
{ Unauthorized: Slug does not match session username}
When we logged in as johndoe and send a request again, here is what we will get:
{ "id": 1, "name": "John"}
** NOTE 1 **
It’s not safe to insert the password to the database without encryption. You should use a library to encrypt the user password before inserting.
** NOTE 2 **
In these examples, session is stored in memory, but you can store them in the database if you prefer.
If you store them in the memory, you will use free_session()
API for rare operations like logout. Ecewo will free the expired sessions when a new session is created.
But if you prefer storing the sessions in a database, you may free the session from memory right after you create and insert it into the database.