NWC Container Auth Token Exchange
The
uma-nwc-server
Docker image does not have its own user database or authentication system. Instead, it relies on
your VASP's existing login flow to authenticate users and generate short-lived JWTs. These tokens are then exchanged
for longer-lived tokens that the NWC server uses to authenticate NWC requests for each connection.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 in the
UMA_VASP_LOGIN_URL
variable of the . 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.
nwc_container_domain = config.get("NWC_APP_ROOT_URL")
parsed_uri = urlparse(redirect_uri)
if not parsed_uri.scheme == "https" or not parsed_uri.netloc:
return "Invalid redirect_uri", 400
if not parsed_uri.netloc.endswith(nwc_container_domain):
return "Invalid redirect_uri", 400
user_nwc_jwt = jwt.encode(
{
# The user's ID in your system.
"sub": user_id,
# Your vasp domain is the audience and issuer of this token.
"aud": "examplevasp.com",
"iss": "examplevasp.com",
# Let this token only last 10 minutes. You can adjust this if needed.
"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,
# The algorithm must be ES256 for the NWC image to verify it.
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 the
UMA NWC Docker Image guide).- 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, if the user's default currency is USD, their currency info might be:
currency = {"code": "USD", "symbol": "$", "decimals": 2, "name": "US Dollar"}
And you'd redirect back to the NWC image like this:
# append the token and currency to the redirect_uri's query string:
query_params = parse_qs(parsed_url.query)
query_params["token"] = [user_nwc_jwt]
query_params["currency"] = [json.dumps(currency)]
new_query_string = urlencode(query_params, doseq=True)
new_url = urlunparse(parsed_url._replace(query=new_query_string))
return redirect(new_url)
This allows the NWC image to authenticate the user and show their UMA address and default currency in the UI.
The short-lived token can only be used as authentication for the
/info
request and to exchange for a long-lived token.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 VASP's UMA Auth API. For example:POST /umanwc/token HTTP/1.1
Content-Type: application/json
Header: Authorization Bearer <short-lived JWT>
{
"permissions": ["get_balance", "pay_invoice", "pay_to_address"],
"expiration": 1630000000
}
To handle this request, you can use the following Python code as an 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")
# TODO: You might also want to check for revoked tokens here if you have a revocation list.
try:
decoded = jwt.decode(
jwt_token,
key=jwt_public_key,
algorithms=["ES256"],
issuer="examplevasp.com",
audience="examplevasp.com",
)
user_id = decoded.get("sub")
return user_id
except jwt.exceptions.InvalidTokenError as 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": "examplevasp.com",
"iss": "examplevasp.com",
"address": user.get_uma_address(),
}
if requested_permissions:
claims["scopes"] = requested_permissions
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
The NWC image already checks all the permissions and budget constraints before making a request to your VASP's API,
so you can trust that the user has the necessary permissions to make the request once you've verified the JWT.
However, if you still want to check the permissions in your API, you can use the
scopes
claim in the JWT to see what
permissions the user has granted. See the details of that mapping in the "Permissions" section of the
VASP UMA Auth OpenAPI Schema guide.