|
| 1 | +# Browser authentication in Scout |
| 2 | + |
| 3 | +Scout uses [SAML](https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/saml) as a unified authentication protocol across all deployment types. In this article we'll explore how to authenticate your Playwright tests using the `browserAuth` fixture. |
| 4 | + |
| 5 | +## Log in with the `browserAuth` fixture |
| 6 | + |
| 7 | +The `browserAuth` fixture provides convenient methods to authenticate with different user roles in your tests, regardless of where your tests run: |
| 8 | + |
| 9 | +| Method | Description | |
| 10 | +| :-------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- | |
| 11 | +| `loginAsAdmin()` | Logs in with the `admin` role (full Kibana and Elasticsearch access). | |
| 12 | +| `loginAsPrivilegedUser()` | Logs in with a privileged non-admin role (`editor`, except for Elasticsearch projects that use the `developer` role). | |
| 13 | +| `loginAsViewer()` | Logs in with the `viewer` role (read-only permissions). | |
| 14 | +| `loginAs(role: string)` | Logs in a specific built-in role by name. The role must exist in the deployment's role configuration, otherwise an error is thrown. | |
| 15 | +| `loginWithCustomRole(role: KibanaRole)` | Creates a custom role with the specified Kibana and Elasticsearch privileges, and then logs in with that role. | |
| 16 | + |
| 17 | +These authentication methods are **asynchronous** and must be awaited. |
| 18 | + |
| 19 | +**Basic usage example:** |
| 20 | + |
| 21 | +```ts |
| 22 | +import { expect, tags } from '@kbn/scout'; |
| 23 | +import { test } from '../fixtures'; |
| 24 | + |
| 25 | +// ... |
| 26 | + |
| 27 | +test.describe('My sample test suite', { tag: tags.DEPLOYMENT_AGNOSTIC }, () => { |
| 28 | + test.beforeEach(async ({ browserAuth, pageObjects }) => { |
| 29 | + // [1] |
| 30 | + // log in as an admin |
| 31 | + await browserAuth.loginAsAdmin(); // [2] |
| 32 | + // ... |
| 33 | + }); |
| 34 | + |
| 35 | + test('my sample test 1', async ({ pageObjects }) => { |
| 36 | + // the browser is already authenticated as admin here |
| 37 | + // ... |
| 38 | + }); |
| 39 | + |
| 40 | + test('my sample test 2', async ({ pageObjects }) => { |
| 41 | + // the browser is already authenticated as admin here |
| 42 | + // ... |
| 43 | + }); |
| 44 | +}); |
| 45 | +``` |
| 46 | + |
| 47 | +1. We first unpack the **`browserAuth`** fixture from the test context. |
| 48 | +2. We then log in with the **`admin`** role before each test in the **`beforeEach`** hook. |
| 49 | + |
| 50 | +> **SAML authentication: local deployment vs Elastic Cloud** |
| 51 | +> |
| 52 | +> When running tests in **local** stateful and serverless environments, Scout creates **on-demand** SAML identities using a trusted **mock Identity Provider (IdP)**, bypassing the need for pre-provisioned users. |
| 53 | +> |
| 54 | +> However, when running tests on Elastic Cloud, Scout authenticates with **real Elastic Cloud accounts** using credentials from the `<KIBANA_ROOT>/.ftr/role_users.json` or `<KIBANA_ROOT>/.scout/role_users.json` files through the actual cloud Identity Provider. |
| 55 | +
|
| 56 | +### Logging in with a custom role |
| 57 | + |
| 58 | +You can log in with a custom role with the `loginWithCustomRole()` method. This is ideal for testing specific permission sets in your plugin: |
| 59 | + |
| 60 | +```ts |
| 61 | +test.describe( |
| 62 | + 'Discover app with a restricted read-only role', |
| 63 | + { tag: tags.DEPLOYMENT_AGNOSTIC }, |
| 64 | + () => { |
| 65 | + test.beforeAll(async () => { |
| 66 | + // load test data |
| 67 | + // ... |
| 68 | + }); |
| 69 | + |
| 70 | + test.beforeEach(async ({ browserAuth, pageObjects }) => { |
| 71 | + // log in with custom role |
| 72 | + await browserAuth.loginWithCustomRole({ |
| 73 | + kibana: [ |
| 74 | + { |
| 75 | + base: [], // [1] |
| 76 | + feature: { |
| 77 | + discover: ['read'], |
| 78 | + }, |
| 79 | + spaces: ['*'], |
| 80 | + }, |
| 81 | + ], |
| 82 | + elasticsearch: { |
| 83 | + cluster: [], // [2] |
| 84 | + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], // [3] |
| 85 | + }, |
| 86 | + }); |
| 87 | + }); |
| 88 | + |
| 89 | + test('should display a disabled save button', async ({ browserAuth }) => { |
| 90 | + // navigate to Discover and verify read-only access |
| 91 | + // ... |
| 92 | + }); |
| 93 | + } |
| 94 | +); |
| 95 | +``` |
| 96 | + |
| 97 | +1. For testing specific features, use `base: []` and explicit `feature` definitions. |
| 98 | +2. We leave this empty as no cluster privileges are needed for this test. |
| 99 | +3. We define the minimum index privileges needed to access Discover. |
| 100 | + |
| 101 | +> **Warning: Scout vs FTR** |
| 102 | +> |
| 103 | +> Scout's `loginWithCustomRole()` method internally uses similar role creation logic to FTR's `samlAuth.setCustomRole()` method, but wraps it with **automatic authentication** and **cleanup**, making it a one-step solution for Playwright tests compared to FTR's multi-step approach. |
| 104 | +
|
| 105 | +> **What happens behind the scenes?** |
| 106 | +> |
| 107 | +> When you call `loginWithCustomRole()`, Scout first creates a uniquely named role in Elasticsearch for each Playwright worker (`custom_role_worker_1`, `custom_role_worker_2`, etc.). If your tests run sequentially, Scout will create just one role named `custom_role_worker_1`. |
| 108 | +> |
| 109 | +> Scout then: |
| 110 | +> |
| 111 | +> 1. Creates an authenticated SAML session for the custom role |
| 112 | +> 2. Caches the session for performance (subsequent calls with the same role definition reuse the cached session) |
| 113 | +> 3. Sets the session cookie in your browser context |
| 114 | +> 4. Automatically deletes the custom role after test completion (no matter if the test passed or failed) |
| 115 | +> |
| 116 | +> If you call `loginWithCustomRole()` again with a different role definition in the same test, Scout **recreates** the role with the new privileges and authenticates with a fresh session. |
| 117 | +
|
| 118 | +### Predefined roles vs custom roles |
| 119 | + |
| 120 | +Let's compare **predefined roles** with **custom roles**: |
| 121 | + |
| 122 | +- **Predefined roles** are built-in Elastic roles available to all customers out-of-the-box (like `admin`, `editor`, `viewer`). These work with `loginAs()` and are defined in the appropriate `roles.yml` file. Here's a [list](https://www.elastic.co/docs/deploy-manage/users-roles/cloud-organization/user-roles) for Elastic Cloud. |
| 123 | +- **Custom roles** are roles you create specifically for testing with particular permission sets. These require `loginWithCustomRole()` instead. |
| 124 | + |
| 125 | +> **Warning: Scout vs FTR** |
| 126 | +> |
| 127 | +> **FTR** allows defining custom roles in the test config file under `security.roles`. **Scout** takes a different approach: custom roles are created dynamically using `loginWithCustomRole()`. |
| 128 | +
|
| 129 | +### Extending the `browserAuth` fixture |
| 130 | + |
| 131 | +Using `loginWithCustomRole()` works well for one-off cases, but if you need to log in with the same custom role across multiple tests, it's more convenient to extend the `browserAuth` fixture with solution-specific or plugin-specific authentication methods. |
| 132 | + |
| 133 | +#### Solution-scoped extension |
| 134 | + |
| 135 | +The `@kbn/scout-security` package [extends](https://github.com/elastic/kibana/blob/main/x-pack/solutions/security/packages/kbn-scout-security/src/playwright/fixtures/test/browser_auth/index.ts) the `browserAuth` fixture to expose Security-specific authentication methods (e.g. `loginAsPlatformEngineer()`): |
| 136 | + |
| 137 | +```ts |
| 138 | +// loginAsPlatformEngineer() is available in all tests importing @kbn/scout-security |
| 139 | +test.beforeEach(async ({ browserAuth }) => { |
| 140 | + await browserAuth.loginAsPlatformEngineer(); |
| 141 | +}); |
| 142 | +``` |
| 143 | + |
| 144 | +All Security tests importing `@kbn/scout-security` can now use this method to log in as a Platform Engineer without redefining the custom role in each test. |
| 145 | + |
| 146 | +#### Plugin-scoped extension |
| 147 | + |
| 148 | +A plugin can also extend the `browserAuth` fixture by creating a custom fixture in the plugin's `test/scout/ui/fixtures` directory. For example, the Observability APM plugin [exposes](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/apm/test/scout/ui/fixtures/index.ts#L82-L99) `loginAsApmMonitor()`: |
| 149 | + |
| 150 | +```ts |
| 151 | +// loginAsApmMonitor() is available in all tests importing the plugin fixture |
| 152 | +test.beforeEach(async ({ browserAuth }) => { |
| 153 | + await browserAuth.loginAsApmMonitor(); |
| 154 | +}); |
| 155 | +``` |
| 156 | + |
| 157 | +Create a custom fixture that extends `browserAuth` in your plugin's test directory: |
| 158 | + |
| 159 | +```ts |
| 160 | +// x-pack/solutions/<solution>/plugins/<private or shared>/my-plugin/test/scout/ui/fixtures/index.ts |
| 161 | + |
| 162 | +import { scoutTestFixtures } from '@kbn/scout'; // [1] |
| 163 | +import type { BrowserAuthFixture } from '@kbn/scout'; // [1] |
| 164 | + |
| 165 | +interface MyPluginAuthFixture extends BrowserAuthFixture { |
| 166 | + loginAsMyPluginUser: () => Promise<void>; |
| 167 | +} |
| 168 | + |
| 169 | +export const fixtures = scoutTestFixtures.extend<{ browserAuth: MyPluginAuthFixture }>({ |
| 170 | + browserAuth: async ({ browserAuth }, use) => { |
| 171 | + const loginAsMyPluginUser = async () => |
| 172 | + browserAuth.loginWithCustomRole({ |
| 173 | + // update as necessary |
| 174 | + elasticsearch: { |
| 175 | + cluster: ['monitor'], |
| 176 | + indices: [{ names: ['my-plugin-*'], privileges: ['read', 'write'] }], |
| 177 | + }, |
| 178 | + // update as necessary |
| 179 | + kibana: [ |
| 180 | + { |
| 181 | + feature: { myPlugin: ['all'] }, |
| 182 | + spaces: ['*'], |
| 183 | + }, |
| 184 | + ], |
| 185 | + }); |
| 186 | + |
| 187 | + // make the new method available via the browserAuth fixture |
| 188 | + await use({ |
| 189 | + ...browserAuth, |
| 190 | + loginAsMyPluginUser, |
| 191 | + }); |
| 192 | + }, |
| 193 | +}); |
| 194 | +``` |
| 195 | + |
| 196 | +1. Import from `@kbn/scout-oblt` or from `@kbn/scout-security` if your plugin belongs to a specific solution. |
| 197 | + |
| 198 | +Then use it in your tests: |
| 199 | + |
| 200 | +```ts |
| 201 | +import { expect, test } from '../fixtures'; |
| 202 | + |
| 203 | +test('my plugin feature works', async ({ browserAuth, page }) => { |
| 204 | + await browserAuth.loginAsMyPluginUser(); |
| 205 | + await page.goto('/app/my-plugin'); |
| 206 | + // Your test logic |
| 207 | +}); |
| 208 | +``` |
| 209 | + |
| 210 | +## Best practices |
| 211 | + |
| 212 | +#### Use deployment-agnostic methods |
| 213 | + |
| 214 | +Scout tests are designed to be deployment agnostic by default. Prefer `loginAsPrivilegedUser()` over `loginAs('editor')` when writing deployment-agnostic tests, as it automatically selects the appropriate role for the deployment type. |
| 215 | + |
| 216 | +#### Keep custom roles minimal |
| 217 | + |
| 218 | +Only grant the **minimum privileges** needed for your test. This makes tests more realistic and helps catch permission-related bugs. |
| 219 | + |
| 220 | +#### Reuse custom roles via fixtures |
| 221 | + |
| 222 | +If multiple tests need the same custom role, extend the `browserAuth` fixture instead of repeating `loginWithCustomRole()` calls. |
| 223 | + |
| 224 | +#### Test permission boundaries |
| 225 | + |
| 226 | +Use custom roles to explicitly test both what users **can** and **cannot** do: |
| 227 | + |
| 228 | +```ts |
| 229 | +test('viewer cannot save dashboards', async ({ browserAuth, page }) => { |
| 230 | + await browserAuth.loginAsViewer(); |
| 231 | + await page.gotoApp('dashboards'); |
| 232 | + |
| 233 | + // Verify save button is disabled |
| 234 | + await expect(page.testSubj.locator('dashboardSaveButton')).toBeDisabled(); |
| 235 | +}); |
| 236 | +``` |
0 commit comments