Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(auth): running Lighthouse on authenticated pages #9628

Merged
merged 60 commits into from
Sep 19, 2019
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
aecfd9f
init
connorjclark Aug 30, 2019
6aa9c08
start doc
connorjclark Aug 30, 2019
69adf71
update
connorjclark Aug 30, 2019
5757985
update
connorjclark Aug 30, 2019
0ae9040
update
connorjclark Aug 30, 2019
5469beb
lint
connorjclark Aug 30, 2019
59aa12f
fix test
connorjclark Aug 30, 2019
eadc8e0
cdt
connorjclark Aug 30, 2019
6ac8d37
Update README.md
connorjclark Aug 30, 2019
f268134
Update docs/auth/README.md
connorjclark Aug 30, 2019
06dd0b0
changes
connorjclark Aug 30, 2019
5e460a5
changes
connorjclark Aug 30, 2019
b54a4ac
changes
connorjclark Aug 30, 2019
ae5f2e5
just run seo
connorjclark Aug 30, 2019
d9feca1
link changes, optional
connorjclark Sep 4, 2019
3d171bf
Update docs/auth/README.md
connorjclark Sep 4, 2019
60cef1d
Merge branch 'auth-docs' of github.com:GoogleChrome/lighthouse into a…
connorjclark Sep 4, 2019
d7b0688
-we
connorjclark Sep 4, 2019
a3425e7
-json middleware
connorjclark Sep 4, 2019
e36b5e1
CHROME_DEBUG_PORT
connorjclark Sep 4, 2019
25ff0f9
await server stuff
connorjclark Sep 4, 2019
ec7f7bb
$eval
connorjclark Sep 4, 2019
836e64a
move
connorjclark Sep 5, 2019
915ad58
fix
connorjclark Sep 5, 2019
6a20cb1
mv
connorjclark Sep 5, 2019
b4f456e
link
connorjclark Sep 5, 2019
56f59c0
Update docs/recipes/auth/README.md
connorjclark Sep 9, 2019
57f2bd2
remove mustache
connorjclark Sep 9, 2019
59bd132
rm loginRequired middleware
connorjclark Sep 9, 2019
cb64b52
stragglers
connorjclark Sep 9, 2019
80e8fb0
lint
connorjclark Sep 9, 2019
a917c02
license, strict
connorjclark Sep 9, 2019
fec4450
rm lock
connorjclark Sep 9, 2019
41e85cf
fileoverview
connorjclark Sep 9, 2019
1de9c39
reduce nestig in jest test
connorjclark Sep 9, 2019
a86d68b
custom matcher
connorjclark Sep 9, 2019
d5f9b5b
remove lh run on 401 page
connorjclark Sep 9, 2019
4a3bc3b
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
ed6bc34
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
558ab99
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
c2669ff
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
b4afaf7
Update docs/authenticated-pages.md
connorjclark Sep 13, 2019
05fb347
Apply suggestions from code review
connorjclark Sep 13, 2019
012c5b2
tweaks
connorjclark Sep 13, 2019
0a25f61
move the thing
connorjclark Sep 13, 2019
e43436b
headless false
connorjclark Sep 13, 2019
51c5bb6
some h1s
connorjclark Sep 13, 2019
5e1d786
one more option
connorjclark Sep 13, 2019
dfe971b
integration test docs
connorjclark Sep 13, 2019
a9da287
yarn
connorjclark Sep 13, 2019
0e43b70
lock
connorjclark Sep 13, 2019
d9d6c67
cut out the integration docs
connorjclark Sep 13, 2019
cfa14b9
Apply suggestions from code review
connorjclark Sep 16, 2019
826f855
pr
connorjclark Sep 16, 2019
3dc265b
Merge branch 'auth-docs' of github.com:GoogleChrome/lighthouse into a…
connorjclark Sep 16, 2019
c5d3cda
pr
connorjclark Sep 16, 2019
e3b520f
files
connorjclark Sep 16, 2019
e389e28
pr
connorjclark Sep 16, 2019
97b4dac
use local lh
connorjclark Sep 18, 2019
a6ceaa2
update
connorjclark Sep 19, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/authenticated-pages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Running Lighthouse on Authenticated Pages

Default runs of Lighthouse load a page as a "new user", with no previous session or storage data. This means that pages requiring authenticated access do not work without additional setup. You have a few options for running Lighthouse on pages behind a login:

## Option 1: Script the login with Puppeteer

