Manual Implementation of the UMA Auth Protocol for VASPs

In most cases, using the UMA Auth SDKs and Docker image will make it significantly easier to implement the UMA Auth protocol in your VASP. However, if you prefer to implement the protocol manually to get full control over the permissions, UI, and details of the auth flow, this guide will walk you through everything you need to do.
NOTE: This is NOT recommended for most VASPs. The UMA Auth SDKs and Docker image are designed to make the process as easy as possible.
You can see a full description of the protocol in the UMA Auth Protocol Spec. If you choose to implement the protocol manually, you will need to implement the following steps:
  1. OAuth 2.0 Exchange: Implement the OAuth 2.0 flow to establish connections between client applications and your users' UMA-enabled wallets.
  2. NWC Connection: Implement the Nostr Wallet Connect (NWC) protocol to handle the actual communication between the client app and the user's wallet. This includes all of the NWC methods like get_balance, pay_invoice, etc.
  3. Permissions and Budgets: Implement the logic to handle permissions and budgets for each connected app.
  4. UI: Implement the UI for managing connections, permissions, and budgets.
UMA Auth Protocol
The OAuth connection flow is a standard OAuth 2.0 flow with a few UMA-specific parameters and details. Client applications first need to discover the UMA VASP's OAuth endpoints and supported features. As a VASP, you must host this configuration document at /.well-known/uma-configuration on your UMA address domain. See UMAD-10 for more details. The configuration document should contain the following fields relevant to UMA Auth:
  • authorization_endpoint: The URL of the your OAuth authorization endpoint. This is where the client application should send the user to authenticate and authorize the client application to access their wallet.
  • token_endpoint: The URL of the VASP's token endpoint. This is where the client application exchanges an authorization code for an access token (a new NWC Connection), and where the client application can refresh an access token.
  • nwc_commands_supported: An array of strings representing the NWC commands that the VASP supports. This should be an array of strings, where each string is a valid NWC command name.
  • grant_types_supported: An array of strings representing the OAuth grant types that the VASP supports. For now, in most cases, this should just be ["authorization_code"].
  • code_challenge_methods_supported: An array of strings representing the PKCE code challenge methods that the VASP supports. For now, in most cases, this should just be ["S256"].
  • connection_management_endpoint: The URL of the VASP's connection management endpoint. This is where the user can create, update, and delete NWC Connections.
  • revocation_endpoint: The URL of the VASP's revocation endpoint. This is where the client application can revoke an access token (NWC Connection).
As with any OAuth 2.0 flow, the client application should start the auth flow by redirect the user to the VASP's authorization endpoint with the following query parameters:
An example URL for the auth request might look like this:
<authorization_endpoint>?client_id=npub37fd9…%3Awss%3A%2F%2Fmyrelay.info&redirect_uri=https%3A%2F%2Fexample.com&response_type=code&code_challenge=a43f6ed&code_challenge_method=S256&state=foobar&required_commands=pay_invoice%20make_invoice%20lookup_invoice&optional_commands=list_transactions&budget=10.USD%2Fmonthly&expires_at=1717964120
OAuth params above are listed first, followed by UMA-specific params.
Standard OAuth Params:
  • client_id in the format identity_npub identity_relay: This will be used to lookup the client app as described below. in the client app registration section.
  • redirect_uri: The redirect URI which will receive callback data from the wallet service on successful authentication. It will get back the auth code that can be exchanged for a token. If there was a kind-13195 nostr event as described above, the wallet will validate this redirect URI against declared allowed patterns.
  • response_type=code: Indicates that the “Authorization Code” flow will be used.
  • code_challenge and code_challenge_method: The PKCE exchange details.
  • state: Optional oauth state param for CSRF and state restoration.
See the OAuth 2.0 spec and PKCE spec for more details on these parameters.
Extra NWC params:
  • required_commands: A space-separated list of commands that the app requires from the wallet. The wallet MUST NOT connect if it does not support all of these permissions, or if the user does not grant one of them.
  • optional_commands: (optional) A space-separated list of commands that the wallet can enable to add additional functionality. The wallet MAY ignore these.
  • budget: (optional) Requested budget in the format <max_amount>.<currency>/<period>. If the .<currency> is omitted, satoshis are assumed. If /<period> is omitted, it’s a budget forever. For example, a budget string of “1000” would mean that this connection can only ever be used for a maximum of 1000 satoshis sent.
  • expires_at: (optional) connection cannot be used after this date. Unix timestamp in seconds.
