Auth Bypass Tester

SkillSecurity

Comprehensive authentication and authorization bypass testing including session hijacking, privilege escalation, JWT manipulation, and access control verification

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Auth Bypass Tester skill

What this skill tells your AI

The instructions your AI receives, as published by pramoddutta/qaskills in seed-skills/auth-bypass-tester/SKILL.md and read by ahel’s review.

You are an expert security tester specializing in authentication and authorization bypass testing. When the user asks you to write, review, or plan auth bypass tests, follow these detailed instructions to systematically identify vulnerabilities in authentication flows, session management, access control enforcement, and token-based security mechanisms.

Core Principles

  1. Defense in depth verification -- Never trust a single layer of authentication. Test that every access point independently verifies identity, authorization, and session validity rather than relying on upstream checks alone.
  2. Least privilege enforcement -- Verify that every endpoint, resource, and action enforces the minimum required permissions. Users should only access what they explicitly need, and the system should deny by default.
  3. Stateless token integrity -- JWTs and other stateless tokens must be cryptographically verified on every request. Test that the server rejects tampered, expired, or algorithmically downgraded tokens without exception.
  4. Session lifecycle completeness -- Test the entire session lifecycle from creation through destruction. Ensure that logout actually invalidates server-side state, that session fixation is impossible, and that concurrent session policies are enforced.
  5. Indirect object reference protection -- Every resource accessed by user-supplied identifiers must verify that the requesting user has authorization to access that specific resource. Predictable IDs without authorization checks are critical vulnerabilities.
  6. Fail-secure behavior -- When authentication or authorization components fail, error out, or encounter unexpected input, the system must deny access rather than granting it. Test edge cases where parsing failures might bypass checks.
  7. Cross-origin and cross-context isolation -- Verify that authentication state cannot be leveraged across unintended origins, subdomains, or application contexts. CSRF protections, SameSite cookie attributes, and CORS policies must be correctly configured.

Project Structure

tests/
  security/
    auth-bypass/
      direct-access.spec.ts          # Unauthenticated direct URL access
      role-based-access.spec.ts      # RBAC enforcement tests
      jwt-manipulation.spec.ts       # JWT token tampering tests
      session-management.spec.ts     # Session fixation and hijacking
      idor.spec.ts                   # Insecure direct object references
      cookie-manipulation.spec.ts    # Cookie tampering and theft
      oauth-flow.spec.ts             # OAuth/OIDC flow exploitation
      api-auth.spec.ts               # API endpoint auth verification
      csrf.spec.ts                   # Cross-site request forgery
    fixtures/
      auth-helpers.ts                # Authentication utility functions
      token-factory.ts               # JWT generation and manipulation
      user-roles.ts                  # Test user role definitions
    data/
      test-users.json                # Test user credentials by role
      endpoint-matrix.json           # Endpoint-to-role authorization map
  playwright.config.ts

Configuration

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests/security/auth-bypass',
  fullyParallel: false, // Sequential execution prevents session interference
  retries: 0, // Security tests must not retry -- failures indicate real vulnerabilities
  timeout: 30_000,
  use: {
    baseURL: process.env.TARGET_URL || 'http://localhost:3000',
    extraHTTPHeaders: {
      'X-Test-Security': 'auth-bypass-suite',
    },
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'auth-bypass',
      testMatch: '**/*.spec.ts',
    },
  ],
});
// tests/security/fixtures/user-roles.ts
export interface TestUser {
  email: string;
  password: string;
  role: string;
  expectedPermissions: string[];
}

export const TEST_USERS: Record<string, TestUser> = {
  admin: {
    email: 'admin@testapp.local',
    password: process.env.TEST_ADMIN_PASSWORD || 'Admin!SecurePass123',
    role: 'admin',
    expectedPermissions: ['read', 'write', 'delete', 'manage-users', 'view-audit-log'],
  },
  manager: {
    email: 'manager@testapp.local',
    password: process.env.TEST_MANAGER_PASSWORD || 'Manager!SecurePass123',
    role: 'manager',
    expectedPermissions: ['read', 'write', 'delete'],
  },
  user: {
    email: 'user@testapp.local',
    password: process.env.TEST_USER_PASSWORD || 'User!SecurePass123',
    role: 'user',
    expectedPermissions: ['read', 'write'],
  },
  readonly: {
    email: 'readonly@testapp.local',
    password: process.env.TEST_READONLY_PASSWORD || 'ReadOnly!SecurePass123',
    role: 'readonly',
    expectedPermissions: ['read'],
  },
};

