Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🛠️ PHP + MySQL REST API  ·  Lesson 04

Register & Login API (Token)

How token login works

The app sends email + password once. If they are right, the server creates a long random token and the app saves it (in DataStore). After that, every request carries the token in a header, and the server knows who is calling.

AppPHP APIMySQLPOST auth/login.php {email, password}SELECT user by email → password_verify()INSERT user_tokens (sha256(token), expires_at){token: "c4a45ae7…", user: {…}}save token in DataStoreGET cart/list.php Authorization: Bearer c4a45ae7…find user by sha256(token)this user’s cart
Login once, then send the token with every call

Passwords: hash, never store

password_hash() turns a password into a one-way hash with a random salt (bcrypt). Even you, the owner, can never read the password back. password_verify() checks a typed password against the hash.

PHP – the hash is different on every run (random salt)
<?php
$hash = password_hash('Test@123', PASSWORD_DEFAULT);
echo $hash, "\n";
var_dump(password_verify('Test@123', $hash));
var_dump(password_verify('test@123', $hash));
$2y$10$uH03CdamJqTh78xX6h8anO2bK5SYInjgY9DUD8F0J54uvIpC.vxje bool(true) bool(false)
🔒 Same message for both mistakes

Login replies "Wrong email or password." whether the email does not exist or the password is wrong. A different message would let attackers find out which emails have accounts.

Tokens: create and check

These two helpers (already in helpers.php) do all the token work. Any endpoint that needs a logged-in user simply calls $user = require_auth(); on its first lines.

PHP – includes/helpers.php (token part, extra comments added)
<?php
// From includes/helpers.php

/** Create a new login token for a user. Only its sha256 hash is stored. */
function create_token($userId)
{
    $token = bin2hex(random_bytes(32));          // 32 random bytes = 64 hex characters
    $stmt = db()->prepare('INSERT INTO user_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)');
    $stmt->execute(array($userId, hash('sha256', $token), date('Y-m-d H:i:s', time() + TOKEN_DAYS * 86400)));
    return $token;                                // the app gets the real token ONCE
}

/** Returns the logged-in user row, or stops with 401. */
function require_auth()
{
    $token = bearer_token();                      // reads "Authorization: Bearer <token>"
    if ($token === null) {
        fail('Please login to continue.', 401);
    }
    $stmt = db()->prepare(
        'SELECT u.id, u.name, u.email, u.phone, u.created_at FROM user_tokens t
         JOIN users u ON u.id = t.user_id
         WHERE t.token_hash = ? AND t.expires_at > NOW()'
    );
    $stmt->execute(array(hash('sha256', $token)));
    $user = $stmt->fetch();
    if (!$user) {
        fail('Session expired. Please login again.', 401);
    }
    $user['id'] = (int) $user['id'];
    return $user;
}

What the app keeps

  • token = c4a45ae77d…(64 hex)
  • saved in DataStore
  • sent in the Authorization header

What the database keeps

  • token_hash = sha256(token)
  • user_id, expires_at (30 days)
  • one row per logged-in device
A stolen database cannot be used to log in

Step 1: auth/register.php

Validate everything on the server — the app also validates, but anyone can call an API directly. Then check the email is free, save the user with a hashed password and log them in straight away.

PHP – backend/api/auth/register.php
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
require_fields(array('name' => 'Name', 'email' => 'Email', 'password' => 'Password'));

$name = in_str('name');
$email = strtolower(in_str('email'));
$phone = in_str('phone');
$password = in_str('password');

if (mb_strlen($name) < 2 || mb_strlen($name) > 80) {
    fail('Name must be 2 to 80 characters.', 422);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    fail('Please enter a valid email.', 422);
}
if ($phone !== '' && !preg_match('/^[6-9][0-9]{9}$/', $phone)) {
    fail('Please enter a valid 10-digit mobile number.', 422);
}
if (strlen($password) < 6) {
    fail('Password must be at least 6 characters.', 422);
}

$stmt = db()->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute(array($email));
if ($stmt->fetch()) {
    fail('This email is already registered. Please login.', 409);
}