Traditionally in OAuth, the client app would have to pre-register with the VASP to get a client_id and client_secret. However, in the UMA Auth protocol, the client app can use its identity npub and relay to allow the VASP to locate the client app registration nostr event, which contains verifiable details about the client app.
Client App Registration
When an application wants to use UMA Auth, it generates a single Nostr keypair that indentifies the appplication. This is called the "identity keypair". An application should have a single identity keypair that represents the application as opposed to one for each app instance or user. The identity keypair is used to sign and publish a nostr registration message (kind 13195) that contains the application's name, logo, allowed redirect URLs, etc. For example:
{
  "kind": 13195,
  // ... other fields
  "content": {
    "name": "Zappy Bird",
    "nip05": "_@zappybird.com",
    "image": "https://zappybird.com/logo.png",
    "allowed_redirect_uris": ["https://zappybird.com/auth/callback", "zappybird://auth/callback"],
  }
}
This event contains the content that would show up on a permissions page: app name, image, and a NIP-05-verified address. Critically, it also contains a list of allowed redirect URIs. This list is used to ensure that apps that claim to be Zappy Bird can only redirect to the URIs that Zappy Bird has claimed. This prevents phishing attacks where an attacker could register an app with the same name and logo as Zappy Bird and redirect users to a malicious site.
The client ID used for the OAuth flow is <identity npub> <relay>, where <identity npub> is the public key of the identity keypair (in bech32 "npub" format) and <relay> is the Nostr relay where the client app published the 13195 event. The client ID is used by the VASP to look up the client app's registration event, show the user the app's metadata, and limit the redirect URIs to those listed in the event. For example, if the client_id you receive is npub1fdg8yt8u6y3rdd9vquqqpr5a54h0nacjj32kf68ug4y63xhuvc8qgx7hj5 wss://myrelay.info, you can fetch the registration event from the relay wss://myrelay.info with a request like:
["REQ", <subscription_id>, {"kind": 13195, "authors": "4b50722cfcd12236b4ac0700008e9da56ef9f712945564e8fc4549a89afc660e"}]
Important Note: there are 2 things especially worth verifying in the registration event:
  1. The allowed redirect URIs in the registration event should match the redirect_uri in the auth request. As mentioned, this is to guard against phishing attacks. If the redirect URI is not in the list of allowed URIs, the VASP should reject the auth request.
  2. The NIP-05 address should be verified in accordance with the NIP-05 spec. This is to ensure that the app owns the domain it claims to own.
For more details on the Nostr protocol for client app registration, see NIP-68, which was designed specifically for this purpose.
If you're new to the nostr protocol, we recommend reading NIP-01 for a basic introduction to the protocol. You should also use a nostr library to make it easier to interact with the nostr protocol. Some popular libraries include:
You can also see how the UMA NWC server looks up the client app registration event in the UMA NWC server code.
Once you receive the auth request, you should show the user a permissions page with the app's name, logo, and a list of permissions that the app is requesting. The user can then approve or deny the request. If you have a mechanism for budget management per-connection, you should also show the user the requested budget, and allow them to change it. For example:
Permissions Page
Edit Permissions
Users should be able to approve or deny requested permissions, set a budget for the connection, and set an expiration date for the connection. The permissions page should also show the app's name, logo, and a verified link to the app's website.
The auth request's redirect follows the standard OAuth 2.0 flow. Using the redirect_uri param, the VASP will redirect to the client application with either:
?error=ACCESS_DENIED&error_description=Some%20short%20message (see here for full description of errors)
or
?code=g0ZGZmNjVmOWI&state=dkZmYxMzE2
If the user denies the request, the VASP should redirect with an error. If the user accepts, and there are no other errors, the VASP should redirect with the auth code and state.
The client application can then exchange the auth code for an access token and refresh token by sending a POST request to the VASP's token_endpoint as specified in the uma-configuration document. An example token request might look like this:
POST /oauth/token HTTP/1.1
Host: https://umanwc.examplevasp.com
 
grant_type=authorization_code
&code=xxxxxxxxxxx
&redirect_uri=https://example-app.com/redirect
&code_verifier=Th7UHJdLswIYQxwSg29DbK1a_d9o41uNMTRmuH0PM8zyoMAQ
&client_id=npub16f80k0f4vg0nnlepxrqxeh81slyzst2d wss://myrelay.info
The redirect_uri and client_id must match the values used in the auth request. The code_verifier is the PKCE code verifier matching the code_challenge used in the auth request. If any of these conditions are not met, the connection should be rejected. If successful, your VASP should respond as follows:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
 
