diff --git a/README.md b/README.md index a16b060..a45a321 100644 --- a/README.md +++ b/README.md @@ -6,111 +6,81 @@ [![GitHub license](https://img.shields.io/github/license/byjg/php-authuser.svg)](https://opensource.byjg.com/opensource/licensing.html) [![GitHub release](https://img.shields.io/github/release/byjg/php-authuser.svg)](https://github.com/byjg/php-authuser/releases/) -A simple and customizable class for enable user authentication inside your application. It is available on XML files, Relational Databases. +A simple and customizable library for user authentication in PHP applications. It supports multiple storage backends including databases and XML files. -The main purpose is just to handle all complexity of validate a user, add properties and create access token abstracting the database layer. -This class can persist into session (or file, memcache, etc) the user data between requests. +The main purpose is to handle all complexity of user validation, authentication, properties management, and access tokens, abstracting the database layer. +This class can persist user data into session (or file, memcache, etc.) between requests. -## Creating a Users handling class +## Documentation -Using the FileSystem (XML) as the user storage: +- [Getting Started](docs/getting-started.md) +- [Installation](docs/installation.md) +- [User Management](docs/user-management.md) +- [Authentication](docs/authentication.md) +- [Session Context](docs/session-context.md) +- [User Properties](docs/user-properties.md) +- [Database Storage](docs/database-storage.md) +- [Password Validation](docs/password-validation.md) +- [JWT Tokens](docs/jwt-tokens.md) +- [Custom Fields](docs/custom-fields.md) +- [Mappers](docs/mappers.md) +- [Examples](docs/examples.md) -```php -isAuthenticated()) { - - // Get the userId of the authenticated users - $userId = $sessionContext->userInfo(); - - // Get the user and your name - $user = $users->getById($userId); - echo "Hello: " . $user->getName(); +use ByJG\Authenticate\UsersDBDataset; +use ByJG\Authenticate\SessionContext; +use ByJG\AnyDataset\Db\Factory as DbFactory; +use ByJG\Cache\Factory; + +// Initialize with database +$users = new UsersDBDataset(DbFactory::getDbInstance('mysql://user:pass@host/db')); + +// Create and authenticate a user +$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); +$authenticatedUser = $users->isValidUser('johndoe', 'SecurePass123'); + +if ($authenticatedUser !== null) { + $sessionContext = new SessionContext(Factory::createSessionPool()); + $sessionContext->registerLogin($authenticatedUser->getUserid()); + echo "Welcome, " . $authenticatedUser->getName(); } ``` -## Saving extra info into the user session - -You can save data in the session data exists only during the user is logged in. Once the user logged off the -data stored with the user session will be released. - -Store the data for the current user session: - -```php -setSessionData('key', 'value'); -``` - -Getting the data from the current user session: - -```php -getSessionData('key'); -``` - -Note: If the user is not logged an error will be throw +See [Getting Started](docs/getting-started.md) for a complete introduction and [Examples](docs/examples.md) for more use cases. -## Adding a custom property to the users +## Features -```php -getById($userId); -$user->setField('somefield', 'somevalue'); -$users->save(); -``` +- **User Management** - Complete CRUD operations. See [User Management](docs/user-management.md) +- **Authentication** - Username/email + password or JWT tokens. See [Authentication](docs/authentication.md) and [JWT Tokens](docs/jwt-tokens.md) +- **Session Management** - PSR-6 compatible cache storage. See [Session Context](docs/session-context.md) +- **User Properties** - Store custom key-value metadata. See [User Properties](docs/user-properties.md) +- **Password Validation** - Built-in strength requirements. See [Password Validation](docs/password-validation.md) +- **Multiple Storage** - Database (MySQL, PostgreSQL, SQLite, etc.) or XML files. See [Database Storage](docs/database-storage.md) +- **Custom Schema** - Map to existing database tables. See [Database Storage](docs/database-storage.md) +- **Field Mappers** - Transform data during read/write. See [Mappers](docs/mappers.md) +- **Extensible Model** - Add custom fields easily. See [Custom Fields](docs/custom-fields.md) -## Logout from a session - -```php -registerLogout(); -``` - -## Important note about SessionContext - -`SessionContext` object will store the info about the current context. -As SessionContext uses CachePool interface defined in PSR-6 you can set any storage -to save your session context. - -In our examples we are using a regular PHP Session for store the user context -(`Factory::createSessionPool()`). But if you are using another store like MemCached -you have to define a UNIQUE prefix for that session. Note if TWO users have the same -prefix you probably have an unexpected result for the SessionContext. +## Running Tests -Example for memcached: +Because this project uses PHP Session you need to run the unit test the following manner: -```php - 'fieldname of userid', - UserDefinition::FIELD_NAME => 'fieldname of name', - UserDefinition::FIELD_EMAIL => 'fieldname of email', - UserDefinition::FIELD_USERNAME => 'fieldname of username', - UserDefinition::FIELD_PASSWORD => 'fieldname of password', - UserDefinition::FIELD_CREATED => 'fieldname of created', - UserDefinition::FIELD_ADMIN => 'fieldname of admin' - ] -); -``` - -### Adding custom modifiers for read and update - -```php - the current value to be updated -// $instance -> The array with all other fields; -$userDefinition->defineClosureForUpdate(UserDefinition::FIELD_PASSWORD, function ($value, $instance) { - return strtoupper(sha1($value)); -}); - -// Defines a custom function to be applied After the field UserDefinition::FIELD_CREATED is read but before -// the user get the result -// $value --> the current value retrieved from database -// $instance -> The array with all other fields; -$userDefinition->defineClosureForSelect(UserDefinition::FIELD_CREATED, function ($value, $instance) { - return date('Y', $value); -}); - -// If you want make the field READONLY just do it: -$userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); -``` - -## Extending UserModel - -It is possible extending the UserModel table, since you create a new class extending from UserModel to add the new fields. - -For example, imagine your table has one field called "otherfield". - -You'll have to extend like this: - -```php -setOtherfield($field); - } - - public function getOtherfield() - { - return $this->otherfield; - } - - public function setOtherfield($otherfield) - { - $this->otherfield = $otherfield; - } -} -``` - -After that you can use your new definition: - -```php - byjg/micro-orm byjg/authuser --> byjg/cache-engine - byjg/authuser --> byjg/jwt-wrapper + byjg/authuser --> byjg/jwt-wrapper ``` - ---- [Open source ByJG](http://opensource.byjg.com) diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000..7ba0842 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,162 @@ +--- +sidebar_position: 4 +title: Authentication +--- + +# Authentication + +## Validating User Credentials + +Use the `isValidUser()` method to validate a username/email and password combination: + +```php +isValidUser('johndoe', 'SecurePass123'); + +if ($user !== null) { + echo "Authentication successful!"; + echo "User ID: " . $user->getUserid(); +} else { + echo "Invalid credentials"; +} +``` + +:::tip Login Field +The `isValidUser()` method uses the login field defined in your `UserDefinition`. This can be either the email or username field. +::: + +## Password Hashing + +By default, passwords are automatically hashed using SHA-1 when saved. The library uses the `PasswordSha1Mapper` for this purpose. + +```php +setPassword('plaintext password'); +$users->save($user); + +// The password is stored as SHA-1 hash in the database +``` + +:::warning SHA-1 Deprecation +SHA-1 is used for backward compatibility. For new projects, consider implementing a custom password hasher using bcrypt or Argon2. See [Mappers](mappers.md#example-bcrypt-password-mapper) for details. +::: + +:::tip Enforce Password Strength +To enforce password policies (minimum length, complexity rules, etc.), see [Password Validation](password-validation.md). +::: + +## JWT Token Authentication (Recommended) + +For modern, stateless authentication, use JWT tokens. This is the **recommended approach** for new applications as it provides better security and scalability. + +```php +createAuthToken( + 'johndoe', // Login + 'SecurePass123', // Password + $jwtWrapper, + 3600, // Expires in 1 hour (seconds) + [], // Additional user info to save + ['role' => 'admin'] // Additional token data +); + +if ($token !== null) { + echo "Token: " . $token; +} +``` + +### Validating JWT Tokens + +```php +isValidToken('johndoe', $jwtWrapper, $token); + +if ($result !== null) { + $user = $result['user']; + $tokenData = $result['data']; + + echo "User: " . $user->getName(); + echo "Role: " . $tokenData['role']; +} +``` + +:::info Token Storage +When a JWT token is created, a hash of the token is stored in the user's properties as `TOKEN_HASH`. This ensures tokens can be invalidated if needed. +::: + +:::tip Why JWT? +JWT tokens provide stateless authentication, better scalability, and easier integration with modern frontend frameworks and mobile applications. They're also more secure than traditional PHP sessions. +::: + +## Session-Based Authentication (Legacy) + +:::warning Deprecated +SessionContext relies on traditional PHP sessions and is less secure than JWT tokens. It's maintained for backward compatibility only. **For new projects, use JWT tokens instead.** +::: + +### Basic Authentication Flow + +```php +isValidUser('johndoe', 'SecurePass123'); + +if ($user !== null) { + // 2. Create session context + $sessionContext = new SessionContext(Factory::createSessionPool()); + + // 3. Register login + $sessionContext->registerLogin($user->getUserid()); + + // 4. User is now authenticated + echo "Welcome, " . $user->getName(); +} +``` + +### Checking Authentication Status + +```php +isAuthenticated()) { + $userId = $sessionContext->userInfo(); + $user = $users->getById($userId); + echo "Hello, " . $user->getName(); +} else { + echo "Please log in"; +} +``` + +### Logging Out + +```php +registerLogout(); +``` + +## Security Best Practices + +1. **Always use HTTPS** in production to prevent credential theft +2. **Implement rate limiting** to prevent brute force attacks +3. **Use strong passwords** - see [Password Validation](password-validation.md) +4. **Set appropriate session timeouts** +5. **Validate and sanitize** all user inputs + +## Next Steps + +- [Session Context](session-context.md) - Manage user sessions +- [JWT Tokens](jwt-tokens.md) - Deep dive into JWT authentication +- [Password Validation](password-validation.md) - Enforce password policies diff --git a/docs/custom-fields.md b/docs/custom-fields.md new file mode 100644 index 0000000..050c6e8 --- /dev/null +++ b/docs/custom-fields.md @@ -0,0 +1,379 @@ +--- +sidebar_position: 10 +title: Custom Fields +--- + +# Custom Fields + +You can extend the `UserModel` to add custom fields that match your database schema. + +:::info When to Use This +This guide is for **adding new fields** beyond the standard user fields. If you just need to **map existing database columns** to the standard fields, see [Database Storage](database-storage.md#custom-database-schema) instead. +::: + +## Extending UserModel + +### Creating a Custom User Model + +```php +phone = $phone; + $this->department = $department; + } + + public function getPhone(): ?string + { + return $this->phone; + } + + public function setPhone(?string $phone): void + { + $this->phone = $phone; + } + + public function getDepartment(): ?string + { + return $this->department; + } + + public function setDepartment(?string $department): void + { + $this->department = $department; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getProfilePicture(): ?string + { + return $this->profilePicture; + } + + public function setProfilePicture(?string $profilePicture): void + { + $this->profilePicture = $profilePicture; + } +} +``` + +## Database Schema + +Add the custom fields to your users table: + +```sql +CREATE TABLE users +( + userid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(50), + email VARCHAR(120), + username VARCHAR(15) NOT NULL, + password CHAR(40) NOT NULL, + created DATETIME, + admin ENUM('Y','N'), + -- Custom fields + phone VARCHAR(20), + department VARCHAR(50), + title VARCHAR(50), + profile_picture VARCHAR(255), + + CONSTRAINT pk_users PRIMARY KEY (userid) +) ENGINE=InnoDB; +``` + +## Configuring UserDefinition + +Map the custom fields in your `UserDefinition`: + +```php + 'userid', + UserDefinition::FIELD_NAME => 'name', + UserDefinition::FIELD_EMAIL => 'email', + UserDefinition::FIELD_USERNAME => 'username', + UserDefinition::FIELD_PASSWORD => 'password', + UserDefinition::FIELD_CREATED => 'created', + UserDefinition::FIELD_ADMIN => 'admin', + // Custom fields + 'phone' => 'phone', + 'department' => 'department', + 'title' => 'title', + 'profilePicture' => 'profile_picture' + ] +); +``` + +## Using the Custom Model + +### Creating Users + +```php +setName('John Doe'); +$user->setEmail('john@example.com'); +$user->setUsername('johndoe'); +$user->setPassword('SecurePass123'); +$user->setPhone('+1-555-1234'); +$user->setDepartment('Engineering'); +$user->setTitle('Senior Developer'); + +$users->save($user); +``` + +### Retrieving Users + +```php +getById($userId); + +// Access custom fields +echo $user->getName(); +echo $user->getPhone(); +echo $user->getDepartment(); +echo $user->getTitle(); +``` + +### Updating Custom Fields + +```php +getById($userId); +$user->setDepartment('Sales'); +$user->setTitle('Sales Manager'); +$users->save($user); +``` + +## Read-Only Fields + +You can mark fields as read-only to prevent updates: + +```php +markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); + +// Make custom field read-only +$userDefinition->markPropertyAsReadOnly('phone'); +``` + +Read-only fields: +- Can be set during creation +- Cannot be updated after creation +- Are ignored during updates + +## Auto-Generated Fields + +### Auto-Increment IDs + +For auto-increment IDs, the database handles generation automatically. No configuration needed. + +### UUID Fields + +For UUID primary keys: + +```php +defineGenerateKey(UserIdGeneratorMapper::class); +``` + +### Custom ID Generation + +Create a custom mapper for custom ID generation: + +```php +defineGenerateKey(CustomIdMapper::class); +``` + +## Field Transformation + +You can transform fields during read/write operations using mappers. See [Mappers](mappers.md) for details. + +## Complex Data Types + +### JSON Fields + +For storing JSON data in custom fields: + +```php +defineMapperForUpdate('metadata', JsonMapper::class); +$userDefinition->defineMapperForSelect('metadata', JsonDecodeMapper::class); +``` + +### Date/Time Fields + +```php +format('Y-m-d H:i:s'); + } + return $value; + } +} + +$userDefinition->defineMapperForUpdate('created', DateTimeMapper::class); +``` + +## Complete Example + +```php + 'userid', + UserDefinition::FIELD_NAME => 'name', + UserDefinition::FIELD_EMAIL => 'email', + UserDefinition::FIELD_USERNAME => 'username', + UserDefinition::FIELD_PASSWORD => 'password', + UserDefinition::FIELD_CREATED => 'created', + UserDefinition::FIELD_ADMIN => 'admin', + 'phone' => 'phone', + 'department' => 'department', + 'title' => 'title', + 'profilePicture' => 'profile_picture' + ] +); + +// Make created field read-only +$userDefinition->markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); + +// Initialize user management +$users = new UsersDBDataset($dbDriver, $userDefinition); + +// Create a user +$user = new CustomUserModel(); +$user->setName('Jane Smith'); +$user->setEmail('jane@example.com'); +$user->setUsername('janesmith'); +$user->setPassword('SecurePass123'); +$user->setPhone('+1-555-5678'); +$user->setDepartment('Marketing'); +$user->setTitle('Marketing Director'); + +$savedUser = $users->save($user); + +// Retrieve and update +$user = $users->getById($savedUser->getUserid()); +$user->setTitle('VP of Marketing'); +$users->save($user); +``` + +## When to Use Custom Fields vs Properties + +| Use Custom Fields When | Use Properties When | +|------------------------|---------------------| +| Field is used frequently | Field is rarely used | +| Field is searched/filtered | Field is key-value metadata | +| Field is fixed schema | Field is dynamic/flexible | +| Better performance needed | Schema flexibility needed | +| Field is required | Field is optional | + +## Next Steps + +- [Mappers](mappers.md) - Custom field transformations +- [Database Storage](database-storage.md) - Schema configuration +- [User Properties](user-properties.md) - Flexible metadata storage diff --git a/docs/database-storage.md b/docs/database-storage.md new file mode 100644 index 0000000..8e9c872 --- /dev/null +++ b/docs/database-storage.md @@ -0,0 +1,256 @@ +--- +sidebar_position: 7 +title: Database Storage +--- + +# Database Storage + +The library supports storing users in relational databases through the `UsersDBDataset` class. + +## Database Setup + +### Default Schema + +The default database structure uses two tables: + +```sql +CREATE TABLE users +( + userid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(50), + email VARCHAR(120), + username VARCHAR(15) NOT NULL, + password CHAR(40) NOT NULL, + created DATETIME, + admin ENUM('Y','N'), + + CONSTRAINT pk_users PRIMARY KEY (userid) +) ENGINE=InnoDB; + +CREATE TABLE users_property +( + customid INTEGER AUTO_INCREMENT NOT NULL, + name VARCHAR(20), + value VARCHAR(100), + userid INTEGER NOT NULL, + + CONSTRAINT pk_custom PRIMARY KEY (customid), + CONSTRAINT fk_custom_user FOREIGN KEY (userid) REFERENCES users (userid) +) ENGINE=InnoDB; +``` + +## Basic Usage + +### Using Default Configuration + +```php + 'user_id', + UserDefinition::FIELD_NAME => 'full_name', + UserDefinition::FIELD_EMAIL => 'email_address', + UserDefinition::FIELD_USERNAME => 'user_name', + UserDefinition::FIELD_PASSWORD => 'password_hash', + UserDefinition::FIELD_CREATED => 'date_created', + UserDefinition::FIELD_ADMIN => 'is_admin' + ] +); + +$users = new UsersDBDataset($dbDriver, $userDefinition); +``` + +### Custom Properties Table + +```php + 'id', + UserDefinition::FIELD_NAME => 'fullname', + UserDefinition::FIELD_EMAIL => 'email', + UserDefinition::FIELD_USERNAME => 'username', + UserDefinition::FIELD_PASSWORD => 'pwd', + UserDefinition::FIELD_CREATED => 'created_at', + UserDefinition::FIELD_ADMIN => 'is_admin' + ] +); + +// Custom properties definition +$propertiesDefinition = new UserPropertiesDefinition( + 'app_user_meta', + 'id', + 'meta_key', + 'meta_value', + 'user_id' +); + +// Initialize +$users = new UsersDBDataset($dbDriver, $userDefinition, $propertiesDefinition); + +// Use it +$user = $users->addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); +``` + +## XML/File Storage + +For simple applications or development, you can use XML file storage: + +```php +addUser('John Doe', 'johndoe', 'john@example.com', 'password123'); +``` + +:::warning Production Use +XML file storage is suitable for development and small applications. For production applications with multiple users, use database storage. +::: + +## Architecture + +```text + ┌───────────────────┐ + │ SessionContext │ + └───────────────────┘ + │ +┌────────────────────────┐ ┌────────────────────────┐ +│ UserDefinition │─ ─ ┐ │ ─ ─ ┤ UserModel │ +└────────────────────────┘ ┌───────────────────┐ │ └────────────────────────┘ +┌────────────────────────┐ └────│ UsersInterface │────┐ ┌────────────────────────┐ +│ UserPropertyDefinition │─ ─ ┘ └───────────────────┘ ─ ─ ┤ UserPropertyModel │ +└────────────────────────┘ ▲ └────────────────────────┘ + │ + ┌────────────────────────┼─────────────────────────┐ + │ │ │ + │ │ │ + │ │ │ + ┌───────────────────┐ ┌───────────────────┐ ┌────────────────────┐ + │ UsersAnyDataset │ │ UsersDBDataset │ │ Custom Impl. │ + └───────────────────┘ └───────────────────┘ └────────────────────┘ +``` + +- **UserInterface**: Base interface for all implementations +- **UsersDBDataset**: Database implementation +- **UsersAnyDataset**: XML file implementation +- **UserModel**: The user data model +- **UserPropertyModel**: The user property data model +- **UserDefinition**: Maps model to database schema +- **UserPropertiesDefinition**: Maps properties to database schema + +## Next Steps + +- [User Management](user-management.md) - Managing users +- [Custom Fields](custom-fields.md) - Extending UserModel +- [Mappers](mappers.md) - Custom field transformations diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..deba131 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,520 @@ +--- +sidebar_position: 12 +title: Complete Examples +--- + +# Complete Examples + +This page contains complete, working examples for common use cases. + +## Simple Web Application + +### Setup + +```php +isValidUser($username, $password); + + if ($user !== null) { + $sessionContext->registerLogin($user->getUserid()); + $sessionContext->setSessionData('login_time', time()); + + header('Location: dashboard.php'); + exit; + } else { + $error = 'Invalid username or password'; + } + } catch (Exception $e) { + $error = 'An error occurred: ' . $e->getMessage(); + } +} +?> + + + + Login + + +

Login

+ + +
+ + +
+
+ + +
+
+ + +
+ +
+ +

Create an account

+ + +``` + +### Registration Page + +```php + 8, + PasswordDefinition::REQUIRE_UPPERCASE => 1, + PasswordDefinition::REQUIRE_LOWERCASE => 1, + PasswordDefinition::REQUIRE_NUMBERS => 1, + ]); + + $result = $passwordDef->matchPassword($password); + if ($result !== PasswordDefinition::SUCCESS) { + throw new Exception('Password does not meet requirements'); + } + + // Create user + $user = $users->addUser($name, $username, $email, $password); + + // Auto-login + $sessionContext->registerLogin($user->getUserid()); + + header('Location: dashboard.php'); + exit; + + } catch (UserExistsException $e) { + $error = 'Username or email already exists'; + } catch (Exception $e) { + $error = $e->getMessage(); + } +} +?> + + + + Register + + +