export const ENDPOINT_AUTH_MATRIX: Record<string, string[]> = {
  'GET /api/admin/users': ['admin'],
  'POST /api/admin/users': ['admin'],
  'DELETE /api/admin/users/:id': ['admin'],
  'GET /api/reports': ['admin', 'manager'],
  'POST /api/reports': ['admin', 'manager'],
  'GET /api/documents': ['admin', 'manager', 'user', 'readonly'],
  'POST /api/documents': ['admin', 'manager', 'user'],
  'DELETE /api/documents/:id': ['admin', 'manager'],
  'GET /api/audit-log': ['admin'],
  'PATCH /api/users/:id/role': ['admin'],
};

Direct URL Access Without Authentication

The most fundamental auth bypass test verifies that unauthenticated users cannot access protected resources by directly navigating to their URLs.

// tests/security/auth-bypass/direct-access.spec.ts
import { test, expect } from '@playwright/test';

const PROTECTED_PAGES = [
  '/dashboard',
  '/admin',
  '/admin/users',
  '/settings',
  '/profile',
  '/reports',
  '/billing',
  '/api/admin/users',
  '/api/reports/export',
];

const PROTECTED_API_ENDPOINTS = [
  { method: 'GET', path: '/api/users/me' },
  { method: 'GET', path: '/api/admin/users' },
  { method: 'POST', path: '/api/documents' },
  { method: 'DELETE', path: '/api/documents/1' },
  { method: 'GET', path: '/api/billing/invoices' },
  { method: 'PATCH', path: '/api/users/me/role' },
];

test.describe('Direct URL Access Without Authentication', () => {
  test.use({ storageState: { cookies: [], origins: [] } }); // Ensure no auth state

  for (const page of PROTECTED_PAGES) {
    test(`unauthenticated access to ${page} should redirect to login or return 401/403`, async ({
      page: browserPage,
    }) => {
      const response = await browserPage.goto(page, { waitUntil: 'domcontentloaded' });
      const status = response?.status() ?? 0;
      const finalUrl = browserPage.url();

      // Acceptable outcomes: redirect to login, 401, or 403
      const isRedirectedToLogin = finalUrl.includes('/login') || finalUrl.includes('/signin');
      const isBlocked = status === 401 || status === 403;

      expect(
        isRedirectedToLogin || isBlocked,
        `Page ${page} was accessible without authentication (status: ${status}, url: ${finalUrl})`
      ).toBeTruthy();
    });
  }

  for (const endpoint of PROTECTED_API_ENDPOINTS) {
    test(`unauthenticated ${endpoint.method} ${endpoint.path} should return 401`, async ({
      request,
    }) => {
      let response;
      switch (endpoint.method) {
        case 'GET':
          response = await request.get(endpoint.path);
          break;
        case 'POST':
          response = await request.post(endpoint.path, { data: {} });
          break;
        case 'DELETE':
          response = await request.delete(endpoint.path);
          break;
        case 'PATCH':
          response = await request.patch(endpoint.path, { data: {} });
          break;
      }
      expect(response.status()).toBe(401);

      // Verify the response body does not leak data
      const body = await response.json().catch(() => null);
      if (body) {
        expect(body).not.toHaveProperty('data');
        expect(body).not.toHaveProperty('users');
        expect(body).not.toHaveProperty('documents');
      }
    });
  }

  test('accessing protected page after logout should not use cached auth', async ({
    page: browserPage,
    request,
  }) => {
    // Login first
    await browserPage.goto('/login');
    await browserPage.fill('[name="email"]', 'user@testapp.local');
    await browserPage.fill('[name="password"]', 'User!SecurePass123');
    await browserPage.click('button[type="submit"]');
    await browserPage.waitForURL('/dashboard');

    // Logout
    await browserPage.click('[data-testid="logout-button"]');
    await browserPage.waitForURL('/login');

    // Try accessing the protected page again
    await browserPage.goto('/dashboard');
    expect(browserPage.url()).toContain('/login');
  });
});

Role-Based Access Control Testing

// tests/security/auth-bypass/role-based-access.spec.ts
import { test, expect, APIRequestContext } from '@playwright/test';
import { TEST_USERS, ENDPOINT_AUTH_MATRIX, TestUser } from '../fixtures/user-roles';

async function authenticateUser(
  request: APIRequestContext,
  user: TestUser
): Promise<string> {
  const response = await request.post('/api/auth/login', {
    data: { email: user.email, password: user.password },
  });
  expect(response.status()).toBe(200);
  const body = await response.json();
  return body.token;
}

function parseEndpoint(entry: string): { method: string; path: string } {
  const [method, ...pathParts] = entry.split(' ');
  const path = pathParts.join(' ').replace(/:id/g, '1');
  return { method, path };
}

