Skip to content

tests: Allow e2e to use injected token (cli) for UI access#1010

Open
queria wants to merge 1 commit into
guacsec:release/0.4.zfrom
queria:e2e-entra-id
Open

tests: Allow e2e to use injected token (cli) for UI access#1010
queria wants to merge 1 commit into
guacsec:release/0.4.zfrom
queria:e2e-entra-id

Conversation

@queria

@queria queria commented Apr 22, 2026

Copy link
Copy Markdown

Entra ID E2E Test Authentication via Token Injection

Problem

Running e2e tests against a TPA instance configured with Microsoft Entra ID as
OIDC provider is not possible when the only available user accounts are
corporate SSO-federated identities requiring 2FA. The existing e2e auth flow
fills a browser login form with PLAYWRIGHT_AUTH_USER / PLAYWRIGHT_AUTH_PASSWORD,
which cannot work with SSO/MFA-protected accounts. Creating local test users in
Entra ID is also not an option when user management permissions are restricted.

Solution

Added a token_injection auth mode for UI tests. Instead of filling a login
form, the test runner:

  1. Fetches the deployed frontend's index.html to discover OIDC_SERVER_URL
    and OIDC_CLIENT_ID from the window._env configuration.
  2. Obtains an access token from Entra ID using the OAuth2 client_credentials
    grant with an App Registration's client_id + client_secret.
  3. Injects the token into sessionStorage (via Playwright's addInitScript)
    using the key format oidc.user:{authority}:{clientId} before the React app
    loads.
  4. When the app initializes, react-oidc-context finds the token in
    sessionStorage, considers the user authenticated, and skips the OIDC login
    redirect.

This requires an Azure App Registration with a client secret and appropriate
API permissions for the TPA backend, but does not require creating any user
accounts.

Files Changed

e2e/tests/common/constants.ts

Added three new exports:

  • UI_AUTH_MODE (PLAYWRIGHT_UI_AUTH_MODE) - Selects auth mode: form
    (default, existing behavior) or token_injection.
  • AUTH_SCOPE (PLAYWRIGHT_AUTH_SCOPE) - OAuth2 scope for the
    client_credentials token request (e.g., api://<app-id>/.default).
  • TRUSTIFY_UI_URL (TRUSTIFY_UI_URL) - Frontend URL used to auto-discover
    OIDC settings from the deployment.

e2e/tests/ui/helpers/Auth.ts

Restructured into two auth paths dispatched by UI_AUTH_MODE:

  • loginWithForm() - Original browser form login, unchanged.
  • loginWithTokenInjection() - New path using client credentials and
    sessionStorage injection.
  • login() - Public entry point that selects the mode.

The token injection path reuses the existing PLAYWRIGHT_AUTH_CLIENT_ID,
PLAYWRIGHT_AUTH_CLIENT_SECRET, and PLAYWRIGHT_AUTH_URL env vars (shared
with API tests).

e2e/README.md

Documented the new environment variables in the "For UI tests" table.

Usage

export TRUSTIFY_UI_URL=https://your-tpa-instance.example.com
export AUTH_REQUIRED=true
export PLAYWRIGHT_UI_AUTH_MODE=token_injection
export PLAYWRIGHT_AUTH_CLIENT_ID=<App Registration client ID>
export PLAYWRIGHT_AUTH_CLIENT_SECRET=<App Registration client secret>
export PLAYWRIGHT_AUTH_SCOPE=api://<backend-app-id-uri>/.default
# Optional - auto-discovered from frontend if not set:
# export PLAYWRIGHT_AUTH_URL=https://login.microsoftonline.com/<tenant-id>/v2.0

Permissions / Authorization

The client_credentials token represents an application identity, not a user.
Authorization depends on how the TPA backend is configured:

  • With auth.yaml on trustd: If the backend's auth.yaml directly maps
    the API/CLI client ID to internal permissions (e.g., read.sbom,
    create.advisory), the token works without any roles claim in the JWT.
    This is useful when you lack Entra ID admin permissions to grant app role
    consent.
  • With Entra ID app roles: If relying on the standard setup from the TPA
    docs, the API App Registration needs app roles (API App / create:document, etc.)
    assigned and admin-consented in API Permissions
    so they appear in the token's roles claim.
    The backend's scopeMappings then maps these to internal permissions.

Other Limitations

The UI will display the API client ID (e.g., 660ae41f-...) as the username
in the top-right corner, since the token has no user profile information. This
is cosmetic and does not affect test execution.

Summary by Sourcery

Add a new token injection authentication mode for Playwright UI tests that uses client credentials to obtain an access token and injects it into the browser session, while keeping the existing form-based login as the default behavior.

New Features:

  • Introduce a configurable UI auth mode that supports both traditional form login and a new token injection flow based on OAuth2 client credentials.
  • Enable automatic discovery of frontend OIDC configuration from the deployed UI to support token-based authentication in e2e tests.

Enhancements:

  • Share auth-related environment variables between API and UI tests, including optional OAuth2 scope configuration.
  • Extend API test token acquisition to include an optional OAuth2 scope parameter derived from environment configuration.

Documentation:

  • Update e2e testing documentation to describe the new UI auth mode, required environment variables, and OAuth2 scope configuration for both UI and API tests.

@sourcery-ai

sourcery-ai Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a new Playwright UI authentication mode that injects an OAuth2 client-credentials access token into sessionStorage for OIDC-based SSO environments, shares auth configuration between API and UI tests (including scope), and documents the new environment variables and behaviour in the e2e README.

Sequence diagram for Playwright UI token_injection auth flow

sequenceDiagram
actor Tester
participant PlaywrightRunner as Playwright_Runner
participant Frontend as Frontend_UI
participant EntraID as Entra_ID
participant SessionStorage as SessionStorage
participant ReactApp as React_App
participant ReactOidcContext as React_OIDC_Context

Tester->>PlaywrightRunner: Run_e2e_tests_with_token_injection
PlaywrightRunner->>Frontend: HTTP_GET_TRUSTIFY_UI_URL_index_html
Frontend-->>PlaywrightRunner: index_html_with_window_env
PlaywrightRunner->>PlaywrightRunner: Parse_OIDC_SERVER_URL_and_OIDC_CLIENT_ID

PlaywrightRunner->>EntraID: POST_token_endpoint(client_id,client_secret,scope,grant_type_client_credentials)
EntraID-->>PlaywrightRunner: access_token

PlaywrightRunner->>PlaywrightRunner: Register_addInitScript_to_inject_token
PlaywrightRunner->>ReactApp: Launch_browser_page_to_TRUSTIFY_UI_URL
ReactApp->>SessionStorage: Read_oidc_user_entry

Note over PlaywrightRunner,SessionStorage: addInitScript_runs_before_app_load
PlaywrightRunner->>SessionStorage: Write_oidc_user_authority_clientId_with_access_token

ReactApp->>ReactOidcContext: Initialize_with_existing_session
ReactOidcContext->>SessionStorage: Load_oidc_user_state
ReactOidcContext-->>ReactApp: User_authenticated_from_injected_token
ReactApp-->>Tester: UI_loaded_without_login_redirect
Loading

Updated class diagram for UI auth helpers and shared constants

classDiagram

class CommonConstants {
  +string UI_AUTH_MODE
  +string AUTH_SCOPE
  +string TRUSTIFY_UI_URL
}

class EnvVariables {
  +string PLAYWRIGHT_UI_AUTH_MODE
  +string PLAYWRIGHT_AUTH_SCOPE
  +string PLAYWRIGHT_AUTH_CLIENT_ID
  +string PLAYWRIGHT_AUTH_CLIENT_SECRET
  +string PLAYWRIGHT_AUTH_URL
  +string TRUSTIFY_UI_URL
}

class UIAuthHelper {
  +login(page)
  +loginWithForm(page, username, password)
  +loginWithTokenInjection(page, clientId, clientSecret, authUrl, scope, uiUrl)
}

class ApiAuthHelper {
  +getClientCredentialsToken(authUrl, clientId, clientSecret, scope)
}

class TokenInjectionConfig {
  +string authority
  +string clientId
  +string accessToken
  +number expiresAt
  +string oidcUserStorageKey
}

CommonConstants <.. EnvVariables : populated_from
UIAuthHelper --> CommonConstants : reads
UIAuthHelper --> EnvVariables : reads
UIAuthHelper --> ApiAuthHelper : reuses_client_credentials_flow
UIAuthHelper --> TokenInjectionConfig : builds_and_injects
TokenInjectionConfig ..> SessionStorageAdapter : stored_in

class SessionStorageAdapter {
  +setItem(key, value)
  +getItem(key)
}
Loading

Flow diagram for selecting UI auth mode and performing login

flowchart TD
  Start([Start_UI_login])
  ReadMode[Read UI_AUTH_MODE or PLAYWRIGHT_UI_AUTH_MODE]
  CheckAuthRequired{AUTH_REQUIRED?}
  ModeDecision{UI auth mode}
  FormLogin[loginWithForm]
  TokenLogin[loginWithTokenInjection]
  End([Tests_continue_in_authenticated_state])

  Start --> CheckAuthRequired
  CheckAuthRequired -- false --> End
  CheckAuthRequired -- true --> ReadMode

  ReadMode --> ModeDecision
  ModeDecision -- form_or_empty --> FormLogin
  ModeDecision -- token_injection --> TokenLogin

  FormLogin --> End

  TokenLogin --> DiscoverOIDC[Fetch index_html_and_discover_OIDC_SERVER_URL_and_OIDC_CLIENT_ID]
  DiscoverOIDC --> GetToken[Call_API_auth_helper_getClientCredentialsToken]
  GetToken --> BuildPayload[Build_oidc_user_payload_and_storage_key]
  BuildPayload --> InjectToken[addInitScript_to_write_payload_to_sessionStorage]
  InjectToken --> End
Loading

File-Level Changes

Change Details Files
Introduce token-injection based UI login path alongside existing form-based login and route selection via an environment-controlled auth mode.
  • Refactor UI auth helper into two flows: a preserved form-based login function and a new token-injection login function, with a dispatcher based on UI_AUTH_MODE and AUTH_REQUIRED.
  • Implement discovery of frontend OIDC configuration by fetching TRUSTIFY_UI_URL index.html and parsing window._env for OIDC_SERVER_URL and OIDC_CLIENT_ID.
  • Implement OAuth2 client_credentials token retrieval against the discovered or configured AUTH_URL, using shared AUTH_CLIENT_ID, AUTH_CLIENT_SECRET and optional AUTH_SCOPE.
  • Inject the acquired access token into sessionStorage using a key format compatible with react-oidc-context via Playwright addInitScript, then navigate to /importers and assert successful login via page heading.
e2e/tests/ui/helpers/Auth.ts
Share auth configuration (including optional OAuth2 scope and UI auth mode) between API and UI tests via environment-backed constants.
  • Promote AUTH_URL, AUTH_CLIENT_ID, AUTH_CLIENT_SECRET into a "shared auth" section and add AUTH_SCOPE as an optional OAuth2 scope value.
  • Add UI_AUTH_MODE with default "form" and TRUSTIFY_UI_URL with default http://localhost:3000 as UI-specific configuration.
  • Ensure both UI and API test helpers import and use AUTH_SCOPE where appropriate.
e2e/tests/common/constants.ts
e2e/tests/api/fixtures.ts
Document new UI auth mode and scope configuration in e2e testing README for both UI and API tests.
  • Extend UI tests environment variable table with PLAYWRIGHT_UI_AUTH_MODE and PLAYWRIGHT_AUTH_SCOPE, clarifying defaults and the semantics of form vs token_injection.
  • Extend API tests environment variable table to include PLAYWRIGHT_AUTH_SCOPE, describing its role for client_credentials, particularly for Entra ID setups.
e2e/README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The discoverFrontendOidcConfig helper relies on atob, which is not guaranteed in all Node runtimes; consider using Buffer.from(serverConfig, 'base64').toString('utf-8') to avoid environment-specific breakage.
  • Fetching the frontend HTML with maxRedirects: 0 and assuming the OIDC config is present on that first response may fail for deployments that redirect / to another path; allowing redirects or making the index path configurable would make token discovery more robust.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `discoverFrontendOidcConfig` helper relies on `atob`, which is not guaranteed in all Node runtimes; consider using `Buffer.from(serverConfig, 'base64').toString('utf-8')` to avoid environment-specific breakage.
- Fetching the frontend HTML with `maxRedirects: 0` and assuming the OIDC config is present on that first response may fail for deployments that redirect `/` to another path; allowing redirects or making the index path configurable would make token discovery more robust.

## Individual Comments

### Comment 1
<location path="e2e/tests/ui/helpers/Auth.ts" line_range="75-41" />
<code_context>
+const loginWithTokenInjection = async (page: Page) => {
</code_context>
<issue_to_address>
**issue (testing):** Add an e2e scenario that explicitly exercises `token_injection` auth mode

Currently `loginWithTokenInjection` is only reachable via `UI_AUTH_MODE`, but there’s no e2e coverage to verify it actually works or fixes the SSO/MFA scenario described in the PR. Please add a smoke test (or a small Playwright project) that runs with `PLAYWRIGHT_UI_AUTH_MODE=token_injection` and `AUTH_REQUIRED=true`, uses a known-good Entra ID client, and asserts the UI is authenticated without using the login form. This will both prevent regressions and prove the injected token is sufficient for UI access.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

return {
oidcServerUrl: envInfo.OIDC_SERVER_URL,
oidcClientId: envInfo.OIDC_CLIENT_ID || "frontend",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (testing): Add an e2e scenario that explicitly exercises token_injection auth mode

Currently loginWithTokenInjection is only reachable via UI_AUTH_MODE, but there’s no e2e coverage to verify it actually works or fixes the SSO/MFA scenario described in the PR. Please add a smoke test (or a small Playwright project) that runs with PLAYWRIGHT_UI_AUTH_MODE=token_injection and AUTH_REQUIRED=true, uses a known-good Entra ID client, and asserts the UI is authenticated without using the login form. This will both prevent regressions and prove the injected token is sufficient for UI access.

@queria

queria commented Apr 22, 2026

Copy link
Copy Markdown
Author

Notes:

  • I am opening this PR to start discussion about such approach
  • not expecting this to be merged right away
  • it is patch currently for release/0.4.z branch,
  • if/after the approach seems appropriate i would add also PR for main branch to get there first

@mrrajan mrrajan added dependencies Pull requests that update a dependency file ci labels Apr 30, 2026
@queria queria requested a review from carlosthe19916 May 13, 2026 11:37

@carlosthe19916 carlosthe19916 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

@carlosthe19916 carlosthe19916 removed the dependencies Pull requests that update a dependency file label Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants