Skip to main content
Version: Next

Grant Management

Grant Management was introduced by FAPI 2.0. This standard aims to replace the various consent management APIs that have been developed in open banking markets, such as OPEN BANKING UK.

These capabilities include

  • Granting access and returning a grant_id.
  • Querying the details of a grant throught a GET.
  • Update of a grant.
  • Deletion of a grant.

The standard also incorporates the use of Rich Authorization Request (RAR). It enables clients to specify their detailed authorization requirements using the expressive nature of JSON data structures. For example:

{
"type":"account_information",
"actions":[
"list_accounts",
"read_balances",
"read_transactions"
],
"locations":[
"https://example.com/accounts"
]
}

For more information please refer to the official documentation.

The OPENBANKING UK Standard identifies two types of Third-Party Providers (TPPs):

  • Offer Account Information Services (AISPs) : gather read-only financial information..
  • Payment Initiation Services (PISPs) : can access and present financial information, but also move money from a user's bank account.

In this tutorial, we will explain how to create an AISP website that fetches all the bank accounts of the authenticated user from an Account REST.API Service.

  • The authorization policy implemented by the REST API is basic. It checks if the access token contains at least one authorization data type equal to list_accounts.
  • The Third-Party Website requires the user's consent to access the list_accounts Authorization Data as well as the openid and profile scopes. When the consent is accepted, the Website stores the grant_id returned by the Identity Server in an in-memory store.

The grant_id can be used by the website to manage the lifecycle of a grant, which includes the following actions:

  • Querying the grant.
  • Revoke the grant.

The TPP website will have the following configuration:

ConfigurationValue
Client Authentication Methodtls_client_auth
Authorization Signed Response AlgorithmES256
Identity Token Signed Response AlgorithmES256
Request Object Signed Response AlgorithmES256
Pushed Authorization RequestYes
Response Modejwt
Authorization Data Typesaccount_information
Scopesgrant_management_query grant_management_revoke openid profile
info

The source code of this project can be found here.

1. Configure client certificate

Utilize the administration UI to create a client certificate.

  1. Open the IdentityServer website at https://localhost:5002.
  2. In the Certificate Authorities screen, choose a Certificate Authority from the available options. Remember that the selected Certificate Authority should be trusted by your machine. You can download the certificate and import it into the appropriate Certificate Store.
  3. Click on the Client Certificates tab and then proceed to click on the Add Client Certificate button.
  4. Set the value of the Subject Name to CN=fapiGrant and click on the Add button.
  5. Click on the Download button located next to the certificate.

2. Configure an application

Utilize the administration UI to configure a new OpenID client :

  1. Open the IdentityServer website at https://localhost:5002.
  2. In the Clients screen, click on Add client button.
  3. Select FAPI2.0.
  4. Select Grant Management and click on next.
  5. Fill-in the form like this and click on the Save button to confirm the creation.
PropertyValue
IdentifierfapiGrant
Secretpassword
NamefapiGrant
Redirection URLshttp://localhost:7000/callback/*
Authorization Data Typesaccount_information
Proof of PossessionMutual-TLS Client Authentication
Subject NameCN=fapiGrant

3. Create Account Info REST.API

Create and configure an Account Info REST.API Service.

  1. Open a command prompt and execute the following commands to create the directory structure for the solution.
mkdir FapiGrantManagement
cd FapiGrantManagement
mkdir src
dotnet new sln -n FapiGrantManagement
  1. Create a web project named AccountInfoApi and install the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package.
cd src
dotnet new webapi -n AccountInfoApi
cd AccountInfoApi
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
  1. Add the AccountInfoApi project into your Visual Studio solution.
cd ..\..
dotnet sln add ./src/AccountInfoApi/AccountInfoApi.csproj
  1. In the file AccountInfoApi\Program.cs, modify the code to configure JWT authentication. Additionally, add an Authorization policy named account_information that verifies the correctness of the Authorization Details.
builder.Services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:5001/master";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidIssuers = new List<string>
{
"https://localhost:5001/master"
}
};
});
builder.Services.AddAuthorization(b =>
{
b.AddPolicy("account_information", p => p.RequireAssertion(c =>
{
var cl = c.User.Claims.First(c => c.Type == "authorization_details");
return JsonObject.Parse(cl.Value)["type"].GetValue<string>() == "account_information";
}));
});
  1. Add a new controller called AccountInfoController. This controller should be responsible for returning the list of bank accounts.
