Repository navigation
Authentication
- Multiple Authentication Guards: Session, Token, JWT, and OAuth
- Multiple User Providers: Database, LDAP, OAuth
- Third-Party OAuth Support: Google, GitHub, Facebook, Twitter, Yahoo, LinkedIn, Microsoft, Instagram, GitLab, Bitbucket, Amazon, Slack
- Remember Me Functionality: Secure token-based persistent login
- Session Security: Fingerprinting, timeout, regeneration
- LDAP Integration: Enterprise directory authentication
- Event System: Hooks for login, logout, and failure events
- Caching: Built-in cache support for user lookups
- Middleware: Authentication and authorization middleware
- Rate Limiting: Throttle middleware for brute-force protection
- Extensible: Easy to add custom guards and providers
use Mlangeni\Machinjiri\Core\Authentication\Auth;
// Check if user is logged in
if (Auth::check()) {
$user = Auth::user();
echo "Welcome, " . $user->name;
}
// Attempt login
if (Auth::attempt(['email' => 'user@example.com', 'password' => 'secret'])) {
// Login successful
redirect('/dashboard');
}
// Logout
Auth::logout();use Mlangeni\Machinjiri\Core\Authentication\Middleware\Authenticate;
// In your route definition
$router->get('/dashboard', [DashboardController::class, 'index'])
->middleware(Authenticate::class);// Use the API token guard
if (Auth::guard('api')->check()) {
$user = Auth::guard('api')->user();
}
// Use the JWT guard
if (Auth::guard('jwt')->check()) {
$user = Auth::guard('jwt')->user();
}Create a configuration array for the auth system:
// config/auth.php
return [
'defaults' => [
'guard' => 'session',
'provider' => 'users',
],
'guards' => [
'session' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
'jwt' => [
'driver' => 'jwt',
'provider' => 'users',
],
'ldap' => [
'driver' => 'session',
'provider' => 'ldap',
],
'oauth' => [
'driver' => 'session',
'provider' => 'oauth',
],
],
'providers' => [
'users' => [
'driver' => 'database',
'model' => App\Models\User::class,
'table' => 'users',
],
'ldap' => [
'driver' => 'ldap',
'model' => Mlangeni\Machinjiri\Core\Authentication\Models\LdapModel::class,
'username_attribute' => 'uid',
'search_fields' => ['uid', 'mail'],
'sync_attributes' => [
'username' => 'uid',
'email' => 'mail',
'name' => 'cn',
],
],
'oauth' => [
'driver' => 'oauth',
'model' => App\Models\User::class,
],
],
'session' => [
'lifetime' => 120, // minutes
'cookie' => 'machinjiri_session',
'path' => '/',
'domain' => '',
'secure' => false,
'http_only' => true,
'same_site' => 'lax',
],
'remember' => [
'expiration' => 2592000, // 30 days in seconds
],
];# Google
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# GitHub
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
# Facebook
FACEBOOK_CLIENT_ID=your-facebook-client-id
FACEBOOK_CLIENT_SECRET=your-facebook-client-secret
# Twitter
TWITTER_CLIENT_ID=your-twitter-client-id
TWITTER_CLIENT_SECRET=your-twitter-client-secret
# LinkedIn
LINKEDIN_CLIENT_ID=your-linkedin-client-id
LINKEDIN_CLIENT_SECRET=your-linkedin-client-secret
# Microsoft
MICROSOFT_CLIENT_ID=your-microsoft-client-id
MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
# GitLab
GITLAB_CLIENT_ID=your-gitlab-client-id
GITLAB_CLIENT_SECRET=your-gitlab-client-secret
# Application URL (for OAuth callbacks)
APP_URL=https://your-app.com┌─────────────────────────────────────────────────────────────────┐
│ Application │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Auth │───▶│ AuthManager │───▶│ Guards │ │
│ │ (Facade) │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ │ ┌───────────────┐ │ │
│ │ │ │ SessionGuard │ │ │
│ │ │ ├───────────────┤ │ │
│ │ │ │ TokenGuard │ │ │
│ │ │ ├───────────────┤ │ │
│ │ │ │ JwtGuard │ │ │
│ │ │ └───────────────┘ │ │
│ │ └─────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ UserProviders │ │ Authenticatable │ │
│ │ │ │ (User) │ │
│ │ ┌─────────────┐ │ └─────────────────────┘ │
│ │ │ Database │ │ │
│ │ ├─────────────┤ │ │
│ │ │ LDAP │ │ │
│ │ ├─────────────┤ │ │
│ │ │ OAuth │ │ │
│ │ └─────────────┘ │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| Component | Description |
|---|---|
Auth |
Static facade for easy access |
AuthManager |
Central manager that coordinates guards |
Guard |
Interface for authentication strategies |
UserProvider |
Interface for retrieving users |
Authenticatable |
Interface for user models |
The default guard for web applications. Stores authentication state in the session.
// Login with session
Auth::guard('session')->attempt($credentials, $remember = false);
// Check authentication
Auth::guard('session')->check();Features:
- Session-based state persistence
- "Remember me" cookie support
- Session fingerprinting for security
- Automatic session regeneration
For API authentication using static tokens.
// Token is passed via Authorization header or query parameter
// Authorization: Bearer <token>
// or ?api_token=<token>
Auth::guard('api')->check();Features:
- Stateless authentication
- Token stored in
api_tokencolumn - Supports Bearer tokens
For JSON Web Token authentication.
// Pass JWT via Authorization header
// Authorization: Bearer <jwt>
Auth::guard('jwt')->check();
// Generate a token
$token = Auth::guard('jwt')->generateToken($user, ['role' => 'admin']);Features:
- Stateless authentication
- Custom claims support
- Token expiration handling
Retrieves users from a database table.
use Mlangeni\Machinjiri\Core\Authentication\UserProviders\DatabaseUserProvider;
$provider = new DatabaseUserProvider(
$queryBuilder,
User::class,
$events,
$logger,
$cache,
$cacheTtl = 300
);Authenticates against an LDAP directory.
use Mlangeni\Machinjiri\Core\Authentication\UserProviders\LdapUserProvider;
$provider = new LdapUserProvider(
$ldapManager,
LdapModel::class,
$syncAttributes,
$config
);Handles OAuth-authenticated users.
use Mlangeni\Machinjiri\Core\Authentication\UserProviders\OAuthUserProvider;
$provider = new OAuthUserProvider(
$thirdPartyAuth,
User::class,
$logger,
$events
);use Mlangeni\Machinjiri\Core\Authentication\Auth;
// Attempt authentication
if (Auth::attempt(['email' => $email, 'password' => $password])) {
// Success - user is now logged in
return redirect('/dashboard');
}
// With "remember me"
if (Auth::attempt($credentials, $remember = true)) {
// User will stay logged in for 30 days
}
// Check authentication status
if (Auth::check()) {
$user = Auth::user();
$id = Auth::id();
}
// Guest check
if (Auth::guest()) {
// User is not logged in
}
// Logout
Auth::logout();$user = User::find(1);
// Login without credentials
Auth::login($user);
// Login with "remember me"
Auth::login($user, $remember = true);// Check credentials without logging in
if (Auth::validate(['email' => $email, 'password' => $password])) {
// Credentials are valid, but user is not logged in
}Your User model must implement the Authenticatable interface:
<?php
namespace App\Models;
use Mlangeni\Machinjiri\Core\Authentication\Authenticatable;
use Mlangeni\Machinjiri\Core\Artisans\Base\AbstractModel;
class User extends AbstractModel implements Authenticatable
{
protected string $table = 'users';
protected string $primaryKey = 'id';
protected array $fillable = ['name', 'email', 'password', 'remember_token'];
protected array $hidden = ['password', 'remember_token'];
public function getAuthIdentifier(): mixed
{
return $this->getAttribute($this->primaryKey);
}
public function getAuthPassword(): string
{
return $this->getAttribute('password') ?? '';
}
public function getRememberToken(): string
{
return $this->getAttribute('remember_token') ?? '';
}
public function setRememberToken(string $token): void
{
$this->setAttribute('remember_token', $token);
}
public function getRememberTokenName(): string
{
return 'remember_token';
}
public function getAuthIdentifierName(): string
{
return $this->primaryKey;
}
public function hasRole(string $role): bool
{
return $this->getAttribute('role') === $role;
}
public function hasPermission(string $permission): bool
{
$permissions = $this->getAttribute('permissions') ?? [];
return in_array($permission, $permissions);
}
}The "remember me" feature uses a secure token stored in both the database and a cookie:
-
On Login with Remember:
- Generate a random 32-byte token
- Hash the token and store in database
- Set cookie with
userId|plainToken
-
On Subsequent Request:
- Parse cookie to extract user ID and plain token
- Retrieve user from database
- Verify plain token against hashed token
- If valid, regenerate session and log in
-
Security Features:
- Tokens are hashed before storage
- Constant-time comparison
- Token rotation on login
Add an api_token column to your users table:
ALTER TABLE users ADD COLUMN api_token VARCHAR(255) UNIQUE NULL;// Client sends token in header
// Authorization: Bearer your-api-token-here
// Or as query parameter
// GET /api/resource?api_token=your-api-token-here
use Mlangeni\Machinjiri\Core\Authentication\Auth;
if (Auth::guard('api')->check()) {
$user = Auth::guard('api')->user();
// Process API request
}$user = User::find(1);
$token = bin2hex(random_bytes(32));
$user->api_token = hash('sha256', $token);
$user->save();
// Return the plain token to the client
return json_encode(['token' => $token]);$router->group(['middleware' => [Authenticate::class . ':api']], function ($router) {
$router->get('/api/user', [UserController::class, 'show']);
$router->get('/api/posts', [PostController::class, 'index']);
});JWT authentication requires the Bangwe encryption class from the core package.
use Mlangeni\Machinjiri\Core\Authentication\Auth;
$user = User::find(1);
// Basic token
$token = Auth::guard('jwt')->generateToken($user);
// Token with custom claims
$token = Auth::guard('jwt')->generateToken($user, [
'role' => 'admin',
'permissions' => ['read', 'write'],
]);// Client sends JWT in header
// Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
// Or as query parameter
// GET /api/resource?token=eyJ0eXAiOiJKV1QiLCJhbGc...
if (Auth::guard('jwt')->check()) {
$user = Auth::guard('jwt')->user();
}
// Validate token without authentication
if (Auth::guard('jwt')->validate(['token' => $token])) {
// Token is valid
}The JWT contains:
{
"sub": 1,
"iat": 1640000000,
"exp": 1640003600,
"role": "admin"
}| Provider | Authorization Endpoint | User Info Endpoint |
|---|---|---|
| accounts.google.com | googleapis.com/oauth2/v3/userinfo | |
| GitHub | github.com/login/oauth | api.github.com/user |
| facebook.com/dialog/oauth | graph.facebook.com/me | |
| twitter.com/i/oauth2 | api.twitter.com/2/users/me | |
| linkedin.com/oauth/v2 | api.linkedin.com/v2/me | |
| Microsoft | login.microsoftonline.com | graph.microsoft.com/v1.0/me |
| GitLab | gitlab.com/oauth | gitlab.com/api/v4/user |
| Bitbucket | bitbucket.org/site/oauth2 | api.bitbucket.org/2.0/user |
| Amazon | amazon.com/ap/oa | api.amazon.com/user/profile |
| Slack | slack.com/oauth/v2 | slack.com/api/users.profile.get |
| Yahoo | api.login.yahoo.com | api.login.yahoo.com/openid/v1/userinfo |
| api.instagram.com/oauth | graph.instagram.com/me |
- Register your application with each provider
- Add credentials to
.env - Configure the ThirdPartyAuth service
use Mlangeni\Machinjiri\Core\Authentication\ThirdParty\ThirdPartyAuth;
use Mlangeni\Machinjiri\Core\Authentication\Session;
use Mlangeni\Machinjiri\Core\Authentication\Cookie;
// Initialize
$thirdPartyAuth = new ThirdPartyAuth([
'redirect_uri' => 'https://your-app.com/auth/callback',
'auto_create_users' => true,
'default_role' => 'user',
]);
// Step 1: Redirect to provider
$router->get('/auth/{provider}', function ($request, $response, $provider) use ($thirdPartyAuth) {
$thirdPartyAuth->redirectToProvider($provider, $response);
});
// Step 2: Handle callback
$router->get('/auth/callback', function ($request, $response) use ($thirdPartyAuth) {
try {
$result = $thirdPartyAuth->handleCallback($request);
// Log the user in
Auth::login(new User($result['user']));
return $response->redirect('/dashboard');
} catch (Exception $e) {
return $response->redirect('/login?error=' . urlencode($e->getMessage()));
}
});$thirdPartyAuth->setUserMapping([
'id' => 'provider_user_id',
'name' => 'display_name',
'email' => 'email_address',
'avatar' => 'profile_image',
]);// Authenticate with an access token directly
$user = $thirdPartyAuth->authenticateWithToken('google', $accessToken);
if ($user) {
Auth::login(new User($user));
}// Get user info without creating/updating user
$userInfo = $thirdPartyAuth->getUserInfo('github', $accessToken);
// Returns:
// [
// 'provider' => 'github',
// 'provider_id' => '12345',
// 'name' => 'John Doe',
// 'email' => 'john@example.com',
// 'avatar_url' => 'https://avatars.githubusercontent.com/u/12345',
// ]// Get all connected providers for a user
$providers = $thirdPartyAuth->getUserProviders($userId);
// Disconnect a provider
$thirdPartyAuth->disconnectProvider('github', $userId);
// Refresh an access token
$newToken = $thirdPartyAuth->refreshToken('google', $userId);// Single button
echo $thirdPartyAuth->getProviderButton('google', ['class' => 'btn btn-google']);
// All available buttons
echo $thirdPartyAuth->getLoginButtons();
// Specific buttons
echo $thirdPartyAuth->getLoginButtons(['google', 'github'], ['class' => 'btn btn-social']);-- Users table (if not already exists)
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255),
email VARCHAR(255) UNIQUE,
password VARCHAR(255) NULL,
avatar_url VARCHAR(500) NULL,
email_verified_at TIMESTAMP NULL,
role VARCHAR(50) DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Provider connections
CREATE TABLE user_providers (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
provider VARCHAR(50) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
access_token TEXT,
refresh_token TEXT,
token_expires TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE KEY unique_provider (provider, provider_id)
);
-- Token storage
CREATE TABLE user_tokens (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
provider VARCHAR(50) NOT NULL,
access_token TEXT,
refresh_token TEXT,
token_type VARCHAR(50) DEFAULT 'bearer',
expires_in INT,
scope TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);<?php
namespace App\Controllers;
use Mlangeni\Machinjiri\Core\Authentication\Auth;
use Mlangeni\Machinjiri\Core\Authentication\ThirdParty\ThirdPartyAuth;
use App\Models\User;
class AuthController
{
private ThirdPartyAuth $oauth;
public function __construct()
{
$this->oauth = new ThirdPartyAuth([
'redirect_uri' => env('APP_URL') . '/auth/callback',
'auto_create_users' => true,
'auto_sync_profile' => true,
'default_role' => 'user',
]);
}
/**
* Show login page with OAuth buttons
*/
public function showLogin($request, $response)
{
$buttons = $this->oauth->getLoginButtons(['google', 'github'], [
'class' => 'btn btn-outline-dark w-100 mb-2'
]);
return $response->render('auth/login', ['buttons' => $buttons]);
}
/**
* Redirect to OAuth provider
*/
public function redirectToProvider($request, $response, string $provider)
{
try {
$this->oauth->redirectToProvider($provider, $response);
} catch (Exception $e) {
return $response->redirect('/login?error=' . urlencode($e->getMessage()));
}
}
/**
* Handle OAuth callback
*/
public function handleCallback($request, $response)
{
try {
$result = $this->oauth->handleCallback($request);
// Find or create local user
$userData = $result['user'];
$user = User::find($userData['id']);
if (!$user) {
$user = new User($userData);
}
// Log the user in
Auth::login($user, $remember = true);
// Redirect to intended page or dashboard
$intended = session('url.intended', '/dashboard');
return $response->redirect($intended);
} catch (Exception $e) {
logger()->error('OAuth callback failed', ['error' => $e->getMessage()]);
return $response->redirect('/login?error=authentication_failed');
}
}
/**
* Logout
*/
public function logout($request, $response)
{
Auth::logout();
$this->oauth->logout();
return $response->redirect('/login');
}
/**
* Link additional provider to existing account
*/
public function linkProvider($request, $response, string $provider)
{
if (!Auth::check()) {
return $response->redirect('/login');
}
// Store current user ID in session
session(['link_user_id' => Auth::id()]);
$this->oauth->redirectToProvider($provider, $response);
}
/**
* Handle provider linking callback
*/
public function handleLinkCallback($request, $response)
{
$userId = session('link_user_id');
if (!$userId) {
return $response->redirect('/settings');
}
try {
$result = $this->oauth->handleCallback($request);
// Link provider to existing user
// The processUser method will find the existing user by email
// and link the provider
session()->remove('link_user_id');
return $response->redirect('/settings?linked=1');
} catch (Exception $e) {
return $response->redirect('/settings?error=link_failed');
}
}
}Configure LDAP connection in your application:
// config/ldap.php
return [
'host' => env('LDAP_HOST', 'ldap.example.com'),
'port' => env('LDAP_PORT', 389),
'base_dn' => env('LDAP_BASE_DN', 'dc=example,dc=com'),
'username' => env('LDAP_USERNAME'),
'password' => env('LDAP_PASSWORD'),
'use_ssl' => env('LDAP_SSL', false),
'use_tls' => env('LDAP_TLS', false),
];'guards' => [
'ldap' => [
'driver' => 'session',
'provider' => 'ldap',
],
],
'providers' => [
'ldap' => [
'driver' => 'ldap',
'model' => LdapModel::class,
'username_attribute' => 'uid',
'search_fields' => ['uid', 'mail', 'sAMAccountName'],
'sync_attributes' => [
'username' => 'uid',
'email' => 'mail',
'name' => 'cn',
'department' => 'department',
],
'cache_ttl' => 300,
],
],use Mlangeni\Machinjiri\Core\Authentication\Auth;
// Attempt LDAP login
if (Auth::guard('ldap')->attempt([
'username' => 'jdoe',
'password' => 'secret',
])) {
// User authenticated against LDAP
$user = Auth::guard('ldap')->user();
}
// With email instead of username
if (Auth::guard('ldap')->attempt([
'email' => 'jdoe@example.com',
'password' => 'secret',
])) {
// ...
}The LdapModel wraps LDAP entries:
use Mlangeni\Machinjiri\Core\Authentication\Models\LdapModel;
class LdapModel extends AbstractModel implements Authenticatable
{
protected array $attributes = ['mail', 'password', 'uid'];
protected array $fillable = ['remember_token'];
public function getAuthIdentifier(): mixed
{
return $this->ldapEntry->getAttribute($this->identifier);
}
public function getAuthPassword(): string
{
return ''; // LDAP users don't have local passwords
}
// ... other Authenticatable methods
}use Mlangeni\Machinjiri\Core\Authentication\UserProviders\LdapUserProvider;
use Mlangeni\Machinjiri\Core\Components\LDAP\Manager as LdapManager;
$ldapManager = new LdapManager([
'host' => 'ldap.company.com',
'port' => 636,
'use_ssl' => true,
'base_dn' => 'ou=users,dc=company,dc=com',
]);
$provider = new LdapUserProvider(
$ldapManager,
LdapModel::class,
[
'username' => 'uid',
'email' => 'mail',
'name' => 'cn',
],
[
'username_attribute' => 'uid',
'search_fields' => ['uid', 'mail'],
'cache_ttl' => 600,
]
);Protects routes requiring authentication.
use Mlangeni\Machinjiri\Core\Authentication\Middleware\Authenticate;
// Single guard
$router->get('/dashboard', [DashboardController::class, 'index'])
->middleware(Authenticate::class);
// Specific guard
$router->get('/api/user', [UserController::class, 'show'])
->middleware(Authenticate::class . ':api');
// Multiple guards (any can authenticate)
$router->get('/admin', [AdminController::class, 'index'])
->middleware(Authenticate::class . ':session,jwt');
// Custom redirect
$router->get('/account', [AccountController::class, 'index'])
->middleware((new Authenticate())->redirectTo('/signin'));Behavior:
- JSON requests receive
401 Unauthorizedresponse - Web requests are redirected to
/login(or custom path)
Checks permissions after authentication.
use Mlangeni\Machinjiri\Core\Authentication\Middleware\Authorize;
$router->get('/admin/users', [AdminController::class, 'users'])
->middleware(Authorize::class . ':manage-users');
$router->delete('/posts/{id}', [PostController::class, 'destroy'])
->middleware(Authorize::class . ':delete-posts');Limits authentication attempts to prevent brute-force attacks.
use Mlangeni\Machinjiri\Core\Authentication\Middleware\Throttle;
$router->post('/login', [AuthController::class, 'login'])
->middleware(new Throttle($cache, $maxAttempts = 5, $decaySeconds = 60));Features:
- Configurable max attempts
- Configurable decay period
- Identifies by IP + username
- Returns
429 Too Many Requestswhen exceeded
// Login route with rate limiting
$router->post('/login', [AuthController::class, 'login'])
->middleware(new Throttle($cache, 5, 300)); // 5 attempts per 5 minutes
// Protected routes
$router->group(['middleware' => Authenticate::class], function ($router) {
$router->get('/dashboard', [DashboardController::class, 'index']);
$router->get('/profile', [ProfileController::class, 'show']);
// Admin-only routes
$router->group(['middleware' => Authorize::class . ':admin'], function ($router) {
$router->get('/admin/users', [AdminController::class, 'users']);
$router->get('/admin/settings', [AdminController::class, 'settings']);
});
});
// API routes with token auth
$router->group(['middleware' => Authenticate::class . ':api'], function ($router) {
$router->get('/api/user', [Api\UserController::class, 'show']);
$router->get('/api/posts', [Api\PostController::class, 'index']);
});The authentication system triggers events at key points:
| Event | Triggered When | Payload |
|---|---|---|
auth.login |
User successfully logs in | $user |
auth.logout |
User logs out | $user |
auth.failed |
Authentication attempt fails | $credentials |
auth.oauth.retrieved |
OAuth user retrieved | ['user' => $user, 'provider' => $provider] |
auth.oauth.failed |
OAuth retrieval failed | ['provider' => $provider] |
ldap.user.retrieving |
Before LDAP user lookup | ['username' => $username] |
ldap.user.retrieved |
LDAP user found | ['user' => $user] |
ldap.user.notfound |
LDAP user not found | ['username' => $username] |
use Mlangeni\Machinjiri\Core\Artisans\Events\EventListener;
$events = resolve(EventListener::class);
// Log successful logins
$events->listen('auth.login', function ($user) {
logger()->info('User logged in', [
'user_id' => $user->getAuthIdentifier(),
'ip' => $_SERVER['REMOTE_ADDR'],
'time' => date('Y-m-d H:i:s'),
]);
});
// Track failed attempts
$events->listen('auth.failed', function ($credentials) {
logger()->warning('Failed login attempt', [
'email' => $credentials['email'] ?? 'unknown',
'ip' => $_SERVER['REMOTE_ADDR'],
]);
// Could trigger additional security measures
// e.g., increment failed attempt counter, send alert
});
// Handle logout
$events->listen('auth.logout', function ($user) {
logger()->info('User logged out', [
'user_id' => $user ? $user->getAuthIdentifier() : null,
]);
});// app/Listeners/AuthenticationListener.php
namespace App\Listeners;
class AuthenticationListener
{
public function onLogin($user): void
{
// Update last login timestamp
$user->last_login_at = date('Y-m-d H:i:s');
$user->last_login_ip = $_SERVER['REMOTE_ADDR'];
$user->save();
// Clear failed attempts
cache()->delete('failed_login:' . $user->getAuthIdentifier());
}
public function onFailed($credentials): void
{
$email = $credentials['email'] ?? 'unknown';
$key = 'failed_login:' . md5($email);
$attempts = (int) cache()->get($key, 0);
cache()->set($key, $attempts + 1, 3600);
// Lock account after 10 failed attempts
if ($attempts >= 10) {
cache()->set('account_locked:' . md5($email), true, 3600);
}
}
public function onLogout($user): void
{
// Cleanup session data, etc.
}
}
// Register listeners
$events->listen('auth.login', [AuthenticationListener::class, 'onLogin']);
$events->listen('auth.failed', [AuthenticationListener::class, 'onFailed']);
$events->listen('auth.logout', [AuthenticationListener::class, 'onLogout']);<?php
namespace App\Authentication\Guards;
use Mlangeni\Machinjiri\Core\Authentication\Guard;
use Mlangeni\Machinjiri\Core\Authentication\Authenticatable;
use Mlangeni\Machinjiri\Core\Authentication\UserProviders\UserProvider;
class ApiKeyGuard implements Guard
{
protected ?Authenticatable $user = null;
protected UserProvider $provider;
public function __construct(UserProvider $provider)
{
$this->provider = $provider;
}
public function check(): bool
{
return !is_null($this->user());
}
public function guest(): bool
{
return !$this->check();
}
public function user(): ?Authenticatable
{
if ($this->user !== null) {
return $this->user;
}
$apiKey = $this->getApiKeyFromRequest();
if (!$apiKey) {
return null;
}
$this->user = $this->provider->retrieveByCredentials(['api_key' => $apiKey]);
return $this->user;
}
public function id(): mixed
{
$user = $this->user();
return $user ? $user->getAuthIdentifier() : null;
}
public function validate(array $credentials): bool
{
$apiKey = $credentials['api_key'] ?? $this->getApiKeyFromRequest();
if (!$apiKey) {
return false;
}
return !is_null($this->provider->retrieveByCredentials(['api_key' => $apiKey]));
}
public function setUser(Authenticatable $user): void
{
$this->user = $user;
}
public function attempt(array $credentials, bool $remember = false): bool
{
return $this->validate($credentials);
}
public function logout(): void
{
$this->user = null;
}
protected function getApiKeyFromRequest(): ?string
{
return $_SERVER['HTTP_X_API_KEY'] ?? null;
}
}// In your service provider or bootstrap
use Mlangeni\Machinjiri\Core\Authentication\AuthManager;
$authManager = resolve(AuthManager::class);
$authManager->extend('apikey', function ($container, $config) {
$provider = $container->resolve(UserProvider::class);
return new ApiKeyGuard($provider);
});// Custom provider that filters by tenant
class TenantUserProvider extends DatabaseUserProvider
{
protected int $tenantId;
public function __construct(
QueryBuilder $query,
string $model,
EventListener $events,
Logger $logger,
CacheManager $cache,
int $tenantId
) {
parent::__construct($query, $model, $events, $logger, $cache);
$this->tenantId = $tenantId;
}
public function retrieveByCredentials(array $credentials): ?Authenticatable
{
$query = clone $this->query;
$query->where('tenant_id', '=', $this->tenantId);
foreach ($credentials as $key => $value) {
if ($key !== 'password' && $key !== 'tenant_id') {
$query->where($key, '=', $value);
}
}
$result = $query->first();
return $result ? $this->hydrateModel($result) : null;
}
}class TwoFactorGuard implements Guard
{
protected Guard $baseGuard;
protected Session $session;
public function __construct(Guard $baseGuard, Session $session)
{
$this->baseGuard = $baseGuard;
$this->session = $session;
}
public function check(): bool
{
if (!$this->baseGuard->check()) {
return false;
}
$user = $this->baseGuard->user();
// If 2FA is not enabled for this user, allow access
if (!$user->two_factor_enabled) {
return true;
}
// Check if 2FA was completed this session
return $this->session->get('2fa_verified') === true;
}
public function user(): ?Authenticatable
{
if (!$this->check()) {
return null;
}
return $this->baseGuard->user();
}
// ... delegate other methods to baseGuard
}class LoginController
{
public function login($request, $response)
{
$email = $request->input('email');
// Check if account is locked
if (cache()->get('account_locked:' . md5($email))) {
return $response->status(423)->json([
'error' => 'Account temporarily locked. Try again later.'
]);
}
if (Auth::attempt($request->only(['email', 'password']))) {
// Clear failed attempts on successful login
cache()->delete('failed_login:' . md5($email));
return $response->redirect('/dashboard');
}
// Track failed attempt
$key = 'failed_login:' . md5($email);
$attempts = (int) cache()->get($key, 0) + 1;
cache()->set($key, $attempts, 3600);
// Lock after 5 failed attempts
if ($attempts >= 5) {
cache()->set('account_locked:' . md5($email), true, 3600);
return $response->status(423)->json([
'error' => 'Too many failed attempts. Account locked for 1 hour.'
]);
}
return $response->status(401)->json([
'error' => 'Invalid credentials',
'attempts_remaining' => 5 - $attempts
]);
}
}The Session class includes built-in fingerprinting:
// Fingerprint is generated from User-Agent + IP
// Stored in session and validated on each request
// If fingerprint doesn't match:
// - Session is destroyed
// - Exception is thrown
// - User must log in againTo customize fingerprinting:
class CustomSession extends Session
{
protected function generateFingerprint(): string
{
$ip = $this->request->getServerParam()['REMOTE_ADDR'] ?? '127.0.0.1';
$agent = $this->request->getServerParam()['HTTP_USER_AGENT'] ?? '';
// Add additional factors
$acceptLanguage = $this->request->getServerParam()['HTTP_ACCEPT_LANGUAGE'] ?? '';
$acceptEncoding = $this->request->getServerParam()['HTTP_ACCEPT_ENCODING'] ?? '';
return hash('sha256', $agent . $ip . $acceptLanguage . $acceptEncoding);
}
}Always use the built-in Hasher class:
use Mlangeni\Machinjiri\Core\Security\Hashing\Hasher;
$hasher = new Hasher();
// Hash a password
$hashed = $hasher->make('plain-password');
// Verify a password
if ($hasher->verify('plain-password', $hashed)) {
// Password is correct
}
// Check if rehash is needed (e.g., after changing cost factor)
if ($hasher->needsRehash($hashed)) {
$user->password = $hasher->make('plain-password');
$user->save();
}// In your session configuration
'session' => [
'lifetime' => 120, // 2 hours
'expire_on_close' => false,
'encrypt' => true, // Encrypt session data
'secure' => true, // HTTPS only
'http_only' => true, // Not accessible via JavaScript
'same_site' => 'lax', // CSRF protection
],
// Regenerate session ID on login
Auth::login($user);
// SessionGuard automatically regenerates the session ID
// Destroy session on logout
Auth::logout();
// SessionGuard automatically destroys the session// Remember tokens are:
// 1. Generated with cryptographically secure random_bytes()
// 2. Hashed with bcrypt before storage
// 3. Verified with constant-time comparison
// 4. Rotated on each login
// Token format: userId|plainToken
// Only the plainToken is hashed in the database
// The plainToken is sent to the client in an HttpOnly cookie// State parameter for OAuth prevents CSRF
// Session-based CSRF token for forms
// In your forms:
<form method="POST">
<input type="hidden" name="_token" value="<?= csrf_token() ?>">
<!-- form fields -->
</form>
// Middleware to verify CSRF
class VerifyCsrfToken
{
public function handle($request, $response, $next)
{
if ($request->method() === 'POST') {
$token = $request->input('_token');
if (!hash_equals(session('_token'), $token)) {
return $response->status(419)->json(['error' => 'CSRF token mismatch']);
}
}
return $next($request, $response);
}
}// Apply Throttle middleware to login routes
$router->post('/login', [AuthController::class, 'login'])
->middleware(new Throttle($cache, 5, 300)); // 5 attempts per 5 minutes// Force HTTPS in production
if (env('APP_ENV') === 'production' && empty($_SERVER['HTTPS'])) {
header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
exit;
}
// Set secure cookies
'secure' => env('APP_ENV') === 'production',class PasswordResetController
{
public function request($request, $response)
{
$email = $request->input('email');
// Rate limit reset requests
$key = 'password_reset:' . md5($email);
if (cache()->get($key . ':count', 0) >= 3) {
return $response->status(429)->json(['error' => 'Too many reset requests']);
}
cache()->set($key . ':count', cache()->get($key . ':count', 0) + 1, 3600);
// Generate secure token
$token = bin2hex(random_bytes(32));
$hashedToken = hash('sha256', $token);
// Store hashed token with expiration
cache()->set($key . ':token', $hashedToken, 3600); // 1 hour
// Send email with plain token
// The URL should be: /password/reset?token=<plain>&email=<email>
// Always return success to prevent email enumeration
return $response->json(['message' => 'If the email exists, a reset link has been sent.']);
}
public function reset($request, $response)
{
$token = $request->input('token');
$email = $request->input('email');
$password = $request->input('password');
$key = 'password_reset:' . md5($email);
$storedHash = cache()->get($key . ':token');
if (!$storedHash || !hash_equals($storedHash, hash('sha256', $token))) {
return $response->status(400)->json(['error' => 'Invalid or expired token']);
}
// Update password
$user = User::where('email', $email)->first();
if ($user) {
$user->password = (new Hasher())->make($password);
$user->save();
}
// Invalidate token
cache()->delete($key . ':token');
return $response->json(['message' => 'Password reset successfully']);
}
}| Method | Description |
|---|---|
Auth::check(): bool |
Check if user is authenticated |
Auth::guest(): bool |
Check if user is a guest |
Auth::user(): ?Authenticatable |
Get authenticated user |
Auth::id(): mixed |
Get authenticated user ID |
Auth::attempt(array $credentials, bool $remember = false): bool |
Attempt login |
Auth::login(Authenticatable $user, bool $remember = false): void |
Login a user |
Auth::logout(): void |
Logout current user |
Auth::validate(array $credentials): bool |
Validate credentials |
Auth::guard(?string $name = null): Guard |
Get a guard instance |
| Method | Description |
|---|---|
guard(?string $name = null): Guard |
Get guard by name |
check(): bool |
Check authentication (default guard) |
guest(): bool |
Check guest status (default guard) |
user(): ?Authenticatable |
Get user (default guard) |
id(): mixed |
Get user ID (default guard) |
attempt(array $credentials, bool $remember = false): bool |
Attempt login |
login(Authenticatable $user, bool $remember = false): void |
Login user |
logout(): void |
Logout user |
validate(array $credentials): bool |
Validate credentials |
setDefaultGuard(string $name): void |
Set default guard |
getGuards(): array |
Get all guard names |
| Method | Description |
|---|---|
check(): bool |
Check if authenticated |
guest(): bool |
Check if guest |
user(): ?Authenticatable |
Get authenticated user |
id(): mixed |
Get user ID |
validate(array $credentials): bool |
Validate credentials |
setUser(Authenticatable $user): void |
Set current user |
attempt(array $credentials, bool $remember = false): bool |
Attempt login |
logout(): void |
Logout user |
| Method | Description |
|---|---|
check(): bool |
Check authentication |
guest(): bool |
Check guest |
user(): ?Authenticatable |
Get user |
id(): mixed |
Get user ID |
validate(array $credentials): bool |
Validate credentials |
setUser(Authenticatable $user): void |
Set current user |
attempt(array $credentials, bool $remember = false): bool |
Attempt login |
login(Authenticatable $user, bool $remember = false): void |
Login user |
logout(): void |
Logout user |
| Method | Description |
|---|---|
check(): bool |
Check authentication |
user(): ?Authenticatable |
Get user via token |
id(): mixed |
Get user ID |
validate(array $credentials): bool |
Validate token |
setUser(Authenticatable $user): void |
Set current user |
attempt(array $credentials, bool $remember = false): bool |
Validate token |
logout(): void |
Clear user |
| Method | Description |
|---|---|
check(): bool |
Check authentication |
user(): ?Authenticatable |
Get user via JWT |
id(): mixed |
Get user ID |
validate(array $credentials): bool |
Validate JWT |
setUser(Authenticatable $user): void |
Set current user |
attempt(array $credentials, bool $remember = false): bool |
Validate JWT |
logout(): void |
Clear user |
generateToken(Authenticatable $user, array $claims = []): string |
Generate JWT |
| Method | Description |
|---|---|
retrieveById($id): ?Authenticatable |
Find user by ID |
retrieveByCredentials(array $credentials): ?Authenticatable |
Find user by credentials |
retrieveByRememberToken(string $token): ?Authenticatable |
Find user by remember token |
updateRememberToken(Authenticatable $user, string $token): void |
Update remember token |
| Method | Description |
|---|---|
getProvider(string $provider): ?OAuth |
Get OAuth provider instance |
getAvailableProviders(): array |
Get available provider names |
isProviderAvailable(string $provider): bool |
Check if provider is configured |
redirectToProvider(string $provider, HttpResponse $response): void |
Redirect to OAuth provider |
handleCallback(HttpRequest $request, ?string $provider = null): array |
Handle OAuth callback |
getUserInfo(string $provider, string $accessToken): array |
Get user info from provider |
authenticateWithToken(string $provider, string $accessToken): ?array |
Authenticate with token |
disconnectProvider(string $provider, ?int $userId = null): bool |
Disconnect provider |
getUserProviders(int $userId): array |
Get user's connected providers |
refreshToken(string $provider, ?int $userId = null): ?array |
Refresh OAuth token |
logout(?string $provider = null): void |
Logout from provider(s) |
getLoginUrls(): array |
Get authorization URLs for all providers |
getProviderButton(string $provider, array $attributes = []): string |
Generate login button HTML |
getLoginButtons(?array $providers = null, array $attributes = []): string |
Generate all login buttons |
| Method | Description |
|---|---|
set(string $key, mixed $value): void |
Set session value |
get(string $key, mixed $default = null): mixed |
Get session value |
has(string $key): bool |
Check if session key exists |
remove(string $key): void |
Remove session key |
destroy(bool $restart = false): void |
Destroy session |
regenerateId(): void |
Regenerate session ID |
| Method | Description |
|---|---|
set(string $name, mixed $value, int $expire = 0, ...): bool |
Set cookie |
get(string $name, mixed $default = null): mixed |
Get cookie |
delete(string $name, string $path = '/', string $domain = ''): void |
Delete cookie |
has(string $name): bool |
Check if cookie exists |
Cause: The guard name doesn't exist in your auth configuration.
Solution: Check your config/auth.php:
'guards' => [
'session' => [
'driver' => 'session',
'provider' => 'users',
],
// Add missing guard here
],Cause: retrieveByCredentials throws when user is not found.
Solution: This is expected behavior. Catch the exception:
try {
if (Auth::attempt($credentials)) {
// Success
}
} catch (AuthenticationException $e) {
// Handle failed login
}Cause: Session configuration issues.
Solution: Check:
-
session.save_pathis writable - Cookie domain matches your application domain
-
secureisfalsefor HTTP (development) - Session lifetime is sufficient
Cause: Various OAuth issues.
Solution: Check:
- Redirect URI matches exactly (including trailing slashes)
- Client ID and secret are correct
- Provider is configured in
.env - State parameter matches session
- Check logs for detailed error
Cause: LDAP connection or credential issues.
Solution: Check:
- LDAP host and port are correct
- Base DN is correct
- Username attribute matches your LDAP schema
- User exists in LDAP directory
- Password is correct
- Check LDAP logs
Machinjiri Docs
- Home
- Learn
- Build
-
Tutorials
- Routing
- Controllers and Middleware
- Views and Templates
- Database ORM
- Eloquent Database ORM
- Errors and Exceptions
- Forms and Validation
- Http
- Events and Logging
- Services and DI
- Security
- Authentication
- Caching
- File System
- Date and Time
- Debugging
- Queue and Jobs
- Task Scheduling
- Transport (Mail, SMS)
- LDAP
- Notifications
- Webhooks
- UUID Generations
- Terminal
- Helper Function
- Reference