Skip to main content

Module 05 — Login System

Objective

Understand and test the user authentication system built in Modules 02-04. By the end of this module you will know how cookie-based sessions work, have tested the login flow with curl, added a second user to the database, and verified everything from the browser.

Prerequisites

  • Module 04 complete — frontend deployed and working on app-server
  • App running at http://192.168.56.12:8080 with the login page visible
  • SSH access to both app-server and db-server

1. Review the Login Architecture

The authentication system spans all three layers of the application: the browser, the Go backend, and the PostgreSQL database.

The full login flow:

  1. User opens http://192.168.56.12:8080 and sees the login form
  2. User enters username and password, clicks Login
  3. Browser sends POST /api/login with a JSON body: {"username":"admin","password":"admin123"}
  4. Go backend receives the request, queries the users table in PostgreSQL
  5. If credentials match, Go responds with a Set-Cookie: session=admin; Path=/ header
  6. Browser stores the cookie and includes it in every subsequent request to the same origin
  7. When the browser requests /api/customers, the session cookie is sent automatically
  8. Go checks for the cookie on protected endpoints — if missing or invalid, it returns 401 Unauthorized

What is a cookie?

A cookie is a small piece of data that the server sends to the browser via the Set-Cookie HTTP header. The browser stores it and automatically includes it in future requests to the same server. Cookies have a name, a value, and optional attributes like Path, Expires, and HttpOnly.

What is a session?

A session is a way to remember who a user is across multiple HTTP requests. HTTP itself is stateless — each request is independent. By setting a cookie after login, the server gives the browser a token that identifies the user. On each subsequent request, the server reads the cookie to determine who is making the request.

Relevant Go code from main.go:

The login handler parses credentials from the request body, queries the database, and sets a cookie on success:

func loginHandler(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
json.NewDecoder(r.Body).Decode(&req)

query := fmt.Sprintf("SELECT id, username FROM users WHERE username = '%s' AND password = '%s'",
req.Username, req.Password)
row := db.QueryRow(query)
// ...
http.SetCookie(w, &http.Cookie{Name: "session", Value: username, Path: "/"})
}

Protected endpoints check for the session cookie before processing the request. If the cookie is missing, the handler returns a 401 response:

func requireAuth(r *http.Request) bool {
cookie, err := r.Cookie("session")
if err != nil || cookie.Value == "" {
return false
}
return true
}

The customersHandler calls requireAuth at the start — if it returns false, the handler writes a 401 status and stops.


2. Test the Login Flow with curl

All curl commands run from your Mac (not inside an SSH session).

2.1 Login and capture cookies

curl -v -X POST http://192.168.56.12:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' \
-c cookies.txt

The -v flag shows verbose output including headers. The -c cookies.txt flag saves any cookies the server sends to a file.

In the response headers, look for a line like:

< Set-Cookie: session=admin; Path=/

This is the server telling the browser (or curl) to store a cookie named session with value admin.

Verify: The response body should contain:

{"message":"Login successful","username":"admin"}
curl http://192.168.56.12:8080/api/customers -b cookies.txt

The -b cookies.txt flag tells curl to send the cookies stored in that file. The server reads the session cookie, confirms the user is authenticated, and returns the customer list.

Verify: You should get a JSON array (possibly empty: [] if no customers exist yet).

curl http://192.168.56.12:8080/api/customers

Without the -b flag, no cookie is sent. The server has no way to identify the user.

Verify: You should get a 401 Unauthorized response:

{"error":"Unauthorized"}

2.4 Login with wrong credentials (expect 401)

curl -X POST http://192.168.56.12:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"wrongpassword"}'

Verify: The server responds with 401 because the password does not match the database record:

{"error":"Invalid credentials"}

3. Add a Second User

The users table currently has one user (admin). Let's add another.

3.1 Insert the user in PostgreSQL

SSH to db-server and open psql:

ssh db-server
psql -U appuser -d customerdb -h localhost

Insert a new user:

INSERT INTO users (username, password) VALUES ('operator', 'operator456');

Verify the user was added:

SELECT id, username FROM users;

You should see both users:

 id | username
----+----------
1 | admin
2 | operator

Exit psql:

\q

Exit the SSH session:

exit

3.2 Test login with the new user

From your Mac:

curl -v -X POST http://192.168.56.12:8080/api/login \
-H "Content-Type: application/json" \
-d '{"username":"operator","password":"operator456"}' \
-c cookies-operator.txt

Verify: The response should contain:

{"message":"Login successful","username":"operator"}

3.3 Verify the new user can access protected endpoints

curl http://192.168.56.12:8080/api/customers -b cookies-operator.txt

Verify: You should get the same customer list as the admin user. Both users have equal access to the data.


4. Test from the Browser

4.1 Login as admin

Open http://192.168.56.12:8080 in your browser. Enter admin as the username and admin123 as the password. Click Login.

Verify: You see the "Welcome, admin!" header and the customer table.

Open the browser Developer Tools:

  • Chrome/Edge: Press F12 or Cmd+Option+I on Mac
  • Navigate to the Application tab (Chrome) or Storage tab (Firefox)
  • In the left sidebar, expand Cookies and click on http://192.168.56.12:8080

You should see a cookie with:

NameValuePath
sessionadmin/

This is the cookie the server set after successful login. The browser sends it automatically with every request to this origin.

4.3 Logout and login as operator

Click the red Logout button. The login form reappears.

Now login with operator / operator456.

Verify: You see "Welcome, operator!" and the same customer data. Check Developer Tools again — the cookie value has changed from admin to operator.

4.4 Confirm both users see the same data

Both users can view, add, edit, and delete customers. The current system does not have role-based access — all authenticated users have full access.


Troubleshooting

"Login returns 401"

  • Are the credentials correct? Check the users table:
    ssh db-server "psql -U appuser -d customerdb -h localhost -c 'SELECT id, username FROM users;'"
  • Remember passwords are case-sensitive: admin123 is not the same as Admin123

"Cookie not being sent"

  • With curl: make sure you are using -b cookies.txt (not -c, which is for saving cookies)
  • In the browser: check Developer Tools > Application > Cookies to confirm the cookie exists
  • If using a different browser or incognito window, you need to login again — cookies are not shared

"Protected endpoint returns 401 even with cookie"

  • The cookie may have the wrong name. The server expects a cookie named session
  • Check the cookie file: cat cookies.txt — look for a line containing session
  • Try logging in again to get a fresh cookie

"New user cannot login"

  • Verify the user exists in the database:
    ssh db-server "psql -U appuser -d customerdb -h localhost -c \"SELECT * FROM users WHERE username = 'operator';\""
  • Make sure you typed the password correctly in both the INSERT statement and the curl command

Summary

You now understand how cookie-based authentication works in this application:

ComponentRole
BrowserSends credentials, stores cookie, includes cookie in requests
Go backendValidates credentials, sets cookie, checks cookie on protected endpoints
PostgreSQLStores usernames and passwords in the users table

The system has two users (admin and operator) who both have full access to customer data. In the next module, we will add Nginx as a reverse proxy in front of the Go backend.