Secure your Domino app with access control 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).
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.
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 |
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)
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.