Skip to content

Commit d397e02

Browse files
committed
feat: Make email and SMS channels configurable via environment variables
- Add MFA_EMAIL_ENABLED and MFA_SMS_ENABLED environment variables - Update config/mfa.php to use env() for enabled settings - Modify registerDefaultChannels() to respect enabled configuration - Add reRegisterDefaultChannels() method for runtime re-registration - Add clear() method to ChannelRegistry for clearing channels - Update README.md to document new environment variables - Add comprehensive test coverage for enabled/disabled channel behavior - Update facade annotations with new reRegisterDefaultChannels method This addresses the inconsistency where email and SMS channels were hardcoded to enabled=true while other features (remember, recovery) were properly configurable via environment variables. Now users can disable email or SMS channels via: - MFA_EMAIL_ENABLED=false - MFA_SMS_ENABLED=false Channels can be re-registered at runtime to pick up config changes.
1 parent 71256d7 commit d397e02

File tree

6 files changed

+206
-4
lines changed

6 files changed

+206
-4
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,13 @@ Configuration
162162

163163
Environment variables (examples)
164164
```
165+
MFA_EMAIL_ENABLED=true
165166
MFA_EMAIL_FROM_ADDRESS="no-reply@example.com"
166167
MFA_EMAIL_FROM_NAME="Example App"
167168
MFA_EMAIL_SUBJECT="Your verification code"
168169
MFA_EMAIL_CHANNEL="App\Channels\CustomEmailChannel"
169170
171+
MFA_SMS_ENABLED=true
170172
MFA_SMS_DRIVER=log
171173
MFA_SMS_FROM="ExampleApp"
172174
MFA_SMS_CHANNEL="App\Channels\CustomSmsChannel"

config/mfa.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
'code_ttl_seconds' => 300,
66

77
'email' => [
8-
'enabled' => true,
8+
'enabled' => env('MFA_EMAIL_ENABLED', true),
99
'from_address' => env('MFA_EMAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')),
1010
'from_name' => env('MFA_EMAIL_FROM_NAME', env('MAIL_FROM_NAME', 'Laravel')),
1111
'subject' => env('MFA_EMAIL_SUBJECT', 'Your verification code'),
1212
'channel' => env('MFA_EMAIL_CHANNEL', \CodingLibs\MFA\Channels\EmailChannel::class),
1313
],
1414

1515
'sms' => [
16-
'enabled' => true,
16+
'enabled' => env('MFA_SMS_ENABLED', true),
1717
'driver' => env('MFA_SMS_DRIVER', 'log'), // log|null
1818
'from' => env('MFA_SMS_FROM', ''),
1919
'channel' => env('MFA_SMS_CHANNEL', \CodingLibs\MFA\Channels\SmsChannel::class),

src/ChannelRegistry.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,10 @@ public function all(): array
2929
{
3030
return $this->channels;
3131
}
32+
33+
public function clear(): void
34+
{
35+
$this->channels = [];
36+
}
3237
}
3338

src/Facades/MFA.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* @method static ?\CodingLibs\MFA\Models\MfaChallenge generateChallenge(\Illuminate\Contracts\Auth\Authenticatable $user, string $method)
1717
* @method static void registerChannel(\CodingLibs\MFA\Contracts\MfaChannel $channel)
1818
* @method static void registerChannelFromConfig(string $type, array $config)
19+
* @method static void reRegisterDefaultChannels()
1920
* @method static ?\CodingLibs\MFA\Contracts\MfaChannel getChannel(string $name)
2021
* @method static ?string generateTotpQrCodeBase64(\Illuminate\Contracts\Auth\Authenticatable $user, ?string $issuer = null, ?string $label = null, int $size = 200)
2122
* @method static ?string generateTotpQrCodeSvg(\Illuminate\Contracts\Auth\Authenticatable $user, ?string $issuer = null, ?string $label = null, int $size = 200)

src/MFA.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,15 @@ public function __construct(array $config)
3232

3333
protected function registerDefaultChannels(): void
3434
{
35-
$this->registry->register($this->createChannel('email', $this->config['email'] ?? []));
36-
$this->registry->register($this->createChannel('sms', $this->config['sms'] ?? []));
35+
// Register email channel if enabled
36+
if (Arr::get($this->config, 'email.enabled', true)) {
37+
$this->registry->register($this->createChannel('email', $this->config['email'] ?? []));
38+
}
39+
40+
// Register SMS channel if enabled
41+
if (Arr::get($this->config, 'sms.enabled', true)) {
42+
$this->registry->register($this->createChannel('sms', $this->config['sms'] ?? []));
43+
}
3744
}
3845

3946
protected function createChannel(string $type, array $config): MfaChannel
@@ -152,6 +159,12 @@ public function registerChannelFromConfig(string $type, array $config): void
152159
$this->registry->register($channel);
153160
}
154161

162+
public function reRegisterDefaultChannels(): void
163+
{
164+
$this->registry->clear();
165+
$this->registerDefaultChannels();
166+
}
167+
155168
protected function createChannelFromConfig(array $config): MfaChannel
156169
{
157170
$channelClass = $config['channel'] ?? throw new \InvalidArgumentException('channel must be specified in config');
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<?php
2+
3+
use CodingLibs\MFA\Channels\EmailChannel;
4+
use CodingLibs\MFA\Channels\SmsChannel;
5+
use CodingLibs\MFA\Facades\MFA;
6+
use CodingLibs\MFA\Contracts\MfaChannel;
7+
use Illuminate\Foundation\Testing\RefreshDatabase;
8+
use Tests\TestCase;
9+
10+
class ChannelEnabledConfigTest extends TestCase
11+
{
12+
use RefreshDatabase;
13+
14+
public function test_email_channel_can_be_disabled_via_config()
15+
{
16+
// Test with email disabled
17+
config(['mfa.email.enabled' => false]);
18+
19+
$user = new TestUser(1, 'test@example.com');
20+
21+
// Re-register channels to pick up new config
22+
MFA::reRegisterDefaultChannels();
23+
24+
// Should not be able to issue email challenges when disabled
25+
$challenge = MFA::issueChallenge($user, 'email');
26+
expect($challenge)->toBeNull();
27+
}
28+
29+
public function test_sms_channel_can_be_disabled_via_config()
30+
{
31+
// Test with SMS disabled
32+
config(['mfa.sms.enabled' => false]);
33+
34+
$user = new TestUser(1, 'test@example.com');
35+
36+
// Re-register channels to pick up new config
37+
MFA::reRegisterDefaultChannels();
38+
39+
// Should not be able to issue SMS challenges when disabled
40+
$challenge = MFA::issueChallenge($user, 'sms');
41+
expect($challenge)->toBeNull();
42+
}
43+
44+
public function test_email_channel_can_be_enabled_via_config()
45+
{
46+
// Test with email enabled (default)
47+
config(['mfa.email.enabled' => true]);
48+
49+
$user = new TestUser(1, 'test@example.com');
50+
51+
// Re-register channels to pick up new config
52+
MFA::reRegisterDefaultChannels();
53+
54+
// Should be able to issue email challenges when enabled
55+
$challenge = MFA::issueChallenge($user, 'email');
56+
expect($challenge)->not->toBeNull();
57+
expect($challenge->method)->toBe('email');
58+
}
59+
60+
public function test_sms_channel_can_be_enabled_via_config()
61+
{
62+
// Test with SMS enabled (default)
63+
config(['mfa.sms.enabled' => true]);
64+
65+
$user = new TestUser(1, 'test@example.com');
66+
67+
// Re-register channels to pick up new config
68+
MFA::reRegisterDefaultChannels();
69+
70+
// Should be able to issue SMS challenges when enabled
71+
$challenge = MFA::issueChallenge($user, 'sms');
72+
expect($challenge)->not->toBeNull();
73+
expect($challenge->method)->toBe('sms');
74+
}
75+
76+
public function test_email_channel_respects_env_variable()
77+
{
78+
// Test with environment variable
79+
putenv('MFA_EMAIL_ENABLED=false');
80+
config(['mfa.email.enabled' => env('MFA_EMAIL_ENABLED', true)]);
81+
82+
$user = new TestUser(1, 'test@example.com');
83+
84+
// Re-register channels to pick up new config
85+
MFA::reRegisterDefaultChannels();
86+
87+
// Should not be able to issue email challenges when disabled via env
88+
$challenge = MFA::issueChallenge($user, 'email');
89+
expect($challenge)->toBeNull();
90+
91+
// Clean up
92+
putenv('MFA_EMAIL_ENABLED');
93+
}
94+
95+
public function test_sms_channel_respects_env_variable()
96+
{
97+
// Test with environment variable
98+
putenv('MFA_SMS_ENABLED=false');
99+
config(['mfa.sms.enabled' => env('MFA_SMS_ENABLED', true)]);
100+
101+
$user = new TestUser(1, 'test@example.com');
102+
103+
// Re-register channels to pick up new config
104+
MFA::reRegisterDefaultChannels();
105+
106+
// Should not be able to issue SMS challenges when disabled via env
107+
$challenge = MFA::issueChallenge($user, 'sms');
108+
expect($challenge)->toBeNull();
109+
110+
// Clean up
111+
putenv('MFA_SMS_ENABLED');
112+
}
113+
114+
public function test_disabled_channels_are_not_registered()
115+
{
116+
// Disable both channels
117+
config(['mfa.email.enabled' => false]);
118+
config(['mfa.sms.enabled' => false]);
119+
120+
// Re-register channels to pick up new config
121+
MFA::reRegisterDefaultChannels();
122+
123+
// Check that channels are not available
124+
expect(MFA::getChannel('email'))->toBeNull();
125+
expect(MFA::getChannel('sms'))->toBeNull();
126+
}
127+
128+
public function test_enabled_channels_are_registered()
129+
{
130+
// Enable both channels (default)
131+
config(['mfa.email.enabled' => true]);
132+
config(['mfa.sms.enabled' => true]);
133+
134+
// Re-register channels to pick up new config
135+
MFA::reRegisterDefaultChannels();
136+
137+
// Check that channels are available
138+
expect(MFA::getChannel('email'))->not->toBeNull();
139+
expect(MFA::getChannel('sms'))->not->toBeNull();
140+
}
141+
}
142+
143+
class TestUser implements \Illuminate\Contracts\Auth\Authenticatable
144+
{
145+
public function __construct(public int|string $id, public ?string $email = null) {}
146+
147+
public function getAuthIdentifierName()
148+
{
149+
return 'id';
150+
}
151+
152+
public function getAuthIdentifier()
153+
{
154+
return $this->id;
155+
}
156+
157+
public function getAuthPassword()
158+
{
159+
return '';
160+
}
161+
162+
public function getAuthPasswordName()
163+
{
164+
return 'password';
165+
}
166+
167+
public function getRememberToken()
168+
{
169+
return '';
170+
}
171+
172+
public function setRememberToken($value)
173+
{
174+
// Not implemented
175+
}
176+
177+
public function getRememberTokenName()
178+
{
179+
return 'remember_token';
180+
}
181+
}

0 commit comments

Comments
 (0)