App security and identity

Secure your Domino app with access control and permissions.

Access controls and permissions

Manage access with the Permission tab:

  • Anyone, including anonymous users - In this mode, anyone with the URL can access your app, even if they don’t have a Domino account.

  • Anyone with an account - Anyone logged in to Domino with an account can access the app.

  • Invited users only - Only users you explicitly invite can access the app.

  • Invited users (others can request access) - Only users you explicitly invite can access the app, but users can request access (that you can approve).

Access the identities of app users

You might want to create apps that need to know who uses them. For example, this is useful if you want to load specific default values or preferences, or if you want to access different data based on who views your app.

To enable this, Domino passes the username of a user who accesses your Domino app in an HTTP header named domino-username.

If your app framework gives you access to the HTTP headers of the active request, retrieve the domino-username for use by your app code. If you allow users who are not logged in to Domino to view your apps, the value of the domino-username header is Anonymous.

Additionally, if the SecureIdentityPropagationToAppsEnabled Feature Flag is turned on, Domino passes a JWT authorization token that can be used to identify the requesting user in the Authorization HTTP header. This JWT token’s integrity can be verified and it can then be decoded to obtain the user’s username, email and Domino user ID.

Note
These identity headers are only available when you use app frameworks that support proxied HTTP headers. These headers are supported by Flask and Dash by default, but Shiny requires that you use Server Pro.

Access username example

Create the files for this Flask example that gets the Domino username of an app viewer in your project:

#!/usr/bin/env bash
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
export FLASK_APP=app.py
export FLASK_DEBUG=1
python -m flask run --host=0.0.0.0 --port=8888

Here is a simple app.py file that renders a template named index.html. This app imports request from flask, which gives you access to the headers of the active HTTP request.

import flask
from flask import request, redirect, url_for

class ReverseProxied(object):
  def __init__(self, app):
      self.app = app
  def __call__(self, environ, start_response):
      script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
      if script_name:
          environ['SCRIPT_NAME'] = script_name
          path_info = environ['PATH_INFO']
          if path_info.startswith(script_name):
              environ['PATH_INFO'] = path_info[len(script_name):]
      return self.app(environ, start_response)

app = flask.Flask(__name__)
app.wsgi_app = ReverseProxied(app.wsgi_app)

# Homepage which uses a template file
@app.route('/')
def index_page():
  return flask.render_template("index.html")

There is a template file at templates/index.html that fetches the domino-username header from the requests object and renders it.

<!DOCTYPE html>
<html>
  <body>
    <h1>Your username is {{ request.headers.get("domino-username") }}</h1>
  </body>
</html>

If you host this app in Domino and open it, you’ll see something like this where the username shown matches the username of the app user.

App secure identity propagation

When the SecureIdentityPropagationToAppsEnabled Feature Flag is turned on, Domino passes a JWT authorization token that can be used to identify the requesting user in the Authorization HTTP header. This JWT token’s integrity can be verified and it can then be decoded to obtain the user’s username, email and Domino user ID.

Note

When SecureIdentityPropagationToAppsEnabled is enabled apps are hosted at https://<your-domino-domain>/apps/<app-id>;. This might require configuring your application server’s base path for your application. For convenience, apps can read the base path from the DOMINO_RUN_HOST_PATH environment variable.

Access the user’s identity example

First extract the token from the Authorization header of the incoming request.

def extract_token(self, headers):
  auth_header = headers.get('Authorization', '')
  return auth_header[7:] if auth_header.startswith('Bearer ') else None

You can then verify the integrity of the token by retrieving the Domino installation’s public certificates and verifying the signature of the token. The token can then be decoded to obtain the user’s identity information. Here is an example of how to retrieve the certificates and use them to verify and decode the token in the app.

from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from http.server import HTTPServer, BaseHTTPRequestHandler
from jwt import decode, get_unverified_header
from jwt.exceptions import InvalidTokenError
from pprint import pformat
import base64
import html
import json
import jwt
import os
import requests
import socket
import sys

