Getting Started - User Management
Introduction
User management is only relevant if users are to be used over a longer period of time.
This also requires that users log in regularly and give new consent, as required by PSD2.
Alternatively, the Process Controller can be used, which completely handles user management and provides a very simple way to create a user. The associated data is then also automatically deleted after a certain period of time.
How to use the Process Controller is described in the following article: Obtain Authorization via Process Controller
Prerequisites
The major prerequisite is to have a valid set of client credentials: client_id
and client_secret
.
TL:TR
Used Systems
finAPI OpenBanking Access (finAPI API Documentation )
finAPI Data Intelligence (finAPI API Documentation )
See Environments page for detailed information.
Used Endpoints
In short, you need to call those 2 endpoints on the Process Controller to obtain a token.
Description | HTTP Method | System | Endpoint | Link to API Doc |
---|---|---|---|---|
Get Token (Login as client / user) | POST | finAPI OpenBanking Access |
| |
Create a user | POST | finAPI OpenBanking Access |
| |
Delete user in Data Intelligence | DELETE | finAPI Data Intelligence |
| |
Deletion status of user in Data Intelligence | GET | finAPI Data Intelligence |
| |
Delete user | DELETE | finAPI OpenBanking Access |
|
A short description for creating a user can be found in finAPI OpenBanking Access documentation.
Process Overview
It is very important that when deleting users, the data is deleted FIRST in Data Intelligence. Only if this was successful, the user may be deleted in finAPI OpenBanking Access!
cURL Example
Translated into cURL it looks like the following:
Step 1 - Login as a client (cURL)
Log in as a client requires
grant_type=client_credentials
and nousername
andpassword
fieldsThe request has to be done with the
Content-Type
headerapplication/x-www-form-urlencoded
.
It is not possible to work with the Data Intelligence Services with the client’s access_token
.
Login as client example request:
curl --location 'https://sandbox.finapi.io/api/v2/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=<your client id>' \
--data-urlencode 'client_secret=<your client secret>' \
--data-urlencode 'grant_type=client_credentials'
The result looks like this:
{
"access_token": "yvMbx_TgwdYE1hgOOb9N4ZOzxOukqfazYOGREcJiCjQuRGkVIBfjjV3YG4zKTGiY2aPn2cQTGaQOT8uo5uo5_QOXts1s2UBSVuRHc6a8X30RrGBTyqV9h26SUHcZPNbZ",
"token_type": "bearer",
"refresh_token": "0b9KjtBVlRLz7a4HhhSEJcFuscStiXT1VzT5mgNYwCQ_dWctTDsaIjedAhD1LpsOFJ7x4K1Emf8M4VOQkwNFR9FHijALYSQw2UeRwAC2MvrOKwfF1dHmOq5VEVYEaGf6",
"expires_in": 3600,
"scope": "all"
}
Step 2 - Create a user (cURL)
The user is required to talk to the services. It is possible to create “named” users, but we do want to create a simple one, without giving any details.
curl --location 'https://sandbox.finapi.io/api/v2/users' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <client's access_token of previous request>' \
--data-raw '{}'
The result looks like this:
{
"id": "965782ed-d57e-46b4-8b96-d5fc4ad8670e",
"password": "9532702e-abd7-49ab-afda-465ab3c1d93f",
"email": null,
"phone": null,
"isAutoUpdateEnabled": false
}
Now we can store the id
as a username and the password
to be able to log in later.
Step 3 - Login as a user (cURL)
For the user login, we need the client credentials, the
id
as username, and thepassword
of the previously created user.
curl --location 'https://sandbox.finapi.io/api/v2/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=<your client id>' \
--data-urlencode 'client_secret=<your client secret>' \
--data-urlencode 'username=<username>' \
--data-urlencode 'password=<user password>' \
--data-urlencode 'grant_type=password'
The result is similar to the login as a client.
Step 4 - Business-related calls to Data Intelligence (cURL)
Now we can use the
access_token
of the user login request to communicate with the Data Intelligence Services.You can learn more about our services in our Products and https://finapi.jira.com/wiki/spaces/DIPPD/pages/3395387407 sections.
Step 5 - Deleting the user’s data in Data Intelligence (cURL)
If the user is no longer required, the data should be deleted.
It is mandatory to delete the user data in Data Intelligence first and then the user in finAPI Open Access.
Request to Data Intelligence’s Delete user data endpoint
:
curl --location --request DELETE 'https://di-sandbox.finapi.io/api/v1/user' \
--header 'Authorization: Bearer <user's access_token of user login request in Step 3>'
The result looks like this:
{
"deletionId": "5e860145-2e65-4242-ac33-488943528c94"
}
Using the deletionId
the status endpoint can now be called to check that the deletion was successful.
Depending on the amount of data, this process may take a few seconds.
Therefore, polling should ideally be performed every 500ms in order to not unnecessarily load the finAPI systems, but also not to wait too long yourself.
The request for this looks like this:
curl --location 'https://di-sandbox.finapi.io/api/v1/user/status/delete/<deletionId from previous request>' \
--header 'Authorization: Bearer <user's access_token of user login request in Step 3>'
A successful result looks like this:
{
"statusReportService": "SUCCESSFUL",
"statusDataGateway": "SUCCESSFUL"
}
Step 6 - Deleting the user (cURL)
If everything else was successful, we can delete the user in finAPI OpenBanking Access.
The request looks like this:
curl --location 'https://sandbox.finapi.io/api/v2/users' \
--header 'Authorization: Bearer <user's access_token of user login request in Step 3>'
The response should be a 2xx
result.
Implementation Guide
See a full working project here: finAPI Data Intelligence Product Platform Examples (Bitbucket)
Code from this guide can be found here: finAPI User Management (Bitbucket)
Environment overview can be found here: Environments
The guidelines and the example project are written in Kotlin with Spring Boot. But it is easily adoptable for other languages and frameworks.
For the HTTP connection, we are using here plain Java HttpClient
, to be not restrictive for the client selection and that everything is transparent.
Only the actual functionality is discussed in this guideline. Used helper classes or the models can be looked up in the source code.
However, we always recommend using a generated client from our API, which reduces the effort of creating models and functions to access the endpoints and eliminates potential sources of error.
To learn how to generate and apply the API client using the Process Controller "Create user and exchange with access_token" as an example, please see the Getting Started - Code Generator (SDK) Integration.
Please also note that the code presented here and also the repository is only an illustration of a possible implementation. Therefore, the code is kept very simple and should not be used 1:1 in a production environment.
Step 1 - Login as a client
This step and the corresponding code is explained under the section Obtain Authorization via Open Banking Access.
Step 2 - Create a user
Since finAPI OpenBanking Access handles user management, the user must be created in this system.
A user can be created with different data. Alternatively, an empty body can be passed, which results in the values such as username or password being generated by the system.
For more information please read the finAPI API Documentation.
Here is a brief summary of the possible values:
Field | Type | Mandatory |
---|---|---|
| The If the field is not passed, a random value is generated as the username. Please note that a username for a mandator must always be unique. | no |
| The password of the user. If the field is not passed, a random value is generated as the username. | no |
| Optionally, the user's e-mail address can be passed here. | no |
| Optionally, the user's phone number can be passed here. | no |
| If the client has activated the finAPI OpenBanking Access feature "AutoUpdate", it can be decided here whether this also applies to the user or not. For more information about this feature, please see the API Documention. | no |
For this example, we decide to create an anonymous user and save the generated credentials.
Let's start with a Spring service, which gets the two URLs for finAPI OpenBanking Access and finAPI Data Intelligence from the configuration. In addition, we need an ObjectMapper
to map the JSON data into objects.
For finAPI Data Intelligence we have already stored the basepath /api/v1
in the URL in the configuration.
The first method creates the HttpRequest for the client. This requires the access_token
of the client from the user login from step 1.
This method is declared private
because it is only needed internally for the class. The division serves a better overview so that everybody can follow the code better.
@Service
class UserManagementService(
@Value("\${finapi.instances.openbanking.url}") private val openBankingUrl: String,
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val objectMapper: ObjectMapper
) {
/**
* Create a HttpRequest with configured URLs and required headers for user creation.
*/
private fun createUserRequest(clientAccessToken: String): HttpRequest {
return HttpRequest.newBuilder()
// build URL for https://<open_banking>/api/v2/users
.uri(URI.create("${openBankingUrl}${URI_OPENBANKING_USERS}"))
// set Content-Type header to application/json
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.AUTHORIZATION, "Bearer $clientAccessToken")
// add the body
.POST(
// use empty body to create random user
HttpRequest.BodyPublishers.ofString("{}")
).build()
}
companion object {
const val URI_OPENBANKING_USERS = "/api/v2/users"
const val URI_DI_USER = "/user"
const val URI_DI_USER_STATUS = "/user/status/delete/"
const val POLLING_WAIT_TIME_SECONDS: Long = 1
const val STATUS_GET_ATTEMPTS = 30
}
}
Now we send the request to finAPI OpenBanking Access. First, a client is created and then the request is sent.
For this, we use the created request from the previously created createUser()
method as the first parameter on the client.
After that, the HTTP status code is checked in the example. If this is not equal to 2xx
, the method StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage()
throws an exception and the process is aborted.
If the check was successful, the result is transferred to an object using the ObjectMapper and returned.
@Service
class UserManagementService(
@Value("\${finapi.instances.openbanking.url}") private val openBankingUrl: String,
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val objectMapper: ObjectMapper
) {
/**
* create a new random user.
*/
fun createUser(clientAccessToken: String): UserModel {
// create a client
val client = HttpClient.newBuilder().build()
// send the request and fetch the response
val response = client.send(
createUserRequest(clientAccessToken),
HttpResponse.BodyHandlers.ofString()
)
// check for status code is 2xx or log and throw an exception
StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
response = response,
errorMessage = "Unable to create a user."
)
// return the object of the mapped result
return objectMapper.readValue(response.body(), UserModel::class.java)
}
[...]
}
This service, together with the login, can now be used anywhere in the application to create a user.
Step 3 - Login as a user
This step and the corresponding code is explained under the section Obtain Authorization via Open Banking Access.
At this step we can now use all the services and have implemented all the essential requirements necessary to work with Data Intelligence.
Step 4 - Business-related calls to Data Intelligence
The business endpoints of the Data Intelligence Platform are explained in the appropriate sections under Products and https://finapi.jira.com/wiki/spaces/DIPPD/pages/3395387407.
Step 5 - Deleting the user’s data in Data Intelligence
Customer data is always worth protecting and should never be kept longer than necessary without good reason.
As soon as all processes that should be carried out with the user have been completed, the data should be deleted from finAPI.
When deleting finAPI user data, always make sure that data in Data Intelligence is deleted first. The background to this is that if the user were to be deleted first in finAPI OpenBanking Access, the token would be invalid and finAPI Data Intelligence would no longer be able to determine the user behind the token.
This would mean that it would no longer be possible to delete the data located there!
Creating the required requests
Now, to be able to delete the user, we extend the previously created UserManagementService
with this functionality.
finAPI Data Intelligence is a distributed system, therefore the deletion of data is done asynchronously.
Therefore, we need 2 steps for deletion:
Create the deletion task
Poll the status endpoint to check, if everything was successful
If the deletion is not successful, the entire deletion process should be aborted! This means that the user should NOT be deleted in finAPI OpenBanking Access.
First, we again create a function that generates the request. Since finAPI OpenBanking Access and finAPI Data Intelligence are both called via a DELETE
endpoint, we can pass the URL to this function to prevent code duplication. The actual target system is then defined in the calling function.
In addition, we need the access_token
of the user. If the deletion is not done during the user session, the user has to login again to get the access_token
.
The request refers to the endpoints:
finAPI Data Intelligence: finAPI API Documentation
finAPI OpenBanking Access: finAPI API Documentation
@Service
class UserManagementService(
@Value("\${finapi.instances.openbanking.url}") private val openBankingUrl: String,
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val objectMapper: ObjectMapper
) {
/**
* Create a HttpRequest with configured URLs and required headers for user creation.
* Because the request is equal for OpenBanking Access and Data Intelligence, except the URL,
* we expect the deleteUrl here.
*/
private fun createDeleteUserRequest(
accessToken: String,
deleteUrl: String
): HttpRequest {
return HttpRequest.newBuilder()
.uri(URI.create(deleteUrl))
// set Content-Type header to application/json
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
// add the body
.DELETE().build()
}
[...]
}
After we have created the general request for the delete job, we do also create the request for the status check, to have everything in place for finAPI Data Intelligence.
Here the request for the finAPI Data Intelligence endpoint /api/v1/user
is prepared (finAPI API Documentation ).
@Service
class UserManagementService(
@Value("\${finapi.instances.openbanking.url}") private val openBankingUrl: String,
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val objectMapper: ObjectMapper
) {
/**
* Create a HttpRequest with configured URLs and required headers for user creation.
* This request calls Data Intelligence.
*/
private fun createGetUserDeleteStatusRequest(
accessToken: String,
deletionId: String
): HttpRequest {
return HttpRequest.newBuilder()
// build URL for https://<data_intelligence>/api/v1/user/status/delete/{deletionId}
.uri(URI.create("${diUrl}${URI_DI_USER_STATUS}${deletionId}"))
// set Content-Type header to application/json
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
// add the body
.GET().build()
}
[...]
}
Now both requests are prepared.
Creating the status polling
Since we need to poll and check the status in finAPI Data Intelligence, we next create a simple validation of the result.
As a result, the system returns multiple statuses, so we create a generic validation that we can pass the respective system status.
If the result is of type FAILED
, we throw an exception to abort the whole code cleanly.
If it is still in IN_PROGRESS
, we return false
, and in case of success true
.
@Service
class UserManagementService(
@Value("\${finapi.instances.openbanking.url}") private val openBankingUrl: String,
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val objectMapper: ObjectMapper
) {
/**
* Validate the deletion status.
* If the deletion failed, the method throws an exception.
* If the deletion is in progress, it returns false.
* If the deletion was successful, it returns true.
*/
@Suppress("TooGenericExceptionThrown")
private fun validateDeletionStatus(status: EnumStatusModel): Boolean {
return when (status) {
EnumStatusModel.FAILED -> throw RuntimeException("Unable to delete user.")
EnumStatusModel.IN_PROGRESS -> false
EnumStatusModel.SUCCESSFUL -> true
}
}
[...]
}
Next, we take care of polling so that we can call it immediately after the delete call.
Pauses should be built into the polling. Otherwise this can lead to a high CPU load on the client and server side. A value between 200ms and 500ms is ideal here.
To the function, we pass again the access_token
, as well as the deletionId
, which we will get from the deletion call.
The first part of the loop checks whether another request is necessary. If we have exceeded the maximum number of requests, we abort with an exception. This stops the following code of this method and later also the code of the calling method.
Next, the request is sent to the status endpoint. For this, we use our previously created createGetUserDeleteStatusRequest()
function to set the request.
Then the HTTP status code is checked and in case of an error, it is also aborted with an exception.
After that, the result is mapped into an object and the statusDataGateway
and statusReportService
are checked with our previously created validation function.
If the operation fails, an exception is thrown in it and the operation is aborted.
If both validations were successful, only the loop is aborted and the calling function can continue.
If this was not the case, we wait a short time, increment the attempts
counter and the loop is restarted.
@Service
class UserManagementService(
@Value("\${finapi.instances.openbanking.url}") private val openBankingUrl: String,
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val objectMapper: ObjectMapper
) {
/**
* Poll the status endpoint to check, if the deletion was successful.
* If the polling exceeds a certain amount, we abort with an exception.
*/
@Throws(RuntimeException::class)
@Suppress("TooGenericExceptionThrown")
private fun pollDataIntelligenceUserDeletionStatus(
accessToken: String,
deletionId: String
) {
// get an access token with Process Controller
val client = HttpClient.newBuilder().build()
// poll until everything was deleted
var attempts = 0
while (true) {
// first we do check, if we have been exceeded with the number of attempts.
// if this is the case, we fail with an exception to stop the following code.
if (STATUS_GET_ATTEMPTS < attempts) {
throw RuntimeException("Attempts for successful deletion exceeded.")
}
// Send the request to the server
val responseCheck = client.send(
createGetUserDeleteStatusRequest(
accessToken = accessToken,
deletionId = deletionId
),
HttpResponse.BodyHandlers.ofString()
)
// check for status code is 2xx or log and throw an exception
StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
response = responseCheck,
errorMessage = "Unable to delete a user in Data Intelligence."
)
// Map the result and check both status fields.
// If it was successful, it stops the loop, else it waits for 1 second in the loop.
val result = objectMapper.readValue(responseCheck.body(), UserDiDeleteStatusResponse::class.java)
if (validateDeletionStatus(result.statusDataGateway) &&
validateDeletionStatus(result.statusReportService)
) {
break
}
// Wait for some time before we call the endpoint next time
TimeUnit.SECONDS.sleep(POLLING_WAIT_TIME_SECONDS)
// increase the counter of attempts
attempts++
}
}
[...]
}
This would mean that most of the class is already implemented.
Executing the deletion
Now we take care of the calling function, which starts the deletion of the user data in finAPI Data Intelligence and monitors it with the help of polling.
This function sends the request for the deletion to finAPI Data Intelligence in the first part. For this, we use our generic createDeleteUserRequest()
function and pass it the URL for the endpoint in finAPI Data Intelligence in addition to the access_token
.
After the HTTP status code has been checked again and everything should be correct, we map the result to get the deletionId
from the response.
With this, we now call the previously created function pollDataIntelligenceUserDeletionStatus()
.
If something would go wrong with the status check or the polling, this method would also throw the exceptions due to the exceptions of the checks.
If everything was ok, the method is terminated and the calling code can continue.
Although this is the actual delete function that should be called, we mark it as private
. This is for better control of the process, which we will discuss in step 6.
@Service
class UserManagementService(
@Value("\${finapi.instances.openbanking.url}") private val openBankingUrl: String,
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val objectMapper: ObjectMapper
) {
/**
* Deletes the user data in Data Intelligence.
* After the call was successful, it calls the polling function
* to monitor the state.
*/
@Throws(RuntimeException::class)
private fun deleteDataIntelligenceUser(accessToken: String) {
// get an access token with Process Controller
val client = HttpClient.newBuilder().build()
// -> First we have to delete the user in Data Intelligence
// send the request and fetch the response
val response = client.send(
createDeleteUserRequest(
accessToken = accessToken,
deleteUrl = "${diUrl}${URI_DI_USER}"
),
HttpResponse.BodyHandlers.ofString()
)
// check for status code is 2xx or log and throw an exception
StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
response = response,
errorMessage = "Unable to delete a user in Data Intelligence."
)
val deletionIdResponse = objectMapper.readValue(response.body(), UserDiDeleteResponse::class.java)
pollDataIntelligenceUserDeletionStatus(
accessToken = accessToken,
deletionId = deletionIdResponse.deletionId
)
}
[...]
}
Now we are able to delete the user data in finAPI Data Intelligence via a call to the function deleteDataIntelligenceUser()
.
Step 6 - Deleting the user
In the last step, we take care of deleting the user itself and any data stored in finAPI OpenBanking Access.
Due to the preparation in the previous step, we already have the function to generate the request ready.
We use it and give it the URL of finAPI OpenBanking Access. This will be extended by the request path /api/v2/users
.
@Service
class UserManagementService(
@Value("\${finapi.instances.openbanking.url}") private val openBankingUrl: String,
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val objectMapper: ObjectMapper
) {
/**
* Deletes the user data and the user in OpenBanking Access.
*/
@Throws(RuntimeException::class)
private fun deleteOpenBankingAccessUser(accessToken: String) {
// get an access token with Process Controller
val client = HttpClient.newBuilder().build()
// send the request and fetch the response
val response = client.send(
createDeleteUserRequest(
accessToken = accessToken,
deleteUrl = "${openBankingUrl}${URI_OPENBANKING_USERS}"
),
HttpResponse.BodyHandlers.ofString()
)
// check for status code is 2xx or log and throw an exception
StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
response = response,
errorMessage = "Unable to delete a user in OpenBanking Access."
)
}
[...]
}
With this, we can now also delete users in finAPI OpenBanking Access.
In order to be able to call the two delete functions, we create a control function that can be called from outside.
This ensures that the sequence is followed in any case. If the data intelligence function throws an exception, the OpenBanking access function will no longer be called.
In this way, we prevent the user from being deleted, even though their data has not yet been completely deleted.
@Service
class UserManagementService(
@Value("\${finapi.instances.openbanking.url}") private val openBankingUrl: String,
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val objectMapper: ObjectMapper
) {
/**
* Delete the authorized user.
* It is important to first delete users in Data Intelligence.
* Else it will be impossible to delete the data there.
* If something went wrong, it throws an exception.
*/
@Throws(RuntimeException::class)
fun deleteUser(accessToken: String) {
// -> First we have to delete the user in Data Intelligence
deleteDataIntelligenceUser(accessToken = accessToken)
// -> Then we can delete the user in OpenBanking Access
deleteOpenBankingAccessUser(accessToken = accessToken)
}
[...]
}