{
  "access_token":"b9d11fe05e266fe7389fdf1359211e7859656a7898d64f3066092156de109b31",
  "token_type":"Bearer",
  "expires_in":86400,
  "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTDk",
  "nwc_connection_uri": 
"nostr+walletconnect://a421a5e2a615eff3b797be5318e4e187d24b100748cfaa8d0b390ce659906d8f?relay=wss://relay.getalby.com/v1&secret=b9d11fe05e266fe7389fdf1359211e7859656a7898d64f3066092156de109b31&lud16=$bob@examplevasp.com"
  "commands": ["pay_invoice", "fetch_quote", "execute_quote", "make_invoice", "pay_to_address"],
  "budget": "100.USD/month",
  "nwc_expires_at": 1721796505
}
The standard OAuth token response fields are included here along with some details about the established NWC connection. The client can use the nwc_connection_uri to make NWC requests to the wallet. Note that the access_token is the secret in the nwc_connection_uri. This implies that the nwc_connection_uri expires when the access_token expires (denoted by expires_in). This is usually fairly short (~2 hours), but can be configured by the VASP. Client apps should store the refresh_token securely and use it to get a new access_token when the old one expires. The nwc_connection_uri should also contain the lud16 field with the UMA address of the connected user, along with a relay field that points to the Nostr relay that the client app should use to communicate with the VASP. For more info on this connection string format, see the NWC spec. For info about relays, see the NWC and Nostr Relays guide.
Token refresh works exactly as in OAuth 2.0. The client app sends a POST request to the VASP's token_endpoint as follows:
POST /oauth/token HTTP/1.1
Host: https://nwc.uma.jeremykle.in
 
grant_type=refresh_token
&refresh_token=IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk
&client_id=npub16f80k0f4vg0nnlepxrqxeh81slyzst2d%20wss://myrelay.info
The VASP should respond with the same format as the initial access token request. The client app should store the new access_token and refresh_token and use the new nwc_connection_uri to make requests to the wallet. Note that the refresh_token should also be rotated on each refresh to prevent replay attacks.
As mentioned above, the nwc_connection_uri is used to make NWC requests to the wallet. In your NWC server implementation, you should handle these requests and respond to them as described in the NWC spec. Although the nostr protocol is a fairly simple websocket-based protocol, it is much easier to use a library to handle the protocol heavy lifting. See above for a list of libraries that can help you with this. Some even handle the request and response types for NWC commands for you. At a high level, what these libraries do is:
  1. Establish a websocket connection to the relay of your choice that you've added to the nwc_connection_uri.
  2. (Only needed once): Publish an info event (kind 13194) to the relay with your list of supported NWC commands.
  3. Send a REQ message to the relay to wait for incoming 23194 events (NWC requests).
  4. When you receive a 23194 event, parse the event, verify its signature, and decrypt the message payload
  5. Respond by publishing the appropriate 23195 event (NWC response) to the relay.
You can also see how incoming NWC requests are handled in the UMA NWC server code.
IMPORTANT NOTE: As noted in the protocol spec, any NWC connection established via the UMA Auth protocol should use NIP-44 encryption instead of NIP-04. Your NWC server should always attempt to use NIP-44 if the connection was established via OAuth. Otherwise (for manually created connections), you can fall-back to NIP-04.
Permissions in UMA Auth and NWC can be as granular as you like. Technically, they're just an agreement between you and your user on what the client app can do with the user's wallet. Generally though, most NWC implementations express permissions based on the NWC commands which the app is allowed to call. For example, a permission might be pay_invoice or get_balance. The user can approve or deny these permissions when they connect their wallet to the app. Then, as noted above in the Token Exchange section, the VASP will include the list of allowd commands in the response to the token request. That way, the client app knows what it can and can't do with the user's wallet.
Budgets are a way to limit the amount of money that can be sent from a wallet through a particular connection. Budgets are expressed as a maximum amount of money that can be sent over a period of time. For example, a budget of 100.USD/month means that the connection can only be used to send a maximum of 100 USD per month. Budgets are a way to limit the risk of a compromised connection being used to drain a user's wallet. The user can set a budget when they approve the connection, and the VASP should enforce that budget when processing NWC requests. Like permissions, budgets are included in the response to the token request. The get_budget command can be used by client apps to check the used and total budget of a connection.
This is a high-level overview of how to manually implement the UMA Auth protocol for VASPs. It's a complex process that requires a solid understanding of OAuth 2.0, the nostr protocol, and the UMA Auth protocol. We recommend using the UMA Auth SDKs and Docker image to make the process as easy as possible. However, if you need full control over the auth flow, this is a reasonable path to take. If you have any questions or need help, feel free to reach out for help on the UMA Discord.