$stmt = db()->prepare('INSERT INTO users (name, email, phone, password_hash) VALUES (?, ?, ?, ?)');
$stmt->execute(array($name, $email, $phone === '' ? null : $phone, password_hash($password, PASSWORD_DEFAULT)));
$userId = (int) db()->lastInsertId();

ok(array(
    'token' => create_token($userId),
    'user'  => array('id' => $userId, 'name' => $name, 'email' => $email, 'phone' => $phone === '' ? null : $phone),
), 'Account created successfully.');
POST onlyrequire_methodRequired fieldsname, email, passwordValidatename length, email, Indian mobile, password ≥ 6Email free?409 if already registeredINSERT userpassword_hash()Return token + userthe app is logged in
register.php from top to bottom

Step 2: auth/login.php

PHP – backend/api/auth/login.php
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
require_fields(array('email' => 'Email', 'password' => 'Password'));

$email = strtolower(in_str('email'));
$password = in_str('password');

$stmt = db()->prepare('SELECT id, name, email, phone, password_hash FROM users WHERE email = ?');
$stmt->execute(array($email));
$user = $stmt->fetch();

// Same message for "no such email" and "wrong password": do not reveal which emails exist.
if (!$user || !password_verify($password, $user['password_hash'])) {
    fail('Wrong email or password.', 401);
}

// Remove this user's expired tokens while we are here.
db()->prepare('DELETE FROM user_tokens WHERE user_id = ? AND expires_at < NOW()')->execute(array($user['id']));

ok(array(
    'token' => create_token((int) $user['id']),
    'user'  => array('id' => (int) $user['id'], 'name' => $user['name'], 'email' => $user['email'], 'phone' => $user['phone']),
), 'Welcome back, ' . $user['name'] . '!');

Step 3: me, update, logout

me.php returns the profile plus three counters the Account screen shows (orders, wishlist, cart) in a single query with sub-selects.

PHP – backend/api/auth/me.php
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('GET');
$user = require_auth();

$counts = db()->prepare(
    'SELECT
       (SELECT COUNT(*) FROM orders WHERE user_id = ?) AS orders,
       (SELECT COUNT(*) FROM wishlist WHERE user_id = ?) AS wishlist,
       (SELECT COALESCE(SUM(qty), 0) FROM cart_items WHERE user_id = ?) AS cart'
);
$counts->execute(array($user['id'], $user['id'], $user['id']));
$c = $counts->fetch();

$user['order_count'] = (int) $c['orders'];
$user['wishlist_count'] = (int) $c['wishlist'];
$user['cart_count'] = (int) $c['cart'];
ok($user);

update.php changes name and phone, and the password only if the current password is correct:

PHP – backend/api/auth/update.php
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
$user = require_auth();
require_fields(array('name' => 'Name'));

$name = in_str('name');
$phone = in_str('phone');
if (mb_strlen($name) < 2 || mb_strlen($name) > 80) {
    fail('Name must be 2 to 80 characters.', 422);
}
if ($phone !== '' && !preg_match('/^[6-9][0-9]{9}$/', $phone)) {
    fail('Please enter a valid 10-digit mobile number.', 422);
}
db()->prepare('UPDATE users SET name = ?, phone = ? WHERE id = ?')
    ->execute(array($name, $phone === '' ? null : $phone, $user['id']));

// Optional password change: needs the current password.
$newPass = in_str('new_password');
if ($newPass !== '') {
    $stmt = db()->prepare('SELECT password_hash FROM users WHERE id = ?');
    $stmt->execute(array($user['id']));
    if (!password_verify(in_str('current_password'), $stmt->fetchColumn())) {
        fail('Current password is wrong.', 422);
    }
    if (strlen($newPass) < 6) {
        fail('New password must be at least 6 characters.', 422);
    }
    db()->prepare('UPDATE users SET password_hash = ? WHERE id = ?')
        ->execute(array(password_hash($newPass, PASSWORD_DEFAULT), $user['id']));
}

ok(array('id' => $user['id'], 'name' => $name, 'email' => $user['email'], 'phone' => $phone === '' ? null : $phone), 'Profile updated.');

logout.php deletes this device's token. Other devices stay logged in.

PHP – backend/api/auth/logout.php
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
require_auth();

db()->prepare('DELETE FROM user_tokens WHERE token_hash = ?')->execute(array(hash('sha256', bearer_token())));
ok(null, 'Logged out.');

Step 4: Test with curl

Terminal
# Login (JSON body)
curl -X POST http://localhost:8000/api/auth/login.php \
  -H "Content-Type: application/json" \
  -d '{"email":"test@karteasy.in","password":"Test@123"}'

# Copy the token from the answer, then call a protected endpoint
TOKEN=paste-the-64-character-token-here
curl http://localhost:8000/api/auth/me.php -H "Authorization: Bearer $TOKEN"

# Logout
curl -X POST http://localhost:8000/api/auth/logout.php -H "Authorization: Bearer $TOKEN"

These are the real answers from the KartEasy API:

Login – success
POST /karteasy/api/auth/login.php {"email": "test@karteasy.in", "password": "Test@123"} HTTP 200 { "success": true, "message": "Welcome back, Riya Sharma!", "data": { "token": "d626105b32…(64 hex chars)", "user": { "id": 1, "name": "Riya Sharma", "email": "test@karteasy.in", "phone": "9876543210" } } }
Login – wrong password
POST /karteasy/api/auth/login.php {"email": "test@karteasy.in", "password": "wrong"} HTTP 401 { "success": false, "message": "Wrong email or password.", "data": null }
Register – email already used
POST /karteasy/api/auth/register.php {"name": "Riya", "email": "test@karteasy.in", "password": "secret1"} HTTP 409 { "success": false, "message": "This email is already registered. Please login.", "data": null }
Register – invalid email
POST /karteasy/api/auth/register.php {"name": "Aman", "email": "aman@x", "password": "abc123"} HTTP 422 { "success": false, "message": "Please enter a valid email.", "data": null }
Profile with token
GET /karteasy/api/auth/me.php Authorization: Bearer 3f9c…e1a7 HTTP 200 { "success": true, "message": "OK", "data": { "id": 1, "name": "Riya Sharma", "email": "test@karteasy.in", "phone": "9876543210", "created_at": "2026-10-19 01:28:55", "order_count": 0, "wishlist_count": 0, "cart_count": 0 } }
Protected endpoint without token
GET /karteasy/api/cart/list.php HTTP 401 { "success": false, "message": "Please login to continue.", "data": null }
Logout, then use the same token again
POST /karteasy/api/auth/logout.php Authorization: Bearer 3f9c…e1a7 {} HTTP 200 { "success": true, "message": "Logged out.", "data": null } GET /karteasy/api/auth/me.php Authorization: Bearer 3f9c…e1a7 HTTP 401 { "success": false, "message": "Session expired. Please login again.", "data": null }

Common mistakes

  • Authorization header missing on hosting → keep the RewriteRule in backend/.htaccess.
  • Comparing passwords with == → always password_verify().
  • Returning password_hash in JSON → select only the columns you need.
  • Using rand() or uniqid() for tokens → they are guessable; use random_bytes().
  • Never expiring tokens → we keep 30 days and clean old ones on login.

Frequently Asked Questions

Why not use PHP sessions for the app?
Sessions depend on cookies, which mobile apps do not handle automatically. A token in the Authorization header is simple, works from any client, and the server can revoke it by deleting one row.
Why store only the SHA-256 of the token?
If someone ever steals a database backup, they get hashes, not working tokens. It is the same idea as password hashing, but SHA-256 is enough here because tokens are long random strings.
Is this JWT?
No. This is an opaque random token checked against the database. It is easier to understand and to revoke than JWT, and fast enough for a shop app.
← E-commerce App Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicProducts API: Search, Filter, Pages Your progress: 4 / 28 (14%) · Lesson 5 / 28 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free