Skip to content

Commit a13948d

Browse files
authored
feat: Support scanning pages which require authentication (#1053)
Fixes github/continuous-ai-for-accessibility#10 (Hubber access only) This PR adds support for scanning pages which require authentication, via 2 different methods: 1. It adds a built-in ‘Auth’ sub-action that can be used when an auth flow requires only a username and password. 2. It delegates complex auth flows to custom, external actions, whose output the scanner can now consume via a new `session_state_path` input. Check out the new section [“Authentication” in the README](https://github.com/github-community-projects/continuous-ai-for-accessibility-scanner/blob/smockle/auth/README.md#authentication) for details.
2 parents 80fce3c + 4c77bb3 commit a13948d

File tree

13 files changed

+427
-5
lines changed

13 files changed

+427
-5
lines changed

.github/actions/auth/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# auth
2+
3+
Log in using Playwright, then write authenticated session state to a file for reuse in other Playwright sessions.
4+
5+
## Usage
6+
7+
### Inputs
8+
9+
#### `login_url`
10+
11+
**Required** Login page URL. For example, `https://github.com/login`
12+
13+
#### `username`
14+
15+
**Required** Username.
16+
17+
#### `password`
18+
19+
**Required** Password.
20+
21+
> [!IMPORTANT]
22+
> Don’t put passwords in your workflow as plain text; instead reference a [repository secret](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets#creating-secrets-for-a-repository).
23+
24+
### Outputs
25+
26+
#### `session_state_path`
27+
28+
Path to a file containing authenticated session state.

.github/actions/auth/action.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: "Auth"
2+
description: "Log in using Playwright, then write authenticated session state to a file for reuse in other Playwright sessions."
3+
4+
inputs:
5+
login_url:
6+
description: "Login page URL"
7+
required: true
8+
username:
9+
description: "Username"
10+
required: true
11+
password:
12+
description: "Password"
13+
required: true
14+
15+
outputs:
16+
session_state_path:
17+
description: "Path to a file containing authenticated session state"
18+
19+
runs:
20+
using: "node20"
21+
main: "bootstrap.js"
22+
23+
branding:
24+
icon: "lock"
25+
color: "blue"

.github/actions/auth/bootstrap.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env node
2+
//@ts-check
3+
4+
import fs from 'node:fs'
5+
import * as url from 'node:url'
6+
import { spawn } from 'node:child_process'
7+
8+
function spawnPromisified(command, args, { quiet = false, ...options } = {}) {
9+
return new Promise((resolve, reject) => {
10+
const proc = spawn(command, args, options)
11+
proc.stdout.setEncoding('utf8')
12+
proc.stdout.on('data', (data) => {
13+
if (!quiet) {
14+
console.log(data)
15+
}
16+
})
17+
proc.stderr.setEncoding('utf8')
18+
proc.stderr.on('data', (data) => {
19+
console.error(data)
20+
})
21+
proc.on('close', (code) => {
22+
if (code !== 0) {
23+
reject(code)
24+
} else {
25+
resolve(code)
26+
}
27+
})
28+
})
29+
}
30+
31+
await (async () => {
32+
// If dependencies are not vendored-in, install them at runtime.
33+
try {
34+
await fs.accessSync(
35+
url.fileURLToPath(new URL('./node_modules', import.meta.url)),
36+
fs.constants.R_OK
37+
)
38+
} catch {
39+
try {
40+
await spawnPromisified('npm', ['ci'], {
41+
cwd: url.fileURLToPath(new URL('.', import.meta.url)),
42+
quiet: true
43+
})
44+
} catch {
45+
process.exit(1)
46+
}
47+
} finally {
48+
// Compile TypeScript.
49+
try {
50+
await spawnPromisified('npm', ['run', 'build'], {
51+
cwd: url.fileURLToPath(new URL('.', import.meta.url)),
52+
quiet: true
53+
})
54+
} catch {
55+
process.exit(1)
56+
}
57+
// Run the main script.
58+
const action = await import('./dist/index.js')
59+
await action.default()
60+
}
61+
})()

.github/actions/auth/package-lock.json

Lines changed: 161 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/actions/auth/package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "auth",
3+
"version": "1.0.0",
4+
"description": "Log in using Playwright, then write authenticated session state to a file for reuse in other Playwright sessions.",
5+
"main": "dist/index.js",
6+
"module": "dist/index.js",
7+
"scripts": {
8+
"start": "node bootstrap.js",
9+
"build": "tsc"
10+
},
11+
"keywords": [],
12+
"author": "GitHub",
13+
"license": "MIT",
14+
"type": "module",
15+
"dependencies": {
16+
"@actions/core": "^1.11.1",
17+
"playwright": "^1.55.1"
18+
},
19+
"devDependencies": {
20+
"@types/node": "^24.5.2",
21+
"typescript": "^5.8.3"
22+
}
23+
}

.github/actions/auth/src/index.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import crypto from "node:crypto";
2+
import process from "node:process";
3+
import * as url from "node:url";
4+
import core from "@actions/core";
5+
import playwright from "playwright";
6+
7+
export default async function () {
8+
core.info("Starting 'auth' action");
9+
10+
let browser: playwright.Browser | undefined;
11+
let context: playwright.BrowserContext | undefined;
12+
let page: playwright.Page | undefined;
13+
try {
14+
// Get inputs
15+
const loginUrl = core.getInput("login_url", { required: true });
16+
const username = core.getInput("username", { required: true });
17+
const password = core.getInput("password", { required: true });
18+
core.setSecret(password);
19+
20+
// Determine storage path for authenticated session state
21+
// Playwright will create missing directories, if needed
22+
const actionDirectory = `${url.fileURLToPath(new URL(import.meta.url))}/..`;
23+
const sessionStatePath = `${
24+
process.env.RUNNER_TEMP ?? actionDirectory
25+
}/.auth/${crypto.randomUUID()}/sessionState.json`;
26+
27+
// Launch a headless browser
28+
browser = await playwright.chromium.launch({
29+
headless: true,
30+
executablePath: process.env.CI ? "/usr/bin/google-chrome" : undefined,
31+
});
32+
context = await browser.newContext();
33+
page = await context.newPage();
34+
35+
// Log in
36+
core.info("Navigating to login page");
37+
await page.goto(loginUrl);
38+
core.info("Filling username");
39+
await page.getByLabel(/username/i).fill(username);
40+
core.info("Filling password");
41+
await page.getByLabel(/password/i).fill(password);
42+
core.info("Logging in");
43+
await page
44+
.getByLabel(/password/i)
45+
.locator("xpath=ancestor::form")
46+
.evaluate((form) => (form as HTMLFormElement).submit());
47+
48+
// Write authenticated session state to a file and output its path
49+
await context.storageState({ path: sessionStatePath });
50+
core.setOutput("session_state_path", sessionStatePath);
51+
core.info(`Wrote authenticated session state to ${sessionStatePath}`);
52+
} catch (error) {
53+
if (page) {
54+
core.info(`Errored at page URL: ${page.url()}`);
55+
}
56+
core.setFailed(`${error}`);
57+
process.exit(1);
58+
} finally {
59+
// Clean up
60+
await context?.close();
61+
await browser?.close();
62+
}
63+
64+
core.info("Finished 'auth' action");
65+
}

.github/actions/auth/tsconfig.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"module": "nodenext",
5+
"moduleResolution": "nodenext",
6+
"esModuleInterop": true,
7+
"strict": true,
8+
"rootDir": "src",
9+
"outDir": "dist"
10+
},
11+
"include": [
12+
"src/**/*.ts"
13+
]
14+
}

.github/actions/find/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ https://primer.style
1515
https://primer.style/octicons/
1616
```
1717

18+
#### `session_state_path`
19+
20+
**Optional** Path to a file containing authenticated session state.
21+
1822
### Outputs
1923

2024
#### `findings`

0 commit comments

Comments
 (0)