VASP Quick Start Guide
This guide will walk you through adding UMA Auth to an existing UMA VASP implementation.
To implement the base UMA standard, see the UMA Standard guide.
Implementation of UMA Auth requires 5 simple steps:
- Implement the UMA Auth OpenAPI Schema
- Configure and run the UMA NWC Docker image
- Implement the token exchange process with the Docker image
- Update your uma-configuration file
- Test your implementation
The UMA Auth OpenAPI Schema is a RESTful API that the Nostr Wallet Connect (NWC) image uses to communicate with
the main VASP server. You can see the full schema file
here. This same github
repository also contains generated model libraries for Python, Go, Kotlin/Java, and TypeScript. You can also generate
your own libraries for whatever framework or language you're using from the schema itself. See the
VASP OpenAPI Schema guide for more details.
You'll need to implement the specified API endpoints in your VASP server to handle the NWC image requests.
Technically, you do not need to support all of the endpoints in the schema, but the more you support, the more
functionality you can provide to your users. At a minimum, your existing UMA VASP implementation will give you a
simple starting point for implementing:
GET /receiver/lud16/{receiver_address}
: This is equivalent to the lnurlp request in UMA.GET /quote/lud16
: This is equivalent to the payreq request in UMA.POST /quote/{payment_hash}
: This actually pays a quote that was previously generated.POST /payments/lud16
: This is all 3 of the above combined into one request.
Here's a simple example of what the
GET /receiver/lud16/{receiver_address}
endpoint might look like in Python:from flask import Flask, request, jsonify
# If importing the generated models libary (pip install uma_auth), you would import them like this:
from uma_auth.models.lookup_user_response import LookupUserResponse
app = Flask(__name__)
@app.route('/receiver/lud16/<receiver_address>', methods=['GET'])
def lookup_user(receiver_address):
# Assuming you have a `sending_vasp` object that can handles the UMA lnurlp lookup:
lookup_response = sending_vasp.handle_uma_lookup(receiver_uma)
currencies = lookup_response.get("receiverCurrencies") or []
return LookupUserResponse(
currencies=[
CurrencyPreference(
currency=Currency(
code=currency.get("code"),
symbol=currency.get("symbol"),
decimals=currency.get("decimals"),
name=currency.get("name"),
),
multiplier=currency.get("multiplier"),
min=(
currency.get("convertible").get("min")
if "convertible" in currency
else currency.get("minSendable")
),
max=(
currency.get("convertible").get("max")
if "convertible" in currency
else currency.get("maxSendable")
),
)
for currency in currencies
]
).to_dict()
The other endpoints above can similarly be implemented simply by mapping your existing UMA VASP functionality to
the OpenAPI schema data types.
You also must support the following endpoints for the NWC image to work:
GET /info
: Get information about the user's wallet connection.GET /budget_estimate
: Used to get a cost estimate in the user's default currency if they are making a payment in a different currency.
The remaining endpoints should also be implemented to provide a full NWC experience. They include things like
creating a bolt11 invoice, paying a bolt11 invoice, getting the user's balance, listing transactions and invoices,
etc. Make sure that you appropriately set your supported NWC commands in the NWC Docker image configuration (see below).
Next, we'll need to configure and run the UMA NWC Docker image. This image handles all of the Nostr Wallet Connect
communication, budget management, connection setup and management UI, etc. It is configurable via environment variables
to control UI branding, database storage, supported NWC commands, and more. You can find the code for the image in
this github repo along with steps for running it from
source. See the UMA NWC Docker Image guide for full details. For now, let's just get the
image up and running locally!
You can just pull the latest version from the github container registry:
docker pull ghcr.io/uma-universal-money-address/uma-nwc-server:latest
Next, we're going to need to configure the image to correctly point to your VASP server, DB, secrets, etc. Here's an
example of a
local.py
configuration file that you can use to run the image locally. It assumes you also have your
UMA VASP server running locally on port 5001.import os
import secrets
# This will create a local sqlite database in instance/nwc.sqlite
DATABASE_URI: str = "sqlite+aiosqlite:///" + os.path.join(
os.getcwd(), "instance", "nwc.sqlite"
)
# A secret key for encrypting cookies
SECRET_KEY: str = secrets.token_hex(32)
# The public key for verifying JWTs from the VASP. See the next section for how to generate this.
UMA_VASP_JWT_PUBKEY = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9\nq9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==\n-----END PUBLIC KEY-----"
# The URL to the VASP's login page. This is where the NWC image will redirect users to login.
UMA_VASP_LOGIN_URL = "http://localhost:5001/auth/nwclogin"
# The URL to the VASP's token exchange endpoint. This is where the NWC image will exchange the user's
# short-lived JWT for a long-lived one. See "VASP Token Exchange".
UMA_VASP_TOKEN_EXCHANGE_URL = "http://localhost:5001/umanwc/token"
# The base URL of the VASP's API where you set up the OpenAPI schema in the previous step.
# This is where the NWC image will send requests to the VASP.
VASP_UMA_API_BASE_URL = "http://localhost:5001/umanwc"
# A name for the VASP that will be displayed in the Permission and Connection Management UI.
VASP_NAME = "Pink Drink NWC"
# Replace with your own constant private key via `openssl rand -hex 32`. Save it somewhere safe!
NOSTR_PRIVKEY: str = secrets.token_hex(32)
# The nostr relay where your NWC image will send and receive messages. For development, you can use Alby's public relay.
# See the "Running a Relay" guide for more details.
RELAY = "wss://relay.getalby.com/v1"
# The URL of the NWC image's root domain.
NWC_APP_ROOT_URL = "http://localhost:8080"
# The NWC commands that your VASP supports. This should be a list of strings from the list below that
# corresond to the endpoints you implemented in the OpenAPI schema.
VASP_SUPPORTED_COMMANDS = [
"pay_invoice",
"make_invoice",
"lookup_invoice",
"get_balance",
"get_budget",
"get_info",
"list_transactions",
"pay_keysend",
"lookup_user",
"fetch_quote",
"execute_quote",
"pay_to_address",
]
Then you can run the image locally with the following command:
docker run -p 8080:8081 \
-e QUART_CONFIG=local.py \
ghcr.io/uma-universal-money-address/uma-nwc-server:latest
It won't load quite yet, but we're only a 2 steps away from that!
The NWC image doesn't have its own user database or authentication system. Instead, it relies on the VASP to handle
user authentication and provide a JWT that the NWC image can use to identify the user. You can see the
NWC Container Auth Token Exchange guide for more details on how this process works.
To jump right into a working solution, you'll need to implement 2 steps on your VASP server:
When the user loads a page in the NWC image frontend for the first time, they will be redirected to your VASP's login
page that you configured above in the
UMA_VASP_LOGIN_URL
variable. This should generally use your existing login flow
to authenticate the user. Once the user is authenticated (or if they were already logged in), you should generate a
short-lived JWT and redirect back to the NWC image with the JWT in the URL. For example:- The user loads the NWC image and is redirected to your login page at
http://localhost:5001/auth/nwclogin?redirect_uri=https://localhost:8080/apps/new
. - If the user is already logged in, skip this step, otherwise, the user logs in.
- After login, generate a short-lived JWT in your backend with the user's ID and any other necessary information. For example:
import jwt
# First double-check that the redirect_uri is one that you know comes from the NWC image.
if not redirect_uri.startswith("http://localhost:8080"):
return "Invalid redirect_uri", 400
user_nwc_jwt = jwt.encode(
{
"sub": user_id,
# Your vasp domain is the audience and issuer of this token.
"aud": "localhost:5001",
"iss": "localhost:5001",
# Let this token only last 10 minutes.
"exp": datetime.timestamp(datetime.now() + timedelta(minutes=10)),
# The NWC image will use this to show the user's UMA in the UI.
"address": user.get_uma_address(),
},
# Store this in secrets!
jwt_private_key,
algorithm="ES256",
)
jwt_private_key
should be a valid ES256 private key that you can generate with the help of jwt.io or a similar tool.
You should store this key securely and not expose it in your code. You also need to configure the public key in the
NWC image configuration in the UMA_VASP_JWT_PUBKEY
variable to verify this JWT (see above).- Redirect back to the NWC image with the JWT in the URL as a
token
query param, and the user's JSON-encoded default currency as another param. For example:
https://localhost:8080/apps/new?token=eyJhb...1X2&currency={"code":"USD","symbol":"$","decimals":2,"name":"US Dollar"}
When the NWC image receives the short-lived JWT, it will show the user the OAuth/NWC permission page. When the user approves,
it will make a POST request to your VASP's token exchange endpoint that you configured above in the
UMA_VASP_TOKEN_EXCHANGE_URL
variable. This endpoint should verify the short-lived JWT, and if it's still valid, return a long-lived JWT that the NWC image
can use as the bearer token for requests to your VASPs UMA Auth API. For example:import jwt
def user_id_from_jwt(request):
auth_header = request.headers.get("Authorization")
if not auth_header:
abort_with_error(401, "Unauthorized")
jwt_token = auth_header.split("Bearer ")[-1]
jwt_public_key = current_app.config.get("NWC_JWT_PUBKEY")
if not jwt_public_key:
abort_with_error(500, "JWT public key not configured")
try:
decoded = jwt.decode(
jwt_token,
key=jwt_public_key,
algorithms=["ES256"],
issuer="localhost:5001",
audience="localhost:5001",
)
user_id = decoded.get("sub")
return user_id
except jwt.exceptions.InvalidTokenError as e:
print("Invalid token error", e)
abort_with_error(401, "Unauthorized")
@app.route("/token", methods=["POST"])
def handle_token_exchange():
user_id = user_id_from_jwt(request)
user = User.from_id(user_id)
if user is None:
return jsonify({"error": f"User {user_id} not found."}), 404
body = request.get_json()
requested_permissions = body.get("permissions")
if not requested_permissions:
return jsonify({"error": "Permissions are required."}), 400
requested_expiration = body.get("expiration")
jwt_private_key = current_app.config.get("NWC_JWT_PRIVKEY")
if not jwt_private_key:
return jsonify({"error": "JWT private key not set in config."}), 500
claims = {
"sub": str(user_id),
"aud": "localhost:5001",
"iss": "localhost:5001",
"address": user.get_uma_address(),
}
# TODO: Consider saving permissions or adding them to claims in some condensed form.
if requested_expiration:
claims["exp"] = requested_expiration
user_nwc_jwt = jwt.encode(
claims,
jwt_private_key,
algorithm="ES256",
)
return jsonify({"token": user_nwc_jwt})
Make sure that in your VASP server UMA Auth API implementation, you verify the long-lived JWT and use the user's ID
from the JWT to make requests on their behalf.
@app.route("/payments/lud16", methods=["POST"])
def make_payment():
user_id = user_id_from_jwt(request) # this might raise a 401 if the JWT is invalid
user = User.from_id(user_id)
if user is None:
return jsonify({"error": f"User {user_id} not found."}), 404
# ... make the payment
We need to update the
uma-configuration
discovery document to point to the NWC image where needed. See
UMAD-10
for a full spec on this file. You'll need to serve a JSON response at /.well-known/uma-configuration
on your
VASP server domain which contains at least the following fields:{
"name": "Cool VASP",
"uma_major_versions": [0, 1],
"authorization_endpoint": "http://localhost:8080/oauth/auth",
"token_endpoint": "http://localhost:8080/oauth/token",
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"connection_management_endpoint": "http://localhost:8080/connection/<connection_id>",
"revocation_endpoint": "http://localhost:8080/oauth/revoke"
}
In production, you'll want to replace
localhost:8080
with the actual domain and port where your NWC image is running.
These configuration settings help client applications know how to complete the OAuth flow to establish and manage connections.Now you're all set up! You can test your implementation by trying to connect to this demo client application.
Since your vasp is running locally on port 5001, you can use localhost:5001 as the VASP domain in the client app.
For example, you can try logging in as
$bob@localhost:5001
. Play with different methods in the UI to make sure they
work as expected.Now you're all done! You have a fully functioning UMA Auth implementation for your VASP. Just update
your configurations and secrets for production, and you're ready to go. Congratulations! 🎉
You can find deeper details on each of these steps in the following guides:
As well as some details about running a relay for your VASP.