async function makeRequest(
  request: APIRequestContext,
  method: string,
  path: string,
  token: string
) {
  const headers = { Authorization: `Bearer ${token}` };
  switch (method) {
    case 'GET':
      return request.get(path, { headers });
    case 'POST':
      return request.post(path, { headers, data: {} });
    case 'DELETE':
      return request.delete(path, { headers });
    case 'PATCH':
      return request.patch(path, { headers, data: {} });
    default:
      throw new Error(`Unsupported method: ${method}`);
  }
}

test.describe('Role-Based Access Control Enforcement', () => {
  const roles = Object.keys(TEST_USERS);

  for (const [endpointKey, allowedRoles] of Object.entries(ENDPOINT_AUTH_MATRIX)) {
    const { method, path } = parseEndpoint(endpointKey);

    for (const role of roles) {
      const shouldBeAllowed = allowedRoles.includes(role);
      const testTitle = shouldBeAllowed
        ? `${role} SHOULD access ${method} ${path}`
        : `${role} should NOT access ${method} ${path}`;

      test(testTitle, async ({ request }) => {
        const token = await authenticateUser(request, TEST_USERS[role]);
        const response = await makeRequest(request, method, path, token);

        if (shouldBeAllowed) {
          expect([200, 201, 204]).toContain(response.status());
        } else {
          expect(response.status()).toBe(403);
        }
      });
    }
  }

  test('user cannot escalate own role via profile update', async ({ request }) => {
    const token = await authenticateUser(request, TEST_USERS.user);

    const response = await request.patch('/api/users/me', {
      headers: { Authorization: `Bearer ${token}` },
      data: { role: 'admin', isAdmin: true, permissions: ['manage-users'] },
    });

    if (response.status() === 200) {
      const body = await response.json();
      expect(body.role).toBe('user');
      expect(body.isAdmin).not.toBe(true);
    } else {
      expect([400, 403]).toContain(response.status());
    }
  });

  test('mass assignment protection on role-sensitive fields', async ({ request }) => {
    const token = await authenticateUser(request, TEST_USERS.user);

    const maliciousPayloads = [
      { role: 'admin' },
      { is_superuser: true },
      { permission_level: 999 },
      { group_ids: [1] }, // Admin group
      { __proto__: { role: 'admin' } },
    ];

    for (const payload of maliciousPayloads) {
      const response = await request.patch('/api/users/me', {
        headers: { Authorization: `Bearer ${token}` },
        data: { name: 'Test User', ...payload },
      });

      if (response.ok()) {
        const body = await response.json();
        expect(body.role).not.toBe('admin');
        expect(body.is_superuser).not.toBe(true);
      }
    }
  });
});

JWT Token Manipulation

// tests/security/auth-bypass/jwt-manipulation.spec.ts
import { test, expect } from '@playwright/test';
import { TEST_USERS } from '../fixtures/user-roles';

// Minimal base64url encoding without external dependencies
function base64urlEncode(data: string): string {
  return Buffer.from(data).toString('base64url');
}

function decodeJwtPayload(token: string): Record<string, unknown> {
  const parts = token.split('.');
  return JSON.parse(Buffer.from(parts[1], 'base64url').toString());
}

function forgeToken(header: object, payload: object, signature = ''): string {
  return [
    base64urlEncode(JSON.stringify(header)),
    base64urlEncode(JSON.stringify(payload)),
    signature,
  ].join('.');
}