def verify_and_decode_jwt_token(self, token):
  # Public key URL
  keycloak_domain = "http://keycloak-http.domino-platform" # Use the external domain if running the app on a remote data plane.
  jwks_url = f"{keycloak_domain}/auth/realms/DominoRealm/protocol/openid-connect/certs"

  # Retrieve JWKS (JSON Web Key Set) from URL
  jwks = requests.get(jwks_url).text
  jwks_dict = json.loads(jwks)

  # Get the key ID from the token header
  unverified_header = get_unverified_header(token)
  kid = unverified_header.get('kid')

  if not kid:
    raise ValueError("No 'kid' found in token header")

  # Find the corresponding public key
  public_key = None
  for key in jwks_dict['keys']:
    if key['kid'] == kid:
      x5c = key['x5c'][0]
      cert_bytes = x5c.encode('ascii')
      cert_der = base64.b64decode(cert_bytes)
      cert = x509.load_der_x509_certificate(cert_der, default_backend())
      public_key = cert.public_key()
      break

  if not public_key:
    raise ValueError(f"No public key found for kid: {kid}")

  # Convert the public key to PEM format
  pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
  )

  try:
    # Verify the token
    payload = jwt.decode(
      token,
      pem,
      algorithms=['RS256'],
      audience="apps",
      options={"verify_signature": True}
    )
    return payload
  except jwt.InvalidSignatureError:
    raise ValueError("Invalid signature")
  except jwt.ExpiredSignatureError:
    raise ValueError("Token has expired")
  except jwt.InvalidTokenError as e:
    raise ValueError(f"Invalid token: {str(e)}")

After decoding the JWT token you will obtain a JSON object containing the user’s identity information in the following format:

{
  "exp": 1726076562,
  "iat": 1726076262,
  "auth_time": 1726005915,
  "jti": "8d3febf7-068b-4240-bb3c-c361406119bf",
  "iss": "https://dominoDomain/auth/realms/DominoRealm",
  "aud": [
    "apps",
    "app-user-token-exchange-client"
  ],
  "sub": "66d9fc7a538b60bef7c02c9c",
  "typ": "Bearer",
  "azp": "domino-play",
  "session_state": "2125b0d6-35aa-49b6-a63a-c274bf07a3f9",
  "scope": "openid email profile",
  "sid": "2125b0d6-35aa-49b6-a63a-c274bf07a3f9",
  "email_verified": true,
  "idp_id": "74e837a7-61ec-4777-8862-772037aec72f",
  "name": "givenName familyName",
  "preferred_username": "userName",
  "given_name": "givenName",
  "family_name": "familyName",
  "email": "userName@domain.com"
}

Configuring application base path

The base path for the application can be read from the DOMINO_RUN_HOST_PATH environment variable and used to set the base path for your application:

import os

from flask import (
    Flask,
)


class ReverseProxied(object):
    def __init__(
        self,
        app,
    ):
        self.app = app

    def __call__(
        self,
        environ,
        start_response,
    ):
        script_name = os.environ.get('DOMINO_RUN_HOST_PATH', '')
        if script_name:
            environ["SCRIPT_NAME"] = script_name
            path_info = environ["PATH_INFO"]
            if path_info.startswith(script_name):
                environ["PATH_INFO"] = path_info[len(script_name) :]
        # Setting wsgi.url_scheme from Headers set by proxy before app
        scheme = environ.get(
            "HTTP_X_SCHEME",
            "https",
        )
        if scheme:
            environ["wsgi.url_scheme"] = scheme
        return self.app(
            environ,
            start_response,
        )


app = Flask(__name__)

from app import (
    views,
)

app.wsgi_app = ReverseProxied(app.wsgi_app)

iFrame security

If your Domino deployment exercises iFrame security or requires a content security policy for web apps and your app behaves in unexpected ways, see Whitelist resources.

By default, Apps are limited to load only within an iFrame. Attempting to access an App URL directly will result in a 400 Bad Request error for users. To control this behavior see the ShortLived.iFrameRequired Feature Flag.