Skip to content

Commit 1ec81b4

Browse files
authored
Merge pull request
Develop multi-factor authentication Laravel package
2 parents a317ed0 + c4f222a commit 1ec81b4

File tree

15 files changed

+685
-0
lines changed

15 files changed

+685
-0
lines changed

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,55 @@
11
# mfa
22
Multi Factor Authentication
3+
CodingLibs Laravel MFA
4+
5+
Installation
6+
- Require in your Laravel 12 app composer.json or via path repository.
7+
- The service provider auto-registers. Publish config and migrations:
8+
```
9+
php artisan vendor:publish --tag=mfa-config
10+
php artisan vendor:publish --tag=mfa-migrations
11+
php artisan migrate
12+
```
13+
14+
Usage
15+
```php
16+
use CodingLibs\MFA\Facades\MFA;
17+
18+
// Email/SMS
19+
$challenge = MFA::issueChallenge(auth()->user(), 'email');
20+
// then later
21+
$ok = MFA::verifyChallenge(auth()->user(), 'email', '123456');
22+
23+
// TOTP
24+
$setup = MFA::setupTotp(auth()->user());
25+
// $setup['otpauth_url'] -> QR code; then verify
26+
$ok = MFA::verifyTotp(auth()->user(), '123456');
27+
28+
// Generate QR code (base64 PNG) from existing TOTP (uses bacon/bacon-qr-code)
29+
$base64 = MFA::generateTotpQrCodeBase64(auth()->user(), issuer: 'MyApp');
30+
// <img src="$base64" />
31+
```
32+
33+
Configuration
34+
- See `config/mfa.php` for email/sms/totp options.
35+
36+
Extending: Custom Channels
37+
```php
38+
use CodingLibs\MFA\Contracts\MfaChannel;
39+
use CodingLibs\MFA\Facades\MFA;
40+
use Illuminate\Contracts\Auth\Authenticatable;
41+
42+
class WhatsAppChannel implements MfaChannel {
43+
public function __construct(private array $config = []) {}
44+
public function getName(): string { return 'whatsapp'; }
45+
public function send(Authenticatable $user, string $code, array $options = []): void {
46+
// send via provider...
47+
}
48+
}
49+
50+
// register at boot
51+
MFA::registerChannel(new WhatsAppChannel(config('mfa.whatsapp', [])));
52+
53+
// then issue
54+
MFA::issueChallenge(auth()->user(), 'whatsapp');
55+
```

composer.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "codinglibs/laravel-mfa",
3+
"description": "Laravel 12 Multi-Factor Authentication package (Email, SMS, Google Authenticator TOTP)",
4+
"type": "library",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "CodingLibs",
9+
"email": "support@codinglibs.example"
10+
}
11+
],
12+
"require": {
13+
"php": ">=8.2",
14+
"illuminate/support": "^12.0",
15+
"illuminate/database": "^12.0",
16+
"illuminate/mail": "^12.0",
17+
"illuminate/config": "^12.0",
18+
"illuminate/console": "^12.0",
19+
"bacon/bacon-qr-code": "^2.0"
20+
},
21+
"autoload": {
22+
"psr-4": {
23+
"CodingLibs\\MFA\\": "src/"
24+
}
25+
},
26+
"autoload-dev": {
27+
"psr-4": {
28+
"CodingLibs\\MFA\\Tests\\": "tests/"
29+
}
30+
},
31+
"extra": {
32+
"laravel": {
33+
"providers": [
34+
"CodingLibs\\MFA\\MFAServiceProvider"
35+
],
36+
"aliases": {
37+
"MFA": "CodingLibs\\MFA\\Facades\\MFA"
38+
}
39+
}
40+
},
41+
"minimum-stability": "stable",
42+
"prefer-stable": true
43+
}
44+

config/mfa.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
return [
4+
'code_length' => 6,
5+
'code_ttl_seconds' => 300,
6+
7+
'email' => [
8+
'enabled' => true,
9+
'from_address' => env('MFA_EMAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')),
10+
'from_name' => env('MFA_EMAIL_FROM_NAME', env('MAIL_FROM_NAME', 'Laravel')),
11+
'subject' => env('MFA_EMAIL_SUBJECT', 'Your verification code'),
12+
],
13+
14+
'sms' => [
15+
'enabled' => true,
16+
'driver' => env('MFA_SMS_DRIVER', 'log'), // log|null
17+
'from' => env('MFA_SMS_FROM', ''),
18+
],
19+
20+
'totp' => [
21+
'issuer' => env('MFA_TOTP_ISSUER', config('app.name')),
22+
'digits' => (int) env('MFA_TOTP_DIGITS', 6),
23+
'period' => (int) env('MFA_TOTP_PERIOD', 30),
24+
'window' => (int) env('MFA_TOTP_WINDOW', 1),
25+
],
26+
];
27+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration {
8+
public function up(): void
9+
{
10+
Schema::create('mfa_methods', function (Blueprint $table) {
11+
$table->id();
12+
$table->string('user_type');
13+
$table->string('user_id');
14+
$table->string('method'); // email|sms|totp
15+
$table->text('secret')->nullable(); // for totp
16+
$table->timestamp('enabled_at')->nullable();
17+
$table->timestamp('last_used_at')->nullable();
18+
$table->timestamps();
19+
$table->index(['user_type', 'user_id', 'method']);
20+
});
21+
22+
Schema::create('mfa_challenges', function (Blueprint $table) {
23+
$table->id();
24+
$table->string('user_type');
25+
$table->string('user_id');
26+
$table->string('method'); // email|sms
27+
$table->string('code');
28+
$table->timestamp('expires_at');
29+
$table->timestamp('consumed_at')->nullable();
30+
$table->timestamps();
31+
$table->index(['user_type', 'user_id', 'method']);
32+
});
33+
}
34+
35+
public function down(): void
36+
{
37+
Schema::dropIfExists('mfa_challenges');
38+
Schema::dropIfExists('mfa_methods');
39+
}
40+
};
41+