[Puppeteer](https://pptr.dev) is the most flexible approach for running Lighthouse on pages requiring authentication.

See [a working demo at /docs/recipes/auth](./recipes/auth).

## Option 2: Leverage logged-in state with Chrome DevTools

The Audits panel in Chrome DevTools will never clear your cookies, so you can log in to the target site and then run Lighthouse. If `localStorage` or `indexedDB` is important for your authentication purposes, be sure to uncheck `Clear storage`.

## Option 3: Pass custom request headers with Lighthouse CLI

CLI:
```sh
lighthouse http://www.example.com --view --extra-headers="{\"Authorization\":\"...\"}"
```

Node:
```js
const result = await lighthouse('http://www.example.com', {
extraHeaders: {
Authorization: '...',
},
});
```

You could also set the `Cookie` header, but beware: it will [override any other Cookies you expect to be there](https://github.com/GoogleChrome/lighthouse/pull/9170). A workaround is to use Puppeteer's [`page.setCookie`](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagesetcookiecookies).

## Option 4: Open a debug instance of Chrome and manually log in

1. Globally install lighthouse: `npm i -g lighthouse` or `yarn global add lighthouse`. `chrome-debug` is now in your PATH. This binary launches a standalone Chrome instance with an open debugging port.
1. Run chrome-debug. This logs the debugging port of your Chrome instance.
1. Navigate to your site and log in.
1. In a separate terminal, run `lighthouse http://mysite.com --port port-number`, using the port number from chrome-debug.

## Option 5: Reuse a prepared Chrome User Profile

This option is currently under development. Track or join the discussion here: [#8957](https://github.com/GoogleChrome/lighthouse/issues/8957).
18 changes: 18 additions & 0 deletions docs/recipes/auth/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

module.exports = {
extends: '../../../.eslintrc.js',
env: {
jest: true,
},
rules: {
'new-cap': 0,
'no-console': 0,
'no-unused-vars': 0,
},
};
96 changes: 96 additions & 0 deletions docs/recipes/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Running Lighthouse on Authenticated Pages with Puppeteer

If you just want to view the code for using Lighthouse with Puppeteer, see [example-lh-auth.js](./example-lh-auth.js).

## The Example Site

There are two pages on the site:

1. `/` - the homepage
2. `/dashboard`

The homepage shows the login form, but only to users that are not signed in.

The dashboard shows a secret to users that are logged in, but shows an error to users that are not.

The server responds with different HTML for each of these pages and session states, so there are four different pages that must have passable Lighthouse SEO scores.
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

(Optional) To run the server:
```sh
# be in root lighthouse directory
yarn # install global project deps
cd docs/auth
yarn # install deps related to just this documentation
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
yarn start # start the server on http://localhost:8000
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
```

## Process

Puppeteer - a browser automation tool - can be used to programatically setup a session.

1. Launch a new browser.
1. Navigate to the login page.
1. Fill and submit the login form.
1. Run Lighthouse using the same browser.

First, launch Chrome:
```js
// This port will be used by Lighthouse later.
const PORT = 8041;
const browser = await puppeteer.launch({
args: [`--remote-debugging-port=${PORT}`],
// Optional, if you want to see the tests in action.
headless: false,
slowMo: 50,
});
```

Navigate to the login form:
```js
const page = await browser.newPage();
await page.goto('http://localhost:8000');
```

Given a login form like this:
```html
<form action="/login" method="post">
<label>
Email:
<input type="email" name="email">
</label>
<label>
Password:
<input type="password" name="password">
</label>
<input type="submit">
</form>
```

Direct Puppeteer to fill and submit it:
```js
const emailInput = await page.$('input[type="email"]');
await emailInput.type('admin@example.com');
const passwordInput = await page.$('input[type="password"]');
await passwordInput.type('password');
await Promise.all([
page.$eval('.login-form', form => form.submit()),
page.waitForNavigation(),
]);
```

At this point, the session that Puppeteer is managing is now logged in.

Close the page used to log in:
```js
await page.close();
// The page has been closed, but the browser still has the relevant session.
```

Now run Lighthouse, using the same port as before:
```js
const result = await lighthouse('http://localhost:8000/dashboard', { port: PORT });
const lhr = result.lhr;
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

# Direct Puppeteer to close the browser as we're done with it.
await browser.close();
```
60 changes: 60 additions & 0 deletions docs/recipes/auth/example-lh-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @license Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

/**
* @fileoverview Example script for running Lighthouse on an authenticated page.
* See docs/recipes/auth/README.md for more.
*/

const puppeteer = require('puppeteer');
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
const lighthouse = require('lighthouse');

const PORT = 8041;
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param {import('puppeteer').Browser} browser
*/
async function login(browser) {
const page = await browser.newPage();
await page.goto('http://localhost:8000');
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
await page.waitForSelector('input[type="email"]', {visible: true});

// Fill in and submit login form.
const emailInput = await page.$('input[type="email"]');
await emailInput.type('admin@example.com');
const passwordInput = await page.$('input[type="password"]');
await passwordInput.type('password');
await Promise.all([
page.$eval('.login-form', form => form.submit()),
page.waitForNavigation(),
]);

await page.close();
}

async function main() {
// Direct Puppeteer to open Chrome with a specific debugging port.
const browser = await puppeteer.launch({
args: [`--remote-debugging-port=${PORT}`],
// Optional, if you want to see the tests in action.
headless: false,
slowMo: 50,
});

// Setup the browser session to be logged into our site.
await login(browser);

// Direct Lighthouse to use the same port.
const result = await lighthouse('http://localhost:8000/dashboard', {port: PORT});
// Direct Puppeteer to close the browser as we're done with it.
await browser.close();
connorjclark marked this conversation as resolved.
Show resolved Hide resolved

// Output the result.
console.log(JSON.stringify(result.lhr, null, 2));
}

main();
11 changes: 11 additions & 0 deletions docs/recipes/auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"scripts": {
"start": "node server/server.js"
},
"dependencies": {
"express": "^4.17.1",
"express-session": "^1.16.2",
"lighthouse": "^5.2.0",
"morgan": "^1.9.1"
}
}
26 changes: 26 additions & 0 deletions docs/recipes/auth/server/public/dashboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!--
Copyright 2019 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
<title>Dashboard</title>
</head>
<body>
<h1>Dashboard</h1>
<div>
secrets here
</div>
<a href="/logout">logout</a>
<style>
body {
background-color: green;
}
</style>
</body>
</html>
22 changes: 22 additions & 0 deletions docs/recipes/auth/server/public/unauthenticated.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!--
Copyright 2019 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
<title>Dashboard</title>
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
</head>
<body>
you need to be logged in.
<style>
body {
background-color: #CD5C5C;
}
</style>
</body>
</html>
97 changes: 97 additions & 0 deletions docs/recipes/auth/server/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @license Copyright 2019 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

/**
* @fileoverview Example Express server for demonstrating how to run Lighthouse on an authenticated
* page. See docs/recipes/auth/README.md for more.
*/

const createError = require('http-errors');
const express = require('express');
const morgan = require('morgan');
const session = require('express-session');
const http = require('http');
const path = require('path');
const PUBLIC_DIR = path.join(__dirname, 'public');

const app = express();

app.use(morgan('dev'));
app.use(express.urlencoded({extended: false}));

app.use(session({
secret: 'notverysecret',
resave: true,
saveUninitialized: false,
}));

app.get('/dashboard', (req, res) => {
if (req.session.user) {
res.sendFile('./dashboard.html', {root: PUBLIC_DIR});
} else {
res.status(401).sendFile('./unauthenticated.html', {root: PUBLIC_DIR});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this remains a file, it should probably be named dashboard-unauthenticated.html to distinguish from an auth'd / login page.

}
});

app.get('/', (req, res) => {
if (req.session.user) {
res.send('<span>You are logged in. Go to <a href="/dashboard">the dashboard</a>.</span>');
} else {
res.send(`
<h1>Plz login</h1>
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
<form class="login-form" action="/login" method="post">
<label>
Email:
<input type="email" name="email">
</label>
<label>
Password:
<input type="password" name="password">
</label>
<input type="submit">
</form>
`);
}
});

app.post('/login', (req, res, next) => {
const {email, password} = req.body;
if (email !== 'admin@example.com' || password !== 'password') {
connorjclark marked this conversation as resolved.
Show resolved Hide resolved
return next(createError(401));
}

req.session.user = {
email,
};
res.redirect('/dashboard');
});

app.get('/logout', (req, res, next) => {
req.session.destroy(() => {
res.redirect('/');
});
});

// Error handlers
app.use(function(req, res, next) {
next(createError(404));
});

app.use(function(err, req, res, next) {
res.locals.message = err.message;
res.locals.error = err;

res.status(err.status || 500);
res.json({err});
});

const server = http.createServer(app);
if (require.main === module) {
server.listen(8000);
} else {
module.exports = server;
}