Login Integration with JWT
Authentication, password management, and JWT issuance are powered by byjg/authuser. This section describes how the default wiring works and what you need to change when you customize users or credentials.
Database Schema
Two tables ship with the reference architecture, in db/base.sql. This is the MySQL version; a project created for
PostgreSQL or SQLite has the same tables in that database's SQL (a uuid or blob user id, and a trigger in place of
ON UPDATE CURRENT_TIMESTAMP):
CREATE TABLE `users` (
`userid` BINARY(16) DEFAULT (uuid_to_bin(uuid())) NOT NULL,
`name` VARCHAR(50),
`email` VARCHAR(120),
`username` VARCHAR(20) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`role` VARCHAR(50),
`created_at` DATETIME DEFAULT (NOW()),
`updated_at` DATETIME ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` DATETIME,
PRIMARY KEY (`userid`),
UNIQUE KEY `ix_username` (`username`),
UNIQUE KEY `ix_email` (`email`)
) ENGINE=InnoDB;
CREATE TABLE `users_property` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50),
`value` VARCHAR(250),
`userid` BINARY(16) NOT NULL,
CONSTRAINT `fk_user_property` FOREIGN KEY (`userid`) REFERENCES `users` (`userid`)
) ENGINE=InnoDB;
users.roledrives the built-in RBAC helpers (User::ROLE_ADMIN/User::ROLE_USER). Add more roles if needed.users_propertystores arbitrary metadata (profile picture, MFA flags, etc.) and is automatically loaded byUsersService.
Model Mapping
Models live in src/Model and already extend the AuthUser abstractions:
RestReferenceArchitecture\Model\UserextendsByJG\Authenticate\Model\UserModel. It usesFieldUuidAttributeand OpenAPI attributes to sync the schema.RestReferenceArchitecture\Model\UserPropertiesextendsByJG\Authenticate\Model\UserPropertiesModel.
To customize fields, either modify these models or create your own classes and update the container bindings described below.
AuthUser Service Configuration
config/dev/02-security.php wires the repositories and service:
use ByJG\Authenticate\Enum\LoginField;
use ByJG\Authenticate\Repository\UserPropertiesRepository;
use ByJG\Authenticate\Repository\UsersRepository;
use ByJG\Authenticate\Service\UsersService;
use RestReferenceArchitecture\Model\User;
use RestReferenceArchitecture\Model\UserProperties;
return [
UsersRepository::class => DI::bind(UsersRepository::class)
->withInjectedConstructorOverrides([
'usersClass' => User::class,
])
->toSingleton(),
UserPropertiesRepository::class => DI::bind(UserPropertiesRepository::class)
->withInjectedConstructorOverrides([
'propertiesClass' => UserProperties::class,
])
->toSingleton(),
UsersService::class => DI::bind(UsersService::class)
->withInjectedConstructorOverrides([
'loginField' => LoginField::Email, // or LoginField::Username
])
->toSingleton(),
];
- Swap
LoginField::EmailforLoginField::Usernameif you prefer username-based logins. - If you add custom user models, update the
usersClass/propertiesClassoverrides.
Password Policy Configuration
The password policy also lives in config/dev/02-security.php:
use ByJG\Authenticate\Definition\PasswordDefinition;
return [
PasswordDefinition::class => DI::bind(PasswordDefinition::class)
->withConstructorArgs([[
PasswordDefinition::MINIMUM_CHARS => 12,
PasswordDefinition::REQUIRE_UPPERCASE => 1,
PasswordDefinition::REQUIRE_LOWERCASE => 1,
PasswordDefinition::REQUIRE_SYMBOLS => 1,
PasswordDefinition::REQUIRE_NUMBERS => 1,
PasswordDefinition::ALLOW_WHITESPACE => 0,
PasswordDefinition::ALLOW_SEQUENTIAL => 0,
PasswordDefinition::ALLOW_REPEATED => 0,
]])
->toSingleton(),
];
UsersRepository receives this binding by type (its binding is withInjectedConstructorOverrides)
and checks it on every save: a plain password outside the rules throws InvalidArgumentException,
also for a new User(...) saved without UsersService; a stored hash is not checked.
UsersService takes it from the repository and applies it to the users it creates or loads, so
setPassword() rejects a weak password right away. Do not pass it to UsersService.
AuthUser uses the mapper configured on User::$password to hash passwords: bcrypt
(PasswordBcryptMapper), set by gluo-core's BaseUser. A SHA-1 hash of an older database
(PasswordSha1Mapper, the default before 7.0) still logs in, and the login replaces it with a bcrypt
hash; while the column is still CHAR(40) the SHA-1 hash is kept. Override the mapper on
User::$password if you need a different algorithm.
JWT Configuration
Secret & Wrapper
JwtWrapper bindings read the secret from each environment’s credentials.env file.
JWT_SECRET=ZGV2LS1qd3Qtc2VjcmV0LWtleS1mb3ItbG9jYWwtZGV2ZWxvcG1lbnQtb25seS1wYWQtQUJDREVGR0hJSktMTU5P
JWT_SECRET must be a base64-encoded string whose decoded value is at least 64 bytes (required by HS512).
composer create-project generates a fresh secret for every environment automatically.
To regenerate manually, use php gluo terminal:
APP_ENV=dev php gluo terminal
php> \ByJG\JwtWrapper\JwtWrapper::generateSecret(64)
# => 'OFbOmC2VxlgQHNrBLa/wyj7/fFkgPnLpckbXMVuIU7Sqb3RTztNx3xzEYaoeA31JUpvBjkD7FRKBFGQ0+fnTig=='
Copy the output and replace JWT_SECRET in the appropriate credentials.env.
Each environment gets its own config/<env>/credentials.env, and it is versioned (it is not in .gitignore). Keep real secrets out of it: put local ones in config/.env, which is in .gitignore, and production ones in OS environment variables (see Configuration).
use ByJG\Config\Param;
use ByJG\JwtWrapper\JwtHashHmacSecret;
use ByJG\JwtWrapper\JwtKeyInterface;
use ByJG\JwtWrapper\JwtWrapper;
return [
JwtKeyInterface::class => DI::bind(JwtHashHmacSecret::class)
->withConstructorArgs([Param::get('JWT_SECRET')])
->toSingleton(),
JwtWrapper::class => DI::bind(JwtWrapper::class)
->withConstructorArgs([Param::get('API_SERVER'), Param::get(JwtKeyInterface::class)])
->toSingleton(),
];
See the caution above — never commit real secrets in credentials.env.
Available Endpoints
src/Controller/LoginController.php and src/Controller/ProfileController.php provide:
| Endpoint | Description |
|---|---|
POST /login | Validate credentials, return JWT |
POST /refreshtoken | Refresh an expiring token (last 5 minutes) |
POST /login/resetrequest | Send password reset token + email code |
POST /login/confirmcode | Confirm the email + code pair |
POST /login/resetpassword | Set a new password after confirmation |
GET /profile | Profile of the authenticated user (from the JWT) |
PUT /profile | Update the authenticated user's name, email, and language (en/fr/pt) |
GET /sampleprotected/ping | Sample endpoint requiring authentication |
The frontend consumes these directly — its frontend/src/lib/api.js stores the JWT and
transparently calls POST /refreshtoken when the token is close to expiring. See the
Frontend guide.
The password reset flow sends emails through ByJG\Mail\Wrapper\MailWrapperInterface. Customize the sender/template in config/dev/06-external.php, which defines the MAIL_ENVELOPE factory.
To change the subject or the template of the reset email, override these hooks in
src/Controller/LoginController.php:
protected function getResetEmailSubject(): string
{
return 'Acme - Password Reset'; // default: 'Password Reset'
}
protected function getResetEmailTemplate(): string
{
return 'email_code.html'; // a file in templates/emails/
}
Testing Authentication
# Obtain a token
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username":"[email protected]","password":"!P4ssw0rdstr!"}'
# Use the token
curl -X GET http://localhost:8080/sampleprotected/ping \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
Using JWT in Your Code
Protect Endpoints
use ByJG\Gluo\Attribute\RequireAuthenticated;
use ByJG\Gluo\Attribute\RequireRole;
use RestReferenceArchitecture\Model\User;
class MyProtectedRest
{
#[RequireAuthenticated]
public function getProtectedData(HttpResponse $response): void
{
$response->write(['message' => 'You are authenticated!']);
}
#[RequireRole(User::ROLE_ADMIN)]
public function getAdminData(HttpResponse $response): void
{
$response->write(['message' => 'You are an admin!']);
}
}
Read JWT Claims
use ByJG\Gluo\Util\JwtContext;
// JwtContext::setRequest() is called by #[RequireAuthenticated] and #[RequireRole]
$userId = JwtContext::getUserId();
$userName = JwtContext::getName();
$userRole = JwtContext::getRole(); // e.g., "admin" or "user"
Need the full payload? Access HttpRequest::attribute('jwt.data') directly, but prefer JwtContext helpers to keep controllers focused.
Additional Resources
src/Controller/LoginController.php– Reference implementation for all endpoints.ByJG\Gluo\Util\JwtContext(byjg/gluo-core) – Helper used by controllers and tests.config/dev/02-security.php– Central location for auth wiring.