> ## Documentation Index
> Fetch the complete documentation index at: https://docs.domino.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Identity for Apps

> Use Domino identity propagation so an App knows who each viewer is, verifies their token, or acts with the viewer's permissions instead of the publisher's.

An App runs as its publisher. Every viewer sees what the publisher can see, and everything the App does, it does as the publisher. Identity propagation passes the viewer's identity into your code. Use it when the App must record who did something, prove it, or act with that person's own permissions.

In the **Synthetic Transaction Review** App from [Develop and test](https://docs.domino.ai/cloud/platform-capabilities/core-concepts/apps/develop-and-test), an **Approve** or **Flag** decision is anonymous. The App knows a button was pressed, not who pressed it. Compliance needs the reviewer's name on each decision. An auditor needs proof that the name is real. A bank with regional teams needs each reviewer to see only their own region. Those are the three levels.

## Choose a level

Each level includes the one before it.

| Level | What your code receives | Use it when | Requires |
| :- | :- | :- | :- |
| **Basic** | The viewer's username, from the `preferred_username` claim of the token in the `Authorization` header, or from the `domino-username` header when no token arrives. | You record who did something or show per-viewer content. | Nothing. |
| **Enhanced** | A signed JSON Web Token (JWT) in the `Authorization` header with the username, email, and Domino user ID. The token works only against a limited set of Domino endpoints. | You need proof of identity, or more than the username. | The `SecureIdentityPropagationToAppsEnabled` feature flag, on by default since Cloud Release 90. |
| **Extended** | A full-scope token, after the viewer consents. The App can call Domino APIs as the viewer. | The App reads data, submits Jobs, or manages Projects with the viewer's permissions. | `SecureIdentityPropagationToAppsEnabled`, the `com.cerebro.domino.apps.extendedIdentityPropagationToAppsEnabled` configuration record, an App that opts in, and a SysAdmin or CloudAdmin publisher. |

Use extended identity. It supports the widest range of Apps, because the App can identify each viewer and also read data, submit Jobs, and call Domino APIs with that viewer's permissions. Basic and enhanced identity tell the App who the viewer is but don't change what the App can reach.

Extended identity isn't required if the App only needs its owner's identity to run. Such an App runs as its publisher, for example a dashboard that shows every viewer the same data.

> [!NOTE]
>
> Identity headers reach your code only in frameworks that support proxied HTTP headers. Flask and Dash support them by default. Shiny requires [Shiny Server Pro](https://docs.rstudio.com/shiny-server/#proxied-headers).

## Basic identity

Domino identifies the viewer on every request, with no setup. With `SecureIdentityPropagationToAppsEnabled` on, the default, each request carries a token in the `Authorization` header whose `preferred_username` claim names the viewer, plus a `domino-username` header copied from it. Read the token when one is present. Apps with extended identity don't receive the `domino-username` header, and a client could supply its own, so read the header only when no token arrived.

Dash runs on Flask, so a callback can read the request from Flask's request context. This version of the transaction review App's `record_decision` callback adds the reviewer's name to each decision:

```python theme={null}
import jwt
from flask import request


def viewer_name():
    auth = request.headers.get("Authorization", "")
    if auth.startswith("Bearer "):
        claims = jwt.decode(auth[7:], options={"verify_signature": False})
        return claims.get("preferred_username", "unknown")
    return request.headers.get("domino-username", "unknown")


def record_decision(_approve_clicks, _flag_clicks, transaction_id, decisions):
    reviewer = viewer_name()
    decisions = decisions or {}
    if not transaction_id:
        return None, decisions
    if ctx.triggered_id == "approve-transaction":
        decisions = {**decisions, transaction_id: "approved"}
    elif ctx.triggered_id == "flag-transaction":
        decisions = {**decisions, transaction_id: "flagged"}
    decision = decisions.get(transaction_id)
    if not decision:
        return None, decisions
    color = "green" if decision == "approved" else "red"
    return dmc.Alert(f"{transaction_id} {decision} by {reviewer} for this session.", color=color), decisions
```

Commit. The preview shows your username on the next decision. This reads the claim without checking the token's signature, which is enough to know who is viewing. [Enhanced identity](#enhanced-identity) verifies it.

## Enhanced identity

With enhanced identity, Domino passes a signed JWT in the `Authorization` header. Your code verifies the signature against Domino's keys, then reads the viewer's username, email, and user ID from it. The token's audience limits it to a small set of Domino endpoints.

Use it when a username in a header isn't proof enough, for example when the decision log feeds an audit.

**1. Extract the token**

```python theme={null}
def extract_token(headers):
    auth_header = headers.get("Authorization", "")
    return auth_header[7:] if auth_header.startswith("Bearer ") else None
```

**2. Verify and decode the token**

Fetch the signing keys from the JSON Web Key Set (JWKS) endpoint, then verify and decode:

```python theme={null}
import base64

import jwt
import requests
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization


def verify_and_decode_jwt_token(token):
    keycloak_domain = "http://keycloak-http.domino-platform"
    jwks_url = f"{keycloak_domain}/auth/realms/DominoRealm/protocol/openid-connect/certs"

    jwks_dict = requests.get(jwks_url).json()
    kid = jwt.get_unverified_header(token).get("kid")

    public_key = None
    for key in jwks_dict["keys"]:
        if key["kid"] == kid:
            x5c = key["x5c"][0]
            cert = x509.load_der_x509_certificate(base64.b64decode(x5c), default_backend())
            public_key = cert.public_key()
            break
    if not public_key:
        raise ValueError("No public key found for kid")

    pem = public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )

    return jwt.decode(token, pem, algorithms=["RS256"], audience="apps")
```

**3. Read the viewer claims**

A decoded payload looks like this:

```python theme={null}
{
  "sub": "66d9fc7a538b60bef7c02c9c",
  "preferred_username": "userName",
  "email": "userName@domain.com",
  "given_name": "givenName",
  "family_name": "familyName"
}
```

## Extended identity

With extended identity, the App receives a full-scope token for each viewer who consents. It can then read their data, submit Jobs, manage Projects, and do anything else the viewer can do in Domino. In the transaction review App, this is how each reviewer sees only the transactions their permissions allow, and how a **Rescore** button could start a Job as the reviewer rather than as the publisher.

> [!NOTE]
>
> The App receives the viewer's token alongside the publisher's, and your code chooses which one to use for each action. Each viewer consents before the App can act for them. Extended identity stays on for the life of the App once you turn it on.

### Before you begin

* The `SecureIdentityPropagationToAppsEnabled` feature flag is `true`.
* The `com.cerebro.domino.apps.extendedIdentityPropagationToAppsEnabled` configuration record is enabled. It's enabled by default.
* You're a SysAdmin or CloudAdmin. Each App opts in individually, and only a SysAdmin or CloudAdmin can publish an App that has opted in.

**1. Opt the App in**

Turn on **Allow App to act for viewers in Domino** in the App's **Access and sharing** settings. This setting is version-defining: turning it on creates a version, and it takes effect after the App restarts. An administrator can also turn it on in the [App Preview](https://docs.domino.ai/cloud/platform-capabilities/core-concepts/apps/develop-and-test#preview-the-app) **Settings** panel to test it before publishing.

**2. Read the viewer's token**

```python theme={null}
auth_token = request.headers.get("Authorization", "")
token = auth_token[7:] if auth_token.startswith("Bearer ") else ""
```

Without extended identity, the same code returns the limited-scope token from enhanced identity.

**3. Call Domino APIs as the viewer**

This Flask route returns the Projects the viewer can access, which can differ from the publisher's. Domino sets the API address in `DOMINO_USER_HOST` for every App.

```python theme={null}
import os

import requests
from flask import Flask, jsonify, request

app = Flask(__name__)

API_HOST = os.environ["DOMINO_USER_HOST"]


def get_viewer_token():
    """Return the viewing user's token, or None if it isn't present."""
    auth_header = request.headers.get("Authorization", "")
    return auth_header[7:] if auth_header.startswith("Bearer ") else None


def get_publisher_token():
    """Return the publisher's token from the local token service."""
    return requests.get("http://localhost:8899/access-token").text.strip()


@app.route("/projects")
def list_viewer_projects():
    token = get_viewer_token()
    if token is None:
        message = (
            "No viewer token. Confirm that extended identity propagation "
            "is enabled for this app and that you granted consent."
        )
        return jsonify(error=message), 401

    response = requests.get(
        f"{API_HOST}/v4/projects",
        headers={"Authorization": f"Bearer {token}"},
    )
    response.raise_for_status()
    return jsonify(projects=[project["name"] for project in response.json()])
```

Call `get_viewer_token()` to act as the viewer and `get_publisher_token()` to act as the publisher. The same pattern works for any Domino API call the viewer is allowed to make.

**4. Publish, deploy, and collect consent**

Publish and deploy the App. The first time a viewer opens it, Domino asks for consent. After consent, the App can retrieve the full-scope token. Consent lasts 8 hours by default. Viewers can extend it to 30 days on the consent page. Consent is audited.

## Scope mounted data to each viewer

Apps mount the NetApp Volumes and Datasets the publisher can access, so every viewer can read them. To apply each viewer's own permissions instead, read the data through the `dominodatalab-data` SDK rather than the mounted path, and pass the raw bearer token from the `Authorization` header to the SDK client. The Domino API then checks the viewer's permissions. The `domino-username` header and the decoded claims identify the viewer but don't authenticate an SDK call.

**Datasets**

1. Read the `Authorization` request header and remove its `Bearer ` prefix.
2. Pass the raw token to `DatasetClient`.
3. Read, write, or download Dataset files with the client. The [Dataset proxy example](https://docs.domino.ai/cloud/reference/api/data_api/datasource_usecases/access-datasets-via-proxy.md) has the Python code.

**NetApp Volumes**

1. Read the `Authorization` request header and remove its `Bearer ` prefix.
2. Pass the raw token to the `dominodatalab-data` client.
3. Read, write, or download volume files with the client.

In the transaction review App, the Dataset read moves out of module scope, where no viewer request exists, into a callback that runs per request and reads through `DatasetClient` with that request's token. Catch the permission error for a reviewer without access to the `AML` Dataset and show an empty queue instead of the publisher's data.

## Headers and environment variables

Domino adds these headers to each request it forwards to your App:

| Header | Description |
| :- | :- |
| `domino-username` | The Domino username of the viewer. |
| `Authorization` | A JWT when `SecureIdentityPropagationToAppsEnabled` is enabled. Decode it to read viewer identity claims, or pass it to Domino APIs. |

Domino also sets these [environment variables](https://docs.domino.ai/cloud/platform-capabilities/core-concepts/compute-environments/manage-compute-environments/manage-environment-variables) when an App starts:

| Variable | Description |
| :- | :- |
| `DOMINO_RUN_HOST_PATH` | The URL path prefix for your App. Use it for route and base path configuration. |
| `DOMINO_USER_HOST` | The Domino API address from inside the App. Older code reads the same value from `DOMINO_API_HOST`, which is deprecated. |

Project-level variables and the user-level variables of the user who starts the App are also available. Use them for secrets and configuration instead of hardcoding values.

## Related

* [Operate](https://docs.domino.ai/cloud/platform-capabilities/core-concepts/apps/operate.md#share-the-app): Choose who can open the App.
* [Configuration](https://docs.domino.ai/cloud/platform-capabilities/core-concepts/apps/configuration.md): The feature flag and configuration record behind each level.
* [Extensions](https://docs.domino.ai/cloud/platform-capabilities/features/extensions): Apps with extended identity that Domino embeds in its own UI.


## Related topics

- [Glossary](https://docs.domino.ai/cloud/reference/glossary.md)
- [Domino Cloud release notes](https://docs.domino.ai/release-notes/cloud-release.md)
- [Sharing and access](https://docs.domino.ai/cloud/platform-capabilities/features/collaboration/sharing-and-access.md)
- [Configuration for Apps](https://docs.domino.ai/cloud/platform-capabilities/core-concepts/apps/configuration.md)