Create Account

+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + Minimum 8 characters, at least 1 uppercase, 1 lowercase, and 1 number +
+
+ + +
+ +
+ +

Already have an account?

+ + +``` + +### Protected Dashboard + +```php +isAuthenticated()) { + header('Location: login.php'); + exit; +} + +// Get current user +$userId = $sessionContext->userInfo(); +$user = $users->getById($userId); +$loginTime = $sessionContext->getSessionData('login_time'); +?> + + + + Dashboard + + +

Welcome, getName()) ?>

+ +

Email: getEmail()) ?>

+

Logged in at:

+ + isAdmin($userId)): ?> +

You are an administrator

+

Admin Panel

+ + +

Edit Profile

+

Logout

+ + +``` + +### Logout + +```php +registerLogout(); +session_destroy(); + +header('Location: login.php'); +exit; +``` + +## REST API with JWT + +### API Configuration + +```php + 'Method not allowed'], 405); +} + +$input = json_decode(file_get_contents('php://input'), true); +$username = $input['username'] ?? ''; +$password = $input['password'] ?? ''; + +try { + $token = $users->createAuthToken( + $username, + $password, + $jwtWrapper, + 3600, // 1 hour + [ + 'last_login' => date('Y-m-d H:i:s'), + 'last_ip' => $_SERVER['REMOTE_ADDR'] + ], + [ + 'ip' => $_SERVER['REMOTE_ADDR'] + ] + ); + + if ($token === null) { + jsonResponse(['error' => 'Invalid credentials'], 401); + } + + jsonResponse([ + 'success' => true, + 'token' => $token, + 'expires_in' => 3600 + ]); + +} catch (Exception $e) { + jsonResponse(['error' => $e->getMessage()], 500); +} +``` + +### Protected Endpoint + +```php + 'No token provided'], 401); +} + +$token = $matches[1]; + +try { + // Decode token to get username + $jwtData = $jwtWrapper->extractData($token); + $username = $jwtData->data['login'] ?? null; + + if (!$username) { + jsonResponse(['error' => 'Invalid token'], 401); + } + + // Validate token + $result = $users->isValidToken($username, $jwtWrapper, $token); + + if ($result === null) { + jsonResponse(['error' => 'Token validation failed'], 401); + } + + $user = $result['user']; + + // Handle request + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + // Get user info + jsonResponse([ + 'id' => $user->getUserid(), + 'name' => $user->getName(), + 'email' => $user->getEmail(), + 'username' => $user->getUsername(), + 'admin' => $users->isAdmin($user->getUserid()) + ]); + } elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') { + // Update user info + $input = json_decode(file_get_contents('php://input'), true); + + if (isset($input['name'])) { + $user->setName($input['name']); + } + if (isset($input['email'])) { + $user->setEmail($input['email']); + } + + $users->save($user); + + jsonResponse(['success' => true, 'message' => 'User updated']); + } else { + jsonResponse(['error' => 'Method not allowed'], 405); + } + +} catch (Exception $e) { + jsonResponse(['error' => $e->getMessage()], 500); +} +``` + +## Multi-Tenant Application + +```php +addProperty($userId, 'organization', $orgId); + $users->addProperty($userId, "org_{$orgId}_role", $role); +} + +// Check if user has access to organization +function hasOrganizationAccess($users, $userId, $orgId) +{ + return $users->hasProperty($userId, 'organization', $orgId); +} + +// Get user's role in organization +function getOrganizationRole($users, $userId, $orgId) +{ + return $users->getProperty($userId, "org_{$orgId}_role"); +} + +// Get all users in organization +function getOrganizationUsers($users, $orgId) +{ + return $users->getUsersByProperty('organization', $orgId); +} + +// Usage +$userId = 1; +$orgId = 'org-123'; + +// Add user to organization +addUserToOrganization($users, $userId, $orgId, 'admin'); + +// Check access +if (hasOrganizationAccess($users, $userId, $orgId)) { + $role = getOrganizationRole($users, $userId, $orgId); + echo "User has access as: $role\n"; + + // Get all members + $members = getOrganizationUsers($users, $orgId); + foreach ($members as $member) { + echo "- " . $member->getName() . "\n"; + } +} +``` + +## Permission System + +```php +users = $users; + } + + public function grantPermission($userId, $resource, $action) + { + $permission = "$resource:$action"; + $this->users->addProperty($userId, 'permission', $permission); + } + + public function revokePermission($userId, $resource, $action) + { + $permission = "$resource:$action"; + $this->users->removeProperty($userId, 'permission', $permission); + } + + public function hasPermission($userId, $resource, $action) + { + $permission = "$resource:$action"; + return $this->users->hasProperty($userId, 'permission', $permission); + } + + public function getPermissions($userId) + { + $permissions = $this->users->getProperty($userId, 'permission'); + return is_array($permissions) ? $permissions : [$permissions]; + } +} + +// Usage +$permissionManager = new PermissionManager($users); + +// Grant permissions +$permissionManager->grantPermission($userId, 'posts', 'create'); +$permissionManager->grantPermission($userId, 'posts', 'edit'); +$permissionManager->grantPermission($userId, 'posts', 'delete'); +$permissionManager->grantPermission($userId, 'users', 'view'); + +// Check permissions +if ($permissionManager->hasPermission($userId, 'posts', 'delete')) { + echo "User can delete posts\n"; +} + +// Get all permissions +$permissions = $permissionManager->getPermissions($userId); +print_r($permissions); + +// Revoke permission +$permissionManager->revokePermission($userId, 'posts', 'delete'); +``` + +## Next Steps + +- [Getting Started](getting-started.md) - Basic concepts +- [User Management](user-management.md) - Managing users +- [Authentication](authentication.md) - Authentication methods +- [JWT Tokens](jwt-tokens.md) - Token-based authentication diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..fecc7c9 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,38 @@ +--- +sidebar_position: 1 +title: Getting Started +--- + +# Getting Started + +Auth User PHP is a simple and customizable library for user authentication in PHP applications. It provides an abstraction layer for managing users, authentication, and user properties, supporting multiple storage backends including databases and XML files. + +## Quick Example + +```php +addUser('John Doe', 'johndoe', 'john@example.com', 'SecurePass123'); +$user = $users->isValidUser('johndoe', 'SecurePass123'); + +if ($user !== null) { + $sessionContext = new SessionContext(Factory::createSessionPool()); + $sessionContext->registerLogin($user->getUserid()); + echo "User authenticated successfully!"; +} +``` + +## Next Steps + +- [Installation](installation.md) - Install the library via Composer +- [User Management](user-management.md) - Learn how to manage users +- [Authentication](authentication.md) - Understand authentication methods +- [Session Context](session-context.md) - Manage user sessions diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..4b46c5c --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 2 +title: Installation +--- + +# Installation + +## Requirements + +- PHP 8.1 or higher +- Composer + +## Install via Composer + +Install the library using Composer: + +```bash +composer require byjg/authuser +``` + +## Running Tests + +Because this project uses PHP Session, you need to run the unit tests with the `--stderr` flag: + +```bash +./vendor/bin/phpunit --stderr +``` + +## Next Steps + +- [Getting Started](getting-started.md) - Learn the basics +- [Database Storage](database-storage.md) - Set up database storage diff --git a/docs/jwt-tokens.md b/docs/jwt-tokens.md new file mode 100644 index 0000000..0993d28 --- /dev/null +++ b/docs/jwt-tokens.md @@ -0,0 +1,382 @@ +--- +sidebar_position: 9 +title: JWT Tokens +--- + +# JWT Tokens + +The library provides built-in support for JWT (JSON Web Token) authentication through integration with [byjg/jwt-wrapper](https://github.com/byjg/jwt-wrapper). + +## What is JWT? + +JWT (JSON Web Tokens) is a compact, URL-safe means of representing claims to be transferred between two parties. JWTs are commonly used for: + +- **Stateless authentication** - No server-side session storage needed +- **API authentication** - Perfect for REST APIs and microservices +- **Single Sign-On (SSO)** - Share authentication across domains +- **Mobile apps** - Efficient token-based authentication + +## Setup + +### Creating a JWT Wrapper + +```php +createAuthToken( + 'johndoe', // Login (username or email) + 'password123', // Password + $jwtWrapper, // JWT wrapper instance + 3600 // Expires in 1 hour (seconds) +); + +if ($token !== null) { + // Return token to client + echo json_encode(['token' => $token]); +} else { + // Authentication failed + http_response_code(401); + echo json_encode(['error' => 'Invalid credentials']); +} +``` + +### Token with Custom Data + +You can include additional data in the JWT payload: + +```php +createAuthToken( + 'johndoe', + 'password123', + $jwtWrapper, + 3600, + [], // Update user properties (optional) + [ // Additional token data + 'role' => 'admin', + 'permissions' => ['read', 'write'], + 'tenant_id' => '12345' + ] +); +``` + +### Update User Properties on Login + +```php +createAuthToken( + 'johndoe', + 'password123', + $jwtWrapper, + 3600, + [ // User properties to update + 'last_login' => date('Y-m-d H:i:s'), + 'login_count' => $loginCount + 1 + ], + [ // Token data + 'role' => 'admin' + ] +); +``` + +## Validating JWT Tokens + +### Token Validation + +```php +isValidToken('johndoe', $jwtWrapper, $token); + + if ($result !== null) { + $user = $result['user']; // UserModel instance + $tokenData = $result['data']; // Token payload data + + echo "Authenticated: " . $user->getName(); + echo "Role: " . $tokenData['role']; + } + +} catch (\ByJG\Authenticate\Exception\UserNotFoundException $e) { + echo "User not found"; +} catch (\ByJG\Authenticate\Exception\NotAuthenticatedException $e) { + echo "Token validation failed: " . $e->getMessage(); +} catch (\ByJG\JwtWrapper\JwtWrapperException $e) { + echo "JWT error: " . $e->getMessage(); +} +``` + +### Validation Checks + +The `isValidToken()` method performs the following checks: + +1. **User exists** - Verifies the user account exists +2. **Token hash matches** - Compares stored token hash +3. **JWT signature** - Validates the token signature +4. **Token expiration** - Checks if token has expired + +## Token Storage and Invalidation + +### How Tokens Are Stored + +When a token is created: + +```php +set('TOKEN_HASH', $tokenHash); +``` + +This allows you to invalidate tokens without maintaining a token blacklist. + +### Invalidating Tokens + +#### Logout (Invalidate Current Token) + +```php +removeProperty($userId, 'TOKEN_HASH'); +``` + +#### Force Re-authentication (Invalidate All Tokens) + +```php +createAuthToken($login, $password, $jwtWrapper, 3600); +``` + +## Complete API Example + +### Login Endpoint + +```php +createAuthToken( + $input['username'], + $input['password'], + $jwtWrapper, + 3600, // 1 hour expiration + [ + 'last_login' => date('Y-m-d H:i:s'), + 'last_ip' => $_SERVER['REMOTE_ADDR'] + ], + [ + 'ip' => $_SERVER['REMOTE_ADDR'], + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] + ] + ); + + if ($token === null) { + throw new Exception('Authentication failed'); + } + + echo json_encode([ + 'success' => true, + 'token' => $token, + 'expires_in' => 3600 + ]); + +} catch (Exception $e) { + http_response_code(401); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); +} +``` + +### Protected Endpoint + +```php + 'No token provided']); + exit; +} + +$token = $matches[1]; + +// Extract username from token (you need to decode it first) +try { + $jwtData = $jwtWrapper->extractData($token); + $username = $jwtData->data['login'] ?? null; + + if (!$username) { + throw new Exception('Invalid token structure'); + } + + // Validate token + $result = $users->isValidToken($username, $jwtWrapper, $token); + + if ($result === null) { + throw new Exception('Invalid token'); + } + + $user = $result['user']; + + // Process request + echo json_encode([ + 'success' => true, + 'user' => [ + 'id' => $user->getUserid(), + 'name' => $user->getName(), + 'email' => $user->getEmail() + ] + ]); + +} catch (Exception $e) { + http_response_code(401); + echo json_encode(['error' => $e->getMessage()]); +} +``` + +### Logout Endpoint + +```php +extractData($token); + $username = $jwtData->data['login'] ?? null; + + $user = $users->getByLoginField($username); + if ($user !== null) { + $users->removeProperty($user->getUserid(), 'TOKEN_HASH'); + } + + echo json_encode(['success' => true, 'message' => 'Logged out']); + + } catch (Exception $e) { + echo json_encode(['success' => false, 'error' => $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(['error' => 'No token provided']); +} +``` + +## Token Expiration + +### Setting Expiration Time + +```php +createAuthToken($login, $password, $jwtWrapper, 900); + +// 1 hour +$token = $users->createAuthToken($login, $password, $jwtWrapper, 3600); + +// 24 hours +$token = $users->createAuthToken($login, $password, $jwtWrapper, 86400); + +// 7 days +$token = $users->createAuthToken($login, $password, $jwtWrapper, 604800); +``` + +### Refresh Tokens + +For long-lived sessions, implement a refresh token pattern: + +```php +createAuthToken( + $login, + $password, + $jwtWrapper, + 900, // 15 minutes + [], + ['type' => 'access'] +); + +// Create long-lived refresh token +$refreshToken = $users->createAuthToken( + $login, + $password, + $jwtWrapperRefresh, // Different wrapper/key + 604800, // 7 days + [], + ['type' => 'refresh'] +); + +echo json_encode([ + 'access_token' => $accessToken, + 'refresh_token' => $refreshToken +]); +``` + +## Security Best Practices + +1. **Use HTTPS** - Always transmit tokens over HTTPS +2. **Short expiration times** - Use short-lived tokens (15-60 minutes) +3. **Implement refresh tokens** - For longer sessions +4. **Validate on every request** - Don't trust the client +5. **Store securely** - Don't store tokens in localStorage if possible +6. **Include audience claims** - Limit token usage scope +7. **Monitor for abuse** - Track token usage patterns +8. **Rotate secrets** - Periodically rotate JWT secrets + +## Common Pitfalls + +❌ **Don't store sensitive data in JWT payload** - It's not encrypted, only signed + +❌ **Don't use weak secret keys** - Use cryptographically random keys + +❌ **Don't skip expiration** - Always set reasonable expiration times + +❌ **Don't forget to invalidate** - Provide logout functionality + +❌ **Don't use HTTP** - Always use HTTPS in production + +## Next Steps + +- [Authentication](authentication.md) - Other authentication methods +- [Session Context](session-context.md) - Session-based authentication +- [User Properties](user-properties.md) - Managing user data diff --git a/docs/mappers.md b/docs/mappers.md new file mode 100644 index 0000000..26e693d --- /dev/null +++ b/docs/mappers.md @@ -0,0 +1,433 @@ +--- +sidebar_position: 11 +title: Mappers and Entity Processors +--- + +# Mappers and Entity Processors + +Mappers and Entity Processors allow you to transform data as it's read from or written to the database. + +## What Are Mappers? + +Mappers implement the `MapperFunctionInterface` and transform individual field values during database operations. + +- **Update Mappers**: Transform values **before** saving to database +- **Select Mappers**: Transform values **after** reading from database + +## Built-in Mappers + +### PasswordSha1Mapper + +Automatically hashes passwords using SHA-1: + +```php +defineMapperForUpdate( + UserDefinition::FIELD_PASSWORD, + PasswordSha1Mapper::class +); +``` + +### StandardMapper + +Default mapper that passes values through unchanged: + +```php +defineMapperForUpdate('name', StandardMapper::class); +``` + +### ReadOnlyMapper + +Prevents field updates: + +```php +markPropertyAsReadOnly(UserDefinition::FIELD_CREATED); + +// Or explicitly +$userDefinition->defineMapperForUpdate( + UserDefinition::FIELD_CREATED, + ReadOnlyMapper::class +); +``` + +## Creating Custom Mappers + +### Mapper Interface + +```php + 12]); + } +} + +// Use it +$userDefinition->defineMapperForUpdate( + UserDefinition::FIELD_PASSWORD, + BcryptPasswordMapper::class +); +``` + +### Example: Email Normalization Mapper + +```php +defineMapperForUpdate( + UserDefinition::FIELD_EMAIL, + EmailNormalizationMapper::class +); +``` + +### Example: JSON Serialization Mappers + +```php +defineMapperForUpdate('preferences', JsonEncodeMapper::class); +$userDefinition->defineMapperForSelect('preferences', JsonDecodeMapper::class); +``` + +### Example: Date Formatting Mapper + +```php +format('Y-m-d H:i:s'); + } + if (is_string($value)) { + return $value; + } + if (is_int($value)) { + return date('Y-m-d H:i:s', $value); + } + return $value; + } +} + +class DateParseMapper implements MapperFunctionInterface +{ + public function processedValue(mixed $value, mixed $instance): mixed + { + if (empty($value)) { + return null; + } + try { + return new \DateTime($value); + } catch (\Exception $e) { + return $value; + } + } +} + +$userDefinition->defineMapperForUpdate('created', DateFormatMapper::class); +$userDefinition->defineMapperForSelect('created', DateParseMapper::class); +``` + +## Entity Processors + +Entity Processors transform the **entire entity** (UserModel) before insert or update operations. + +### Entity Processor Interface + +```php +setBeforeInsert(new PassThroughEntityProcessor()); +``` + +### Custom Entity Processors + +#### Example: Auto-Set Created Timestamp + +```php +getCreated())) { + $instance->setCreated(date('Y-m-d H:i:s')); + } + } + } +} + +$userDefinition->setBeforeInsert(new CreatedTimestampProcessor()); +``` + +#### Example: Username Validation + +```php +getUsername(); + + if (strlen($username) < 3) { + throw new \InvalidArgumentException('Username must be at least 3 characters'); + } + + if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) { + throw new \InvalidArgumentException('Username can only contain letters, numbers, and underscores'); + } + } + } +} + +$userDefinition->setBeforeInsert(new UsernameValidationProcessor()); +$userDefinition->setBeforeUpdate(new UsernameValidationProcessor()); +``` + +#### Example: Audit Trail + +```php +userId = $userId; + } + + public function process(mixed $instance): void + { + if ($instance instanceof UserModel) { + $instance->set('modified_by', $this->userId); + $instance->set('modified_at', date('Y-m-d H:i:s')); + } + } +} + +$userDefinition->setBeforeUpdate(new AuditProcessor($currentUserId)); +``` + +## Using Closures (Legacy) + +For backward compatibility, you can use closures instead of dedicated mapper classes: + +```php +defineMapperForUpdate( + UserDefinition::FIELD_EMAIL, + new ClosureMapper(function ($value, $instance) { + return strtolower(trim($value)); + }) +); + +// Select mapper +$userDefinition->defineMapperForSelect( + UserDefinition::FIELD_CREATED, + new ClosureMapper(function ($value, $instance) { + return date('Y', strtotime($value)); + }) +); +``` + +:::warning Deprecated Methods +The following methods are deprecated but still work: +- `defineClosureForUpdate()` - Use `defineMapperForUpdate()` with `ClosureMapper` +- `defineClosureForSelect()` - Use `defineMapperForSelect()` with `ClosureMapper` +- `getClosureForUpdate()` - Use `getMapperForUpdate()` +- `getClosureForSelect()` - Use `getMapperForSelect()` +::: + +## Complete Example + +```php +getCreated())) { + $instance->setCreated(date('Y-m-d H:i:s')); + } + if (empty($instance->getAdmin())) { + $instance->setAdmin('no'); + } + } + } +} + +// Configure User Definition +$userDefinition = new UserDefinition(); + +// Apply mappers +$userDefinition->defineMapperForUpdate('name', TrimMapper::class); +$userDefinition->defineMapperForUpdate('email', LowercaseMapper::class); +$userDefinition->defineMapperForUpdate('username', LowercaseMapper::class); + +// Apply entity processors +$userDefinition->setBeforeInsert(new DefaultsProcessor()); + +// Initialize +$users = new UsersDBDataset($dbDriver, $userDefinition); +``` + +## Best Practices + +1. **Keep mappers simple** - Each mapper should do one thing +2. **Chain mappers** - Use composition for complex transformations +3. **Handle null values** - Always check for null/empty values +4. **Be idempotent** - Applying mapper multiple times should be safe +5. **Use entity processors for validation** - Validate complete entities +6. **Document side effects** - Make it clear what each mapper does + +## Next Steps + +- [Custom Fields](custom-fields.md) - Extending UserModel +- [Password Validation](password-validation.md) - Password policies +- [Database Storage](database-storage.md) - Schema configuration diff --git a/docs/password-validation.md b/docs/password-validation.md new file mode 100644 index 0000000..8df36b7 --- /dev/null +++ b/docs/password-validation.md @@ -0,0 +1,288 @@ +--- +sidebar_position: 8 +title: Password Validation +--- + +# Password Validation + +The `PasswordDefinition` class provides comprehensive password strength validation and generation capabilities. + +## Basic Usage + +### Creating a Password Definition + +```php +withPasswordDefinition($passwordDefinition); + +// Now password is validated when set +$userModel->setPassword('WeakPwd'); // Throws InvalidArgumentException +``` + +## Password Rules + +### Default Rules + +The default password policy requires: + +| Rule | Default Value | Description | +|---------------------|---------------|------------------------------------------| +| `minimum_chars` | 8 | Minimum password length | +| `require_uppercase` | 0 | Number of uppercase letters required | +| `require_lowercase` | 1 | Number of lowercase letters required | +| `require_symbols` | 0 | Number of symbols required | +| `require_numbers` | 1 | Number of digits required | +| `allow_whitespace` | 0 | Allow whitespace characters (0 = no) | +| `allow_sequential` | 0 | Allow sequential characters (0 = no) | +| `allow_repeated` | 0 | Allow repeated patterns (0 = no) | + +### Custom Rules + +```php + 12, + PasswordDefinition::REQUIRE_UPPERCASE => 2, + PasswordDefinition::REQUIRE_LOWERCASE => 2, + PasswordDefinition::REQUIRE_SYMBOLS => 1, + PasswordDefinition::REQUIRE_NUMBERS => 2, + PasswordDefinition::ALLOW_WHITESPACE => 0, + PasswordDefinition::ALLOW_SEQUENTIAL => 0, + PasswordDefinition::ALLOW_REPEATED => 0 +]); +``` + +### Setting Individual Rules + +```php +setRule(PasswordDefinition::MINIMUM_CHARS, 10); +$passwordDefinition->setRule(PasswordDefinition::REQUIRE_UPPERCASE, 1); +$passwordDefinition->setRule(PasswordDefinition::REQUIRE_SYMBOLS, 1); +``` + +## Validating Passwords + +### Validation Result Codes + +The `matchPassword()` method returns a bitwise result: + +```php +matchPassword('weak'); + +if ($result === PasswordDefinition::SUCCESS) { + echo "Password is valid"; +} else { + // Check specific failures + if ($result & PasswordDefinition::FAIL_MINIMUM_CHARS) { + echo "Password is too short\n"; + } + if ($result & PasswordDefinition::FAIL_UPPERCASE) { + echo "Missing uppercase letters\n"; + } + if ($result & PasswordDefinition::FAIL_LOWERCASE) { + echo "Missing lowercase letters\n"; + } + if ($result & PasswordDefinition::FAIL_NUMBERS) { + echo "Missing numbers\n"; + } + if ($result & PasswordDefinition::FAIL_SYMBOLS) { + echo "Missing symbols\n"; + } + if ($result & PasswordDefinition::FAIL_WHITESPACE) { + echo "Whitespace not allowed\n"; + } + if ($result & PasswordDefinition::FAIL_SEQUENTIAL) { + echo "Sequential characters detected\n"; + } + if ($result & PasswordDefinition::FAIL_REPEATED) { + echo "Repeated patterns detected\n"; + } +} +``` + +### Available Failure Codes + +| Constant | Value | Description | +|---------------------------|-------|----------------------------------| +| `SUCCESS` | 0 | Password is valid | +| `FAIL_MINIMUM_CHARS` | 1 | Password too short | +| `FAIL_UPPERCASE` | 2 | Missing uppercase letters | +| `FAIL_LOWERCASE` | 4 | Missing lowercase letters | +| `FAIL_SYMBOLS` | 8 | Missing symbols | +| `FAIL_NUMBERS` | 16 | Missing numbers | +| `FAIL_WHITESPACE` | 32 | Whitespace not allowed | +| `FAIL_SEQUENTIAL` | 64 | Sequential characters detected | +| `FAIL_REPEATED` | 128 | Repeated patterns detected | + +## Password Generation + +### Generate a Random Password + +```php +generatePassword(); +echo $password; // e.g., "aB3dE7fG9" +``` + +### Generate Longer Passwords + +```php +generatePassword(5); +``` + +The generated password will: +- Meet all defined rules +- Be cryptographically random +- Avoid sequential and repeated patterns + +## User Registration with Password Validation + +### Complete Example + +```php + 10, + PasswordDefinition::REQUIRE_UPPERCASE => 1, + PasswordDefinition::REQUIRE_LOWERCASE => 1, + PasswordDefinition::REQUIRE_SYMBOLS => 1, + PasswordDefinition::REQUIRE_NUMBERS => 1, +]); + +// Create user with password validation +try { + $user = new UserModel(); + $user->withPasswordDefinition($passwordDefinition); + + $user->setName('John Doe'); + $user->setEmail('john@example.com'); + $user->setUsername('johndoe'); + $user->setPassword($_POST['password']); // Validated automatically + + $users->save($user); + echo "User created successfully"; + +} catch (InvalidArgumentException $e) { + echo "Password validation failed: " . $e->getMessage(); +} +``` + +## User-Friendly Error Messages + +```php +matchPassword($_POST['password']); +if ($result !== PasswordDefinition::SUCCESS) { + $errors = getPasswordErrors($result); + foreach ($errors as $error) { + echo "- " . $error . "\n"; + } +} +``` + +## Sequential and Repeated Patterns + +### Sequential Characters + +Sequential patterns that are detected include: +- **Alphabetic**: abc, bcd, cde, xyz, etc. (case-insensitive) +- **Numeric**: 012, 123, 234, 789, 890, etc. +- **Reverse**: 987, 876, 765, 321, etc. + +### Repeated Patterns + +Repeated patterns include: +- **Repeated characters**: aaa, 111, etc. +- **Repeated sequences**: ababab, 123123, etc. + +## Password Change Flow + +```php +getById($userId); + $user->withPasswordDefinition($passwordDefinition); + + // Verify old password + $existingUser = $users->isValidUser($user->getUsername(), $_POST['old_password']); + if ($existingUser === null) { + throw new Exception("Current password is incorrect"); + } + + // Set new password (validated automatically) + $user->setPassword($_POST['new_password']); + $users->save($user); + + echo "Password changed successfully"; + +} catch (InvalidArgumentException $e) { + echo "New password validation failed: " . $e->getMessage(); +} +``` + +## Best Practices + +1. **Balance security and usability** - Don't make rules too restrictive +2. **Educate users** - Provide clear error messages +3. **Use password generation** - Offer to generate strong passwords +4. **Consider passphrases** - Allow longer passwords with spaces if appropriate +5. **Combine with rate limiting** - Prevent brute force attacks + +## Next Steps + +- [Authentication](authentication.md) - Validating credentials +- [User Management](user-management.md) - Managing users +- [Mappers](mappers.md) - Custom password hashing diff --git a/docs/session-context.md b/docs/session-context.md new file mode 100644 index 0000000..d0f9290 --- /dev/null +++ b/docs/session-context.md @@ -0,0 +1,197 @@ +--- +sidebar_position: 5 +title: Session Context +--- + +# Session Context + +The `SessionContext` class manages user authentication state using PSR-6 compatible cache storage. + +## Creating a Session Context + +```php +registerLogin($userId); + +// With additional session data +$sessionContext->registerLogin($userId, ['ip' => $_SERVER['REMOTE_ADDR']]); +``` + +### Check Authentication Status + +```php +isAuthenticated()) { + echo "User is logged in"; +} else { + echo "User is not authenticated"; +} +``` + +### Get Current User Info + +```php +isAuthenticated()) { + $userId = $sessionContext->userInfo(); + // Use $userId to fetch user details +} +``` + +### Logout + +```php +registerLogout(); +``` + +## Storing Session Data + +You can store custom data in the user's session. This data exists only while the user is logged in. + +### Store Data + +```php +setSessionData('shopping_cart', [ + 'item1' => 'Product A', + 'item2' => 'Product B' +]); + +$sessionContext->setSessionData('last_page', '/products'); +``` + +:::warning Authentication Required +The user must be authenticated to use `setSessionData()`. If not, a `NotAuthenticatedException` will be thrown. +::: + +### Retrieve Data + +```php +getSessionData('shopping_cart'); +$lastPage = $sessionContext->getSessionData('last_page'); +``` + +Returns `false` if: +- The user is not authenticated +- The key doesn't exist + +### Session Data Lifecycle + +- Session data is stored when the user logs in +- It persists across requests while the user remains logged in +- It is automatically deleted when the user logs out +- It is lost if the session expires + +## Complete Example + +```php +isValidUser($_POST['username'], $_POST['password']); + + if ($user !== null) { + $sessionContext->registerLogin($user->getUserid()); + $sessionContext->setSessionData('login_time', time()); + header('Location: /dashboard'); + exit; + } +} + +// Protected pages +if (!$sessionContext->isAuthenticated()) { + header('Location: /login'); + exit; +} + +$userId = $sessionContext->userInfo(); +$user = $users->getById($userId); +$loginTime = $sessionContext->getSessionData('login_time'); + +echo "Welcome, " . $user->getName(); +echo "Logged in at: " . date('Y-m-d H:i:s', $loginTime); + +// Logout +if (isset($_POST['logout'])) { + $sessionContext->registerLogout(); + header('Location: /login'); + exit; +} +``` + +## Best Practices + +1. **Use PHP Session storage** unless you have specific requirements for distributed sessions +2. **Always check authentication** before accessing protected resources +3. **Clear sensitive session data** when no longer needed +4. **Set appropriate session timeouts** based on your security requirements +5. **Regenerate session IDs** after login to prevent session fixation attacks + +## Next Steps + +- [Authentication](authentication.md) - User authentication methods +- [User Properties](user-properties.md) - Store persistent user data diff --git a/docs/user-management.md b/docs/user-management.md new file mode 100644 index 0000000..3ed82a4 --- /dev/null +++ b/docs/user-management.md @@ -0,0 +1,154 @@ +--- +sidebar_position: 3 +title: User Management +--- + +# User Management + +## Creating Users + +### Using addUser() Method + +The simplest way to add a user: + +```php +addUser( + 'John Doe', // Full name + 'johndoe', // Username + 'john@example.com', // Email + 'SecurePass123' // Password +); +``` + +### Using UserModel + +For more control, create a `UserModel` instance: + +```php +setName('John Doe'); +$userModel->setUsername('johndoe'); +$userModel->setEmail('john@example.com'); +$userModel->setPassword('SecurePass123'); +$userModel->setAdmin('no'); + +$savedUser = $users->save($userModel); +``` + +## Retrieving Users + +### Get User by ID + +```php +getById($userId); +``` + +### Get User by Email + +```php +getByEmail('john@example.com'); +``` + +### Get User by Username + +```php +getByUsername('johndoe'); +``` + +### Get User by Login Field + +The login field is determined by the `UserDefinition` (either email or username): + +```php +getByLoginField('johndoe'); +``` + +### Using Custom Filters + +For advanced queries, use `IteratorFilter`: + +```php +and('email', Relation::EQUAL, 'john@example.com'); +$filter->and('admin', Relation::EQUAL, 'yes'); + +$user = $users->getUser($filter); +``` + +## Updating Users + +```php +getById($userId); + +// Update fields +$user->setName('Jane Doe'); +$user->setEmail('jane@example.com'); + +// Save changes +$users->save($user); +``` + +## Deleting Users + +### Delete by ID + +```php +removeUserById($userId); +``` + +### Delete by Login + +```php +removeByLoginField('johndoe'); +``` + +## Checking Admin Status + +```php +isAdmin($userId)) { + echo "User is an administrator"; +} +``` + +The admin field accepts the following values as `true`: +- `yes`, `YES`, `y`, `Y` +- `true`, `TRUE`, `t`, `T` +- `1` +- `s`, `S` (from Portuguese "sim") + +## UserModel Properties + +The `UserModel` class provides the following properties: + +| Property | Type | Description | +|------------|---------------------|--------------------------------| +| userid | string\|int\|null | User ID (auto-generated) | +| name | string\|null | User's full name | +| email | string\|null | User's email address | +| username | string\|null | User's username | +| password | string\|null | User's password (hashed) | +| created | string\|null | Creation timestamp | +| admin | string\|null | Admin flag (yes/no) | + +## Next Steps + +- [Authentication](authentication.md) - Validate user credentials +- [User Properties](user-properties.md) - Store custom user data +- [Password Validation](password-validation.md) - Enforce password policies diff --git a/docs/user-properties.md b/docs/user-properties.md new file mode 100644 index 0000000..1666821 --- /dev/null +++ b/docs/user-properties.md @@ -0,0 +1,248 @@ +--- +sidebar_position: 6 +title: User Properties +--- + +# User Properties + +User properties allow you to store custom key-value data associated with users. This is useful for storing additional information beyond the standard user fields. + +## Adding Properties + +### Add a Single Property + +```php +addProperty($userId, 'phone', '555-1234'); +$users->addProperty($userId, 'department', 'Engineering'); +``` + +:::info Duplicates +`addProperty()` will not add the property if it already exists with the same value. +::: + +### Add Multiple Values for the Same Property + +Users can have multiple values for the same property: + +```php +addProperty($userId, 'role', 'developer'); +$users->addProperty($userId, 'role', 'manager'); +``` + +### Set a Property (Update or Create) + +Use `setProperty()` to update an existing property or create it if it doesn't exist: + +```php +setProperty($userId, 'phone', '555-5678'); +``` + +## Using UserModel + +You can also manage properties directly through the `UserModel`: + +```php +getById($userId); + +// Set a property value +$user->set('phone', '555-1234'); + +// Add a property model +$property = new UserPropertiesModel('department', 'Engineering'); +$user->addProperty($property); + +// Save the user to persist properties +$users->save($user); +``` + +## Retrieving Properties + +### Get a Single Property + +```php +getProperty($userId, 'phone'); +// Returns: '555-1234' +``` + +### Get Multiple Values + +If a property has multiple values, an array is returned: + +```php +getProperty($userId, 'role'); +// Returns: ['developer', 'manager'] +``` + +Returns `null` if the property doesn't exist. + +### Get Properties from UserModel + +```php +getById($userId); + +// Get property value(s) +$phone = $user->get('phone'); + +// Get property as UserPropertiesModel instance +$propertyModel = $user->get('phone', true); + +// Get all properties +$allProperties = $user->getProperties(); +foreach ($allProperties as $property) { + echo $property->getName() . ': ' . $property->getValue(); +} +``` + +## Checking Properties + +### Check if User Has a Property + +```php +hasProperty($userId, 'phone')) { + echo "User has a phone number"; +} + +// Check if property has a specific value +if ($users->hasProperty($userId, 'role', 'admin')) { + echo "User is an admin"; +} +``` + +:::tip Admin Bypass +The `hasProperty()` method always returns `true` for admin users, regardless of the actual property values. +::: + +## Removing Properties + +### Remove a Specific Property Value + +```php +removeProperty($userId, 'role', 'developer'); +``` + +### Remove All Values of a Property + +```php +removeProperty($userId, 'phone'); +``` + +### Remove Property from All Users + +```php +removeAllProperties('temporary_flag'); + +// Remove a specific value from all users +$users->removeAllProperties('role', 'guest'); +``` + +## Finding Users by Properties + +### Find Users with a Specific Property Value + +```php +getUsersByProperty('department', 'Engineering'); +// Returns array of UserModel objects +``` + +### Find Users with Multiple Properties + +```php +getUsersByPropertySet([ + 'department' => 'Engineering', + 'role' => 'senior', + 'status' => 'active' +]); +// Returns users that have ALL these properties with the specified values +``` + +## Common Use Cases + +### User Roles and Permissions + +```php +addProperty($userId, 'role', 'viewer'); +$users->addProperty($userId, 'role', 'editor'); +$users->addProperty($userId, 'role', 'admin'); + +// Check permissions +if ($users->hasProperty($userId, 'role', 'admin')) { + // Allow admin actions +} + +// Get all roles +$roles = $users->getProperty($userId, 'role'); +``` + +### User Preferences + +```php +setProperty($userId, 'theme', 'dark'); +$users->setProperty($userId, 'language', 'en'); +$users->setProperty($userId, 'timezone', 'America/New_York'); + +// Retrieve preferences +$theme = $users->getProperty($userId, 'theme'); +``` + +### Multi-tenant Applications + +```php +addProperty($userId, 'organization', 'org-123'); +$users->addProperty($userId, 'organization', 'org-456'); + +// Find all users in an organization +$orgUsers = $users->getUsersByProperty('organization', 'org-123'); + +// Check access +if ($users->hasProperty($userId, 'organization', $requestedOrgId)) { + // Grant access +} +``` + +## Property Storage + +### Database Storage + +Properties are stored in a separate table (default: `users_property`): + +| Column | Description | +|------------|--------------------------| +| customid | Property ID | +| userid | User ID (foreign key) | +| name | Property name | +| value | Property value | + +### XML/AnyDataset Storage + +Properties are stored as fields within each user's record, with arrays used for multiple values. + +## Next Steps + +- [User Management](user-management.md) - Basic user operations +- [Database Storage](database-storage.md) - Configure property storage +- [Custom Fields](custom-fields.md) - Extend the UserModel diff --git a/example.php b/example.php index 99f0e14..49c2da6 100644 --- a/example.php +++ b/example.php @@ -2,20 +2,41 @@ require "vendor/autoload.php"; -$users = new ByJG\Authenticate\UsersAnyDataset('/tmp/pass.anydata.xml'); +use ByJG\Authenticate\UsersAnyDataset; +use ByJG\Authenticate\SessionContext; +use ByJG\AnyDataset\Core\AnyDataset; +use ByJG\Cache\Factory; -$users->addUser('Some User Full Name', 'someuser', 'someuser@someemail.com', '12345'); -//$users->save(); +// Create or load AnyDataset from XML file +$anyDataset = new AnyDataset('/tmp/users.xml'); -$user = $users->isValidUser('someuser', '12345'); -var_dump($user); -if (!is_null($user)) -{ - $session = new \ByJG\Authenticate\SessionContext(); - $session->registerLogin($userId); +// Initialize user management +$users = new UsersAnyDataset($anyDataset); - echo "Authenticated: " . $session->isAuthenticated(); - print_r($session->userInfo()); +// Add a new user +$user = $users->addUser('Some User Full Name', 'someuser', 'someuser@someemail.com', '12345'); +echo "User created with ID: " . $user->getUserid() . "\n"; + +// Validate user credentials +$authenticatedUser = $users->isValidUser('someuser', '12345'); +var_dump($authenticatedUser); + +if ($authenticatedUser !== null) { + // Create session context + $session = new SessionContext(Factory::createSessionPool()); + + // Register login + $session->registerLogin($authenticatedUser->getUserid()); + + echo "Authenticated: " . ($session->isAuthenticated() ? 'yes' : 'no') . "\n"; + echo "User ID: " . $session->userInfo() . "\n"; + + // Store some session data + $session->setSessionData('login_time', time()); + + // Get the user info + $currentUser = $users->getById($session->userInfo()); + echo "Welcome, " . $currentUser->getName() . "\n"; }