How do I handle authentication forms with Symfony Panther?
Symfony Panther provides powerful capabilities for handling authentication forms in web applications. As a browser automation tool built on ChromeDriver and Puppeteer, Panther allows you to interact with login forms, submit credentials, and manage authenticated sessions seamlessly.
Understanding Symfony Panther Authentication
Symfony Panther excels at handling dynamic authentication scenarios because it runs a real browser instance. This means it can handle JavaScript-heavy login forms, CSRF tokens, captchas, and complex multi-step authentication flows that traditional HTTP clients might struggle with.
Basic Authentication Form Handling
Simple Login Form Example
Here's how to handle a basic username/password authentication form:
<?php
use Symfony\Component\Panther\PantherTestCase;
class AuthenticationTest extends PantherTestCase
{
public function testLoginForm()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/login');
// Fill in the login form
$form = $crawler->selectButton('Login')->form([
'username' => 'your_username',
'password' => 'your_password'
]);
// Submit the form
$client->submit($form);
// Wait for redirect after successful login
$client->waitFor('.dashboard'); // Wait for dashboard element
// Verify successful authentication
$this->assertStringContains('Welcome', $client->getPageSource());
}
}
Advanced Form Interaction
For more complex forms, you can interact with individual elements:
public function testAdvancedLogin()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/login');
// Find and fill individual form fields
$usernameField = $crawler->filter('#username');
$passwordField = $crawler->filter('#password');
$loginButton = $crawler->filter('button[type="submit"]');
// Clear and type values
$usernameField->clear();
$usernameField->sendKeys('your_username');
$passwordField->clear();
$passwordField->sendKeys('your_password');
// Click the login button
$loginButton->click();
// Wait for authentication to complete
$client->waitForElementToContain('.user-menu', 'Profile');
}
Using DOM Crawler for Form Elements
Symfony Panther's DOM Crawler provides powerful element selection capabilities:
public function testFormElementSelection()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/login');
// Select by CSS class
$emailInput = $crawler->filter('.email-field');
// Select by attribute
$passwordInput = $crawler->filter('input[type="password"]');
// Select by name attribute
$submitButton = $crawler->filter('input[name="submit"]');
// Fill form fields
$emailInput->sendKeys('user@example.com');
$passwordInput->sendKeys('secure_password');
$submitButton->click();
// Wait for successful authentication
$client->waitForVisibility('.welcome-message');
}
Handling Different Authentication Scenarios
CSRF Token Protection
Many applications include CSRF tokens in login forms. Panther handles these automatically:
public function testLoginWithCSRF()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/login');
// Panther automatically extracts and includes CSRF tokens
$form = $crawler->selectButton('Sign In')->form();
$form['_username'] = 'testuser';
$form['_password'] = 'password123';
$client->submit($form);
// Verify successful login
$this->assertTrue($client->getCrawler()->filter('.logout-link')->count() > 0);
}
Two-Factor Authentication
For 2FA scenarios, you'll need to handle multiple steps:
public function testTwoFactorAuthentication()
{
$client = static::createPantherClient();
// Step 1: Initial login
$crawler = $client->request('GET', 'https://example.com/login');
$form = $crawler->selectButton('Login')->form([
'email' => 'user@example.com',
'password' => 'userpassword'
]);
$client->submit($form);
// Step 2: Wait for 2FA page
$client->waitFor('#two-factor-code');
// Step 3: Enter 2FA code
$twoFactorForm = $client->getCrawler()->selectButton('Verify')->form();
$twoFactorForm['code'] = '123456'; // In real scenarios, get from authenticator
$client->submit($twoFactorForm);
// Step 4: Verify final authentication
$client->waitFor('.authenticated-content');
$this->assertStringContains('Dashboard', $client->getTitle());
}
OAuth and Social Login
Handling OAuth flows requires managing redirects and external authentication providers:
public function testOAuthLogin()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/login');
// Click OAuth login button (e.g., "Login with Google")
$oauthButton = $crawler->filter('.oauth-google-btn');
$oauthButton->click();
// Wait for redirect to OAuth provider
$client->waitForElementToContain('title', 'Google');
// Fill OAuth provider login form
$client->waitFor('#identifierId');
$emailField = $client->getCrawler()->filter('#identifierId');
$emailField->sendKeys('oauth.user@gmail.com');
$nextButton = $client->getCrawler()->filter('#identifierNext');
$nextButton->click();
// Handle password step
$client->waitFor('input[type="password"]');
$passwordField = $client->getCrawler()->filter('input[type="password"]');
$passwordField->sendKeys('oauth_password');
$signInButton = $client->getCrawler()->filter('#passwordNext');
$signInButton->click();
// Wait for redirect back to application
$client->waitForElementToContain('.user-profile', 'Welcome');
}
Session Management and Persistence
Maintaining Sessions Across Requests
Once authenticated, you'll want to maintain the session for subsequent requests:
class AuthenticatedTestCase extends PantherTestCase
{
protected $authenticatedClient;
protected function setUp(): void
{
parent::setUp();
$this->authenticatedClient = $this->createAuthenticatedClient();
}
private function createAuthenticatedClient()
{
$client = static::createPantherClient();
// Perform login
$crawler = $client->request('GET', 'https://example.com/login');
$form = $crawler->selectButton('Login')->form([
'username' => 'testuser',
'password' => 'testpass'
]);
$client->submit($form);
// Wait for successful authentication
$client->waitFor('.authenticated-indicator');
return $client;
}
public function testAuthenticatedAction()
{
// Use the authenticated client for protected actions
$crawler = $this->authenticatedClient->request('GET', 'https://example.com/protected-page');
$this->assertStringContains('Protected Content', $crawler->text());
}
}
Cookie and Session Handling
Panther automatically handles cookies, but you can also manipulate them manually:
public function testSessionCookies()
{
$client = static::createPantherClient();
// Login and get session cookie
$this->performLogin($client);
// Get current cookies
$cookies = $client->getCookieJar()->all();
// You can save cookies for later use
$sessionCookie = null;
foreach ($cookies as $cookie) {
if ($cookie->getName() === 'PHPSESSID') {
$sessionCookie = $cookie;
break;
}
}
// Create new client with existing session
$newClient = static::createPantherClient();
if ($sessionCookie) {
$newClient->getCookieJar()->set($sessionCookie);
}
// Access protected content with existing session
$crawler = $newClient->request('GET', 'https://example.com/dashboard');
$this->assertStringContains('Dashboard', $crawler->text());
}
Advanced Authentication Patterns
Remember Me Functionality
Testing "Remember Me" checkboxes and persistent sessions:
public function testRememberMeLogin()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/login');
// Fill login form with remember me checkbox
$form = $crawler->selectButton('Login')->form([
'username' => 'testuser',
'password' => 'testpass',
'remember_me' => true
]);
$client->submit($form);
$client->waitFor('.dashboard');
// Verify remember me cookie is set
$cookies = $client->getCookieJar()->all();
$rememberCookie = array_filter($cookies, function($cookie) {
return strpos($cookie->getName(), 'remember') !== false;
});
$this->assertNotEmpty($rememberCookie);
}
Multi-Domain Authentication
Handling authentication across different domains:
public function testCrossDomainAuth()
{
$client = static::createPantherClient();
// Login on main domain
$crawler = $client->request('GET', 'https://auth.example.com/login');
$this->performLogin($client, 'user@example.com', 'password');
// Navigate to subdomain and verify session persistence
$client->request('GET', 'https://app.example.com/dashboard');
$client->waitFor('.authenticated-user');
// Verify user is authenticated on subdomain
$this->assertStringContains('Welcome', $client->getPageSource());
}
Error Handling and Debugging
Handling Authentication Failures
Always implement proper error handling for authentication scenarios:
public function testLoginFailure()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/login');
// Attempt login with invalid credentials
$form = $crawler->selectButton('Login')->form([
'username' => 'invalid_user',
'password' => 'wrong_password'
]);
$client->submit($form);
try {
// Wait for error message
$client->waitForElementToContain('.error-message', 'Invalid credentials');
$this->assertTrue(true); // Test passes if error message appears
} catch (\Exception $e) {
// Handle timeout or other errors
$this->fail('Expected authentication error message not found');
}
}
Debugging Authentication Issues
When authentication fails, use Panther's debugging capabilities:
public function debugAuthenticationFlow()
{
$client = static::createPantherClient([
'browser' => static::CHROME,
'external_base_uri' => 'http://127.0.0.1:9080',
]);
// Take screenshot before login
$client->takeScreenshot('before_login.png');
$crawler = $client->request('GET', 'https://example.com/login');
// Log page source for debugging
file_put_contents('login_page.html', $client->getPageSource());
// Perform login
$form = $crawler->selectButton('Login')->form([
'username' => 'testuser',
'password' => 'testpass'
]);
$client->submit($form);
// Take screenshot after login attempt
$client->takeScreenshot('after_login.png');
// Log final page source
file_put_contents('post_login_page.html', $client->getPageSource());
}
Best Practices for Authentication Testing
Environment Configuration
Set up proper test environments for authentication testing:
# config/packages/test/panther.yaml
panther:
options:
# Disable web security for testing
disable_web_security: true
# Set longer timeouts for authentication flows
timeout: 30
external_base_uri: 'http://localhost:8080'
Reusable Authentication Methods
Create helper methods for common authentication patterns:
trait AuthenticationHelpers
{
protected function loginUser($client, $username, $password)
{
$crawler = $client->request('GET', '/login');
$form = $crawler->selectButton('Login')->form([
'username' => $username,
'password' => $password
]);
$client->submit($form);
$client->waitFor('.authenticated-user');
}
protected function loginAsAdmin($client)
{
$this->loginUser($client, 'admin@example.com', 'admin_password');
}
protected function loginAsRegularUser($client)
{
$this->loginUser($client, 'user@example.com', 'user_password');
}
protected function logout($client)
{
$logoutLink = $client->getCrawler()->filter('.logout-link');
if ($logoutLink->count() > 0) {
$logoutLink->click();
$client->waitFor('.login-form');
}
}
}
Integration with CI/CD
When running authentication tests in CI/CD pipelines, consider these configurations:
# .github/workflows/panther-tests.yml
name: Panther Authentication Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
- name: Install dependencies
run: composer install
- name: Start Chrome
run: google-chrome --headless --remote-debugging-port=9222 --no-sandbox &
- name: Run Panther tests
run: ./vendor/bin/phpunit tests/AuthenticationTest.php
env:
PANTHER_CHROME_DRIVER_BINARY: /usr/bin/google-chrome
Waiting Strategies for Authentication
Proper timing is crucial for authentication flows. Similar to how Puppeteer's waitFor function handles dynamic content, Panther provides several waiting mechanisms:
public function testWaitingStrategies()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/login');
// Submit login form
$form = $crawler->selectButton('Login')->form([
'username' => 'testuser',
'password' => 'testpass'
]);
$client->submit($form);
// Different waiting strategies
$client->waitFor('.dashboard-content'); // Wait for element to exist
$client->waitForVisibility('.user-menu'); // Wait for element to be visible
$client->waitForElementToContain('.welcome-msg', 'Welcome back'); // Wait for text
$client->waitForInvisibility('.loading-spinner'); // Wait for element to disappear
}
Security Considerations
When handling authentication in tests:
- Never use production credentials in test environments
- Use environment variables for sensitive test data
- Implement proper test data cleanup after authentication tests
- Consider using test-specific authentication endpoints when possible
// Using environment variables for test credentials
public function testSecureAuthentication()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', $_ENV['TEST_LOGIN_URL']);
$form = $crawler->selectButton('Login')->form([
'username' => $_ENV['TEST_USERNAME'],
'password' => $_ENV['TEST_PASSWORD']
]);
$client->submit($form);
$client->waitFor('.authenticated-content');
// Cleanup: logout after test
$this->logout($client);
}
Handling Browser Sessions
Just like how Puppeteer manages browser sessions, Symfony Panther provides excellent session management capabilities for maintaining authentication state across multiple test scenarios.
Conclusion
Symfony Panther excels at handling complex authentication scenarios through its browser automation capabilities. Whether you're dealing with simple login forms, multi-factor authentication, or OAuth flows, Panther provides the tools needed to automate and test these critical user journeys. The key is understanding how to properly wait for page changes, handle form submissions, and manage session state throughout your test scenarios.
By following these patterns and best practices, you can build reliable authentication tests that ensure your application's security features work correctly across different browsers and scenarios. Remember to always prioritize security by using appropriate test credentials and environments when implementing authentication testing in your development workflow.