Awesome-omni-skill auth-generic
Generic authentication skill for any custom auth system. Use when project.json has authentication.provider: custom or when no specific auth skill exists. Triggers on: custom auth, generic login, form-based auth, custom login flow.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/testing-security/auth-generic" ~/.claude/skills/diegosouzapw-awesome-omni-skill-auth-generic && rm -rf "$T"
skills/testing-security/auth-generic/SKILL.mdGeneric Authentication
Authenticate with any form-based authentication system using configurable selectors. This is the fallback skill when no provider-specific skill exists.
Prerequisites
-
Project configuration in
:docs/project.json{ "authentication": { "method": "email-password", "provider": "custom", "testUser": { "mode": "fixed", "emailVar": "TEST_EMAIL", "emailDefault": "test@example.com", "passwordVar": "TEST_PASSWORD" }, "routes": { "login": "/login", "authenticated": "/dashboard" }, "selectors": { "emailInput": "input[name='email']", "passwordInput": "input[name='password']", "submitButton": "button[type='submit']" } } } -
Environment variables in
:.env.localTEST_EMAIL=test@example.com TEST_PASSWORD=your-test-password -
Test user exists in your authentication system
The Job
- Read authentication config from
project.json - Navigate to login page
- Fill form fields using configured selectors
- Submit the form
- Wait for redirect to authenticated page
- Return authenticated page context
Supported Authentication Methods
Email/Password
Standard username/password login:
{ "authentication": { "method": "email-password", "selectors": { "emailInput": "input[type='email'], input[name='email'], input#email", "passwordInput": "input[type='password'], input[name='password']", "submitButton": "button[type='submit']" } } }
Passwordless OTP
Two-step OTP verification:
{ "authentication": { "method": "passwordless-otp", "selectors": { "emailInput": "input[type='email']", "submitEmailButton": "button:has-text('Send code')", "otpInputs": "input.otp-digit", "submitOtpButton": "button:has-text('Verify')" }, "verification": { "source": "api", "endpoint": "/api/test/get-otp", "method": "POST", "bodyTemplate": "{\"email\": \"{{email}}\"}" }, "routes": { "login": "/login", "verify": "/verify", "authenticated": "/dashboard" } } }
Magic Link (with test override)
For magic link flows, configure a test bypass:
{ "authentication": { "method": "magic-link", "selectors": { "emailInput": "input[type='email']", "submitButton": "button:has-text('Send link')" }, "verification": { "source": "database", "query": "SELECT token FROM magic_links WHERE email = ? ORDER BY created_at DESC LIMIT 1" }, "routes": { "login": "/login", "magicLinkPattern": "/auth/verify?token={{token}}", "authenticated": "/dashboard" } } }
Authentication Flow
Step 1: Load Configuration
import * as fs from 'fs'; import * as path from 'path'; interface Selectors { emailInput?: string; passwordInput?: string; submitButton?: string; submitEmailButton?: string; otpInputs?: string; submitOtpButton?: string; usernameInput?: string; } interface AuthConfig { method: string; provider: string; testUser: { mode: 'fixed' | 'dynamic'; emailVar?: string; emailDefault?: string; emailPattern?: string; passwordVar?: string; passwordDefault?: string; usernameVar?: string; }; routes: { login: string; verify?: string; authenticated: string; magicLinkPattern?: string; }; selectors: Selectors; verification?: { source: string; endpoint?: string; method?: string; bodyTemplate?: string; query?: string; }; steps?: AuthStep[]; } interface AuthStep { action: 'fill' | 'click' | 'wait' | 'navigate' | 'custom'; selector?: string; value?: string; waitFor?: string; url?: string; timeout?: number; } function loadAuthConfig(projectRoot: string): AuthConfig { const projectJsonPath = path.join(projectRoot, 'docs', 'project.json'); const projectJson = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8')); if (!projectJson.authentication) { throw new Error('No authentication config found in project.json. Run /setup-auth first.'); } // Ensure selectors exist with defaults const config = projectJson.authentication; config.selectors = config.selectors || {}; return config; } function loadEnv(projectRoot: string): void { const envPath = path.join(projectRoot, '.env.local'); if (fs.existsSync(envPath)) { const content = fs.readFileSync(envPath, 'utf-8'); content.split('\n').forEach(line => { const match = line.trim().match(/^([^=]+)=(.*)$/); if (match && !process.env[match[1]]) { process.env[match[1]] = match[2]; } }); } } function getTestCredentials(config: AuthConfig): { email: string; password?: string; username?: string } { let email: string; let password: string | undefined; let username: string | undefined; if (config.testUser.mode === 'dynamic') { const uuid = crypto.randomUUID().slice(0, 8); const pattern = config.testUser.emailPattern || 'test-{uuid}@example.com'; email = pattern.replace('{uuid}', uuid); password = process.env[config.testUser.passwordVar || 'TEST_PASSWORD'] || config.testUser.passwordDefault; } else { const emailVar = config.testUser.emailVar || 'TEST_EMAIL'; email = process.env[emailVar] || config.testUser.emailDefault || 'test@example.com'; if (config.method === 'email-password') { const passwordVar = config.testUser.passwordVar || 'TEST_PASSWORD'; password = process.env[passwordVar] || config.testUser.passwordDefault; if (!password) { throw new Error(`Password not configured. Set ${passwordVar} in .env.local`); } } if (config.testUser.usernameVar) { username = process.env[config.testUser.usernameVar]; } } return { email, password, username }; }
Step 2: Default Selectors
const DEFAULT_SELECTORS: Selectors = { emailInput: 'input[type="email"], input[name="email"], input#email, input[autocomplete="email"]', passwordInput: 'input[type="password"], input[name="password"], input#password', submitButton: 'button[type="submit"], button:has-text("Sign in"), button:has-text("Log in"), button:has-text("Continue")', otpInputs: 'input[maxlength="1"], input.otp-input, input[inputmode="numeric"]', usernameInput: 'input[name="username"], input#username, input[autocomplete="username"]' }; function getSelector(config: AuthConfig, key: keyof Selectors): string { return config.selectors[key] || DEFAULT_SELECTORS[key] || ''; }
Step 3: Authenticate with Playwright
import { Page } from 'playwright'; interface AuthResult { success: boolean; email: string; error?: string; } async function authenticateGeneric( page: Page, baseUrl: string, projectRoot: string ): Promise<AuthResult> { loadEnv(projectRoot); const config = loadAuthConfig(projectRoot); const credentials = getTestCredentials(config); try { // Navigate to login await page.goto(`${baseUrl}${config.routes.login}`); // Handle based on method switch (config.method) { case 'email-password': await handleEmailPassword(page, config, credentials); break; case 'passwordless-otp': await handlePasswordlessOtp(page, config, credentials, baseUrl); break; case 'magic-link': await handleMagicLink(page, config, credentials, baseUrl); break; default: // If custom steps are defined, execute them if (config.steps) { await executeCustomSteps(page, config.steps, credentials); } else { throw new Error(`Unknown auth method: ${config.method}. Define custom steps.`); } } // Wait for authenticated page await page.waitForURL(new RegExp(config.routes.authenticated), { timeout: 15000 }); console.log(`Authentication successful, landed on ${config.routes.authenticated}`); return { success: true, email: credentials.email }; } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`Authentication failed: ${message}`); return { success: false, email: credentials.email, error: message }; } } async function handleEmailPassword( page: Page, config: AuthConfig, credentials: { email: string; password?: string; username?: string } ): Promise<void> { const emailSelector = getSelector(config, 'emailInput'); const passwordSelector = getSelector(config, 'passwordInput'); const submitSelector = getSelector(config, 'submitButton'); await page.waitForSelector(emailSelector); // Fill username if configured, otherwise email if (credentials.username && config.selectors.usernameInput) { await page.fill(config.selectors.usernameInput, credentials.username); } else { await page.fill(emailSelector, credentials.email); } await page.fill(passwordSelector, credentials.password!); await page.click(submitSelector); } async function handlePasswordlessOtp( page: Page, config: AuthConfig, credentials: { email: string }, baseUrl: string ): Promise<void> { const emailSelector = getSelector(config, 'emailInput'); const submitEmailSelector = config.selectors.submitEmailButton || getSelector(config, 'submitButton'); // Step 1: Enter email await page.waitForSelector(emailSelector); await page.fill(emailSelector, credentials.email); await page.click(submitEmailSelector); // Step 2: Wait for verify page if (config.routes.verify) { await page.waitForURL(new RegExp(config.routes.verify)); } await page.waitForTimeout(1000); // Allow OTP to be generated // Step 3: Get OTP code const otp = await fetchOtpCode(config, credentials.email, baseUrl); // Step 4: Enter OTP const otpSelector = getSelector(config, 'otpInputs'); const otpInputs = page.locator(otpSelector); const inputCount = await otpInputs.count(); if (inputCount >= 6) { // Multiple single-digit inputs for (let i = 0; i < Math.min(otp.length, inputCount); i++) { await otpInputs.nth(i).fill(otp[i]); } } else { // Single input for full code await otpInputs.first().fill(otp); } // Step 5: Submit OTP const submitOtpSelector = config.selectors.submitOtpButton || getSelector(config, 'submitButton'); await page.click(submitOtpSelector); } async function handleMagicLink( page: Page, config: AuthConfig, credentials: { email: string }, baseUrl: string ): Promise<void> { const emailSelector = getSelector(config, 'emailInput'); const submitSelector = getSelector(config, 'submitButton'); // Enter email await page.waitForSelector(emailSelector); await page.fill(emailSelector, credentials.email); await page.click(submitSelector); // Wait for magic link to be created await page.waitForTimeout(1000); // Fetch token const token = await fetchMagicLinkToken(config, credentials.email); // Navigate directly to magic link URL const magicLinkUrl = config.routes.magicLinkPattern!.replace('{{token}}', token); await page.goto(`${baseUrl}${magicLinkUrl}`); } async function fetchOtpCode( config: AuthConfig, email: string, baseUrl: string ): Promise<string> { if (!config.verification) { throw new Error('OTP verification config required for passwordless-otp method'); } if (config.verification.source === 'api') { const body = config.verification.bodyTemplate?.replace('{{email}}', email) || JSON.stringify({ email }); const response = await fetch(`${baseUrl}${config.verification.endpoint}`, { method: config.verification.method || 'POST', headers: { 'Content-Type': 'application/json' }, body }); const data = await response.json(); return data.code || data.otp || data.token; } throw new Error(`OTP source "${config.verification.source}" not implemented in generic skill`); } async function fetchMagicLinkToken(config: AuthConfig, email: string): Promise<string> { // This would need project-specific implementation // Generic skill can call an API endpoint if configured throw new Error('Magic link token retrieval requires project-specific implementation'); } async function executeCustomSteps( page: Page, steps: AuthStep[], credentials: { email: string; password?: string } ): Promise<void> { for (const step of steps) { const value = step.value ?.replace('{{email}}', credentials.email) ?.replace('{{password}}', credentials.password || ''); switch (step.action) { case 'fill': await page.fill(step.selector!, value!); break; case 'click': await page.click(step.selector!); break; case 'wait': if (step.selector) { await page.waitForSelector(step.selector, { timeout: step.timeout || 5000 }); } else if (step.url) { await page.waitForURL(new RegExp(step.url), { timeout: step.timeout || 10000 }); } else { await page.waitForTimeout(step.timeout || 1000); } break; case 'navigate': await page.goto(step.url!); break; } } }
Complete Script Template
import { chromium } from 'playwright'; import * as fs from 'fs'; import * as path from 'path'; const PROJECT_ROOT = process.cwd(); const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000'; // (Include helper functions from above) async function main() { const browser = await chromium.launch(); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); try { const result = await authenticateGeneric(page, BASE_URL, PROJECT_ROOT); if (!result.success) { console.error('Authentication failed:', result.error); process.exit(1); } console.log(`Logged in as ${result.email}`); } finally { await browser.close(); } } main().catch(console.error);
Custom Steps Example
For complex multi-step authentication flows, define custom steps:
{ "authentication": { "method": "custom", "provider": "custom", "testUser": { "mode": "fixed", "emailVar": "TEST_EMAIL", "passwordVar": "TEST_PASSWORD" }, "routes": { "login": "/login", "authenticated": "/app" }, "steps": [ { "action": "fill", "selector": "input#username", "value": "{{email}}" }, { "action": "click", "selector": "button:has-text('Next')" }, { "action": "wait", "selector": "input#password" }, { "action": "fill", "selector": "input#password", "value": "{{password}}" }, { "action": "click", "selector": "input#remember-me" }, { "action": "click", "selector": "button:has-text('Sign in')" } ] } }
Selector Best Practices
Prefer Stable Selectors
{ "selectors": { "emailInput": "input[data-testid='email-input']", "passwordInput": "input[data-testid='password-input']", "submitButton": "button[data-testid='submit-btn']" } }
Fallback Chains
The skill tries selectors in order. Configure multiple fallbacks:
{ "selectors": { "emailInput": "input[data-testid='email'], input[name='email'], input#email" } }
Avoid Fragile Selectors
- ❌
.login-form > div:nth-child(2) > input - ❌
body > main > form > button - ✅
input[name='email'] - ✅
button[type='submit'] - ✅
[data-testid='login-button']
Troubleshooting
"Selector not found"
- Use browser DevTools to verify the selector
- Check if the element is inside an iframe
- Wait for dynamic content to load
"Timeout waiting for authenticated page"
- Verify
matches the actual redirect URLroutes.authenticated - Check for JavaScript errors in browser console
- Increase timeout if the server is slow
"Unknown auth method"
- Set
to one of:method
,email-password
,passwordless-otpmagic-link - Or define custom
for non-standard flowssteps
Multi-step forms
Use the
steps array for forms that span multiple pages or have conditional logic.
When to Use Generic vs Provider-Specific Skills
| Scenario | Recommendation |
|---|---|
| Standard Supabase setup | Use or |
| Standard NextAuth setup | Use |
| Custom auth with standard form | Use with selectors |
| Complex multi-step auth | Use with custom steps |
| OAuth flows | Defer to provider-specific skill (future) |
Integration with Other Skills
This skill is the fallback used by:
- when no provider-specific skill matchesscreenshot
- for custom auth systemse2e-playwright
- for any form-based authqa-browser-tester
Agents should:
- Check
forproject.json
configauthentication - Try provider-specific skill first (
,auth-supabase-*
)auth-nextauth-* - Fall back to
if provider isauth-generic
or unrecognizedcustom - Call
before accessing protected pagesauthenticateGeneric()