[ApiController]
[Route("[controller]")]
[Authorize("account_information")]
public class AccountInfoController : ControllerBase
{
[HttpGet(Name = "Accounts")]
public IEnumerable<string> Get()
{
return new List<string> { "BE91798829733676", "BE90321175762332", "BE56631811788388" };
}
}

Now that your REST API is configured, you can launch it on port 7001.

dotnet run --urls=http://localhost:7001

4. Create TPP Website

Finally, create and configure a TPP Website.

  1. Create a web project named Website.
cd src
dotnet new mvc -n Website
  1. Add the Website project into your Visual Studio solution.
cd ..
dotnet sln add ./src/Website/Website.csproj
  1. Add a BankInfo class into the Models directory.
namespace Website.Models;

public class BankInfo
{
public string Name { get; set; } = null!;
public string ClientId { get; set; } = null!;
public string ClientSecret { get; set; } = null!;
public string AuthorizationUrl { get; set; } = null!;
public string TokenUrl { get; set; } = null!;
public string GrantId { get; set; } = null!;
}
  1. Create a BankInfoStore class that will be utilized to store the information of banks.
using Website.Models;

namespace Website.Stores;

public interface IBankInfoStore
{
IQueryable<BankInfo> GetAll();
Task<int> SaveChanges(CancellationToken cancellationToken);
}

public class BankInfoStore : IBankInfoStore
{
private readonly ICollection<BankInfo> _bankInfos;

public BankInfoStore(ICollection<BankInfo> bankInfos)
{
_bankInfos = bankInfos;
}

public IQueryable<BankInfo> GetAll() => _bankInfos.AsQueryable();

public Task<int> SaveChanges(CancellationToken cancellationToken) => Task.FromResult(1);
}
  1. Create a AccessTokenStore class that will be used to store the access token.
namespace Website.Stores;

public class AccessTokenStore
{
private static AccessTokenStore _instance;
private Dictionary<string, string> _accessTokens = new Dictionary<string, string>();

private AccessTokenStore() { }

public string GetAccessToken(string bankName) => _accessTokens[bankName];

public void Add(string bankName, string token)
{
if(_accessTokens.ContainsKey(bankName)) _accessTokens.Remove(bankName);
_accessTokens.Add(bankName, token);
}

public static AccessTokenStore Instance()
{
if(_instance == null) _instance = new AccessTokenStore();
return _instance;
}
}
  1. In the Program.cs class, modify the code to register the dependencies and the certificate. Make sure to replace the certificate CN=fapiGrant.pfx with the one you downloaded earlier (step 1.5).
var certificate = new X509Certificate2(Path.Combine(Directory.GetCurrentDirectory(), "CN=fapiGrant.pfx"));
builder.Services.AddControllersWithViews();
builder.Services.Configure<WebsiteOptions>(o =>
{
o.MTLSCertificate = certificate;
});
builder.Services.AddSingleton<IBankInfoStore>(new BankInfoStore(new List<BankInfo>
{
new BankInfo { Name = "Bank", ClientId = "fapiGrant", AuthorizationUrl = "https://localhost:5001/master/authorization", ClientSecret = "password", TokenUrl = "https://localhost:5001/master/token" }
}));
  1. In the HomeController.cs class, include a new action named Callback. This action will be invoked by the Identity Server once the authorization is granted. Inside this action, retrieve the access_token and grant_id, and then utilize the AccessTokenStoreclass to store these values.
    [Route("callback/{bankName}")]
public async Task<IActionResult> Callback(string bankName)
{
var bankInfo = _bankInfoStore.GetAll().First(b => b.Name == bankName);
var accessToken = await GetAccessToken();
AccessTokenStore.Instance().Add(bankName, accessToken);
await _bankInfoStore.SaveChanges(CancellationToken.None);
return RedirectToAction("Index");

async Task<string> GetAccessToken()
{
var authorizationCode = Request.Query["code"].First();
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; },
CheckCertificateRevocationList = false,
ClientCertificateOptions = ClientCertificateOption.Manual,
SslProtocols = SslProtocols.Tls12
};
handler.ClientCertificates.Add(_options.MTLSCertificate);
using (var httpClient = new HttpClient(handler))
{
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(bankInfo.TokenUrl),
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "client_id", bankInfo.ClientId },
{ "grant_type", "authorization_code" },
{ "code", authorizationCode },
{ "redirect_uri", $"{_options.CallbackUrl}/{bankName}" }
})
};
var httpResult = await httpClient.SendAsync(requestMessage);
var json = await httpResult.Content.ReadAsStringAsync();
var jsonObj = JsonObject.Parse(json).AsObject();
if (jsonObj.ContainsKey("grant_id"))
{
var grantId = jsonObj["grant_id"].GetValue<string>();
bankInfo.GrantId = grantId;
}

return jsonObj["access_token"].GetValue<string>();
}
}
}
  1. Create a controller named BanksController and paste the following content into it. This controller enables the end-user to link one or multiple bank accounts.
namespace Website.Controllers;

public class BanksController : Controller
{
private readonly IBankInfoStore _bankInfoStore;
private readonly WebsiteOptions _options;

public BanksController(IBankInfoStore bankInfoStore, IOptions<WebsiteOptions> options)
{
_bankInfoStore = bankInfoStore;
_options = options.Value;
}

public IActionResult Index()
{
var bankInfos = _bankInfoStore.GetAll().Select(b => new BankInfoViewModel
{
Name = b.Name
});
return View(bankInfos);
}

public IActionResult Link(string name)
{
var bankInfo = _bankInfoStore.GetAll().First(b => b.Name == name);
const string authorizationDetails = "{ \"type\" : \"account_information\", \"actions\" : [\"read\"] }";
var url = $"{bankInfo.AuthorizationUrl}?client_id={bankInfo.ClientId}&redirect_uri={_options.CallbackUrl}/{name}&response_type=code&scope=openid profile&authorization_details={authorizationDetails}&grant_management_action=create";
return Redirect(url);
}
}
  1. Create a controller named AccountsController. This controller is responsible for displaying the information of the bank account.
namespace Website.Controllers;

public class AccountsController : Controller
{
private readonly IBankInfoStore _bankInfoStore;
private readonly WebsiteOptions _options;

public AccountsController(IBankInfoStore bankInfoStore, IOptions<WebsiteOptions> options)
{
_bankInfoStore = bankInfoStore;
_options = options.Value;
}

public IActionResult Index()
{
var result = _bankInfoStore.GetAll().Where(b => !string.IsNullOrWhiteSpace(b.GrantId)).Select(b => new GrantedBankInfoViewModel
{
GrantId = b.GrantId,
BankName = b.Name
});
return View(result);
}

public async Task<IActionResult> Details(string bankName)
{
var accessToken = AccessTokenStore.Instance().GetAccessToken(bankName);
using (var httpClient = new HttpClient())
{
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(_options.AccountInfoUrl)
};
requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}");
var httpResponse = await httpClient.SendAsync(requestMessage);
var json = await httpResponse.Content.ReadAsStringAsync();
var viewModel = new BankDetailsViewModel
{
Name = bankName,
Accounts = JsonArray.Parse(json).AsArray().Select(x => x.GetValue<string>())
};
return View(viewModel);
}
}
}
  1. Finally add the following views :

Views\Banks\Index.cshtml

@using Website.ViewModels;

@{
ViewData["Title"] = "Link a bank account";
}

@model IEnumerable<BankInfoViewModel>

<h1>Request account information</h1>

<div class="row">
@foreach(var bankInfo in Model)
{
<a href="@Url.Action("Link", "Banks", new { name = bankInfo.Name })">
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">@bankInfo.Name</h5>
</div>
</div>
</div>
</a>
}
</div>

Views\Accounts\Index.cshtml

@using Website.ViewModels;

@{
ViewData["Title"] = "Link a bank account";
}

@model IEnumerable<GrantedBankInfoViewModel>

<h1>You've access to the following bank accounts</h1>

<div class="row">
@foreach(var bankInfo in Model)
{
<a href="@Url.Action("Details", "Accounts", new { bankName = bankInfo.BankName })">
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">@bankInfo.BankName</h5>
<p class="text-muted">The grant_id is <b>@bankInfo.GrantId</b></p>
</div>
</div>
</div>
</a>
}
</div>

Views\Accounts\Details.cshtml

@using Website.ViewModels;

@{
ViewData["Title"] = "Bank accounts";
}

@model BankDetailsViewModel

<h1>@Model.Name</h1>

<h3>List of accounts</h3>

<ul class="list-group">
@foreach(var account in Model.Accounts)
{
<li class="list-group-item">
@account
</li>
}
</ul>

Now that your TPP website configured, you can launch it on port 7000.

dotnet run --urls=http://localhost:7000

Browse the website http://localhost:7000 and then navigate to http://localhost:7000/Banks.

Next, click on the Bank button, which will redirect you to the Identity Server. Authenticate using the following credentials and confirm the consent.

Upon successful authentication, proceed to navigate to http://localhost:7000/Accounts/Details?bankName=Bank, where you will find the list of bank accounts displayed.

CredentialValue
Loginadministrator
Passwordpassword