Authentication and Security
The dapcli authentication module supports two distinct operational modes: interactive browser-based OIDC login for human operators and headless client-credentials token minting for automation. The browser flow simulates a full Keycloak redirect chain to capture portal session cookies, which are then persisted with full attribute fidelity to avoid cross-domain routing issues. Conversely, the headless flow mints short-lived bearer tokens via the on-prem token service, which are required for the generic REST API layer. Both modes enforce strict secret handling, rejecting command-line or environment variable inputs in favor of interactive prompts, stdin, or file-based reading.
Browser-based OIDC Login
Section titled “Browser-based OIDC Login”The browser_login function implements a programmatic walk of the OIDC authorization code flow, mimicking the behavior of a real browser interacting with Keycloak 1. The process begins by constructing an authentication URL that includes a state parameter. This state must be a URL-encoded base64 string representing the post-login landing path (defaulting to /home). If the state is not properly encoded, the portal’s callback handler returns a 4xx error, which the portal’s nginx layer redirects to dell.com, causing a silent failure rather than a clear OIDC error.
The client retrieves the Keycloak login form, extracts the action URL, and submits the username and password. If the login succeeds, the client follows the 302 redirect chain to the portal’s callback endpoint, where the session cookie is set [src: src: dap/auth/browser.py:L1-120]. The function returns a BrowserLoginState containing the captured cookies and cluster configuration.
To handle transient infrastructure issues, specifically a bug in DAP 2.0.0.1 where stale Keycloak session_code requests bounce to a marketing page, the login function retries up to five times. Each retry opens a fresh httpx client and a new Keycloak session code. Terminal failures, such as invalid credentials or realm changes, are raised immediately without retry.
Session Persistence
Section titled “Session Persistence”After a successful browser login, the resulting session cookies are persisted to disk to avoid repeated authentication prompts. The save_session function writes the session data to ~/.config/dap-cli/session-<cluster>.json with file permissions set to 0600 2.
Crucially, the persistence mechanism saves the full cookie attributes, including domain, path, secure, and http-only flags, rather than just the name and value pairs. This is necessary because portal session cookies are domain-scoped to the portal host, while orchestrator-side cookies may be scoped to the orchestrator host. Stripping the domain attribute would cause httpx to send portal cookies to the orchestrator (and vice versa), leading to rejection by the respective nginx filters.
When a command is executed, load_session reads the JSON file and reconstructs the Session object. The Session.attach method then populates the httpx.Client’s cookie jar with these cookies, ensuring the correct domain scoping is maintained for subsequent requests. The portal session is short-lived (typically 1-2 hours); if a command receives a 401 from the orchestrator, the system surfaces a “session expired” message rather than silently re-authenticating, keeping the operator in control of credential entry.
On-Prem Token Minting
Section titled “On-Prem Token Minting”For headless automation, the system uses the mint_onprem_token function to obtain a bearer token from the on-prem token service at /api/v1/oidc/token 3. This token universe is distinct from Keycloak realm tokens; the on-prem token service is the issuer for internal services and is the only type accepted by the orchestrator’s hzp-iam-proxy for customer-facing REST APIs.
The minting process requires client_id and client_secret corresponding to an admin client registered in iam_mgmt_svc.clients. The on-prem token service enforces strict requirements: the grant_type must be client_credentials, and the org_id must be explicitly included in the form body, even though it is implied by the client registration. The request is sent with Content-Type: application/x-www-form-urlencoded.
Upon receiving a 200 OK response, the function extracts the access_token, expires_in, and token_type. It also performs a best-effort decode of the JWT payload to surface the roles claim to the operator, though this is never used for authorization decisions.
Secure Secret Handling
Section titled “Secure Secret Handling”The authentication module enforces strict policies regarding the handling of sensitive credentials. Passwords are never accepted via command-line arguments or environment variables 4. Instead, the dap auth login command provides three secure input methods:
- Interactive Prompt: The default method uses
hide_input=Trueto prompt the user in the TTY. - Stdin: The
--password-stdinflag allows scripts to pipe the password securely. - File: The
--password-fileflag reads the password from a managed file, with whitespace trimmed.
For the headless token minting path, the client_secret is retrieved from a persisted credential store rather than being passed as a command-line argument. The dap auth mint-token command loads the persisted admin client credentials and passes them to mint_onprem_token. This ensures that secrets are stored securely and only accessed programmatically when needed for token exchange.
"""Browser-style OIDC code-flow login.
Walks the same redirect chain a real browser would: hit the Keycloak
``/auth`` endpoint, fill the login form, follow the 302 back to the
portal's ``/api/v2/auth/callback``, and let the portal set its
session cookie.
The ``state`` parameter passed to Keycloak is opaque from Keycloak's
side, but the portal's callback handler decodes it as base64 to
determine where to redirect after login. Sending a non-base64 state
causes the callback to return a 4xx, which the portal nginx then
maps to a redirect to dell.com -- not an OIDC failure, a state-
encoding failure. ``state`` therefore MUST be the URL-encoding of a
base64 string (``L2hvbWU%3D`` for the standard ``"/home"`` landing).
"""
from __future__ import annotations
import base64
import re
import urllib.parse
from dataclasses import dataclass
import httpx
from ..config import ClusterConfig
from ..errors import DapError
@dataclass
class BrowserLoginState:
"""Mutable state held across the OIDC code-flow redirect chain."""
cookies: httpx.Cookies
cluster: ClusterConfig
def _state_for(landing_path: str = "/home") -> str:
"""Encode *landing_path* the way the portal callback expects.
"""Session persistence for the primary login path.
After ``dap auth login`` completes an OIDC code-flow against the
portal, the resulting session cookies are written to
``~/.config/dap-cli/session-<cluster>.json`` (mode 0600). Every
subsequent ``dap`` command rehydrates those cookies into an
``httpx.Client`` so the operator does not re-authenticate per call.
Full cookie attributes (domain, path, expires, secure, http-only)
are preserved -- the portal session cookies are domain-scoped to
the portal host, and the orchestrator-side cookies may be scoped to
the orchestrator host. Saving only ``(name, value)`` strips the
domain attribute and httpx ends up sending portal cookies to the
orchestrator (and vice versa), which the relevant nginx filters
will then reject.
The portal session is short-lived (typically 1-2 hours). When a
command hits ``401`` from the orchestrator, the caller should surface
"session expired -- run `dap auth login` again" rather than silently
reauthenticating, so the operator stays in control of when
credentials are entered.
"""
from __future__ import annotations
import http.cookiejar
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
import httpx
from ..errors import DapError
@dataclass(frozen=True)
class Session:
"""A persisted portal session, with full cookie attributes."""
"""On-prem OIDC token mint.
DAP exposes two parallel token universes that both live behind
``https://dap-portal.<domain>`` and both look like OIDC:
- **Keycloak realm tokens** at
``/auth/realms/<realm>/protocol/openid-connect/token``. The browser
SPA receives these after a successful user login. Issuer is
``.../auth/realms/<realm>``.
- **On-prem token-svc tokens** at ``/api/v1/oidc/token``. Every
internal service uses these. Issuer is just
``https://dap-portal.<domain>`` (no ``/auth/realms`` suffix). The
orchestrator's hzp-iam-proxy only accepts THESE tokens for the
customer-facing REST APIs.
For headless automation the bootstrap helper provisions an admin
client registered in ``iam_mgmt_svc.clients``; subsequent calls go
through :func:`mint_onprem_token` to exchange that client's
credentials for a short-lived bearer token.
"""
from __future__ import annotations
import base64
import json
from dataclasses import dataclass
from typing import Any
import httpx
from ..config import ClusterConfig
from ..errors import DapError
@dataclass(frozen=True)
class AccessToken:
"""A minted OIDC access token plus its decoded claims."""
raw: str
expires_in: int
"""``dap auth`` subcommand surface.
Primary path is ``dap auth login`` (interactive browser OIDC). The
advanced ``dap auth bootstrap-service-account`` is registered by
``dap.bootstrap.commands`` for installs where the portal OIDC
callback is broken; see ``docs/chart-bugs.md``.
Passwords are NEVER accepted on argv or through environment
variables. The login command prompts via ``hide_input=True`` (TTY),
reads via ``--password-stdin`` (script), or reads via
``--password-file PATH`` (managed file).
"""
from __future__ import annotations
import json
import click
from ..config import load_config
from ..secret_input import read_secret
from .browser import browser_login
from .session import build_session, save_session
from .token import mint_onprem_token
@click.group()
def auth() -> None:
"""Authentication helpers."""
@auth.command("login")
@click.option(
"--username",
default=None,
help="Admin username. If omitted, prompted interactively.",
)
@click.option(
"--password-stdin",
is_flag=True,