Step 9 — Authentication
Anyone on the internet can browse mugs. Creating products and placing orders should only work for someone the shop recognizes — Ada after she logs in, or a staff account that manages the catalog.
This step teaches what “authentication” means in any web app, then wires it for Pionia Shop with a JWT.
What is authentication? (in general)
Two related ideas get mixed up. Keep them separate:
| Word | Plain English | Shop example |
|---|---|---|
| Authentication | Who are you? | Ada proves email + password |
| Authorization | What may you do? | Staff may product.create; Ada may order.place |
Without authentication, every request is anonymous. Your API cannot tell “Ada” from “a random bot.”
Sessions vs tokens (big picture)
Browsers historically used cookies + server sessions: “remember this browser.” Mobile apps and SPAs often use tokens: after login, the server returns a string; the client sends it on later calls as Authorization: Bearer ….
Pionia Shop uses the token style with a JWT (JSON Web Token): a signed blob that carries Ada’s id (and optional permissions) so the server does not need to look up a session row on every request.
1. Ada → customer.login (email + password)
2. API → { "token": "eyJ…" }
3. Ada → order.place + header Authorization: Bearer eyJ…
4. API verifies signature → knows it is Ada → runs the actionIf the token is missing or invalid, protected actions return 401. If the token is fine but she lacks a permission, you return 403 (authorization).
How Pionia Shop uses it
- Configure a signing secret (
JWT_SECRET). - Register
JwtAuthenticationso Bearer tokens are read automatically. - Implement
customer.loginto issue a token withjwt_encode(). - Mark write actions with
#[Authenticated](or#[Can(...)]for permissions).
What you will learn
- Configure
JWT_SECRETand registerJwtAuthentication - Implement
customer.loginwithjwt_encode() - Protect
product.createwhile leavingproduct.listpublic
Configure JWT
Step 1 Step
1. Secret in .env
In environment/.env:
JWT_SECRET=paste-a-long-random-value
JWT_TTL=3600Generate a secret in the shell:
php pionia shell
# then: secure_random_hex(32);Never commit a real secret. Local .env stays on your machine.
Step 1 Step
2. Register the authentication backend
In environment/settings.ini:
[app_authentications]
jwt = "Pionia\Auth\JwtAuthentication"This tells Pionia: “on each request, look for a Bearer JWT and attach the user when it is valid.”
CustomerService login
You need a place to store shoppers. Scaffold a customers table (email + password_hash), migrate, and store passwords with hash_password() when someone registers.
php pionia make:service Customeruse Pionia\Auth\Attributes\Authenticated;
#[Authenticated(except: ['login', 'register'])]
class CustomerService extends Service
{
protected function loginAction(Arrayable $data): ApiResponse
{
$customer = table('customers')
->filter(['email' => $data->get('email')])
->get();
if (!$customer || !verify_password($data->get('password'), $customer->password_hash)) {
return response(401, 'Invalid credentials');
}
$token = jwt_encode([
'sub' => $customer->id,
'email' => $customer->email,
'permissions' => ['product.manage', 'order.place'],
]);
return response(0, 'Logged in', ['token' => $token]);
}
}Register 'customer' => CustomerService::class on MainSwitch.
except: ['login', 'register'] keeps those actions public — otherwise Ada could never log in.
Protect catalog writes
use Pionia\Auth\Attributes\Authenticated;
// or #[Can('product.manage')] when you care about permissions
#[Authenticated]
protected function createAction(Arrayable $data): ApiResponse
{
// same save as Step 7–8
}Leave listAction without the attribute so guests can still browse.
Try the flow
# 1) login
curl -s -X POST http://127.0.0.1:8000/api/v1/ \
-H "Content-Type: application/json" \
-d '{"service":"customer","action":"login","email":"ada@pionia.shop","password":"secret"}'
# 2) create with token (paste token from step 1)
curl -s -X POST http://127.0.0.1:8000/api/v1/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"service":"product","action":"create","name":"Cap","price":15,"stock":20}'Try create without the header — you should get 401.
More depth: JWT authentication and Protecting actions.
Common mistakes
- Protecting
loginby accident — keep it inexcept - Forgetting
[app_authentications]— tokens never attach a user - Putting the password in the JWT — never; only ids / permissions
- Confusing 401 (not logged in) with 403 (logged in but not allowed)