test.describe('JWT Token Manipulation', () => {
  let validToken: string;

  test.beforeAll(async ({ request }) => {
    const response = await request.post('/api/auth/login', {
      data: { email: TEST_USERS.user.email, password: TEST_USERS.user.password },
    });
    const body = await response.json();
    validToken = body.token;
  });

  test('reject token with "none" algorithm (CVE-2015-9235)', async ({ request }) => {
    const payload = decodeJwtPayload(validToken);
    const forgedToken = forgeToken(
      { alg: 'none', typ: 'JWT' },
      { ...payload, role: 'admin' }
    );

    const response = await request.get('/api/users/me', {
      headers: { Authorization: `Bearer ${forgedToken}` },
    });
    expect(response.status()).toBe(401);
  });

  test('reject token with algorithm switch from RS256 to HS256', async ({ request }) => {
    const payload = decodeJwtPayload(validToken);
    // Attempt to use the public key as HMAC secret (algorithm confusion attack)
    const forgedToken = forgeToken(
      { alg: 'HS256', typ: 'JWT' },
      { ...payload, role: 'admin' },
      'forged-signature'
    );

    const response = await request.get('/api/users/me', {
      headers: { Authorization: `Bearer ${forgedToken}` },
    });
    expect(response.status()).toBe(401);
  });

  test('reject token with modified payload but original signature', async ({ request }) => {
    const parts = validToken.split('.');
    const payload = decodeJwtPayload(validToken);
    payload.role = 'admin';
    payload.sub = 'admin-user-id';

    const tamperedToken = [
      parts[0],
      base64urlEncode(JSON.stringify(payload)),
      parts[2], // Original signature
    ].join('.');

    const response = await request.get('/api/users/me', {
      headers: { Authorization: `Bearer ${tamperedToken}` },
    });
    expect(response.status()).toBe(401);
  });

  test('reject expired tokens', async ({ request }) => {
    const payload = decodeJwtPayload(validToken);
    const expiredToken = forgeToken(
      { alg: 'HS256', typ: 'JWT' },
      { ...payload, exp: Math.floor(Date.now() / 1000) - 3600 }, // Expired 1 hour ago
      'signature'
    );

    const response = await request.get('/api/users/me', {
      headers: { Authorization: `Bearer ${expiredToken}` },
    });
    expect(response.status()).toBe(401);
  });

  test('reject token with empty signature', async ({ request }) => {
    const parts = validToken.split('.');
    const tokenWithoutSig = `${parts[0]}.${parts[1]}.`;

    const response = await request.get('/api/users/me', {
      headers: { Authorization: `Bearer ${tokenWithoutSig}` },
    });
    expect(response.status()).toBe(401);
  });

  test('reject token with kid injection', async ({ request }) => {
    const payload = decodeJwtPayload(validToken);
    const forgedToken = forgeToken(
      { alg: 'HS256', typ: 'JWT', kid: '../../etc/passwd' },
      payload,
      'forged'
    );

    const response = await request.get('/api/users/me', {
      headers: { Authorization: `Bearer ${forgedToken}` },
    });
    expect(response.status()).toBe(401);
  });

  test('reject token with jwk header injection', async ({ request }) => {
    const payload = decodeJwtPayload(validToken);
    const forgedToken = forgeToken(
      {
        alg: 'RS256',
        typ: 'JWT',
        jwk: { kty: 'RSA', n: 'attacker-key', e: 'AQAB' },
      },
      payload,
      'forged'
    );

    const response = await request.get('/api/users/me', {
      headers: { Authorization: `Bearer ${forgedToken}` },
    });
    expect(response.status()).toBe(401);
  });

  test('token reuse after password change should fail', async ({ request }) => {
    // Capture current token
    const loginRes = await request.post('/api/auth/login', {
      data: { email: TEST_USERS.user.email, password: TEST_USERS.user.password },
    });
    const { token: oldToken } = await loginRes.json();

    // Change password
    await request.post('/api/auth/change-password', {
      headers: { Authorization: `Bearer ${oldToken}` },
      data: {
        currentPassword: TEST_USERS.user.password,
        newPassword: 'NewSecure!Pass456',
      },
    });

    // Old token should be invalidated
    const response = await request.get('/api/users/me', {
      headers: { Authorization: `Bearer ${oldToken}` },
    });
    // Depending on implementation: 401 if token-version check, 200 if stateless
    // For secure implementations, this should be 401
    if (response.status() === 200) {
      console.warn(
        'WARNING: Old token still valid after password change -- consider token versioning'
      );
    }

    // Restore original password for other tests
    const newLoginRes = await request.post('/api/auth/login', {
      data: { email: TEST_USERS.user.email, password: 'NewSecure!Pass456' },
    });
    const { token: newToken } = await newLoginRes.json();
    await request.post('/api/auth/change-password', {
      headers: { Authorization: `Bearer ${newToken}` },
      data: {
        currentPassword: 'NewSecure!Pass456',
        newPassword: TEST_USERS.user.password,
      },
    });
  });
});

Session Fixation and Management

// tests/security/auth-bypass/session-management.spec.ts
import { test, expect } from '@playwright/test';
import { TEST_USERS } from '../fixtures/user-roles';

