Verify ownership of a wallet address
Open Payments supports verifying that the correct user has access to a specific wallet address through an interactive authentication flow. Rather than requesting a grant to make or receive payments (which requires an access_token with permissions like outgoing-payment or incoming-payment), a client can request a grant targeting a specific subject.
The authorization server interacts with the user through the identity provider (IdP) to confirm their identity. The IdP verifies that the user has access to the wallet address provided in the subject field.
Scenario
Section titled “Scenario”For this guide, you’ll assume the role of an app developer working for a payout platform. You want to ensure that each new user has access to the wallet address they provide when registering.
The guide explains how to verify that a new user has access to the wallet address https://cloudninebank.example.com/user using a subject-based grant.
Endpoints
Section titled “Endpoints”- GET Get Wallet Address
- POST Grant Request
- POST Grant Continuation Request
1. Get wallet address information
Section titled “1. Get wallet address information”First, call the GET Get Wallet Address API for the user’s provided wallet address.
const userWalletAddress = await client.walletAddress.get({ url: 'https://cloudninebank.example.com/user'})let user_wallet_address = client.wallet_address().get("https://cloudninebank.example.com/user").await?;$userWalletAddress = $client->walletAddress()->get([ 'url' => 'https://cloudninebank.example.com/user']);userWalletAddress, err := client.WalletAddress.Get(context.TODO(), op.WalletAddressGetParams{ URL: "https://cloudninebank.example.com/user",})if err != nil { log.Fatalf("Error fetching user wallet address: %v\n", err)}var userWalletAddress = client.walletAddress().get("https://cloudninebank.example.com/user");var userWalletAddress = await client.GetWalletAddressAsync("https://cloudninebank.example.com/user");Example response
{ "id": "https://cloudninebank.example.com/user", "assetCode": "CAD", "assetScale": 2, "authServer": "https://auth.cloudninebank.example.com/", "resourceServer": "https://cloudninebank.example.com/op"}2. Request an interactive outgoing payment grant
Section titled “2. Request an interactive outgoing payment grant”Use the authorization server information received in the previous step to call the POST Grant Request API.
Provide a subject with the wallet address in the list of subject IDs.
Example response
{ "interact": { "redirect": "https://auth.cloudninebank.example.com/{...}", // uri to redirect the user to, to begin interaction "finish": "..." // unique key to secure the callback }, "continue": { "access_token": { "value": "..." // access token for continuing the outgoing payment grant request }, "uri": "https://auth.cloudninebank.example.com/continue/{...}", // uri for continuing the outgoing payment grant request "wait": 30 }}Here is how to specify the subject structure in your grant request:
const grant = await client.grant.request( { url: userWalletAddress.authServer }, { subject: { sub_ids: [ { id: userWalletAddress.id, format: 'uri' } ] }, client: userWalletAddress.id, interact: { start: ['redirect'], finish: { method: 'redirect', uri: 'https://paymentplatform.example/finish/{...}', // where to redirect the user to after they've completed the interaction nonce: NONCE } } })use open_payments::types::{InteractRequest, InteractStart, InteractFinish, InteractFinishMethod, GrantRequest, Subject, SubjectId};
let subject = Subject { sub_ids: vec![SubjectId { id: user_wallet_address.id.clone(), format: "uri".into(), }],};
let interact = InteractRequest { start: Some(vec![InteractStart::Redirect]), finish: Some(InteractFinish { method: InteractFinishMethod::Redirect, uri: Some("https://paymentplatform.example/finish/{...}".into()), nonce: Some("NONCE".into()), }),};
let grant_request = GrantRequest::new_with_subject(subject, Some(interact));
let pending_grant = client .grant() .request(&user_wallet_address.auth_server, &grant_request) .await?;$grant = $client->grant()->request( [ 'url' => $userWalletAddress->authServer, ], [ 'subject' => [ 'sub_ids' => [ [ 'id' => $userWalletAddress->id, 'format' => 'uri', ], ], ], 'client' => $userWalletAddress->id, 'interact' => [ 'start' => ['redirect'], 'finish' => [ 'method' => 'redirect', 'uri' => 'https://paymentplatform.example/finish/{...}', // where to redirect the user to after they've completed the interaction 'nonce' => NONCE, ], ], ],);subject := as.Subject{ SubIds: []as.SubjectId{ { Id: *userWalletAddress.Id, Format: "uri", }, },}
interact := &as.InteractRequest{ Start: []as.InteractRequestStart{as.InteractRequestStartRedirect}, Finish: &as.InteractRequestFinish{ Method: as.Redirect, Uri: "https://paymentplatform.example/finish/{...}", Nonce: NONCE, },}
pendingGrant, err := client.Grant.Request(context.TODO(), op.GrantRequestParams{ URL: *userWalletAddress.AuthServer, RequestBody: as.GrantRequestWithSubject{ Subject: subject, Interact: interact, },})if err != nil { log.Fatalf("Error requesting grant: %v\n", err)}var subject = Subject.build(userWalletAddress.getId(), "uri");
var urlToOpen = "https://paymentplatform.example/finish/{...}";
var grantRequest = client.auth().grant().request( userWalletAddress, subject, URI.create(urlToOpen), "NONCE");var grant = await client.RequestGrantAsync( new RequestArgs { Url = userWalletAddress.AuthServer, }, new GrantCreateBodyWithInteract { Subject = new Subject { SubIds = [ new SubjectId { Id = userWalletAddress.Id, Format = "uri" } ] }, Client = userWalletAddress.Id, Interact = new InteractRequest { Start = [Start.Redirect], Finish = new Finish { Method = FinishMethod.Redirect, Uri = new Uri( "https://paymentplatform.example/finish/{...}"), // where to redirect your user after they've completed the interaction Nonce = NONCE } } });3. Start interaction with your user
Section titled “3. Start interaction with your user”Once the client receives the authorization server’s response, it must send the user to the interact.redirect URI contained in the response. This starts the interaction flow.
The response also includes a continue object, which is essential for managing the interaction and obtaining explicit user consent for outgoing payment grants. The continue object contains an access token and a URI that the client will use to finalize the grant request after the user has completed their interaction with the identity provider (IdP). This ensures that the client can securely obtain the necessary permissions to proceed with the payment process.
4. Finish interaction with your user
Section titled “4. Finish interaction with your user”The user interacts with the authorization server through the server’s interface and approves or denies the grant.
Provided the user approves the grant, the authorization server:
- Sends the user to the
finish.uriprovided in the interactive outgoing payment grant request. The means by which the server sends the user to the URI is out of scope, but common options include redirecting the user from a web page and launching the system browser with the target URI. - Secures the redirect by adding a unique hash, allowing your client to validate the
finishcall, and an interaction reference as query parameters to the URI.
5. Request a grant continuation
Section titled “5. Request a grant continuation”Once the user completes the interaction and is redirected back to your app, call the POST Grant Continuation Request API to finalize the verification.
Issue the request to the continue.uri provided in the initial outgoing payment grant response (Step 2).
Include the interact_ref returned in the redirect URI’s query parameters.
const userVerificationGrant = await client.grant.continue( { accessToken: pendingGrant.continue.access_token.value, url: pendingGrant.continue.uri }, { interact_ref: interactRef })let continue_field = match &pending_grant.continue_field { Some(c) => c, None => { eprintln!("Missing continue field on pending grant"); return Ok(()); }};
let user_verification_grant = client .grant() .continue_grant( &continue_field.uri, &interact_ref, Some(&continue_field.access_token.value), ) .await?;$userVerificationGrant = $client->grant()->continue( [ 'accessToken' => $pendingGrant->continue->access_token->value, 'url' => $pendingGrant->continue->uri, ], [ 'interact_ref' => $interactRef, ],);userVerificationGrant, err := client.Grant.Continue(context.TODO(), op.GrantContinueParams{ URL: pendingGrant.Continue.Uri, AccessToken: pendingGrant.Continue.AccessToken.Value, InteractRef: INTERACT_REF,})if err != nil { log.Fatalf("Error continuing grant: %v\n", err)}var userVerificationGrant = client.auth().grant().finalize( grantRequest, interactRef);var userVerificationGrant = await client.ContinueGrantAsync( new AuthRequestArgs { Url = grant.Continue.Uri, AccessToken = grant.Continue.AccessToken.Value }, new GrantContinueBody() { InteractRef = interactRef });Example response
{ "access_token": { "value": "...", // final access token "manage": "https://auth.cloudninebank.example.com/token/{...}" // management uri for the access token }, "continue": { "access_token": { "value": "..." }, "uri": "https://auth.cloudninebank.example.com/continue/{...}" }}If the continuation request succeeds and returns the access token details, the identity provider (IdP) has successfully verified that the user owns the specified wallet address.