Authentication#

AIQ authentication is disabled by default for local development. When enabled, the web UI signs users in with an OAuth/OIDC provider and the backend validates the resulting JWT before serving protected API routes.

Use this guide when you need to:

  • Require users to sign in before using AIQ.

  • Add an OAuth/OIDC provider to the AIQ UI.

  • Configure backend JWT validation.

  • Gate data sources that need an authenticated AIQ user.

  • Forward the AIQ user token to custom tools or MCP pass-through integrations.

How AIQ Auth Works#

AIQ has two auth layers that must be configured together:

Layer

Responsibility

Frontend UI

Runs the OAuth/OIDC sign-in flow with NextAuth, stores the session, and sets an idToken cookie after login.

Backend API

Runs AuthMiddleware, validates bearer tokens or the idToken cookie with registered validators, and exposes the verified principal to tools and jobs.

The same REQUIRE_AUTH=true setting is used by both services, but each service also needs its own configuration. The frontend needs an OAuth provider. The backend needs at least one token validator.

Step 1: Add a UI OAuth Provider#

Create a provider file in:

frontends/ui/src/adapters/auth/providers/

For example:

// frontends/ui/src/adapters/auth/providers/my-sso.ts
import type { TokenRefreshResult } from './types'

export const MySSOProvider = {
  id: 'my-sso',
  name: 'My SSO',
  type: 'oauth' as const,
  wellKnown: `${process.env.MY_SSO_ISSUER}/.well-known/openid-configuration`,
  authorization: {
    params: { scope: 'openid profile email', response_type: 'code' },
  },
  clientId: process.env.MY_SSO_CLIENT_ID,
  // MY_SSO_CLIENT_SECRET must be set; an empty string causes silent OAuth failures.
  clientSecret: process.env.MY_SSO_CLIENT_SECRET || '',
  checks: ['pkce', 'state'] as ('pkce' | 'state' | 'nonce')[],
  idToken: true,
  profile(profile: { sub: string; email: string; name: string; picture?: string }) {
    return {
      id: profile.sub,
      email: profile.email,
      name: profile.name,
      image: profile.picture,
    }
  },
}

export const refreshMySSOToken = async (refreshToken: string): Promise<TokenRefreshResult> => {
  const response = await fetch(process.env.MY_SSO_TOKEN_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.MY_SSO_CLIENT_ID || '',
      client_secret: process.env.MY_SSO_CLIENT_SECRET || '',
    }),
  })

  const tokens = await response.json()
  if (!response.ok) {
    throw tokens
  }
  return tokens
}

Then update the provider registry:

// frontends/ui/src/adapters/auth/providers/index.ts
import type { AuthProviderConfig } from './types'
import { MySSOProvider, refreshMySSOToken } from './my-sso'

export type { AuthProviderConfig, TokenRefreshResult } from './types'

export const getAuthProviderConfig = (): AuthProviderConfig => ({
  provider: MySSOProvider,
  providerId: 'my-sso',
  refreshToken: refreshMySSOToken,
})

Use frontends/ui/src/adapters/auth/providers/auth-example.ts as the implementation checklist for a new provider.

Step 2: Configure UI Auth Environment#

Set these environment variables for the frontend:

REQUIRE_AUTH=true
NEXTAUTH_SECRET=<generate-with-openssl-rand-base64-32>
NEXTAUTH_URL=https://aiq.example.com
SESSION_MAX_AGE_HOURS=24

MY_SSO_ISSUER=https://sso.example.com
MY_SSO_CLIENT_ID=<client-id>
MY_SSO_CLIENT_SECRET=<client-secret>
MY_SSO_TOKEN_URL=https://sso.example.com/token

Set NEXTAUTH_URL to the public URL users open in their browser. If the public URL is HTTPS, cookies are secure by default. For reverse proxies that terminate TLS, still use the external HTTPS URL.

Step 3: Add a Backend Token Validator#

The backend only enforces auth when REQUIRE_AUTH=true and a request is classified as external. Set AIQ_EXTERNAL_HOSTNAMES to the hostnames that should be treated as externally reachable:

REQUIRE_AUTH=true
AIQ_EXTERNAL_HOSTNAMES=aiq-api.example.com

Register a validator before the backend starts. The recommended approach for deployment packages is an entry point:

# pyproject.toml of your deployment package
[project.entry-points."aiq_api.validators"]
my_sso = "my_aiq_auth.validators:get_validators"
# my_aiq_auth/validators.py
import os

from aiq_api.auth.jwt_validator import JWTValidator


def get_validators() -> list:
    issuer = os.environ["AIQ_JWT_ISSUER"]
    audience = os.environ.get("AIQ_JWT_AUDIENCE")
    return [JWTValidator(issuer_url=issuer, audience=audience)]

Then set the validator environment:

AIQ_JWT_ISSUER=https://sso.example.com
AIQ_JWT_AUDIENCE=<optional-api-audience>

Install the deployment package into the backend environment before starting AIQ. At startup, AIQ loads validators from the aiq_api.validators entry point group. If REQUIRE_AUTH=true and no validators are registered, the backend fails fast.

For simple embedded deployments, you can also register programmatically before nat serve starts:

from aiq_api.auth.jwt_validator import JWTValidator
from aiq_api.plugin import register_validator

register_validator(JWTValidator(issuer_url="https://sso.example.com", audience="api://aiq"))

Step 4: Mark Authenticated Data Sources#

Use requires_auth: true for sources that require a signed-in AIQ user:

functions:
  data_sources:
    _type: data_source_registry
    sources:
      - id: internal_mcp
        name: "Internal MCP"
        description: "Call internal MCP tools using your AIQ sign-in."
        requires_auth: true
        tools:
          - internal_mcp

The UI disables these sources until the user signs in to AIQ. This flag does not automatically authenticate to an upstream MCP server or API. Tools still need service-account credentials, native MCP auth, or AIQ token pass-through.

Step 5: Use the Current User Token in Tools#

Custom tools can read the current AIQ request token with get_auth_token():

from aiq_agent.auth import get_auth_token
from nat.builder.function_info import FunctionInfo
from nat.cli.register_workflow import register_function
from nat.data_models.function import FunctionBaseConfig


class InternalLookupConfig(FunctionBaseConfig, name="internal_lookup"):
    endpoint: str


@register_function(config_type=InternalLookupConfig)
async def internal_lookup(config: InternalLookupConfig, builder):
    async def _lookup(query: str) -> str:
        token = get_auth_token()
        if not token:
            return "Sign in before using Internal Lookup."

        # `call_internal_service` is a placeholder for your own async HTTP call
        # (e.g. via httpx.AsyncClient) — replace with the real client invocation.
        return await call_internal_service(
            endpoint=config.endpoint,
            query=query,
            headers={"Authorization": f"Bearer {token}"},
        )

    yield FunctionInfo.from_fn(
        _lookup,
        description="Look up internal information using the signed-in AIQ user's token.",
    )

AIQ also propagates the request token into async Dask jobs, so custom tools should use get_auth_token() instead of reading HTTP headers directly.

Use get_current_principal() when you need trusted identity metadata:

from aiq_agent.auth import get_current_principal

principal = get_current_principal()
if principal:
    user_id = principal.sub

Do not use unverified JWT payloads for authorization decisions.

Headless API Callers#

API clients can send a bearer token directly:

curl -H "Authorization: Bearer ${AIQ_ID_TOKEN}" \
     -H "X-AIQ-Mode: headless" \
     https://aiq-api.example.com/v1/data_sources

X-AIQ-Mode: headless tells AIQ the caller cannot participate in interactive clarifier back-and-forth.

Troubleshooting#

Backend Fails at Startup#

If REQUIRE_AUTH=true, make sure at least one validator is registered. The backend will fail with a clear error if no validators are available.

Requests Are Not Rejected#

Set AIQ_EXTERNAL_HOSTNAMES to the external backend hostname. AIQ treats requests to other hostnames as internal traffic for cluster-to-cluster communication.

UI Shows Default User#

Confirm frontends/ui/src/adapters/auth/providers/index.ts returns your provider, not the default provider: null configuration.

Authenticated Data Source Is Disabled#

The source has requires_auth: true, but the UI does not have an idToken. Confirm the user is signed in and that the NextAuth callback sets the idToken cookie.

Tool Cannot See the Token#

Use get_auth_token() inside the tool call body. Do not capture the token at startup, because startup does not run in a user request context.