src/ChannelRegistry.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace CodingLibs\MFA;
4+
5+
use CodingLibs\MFA\Contracts\MfaChannel;
6+
7+
class ChannelRegistry
8+
{
9+
/** @var array<string, MfaChannel> */
10+
protected array $channels = [];
11+
12+
public function register(MfaChannel $channel): void
13+
{
14+
$this->channels[strtolower($channel->getName())] = $channel;
15+
}
16+
17+
public function has(string $name): bool
18+
{
19+
return array_key_exists(strtolower($name), $this->channels);
20+
}
21+
22+
public function get(string $name): ?MfaChannel
23+
{
24+
return $this->channels[strtolower($name)] ?? null;
25+
}
26+
27+
/** @return array<string, MfaChannel> */
28+
public function all(): array
29+
{
30+
return $this->channels;
31+
}
32+
}
33+

src/Channels/EmailChannel.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace CodingLibs\MFA\Channels;
4+
5+
use CodingLibs\MFA\Contracts\MfaChannel;
6+
use Illuminate\Contracts\Auth\Authenticatable;
7+
use Illuminate\Support\Facades\Mail;
8+
9+
class EmailChannel implements MfaChannel
10+
{
11+
public function __construct(protected array $config = [])
12+
{
13+
}
14+
15+
public function getName(): string
16+
{
17+
return 'email';
18+
}
19+
20+
public function send(Authenticatable $user, string $code, array $options = []): void
21+
{
22+
if (! ($this->config['enabled'] ?? true)) {
23+
return;
24+
}
25+
26+
$to = method_exists($user, 'getEmailForVerification') ? $user->getEmailForVerification() : ($user->email ?? null);
27+
if (! $to) {
28+
return;
29+
}
30+
31+
$fromAddress = $this->config['from_address'] ?? null;
32+
$fromName = $this->config['from_name'] ?? null;
33+
$subject = $this->config['subject'] ?? 'Your verification code';
34+
if (isset($options['subject'])) {
35+
$subject = $options['subject'];
36+
}
37+
38+
Mail::raw("Your verification code is: {$code}", function ($message) use ($to, $fromAddress, $fromName, $subject) {
39+
$message->to($to)->subject($subject);
40+
if ($fromAddress) {
41+
$message->from($fromAddress, $fromName);
42+
}
43+
});
44+
}
45+
}
46+

src/Channels/SmsChannel.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace CodingLibs\MFA\Channels;
4+
5+
use CodingLibs\MFA\Contracts\MfaChannel;
6+
use Illuminate\Contracts\Auth\Authenticatable;
7+
use Illuminate\Support\Facades\Log;
8+
9+
class SmsChannel implements MfaChannel
10+
{
11+
public function __construct(protected array $config = [])
12+
{
13+
}
14+
15+
public function getName(): string
16+
{
17+
return 'sms';
18+
}
19+
20+
public function send(Authenticatable $user, string $code, array $options = []): void
21+
{
22+
if (! ($this->config['enabled'] ?? true)) {
23+
return;
24+
}
25+
26+
$driver = $this->config['driver'] ?? 'log';
27+
$to = method_exists($user, 'getPhoneNumberForVerification') ? $user->getPhoneNumberForVerification() : ($user->phone ?? null);
28+
if (! $to) {
29+
return;
30+
}
31+
32+
$message = $options['message'] ?? "Your verification code is: {$code}";
33+
34+
if ($driver === 'log') {
35+
Log::info('MFA SMS', ['to' => $to, 'message' => $message]);
36+
return;
37+
}
38+
39+
// Placeholder: extendable for twilio/aws pinoint/etc via events or bindings.
40+
Log::warning('MFA SMS driver not implemented', ['driver' => $driver, 'to' => $to]);
41+
}
42+
}
43+

src/Contracts/MfaChannel.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace CodingLibs\MFA\Contracts;
4+
5+
use Illuminate\Contracts\Auth\Authenticatable;
6+
7+
interface MfaChannel
8+
{
9+
public function getName(): string;
10+
11+
public function send(Authenticatable $user, string $code, array $options = []): void;
12+
}
13+

src/Facades/MFA.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace CodingLibs\MFA\Facades;
4+
5+
use Illuminate\Support\Facades\Facade;
6+
7+
class MFA extends Facade
8+
{
9+
protected static function getFacadeAccessor(): string
10+
{
11+
return 'mfa';
12+
}
13+
}
14+

0 commit comments

Comments
 (0)