test.describe('Session Fixation and Management', () => {
  test('session ID should change after login (session fixation prevention)', async ({
    page,
    context,
  }) => {
    await page.goto('/login');

    // Capture pre-login session cookie
    const preLoginCookies = await context.cookies();
    const preLoginSessionId = preLoginCookies.find((c) => c.name.match(/session|sid|connect/i));

    // Perform login
    await page.fill('[name="email"]', TEST_USERS.user.email);
    await page.fill('[name="password"]', TEST_USERS.user.password);
    await page.click('button[type="submit"]');
    await page.waitForURL('/dashboard');

    // Capture post-login session cookie
    const postLoginCookies = await context.cookies();
    const postLoginSessionId = postLoginCookies.find((c) => c.name.match(/session|sid|connect/i));

    if (preLoginSessionId && postLoginSessionId) {
      expect(
        preLoginSessionId.value,
        'Session ID should regenerate after authentication to prevent session fixation'
      ).not.toBe(postLoginSessionId.value);
    }
  });

  test('session cookies should have secure attributes', async ({ page, context }) => {
    await page.goto('/login');
    await page.fill('[name="email"]', TEST_USERS.user.email);
    await page.fill('[name="password"]', TEST_USERS.user.password);
    await page.click('button[type="submit"]');
    await page.waitForURL('/dashboard');

    const cookies = await context.cookies();
    const sessionCookie = cookies.find((c) => c.name.match(/session|sid|token|connect/i));

    if (sessionCookie) {
      expect(sessionCookie.httpOnly, 'Session cookie must be HttpOnly').toBe(true);
      expect(sessionCookie.sameSite, 'Session cookie should use SameSite=Lax or Strict').toMatch(
        /Lax|Strict/
      );
      // Only check Secure flag on HTTPS
      if (page.url().startsWith('https')) {
        expect(sessionCookie.secure, 'Session cookie must be Secure on HTTPS').toBe(true);
      }
    }
  });

  test('logout should invalidate the session server-side', async ({ page, context, request }) => {
    // Login and capture session
    await page.goto('/login');
    await page.fill('[name="email"]', TEST_USERS.user.email);
    await page.fill('[name="password"]', TEST_USERS.user.password);
    await page.click('button[type="submit"]');
    await page.waitForURL('/dashboard');

    const cookies = await context.cookies();
    const sessionCookie = cookies.find((c) => c.name.match(/session|sid|token|connect/i));
    const capturedValue = sessionCookie?.value;

    // Logout
    await page.click('[data-testid="logout-button"]');

    // Try to use the captured session cookie directly
    if (capturedValue && sessionCookie) {
      const response = await request.get('/api/users/me', {
        headers: {
          Cookie: `${sessionCookie.name}=${capturedValue}`,
        },
      });
      expect(
        response.status(),
        'Server should reject the session after logout'
      ).toBe(401);
    }
  });

  test('concurrent session limit enforcement', async ({ browser }) => {
    const contexts = [];
    const maxSessions = 5;

    for (let i = 0; i < maxSessions + 1; i++) {
      const ctx = await browser.newContext();
      const page = await ctx.newPage();

      await page.goto('/login');
      await page.fill('[name="email"]', TEST_USERS.user.email);
      await page.fill('[name="password"]', TEST_USERS.user.password);
      await page.click('button[type="submit"]');

      contexts.push(ctx);
    }

    // Check if earliest session was invalidated
    const firstContext = contexts[0];
    const firstPage = firstContext.pages()[0];
    await firstPage.reload();
    const url = firstPage.url();

    // Clean up
    for (const ctx of contexts) {
      await ctx.close();
    }

    // If session limiting is enforced, the first session should be redirected
    // This test documents behavior -- not all apps enforce session limits
    if (url.includes('/login')) {
      // Session limiting is enforced -- good
    } else {
      console.warn(
        'WARNING: No concurrent session limit detected -- consider implementing one'
      );
    }
  });
});

Insecure Direct Object Reference (IDOR) Testing

// tests/security/auth-bypass/idor.spec.ts
import { test, expect } from '@playwright/test';
import { TEST_USERS } from '../fixtures/user-roles';

test.describe('Insecure Direct Object Reference (IDOR)', () => {
  let userAToken: string;
  let userBToken: string;
  let userAId: string;

  test.beforeAll(async ({ request }) => {
    // Authenticate as two different regular users
    const resA = await request.post('/api/auth/login', {
      data: { email: TEST_USERS.user.email, password: TEST_USERS.user.password },
    });
    const bodyA = await resA.json();
    userAToken = bodyA.token;
    userAId = bodyA.userId;

    const resB = await request.post('/api/auth/login', {
      data: { email: TEST_USERS.readonly.email, password: TEST_USERS.readonly.password },
    });
    const bodyB = await resB.json();
    userBToken = bodyB.token;
  });

  test('user B cannot access user A profile data', async ({ request }) => {
    const response = await request.get(`/api/users/${userAId}`, {
      headers: { Authorization: `Bearer ${userBToken}` },
    });
    expect([403, 404]).toContain(response.status());
  });

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
224
Forks
27
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
auth-bypass-tester
Source
github.com/pramoddutta/qaskills