Present and Verify Verifiable Credentials in ASP.NET Core using Decentralized Identities and MATTR

This article shows how use verifiable credentials stored on a digital wallet to verify a digital identity and use in an application. For this to work, a trust needs to exist between the verifiable credential issuer and the application which requires the verifiable credentials to verify. A blockchain decentralized database is used and MATTR is used as a access layer to this ledger and blockchain. The applications are implemented in ASP.NET Core.

The verifier application Bo Insurance is used to implement the verification process and to create a presentation template. The application sends a HTTP post request to create a presentation request using the DID Id from the OIDC credential Issuer, created in the previous article. This DID is created from the National Driving license application which issues verifiable credentials and so a trust needs to exist between the two applications. Once the credentials have been issued to a holder of the verifiable credentials and stored for example in a digital wallet, the issuer is no longer involved in the process. Verifying the credentials only requires the holder and the verifier and the decentralized database which holds the digital identities and documents. The verifier application gets the DID from the ledger and signs the verify request. The request can then be presented as a QR Code. The holder can scan this using a MATTR digital wallet and grant consent to share the credentials with the application. The digital wallet calls the callback API defined in the request presentation body and sends the data to the API. The verifier application hosting the API would need to verify the data and can update the application UI using SignalR to continue the business process with the verified credentials.

Code https://github.com/swiss-ssi-group/MattrGlobalAspNetCore

Blogs in the series

Create the presentation template for the Verifiable Credential

A presentation template is required to verify the issued verifiable credentials stored on a digital wallet.

The digital identity (DID) Id of the OIDC credential issuer is all that is required to create a presentation request template. In the application which issues credentials, ie the NationalDrivingLicense, a Razor page was created to view the DID of the OIDC credential issuer.

The DID can be used to create the presentation template. The MATTR documentation is really good here:

https://learn.mattr.global/tutorials/verify/presentation-request-template

A Razor page was created to start this task from the UI. This would normally require authentication as this is an administrator task from the application requesting the verified credentials. The code behind the Razor page takes the DID request parameter and calls the MattrPresentationTemplateService to create the presentation template and present this id a database.

public class CreatePresentationTemplateModel : PageModel
{
	private readonly MattrPresentationTemplateService _mattrVerifyService;
	public bool CreatingPresentationTemplate { get; set; } = true;
	public string TemplateId { get; set; }

	[BindProperty]
	public PresentationTemplate PresentationTemplate { get; set; }
	public CreatePresentationTemplateModel(MattrPresentationTemplateService mattrVerifyService)
	{
		_mattrVerifyService = mattrVerifyService;
	}
	public void OnGet()
	{
		PresentationTemplate = new PresentationTemplate();
	}

	public async Task<IActionResult> OnPostAsync()
	{
		if (!ModelState.IsValid)
		{
			return Page();
		}

		TemplateId = await _mattrVerifyService.CreatePresentationTemplateId(PresentationTemplate.DidId);
		CreatingPresentationTemplate = false;
		return Page();
	}
}

public class PresentationTemplate
{
	[Required]
	public string DidId { get; set; }
}

The Razor page html template creates a form to post the request to the server rendered page and displays the templateId after, if the creation was successful.

@page
@model BoInsurance.Pages.CreatePresentationTemplateModel

<div class="container-fluid">
    <div class="row">
        <div class="col-sm">
            <form method="post">
                <div>
                    <div class="form-group">
                        <label class="control-label">DID ID</label>
                        <input asp-for="PresentationTemplate.DidId" class="form-control" />
                        <span asp-validation-for="PresentationTemplate.DidId" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        @if (Model.CreatingPresentationTemplate)
                        {
                            <input class="form-control"
                                   type="submit" readonly="@Model.CreatingPresentationTemplate"
                                   value="Create Presentation Template" />
                        }

                    </div>
                    <div class="form-group">
                        @if (!Model.CreatingPresentationTemplate)
                        {
                            <div class="alert alert-success">
                                <strong>Mattr Presentation Template created</strong>
                            </div>
                        }

                        
                    </div>
                </div>
            </form>
            <hr />
            <p>When the templateId is created, you can use the template ID to verify</p>
        </div>
        <div class="col-sm">
            <div>
                <img src="~/ndl_car_01.png" width="200" alt="Driver License">
                <div>
                    <b>Driver Licence templateId from presentation template</b>
                    <hr />
                    <dl class="row">
                        <dt class="col-sm-4">templateId</dt>
                        <dd class="col-sm-8">
                            @Model.TemplateId
                        </dd>
                    </dl>
                </div>
            </div>
        </div>
    </div>
</div>


The MattrPresentationTemplateService is used to create the MATTR presentation template. This class uses the MATTR API and sends a HTTP post request with the DID Id of the OIDC credential issuer and creates a presentation template. The service saves the returned payload to a database and returns the template ID as the result. The template ID is required to verify the verifiable credentials.

The MattrTokenApiService is used to request an API token for the MATTR API using the credential of your MATTR account. This service has a simple token cache and only requests new access tokens when no token exists or the token has expired.

The BoInsuranceDbService service is used to access the SQL database using Entity Framework Core. This provides simple methods to persist or select the data as required.

private readonly IHttpClientFactory _clientFactory;
private readonly MattrTokenApiService _mattrTokenApiService;
private readonly BoInsuranceDbService _boInsuranceDbService;
private readonly MattrConfiguration _mattrConfiguration;

public MattrPresentationTemplateService(IHttpClientFactory clientFactory,
	IOptions<MattrConfiguration> mattrConfiguration,
	MattrTokenApiService mattrTokenApiService,
	BoInsuranceDbService boInsuranceDbService)
{
	_clientFactory = clientFactory;
	_mattrTokenApiService = mattrTokenApiService;
	_boInsuranceDbService = boInsuranceDbService;
	_mattrConfiguration = mattrConfiguration.Value;
}

public async Task<string> CreatePresentationTemplateId(string didId)
{
	// create a new one
	var v1PresentationTemplateResponse = await CreateMattrPresentationTemplate(didId);

	// save to db
	var drivingLicensePresentationTemplate = new DrivingLicensePresentationTemplate
	{
		DidId = didId,
		TemplateId = v1PresentationTemplateResponse.Id,
		MattrPresentationTemplateReponse = JsonConvert
			.SerializeObject(v1PresentationTemplateResponse)
	};
	await _boInsuranceDbService
		.CreateDriverLicensePresentationTemplate(drivingLicensePresentationTemplate);

	return v1PresentationTemplateResponse.Id;
}

private async Task<V1_PresentationTemplateResponse> CreateMattrPresentationTemplate(string didId)
{
	HttpClient client = _clientFactory.CreateClient();
	var accessToken = await _mattrTokenApiService.GetApiToken(client, "mattrAccessToken");

	client.DefaultRequestHeaders.Authorization =
		new AuthenticationHeaderValue("Bearer", accessToken);
	client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

	var v1PresentationTemplateResponse = await CreateMattrPresentationTemplate(client, didId);
	return v1PresentationTemplateResponse;
}

The CreateMattrPresentationTemplate method sends the HTTP Post request like in the MATTR API documentation. Creating the payload for the HTTP post request using the MATTR Open API definitions is a small bit complicated. This could be improved with a better Open API definition. In our use case, we just want to create the default template for the OIDC credential issuer and so just require the DID Id. Most of the other properties are fixed values, see the MATTR API docs for more information.

private async Task<V1_PresentationTemplateResponse> CreateMattrPresentationTemplate(
	HttpClient client, string didId)
{
	// create presentation, post to presentations templates api
	// https://learn.mattr.global/tutorials/verify/presentation-request-template

	var createPresentationsTemplatesUrl = $"https://{_mattrConfiguration.TenantSubdomain}/v1/presentations/templates";

	var additionalProperties = new Dictionary<string, object>();
	additionalProperties.Add("type", "QueryByExample");
	additionalProperties.Add("credentialQuery", new List<CredentialQuery> {
		new CredentialQuery
		{
			Reason = "Please provide your driving license",
			Required = true,
			Example = new Example
			{
				Context = new List<object>{ "https://schema.org" },
				Type = "VerifiableCredential",
				TrustedIssuer = new List<TrustedIssuer2>
				{
					new TrustedIssuer2
					{
						Required = true,
						Issuer = didId // DID use to create the oidc
					}
				}
			}
		}
	});

	var payload = new MattrOpenApiClient.V1_CreatePresentationTemplate
	{
		Domain = _mattrConfiguration.TenantSubdomain,
		Name = "certificate-presentation",
		Query = new List<Query>
		{
			new Query
			{
				AdditionalProperties = additionalProperties
			}
		}
	};

	var payloadJson = JsonConvert.SerializeObject(payload);

	var uri = new Uri(createPresentationsTemplatesUrl);

	using (var content = new StringContentWithoutCharset(payloadJson, "application/json"))
	{
		var presentationTemplateResponse = await client.PostAsync(uri, content);

		if (presentationTemplateResponse.StatusCode == System.Net.HttpStatusCode.Created)
		{

			var v1PresentationTemplateResponse = JsonConvert
					.DeserializeObject<MattrOpenApiClient.V1_PresentationTemplateResponse>(
					await presentationTemplateResponse.Content.ReadAsStringAsync());

			return v1PresentationTemplateResponse;
		}

		var error = await presentationTemplateResponse.Content.ReadAsStringAsync();

	}

	throw new Exception("whoops something went wrong");
}

The application can be started and the presentation template can be created. The ID is returned back to the UI for the next step.

Verify the verifiable credentials

Now that a template exists to request the verifiable data from the holder of the data which is normally stored in a digital wallet, the verifier application can create and start a verification process. A post request is sent to the MATTR APIs which creates a presentation request using a DID ID and the required template. The application can request the DID from the OIDC credential issuer. The request is signed using the correct key from the DID and the request is published in the UI as a QR Code. A digital wallet is used to scan the code and the user of the wallet can grant consent to share the personal data. The wallet sends a HTTP post request to the callback API. This API handles the request, would validate the data and updates the UI using SignalR to move to the next step of the business process using the verified data.

Step 1 Invoke a presentation request

The InvokePresentationRequest method implements the presentation request. This method requires the DID Id of the OIDC credential issuer which will by used to get the data from the holder of the data. The template ID is also required from the template created above. A challenge is also used to track the verification. The challenge is a random value and is used when the digital wallet calls the API with the verified data. The callback URL is where the data is returned to. This could be unique for every request or anything you want. The payload is created like the docs from the MATTR API defines. The post request is sent to the MATTR API and a V1_CreatePresentationRequestResponse is returned if all is configured correctly.

private async Task<V1_CreatePresentationRequestResponse> InvokePresentationRequest(
	HttpClient client,
	string didId,
	string templateId,
	string challenge,
	string callbackUrl)
{
	var createDidUrl = $"https://{_mattrConfiguration.TenantSubdomain}/v1/presentations/requests";

	var payload = new MattrOpenApiClient.V1_CreatePresentationRequestRequest
	{
		Did = didId,
		TemplateId = templateId,
		Challenge = challenge,
		CallbackUrl = new Uri(callbackUrl),
		ExpiresTime = MATTR_EPOCH_EXPIRES_TIME_VERIFIY // Epoch time
	};
	var payloadJson = JsonConvert.SerializeObject(payload);
	var uri = new Uri(createDidUrl);

	using (var content = new StringContentWithoutCharset(payloadJson, "application/json"))
	{
		var response = await client.PostAsync(uri, content);

		if (response.StatusCode == System.Net.HttpStatusCode.Created)
		{
			var v1CreatePresentationRequestResponse = JsonConvert
				.DeserializeObject<V1_CreatePresentationRequestResponse>(
					await response.Content.ReadAsStringAsync());

			return v1CreatePresentationRequestResponse;
		}

		var error = await response.Content.ReadAsStringAsync();
	}

	return null;
}

Step 2 Get the OIDC Issuer DID

The RequestDID method uses the MATTR API to get the DID data from the blochchain for the OIDC credential issuer. Only the DID Id is required.

private async Task<V1_GetDidResponse> RequestDID(string didId, HttpClient client)
{
	var requestUrl = $"https://{_mattrConfiguration.TenantSubdomain}/core/v1/dids/{didId}";
	var uri = new Uri(requestUrl);

	var didResponse = await client.GetAsync(uri);

	if (didResponse.StatusCode == System.Net.HttpStatusCode.OK)
	{
		var v1CreateDidResponse = JsonConvert.DeserializeObject<V1_GetDidResponse>(
				await didResponse.Content.ReadAsStringAsync());

		return v1CreateDidResponse;
	}

	var error = await didResponse.Content.ReadAsStringAsync();
	return null;
}

Step 3 Sign the request using correct key and display QR Code

To verify data using a digital wallet, the payload must be signed using the correct key. The SignAndEncodePresentationRequestBody uses the DID payload and the request from the presentation request to create the payload to sign. Creating the payload is a big messy due to the OpenAPI definitions created for the MATTR API. A HTTP post request with the payload returns the signed JWT in a payload in a strange data format so we parse this as a string and manually get the JWT payload.

private async Task<string> SignAndEncodePresentationRequestBody(
	HttpClient client,
	V1_GetDidResponse did,
	V1_CreatePresentationRequestResponse v1CreatePresentationRequestResponse)
{
	var createDidUrl = $"https://{_mattrConfiguration.TenantSubdomain}/v1/messaging/sign";

	object didUrlArray;
	did.DidDocument.AdditionalProperties.TryGetValue("authentication", out didUrlArray);
	var didUrl = didUrlArray.ToString().Split("\"")[1];
	var payload = new MattrOpenApiClient.SignMessageRequest
	{
		DidUrl = didUrl,
		Payload = v1CreatePresentationRequestResponse.Request
	};
	var payloadJson = JsonConvert.SerializeObject(payload);
	var uri = new Uri(createDidUrl);

	using (var content = new StringContentWithoutCharset(payloadJson, "application/json"))
	{
		var response = await client.PostAsync(uri, content);

		if (response.StatusCode == System.Net.HttpStatusCode.OK)
		{
			var result = await response.Content.ReadAsStringAsync();
			return result;
		}

		var error = await response.Content.ReadAsStringAsync();
	}

	return null;
}

The CreateVerifyCallback method uses the presentation request, the get DID and the sign HTTP post requests to create a URL which can be displayed in a UI. The challenge is created using the RNGCryptoServiceProvider class which creates a random string. The access token to access the API is returned from the client credentials OAuth requests or from the in memory cache. The DrivingLicensePresentationVerify class is persisted to a database and the verify URL is returned so that this could be displayed as a QR Code in the UI.

/// <summary>
/// https://learn.mattr.global/tutorials/verify/using-callback/callback-e-to-e
/// </summary>
/// <param name="callbackBaseUrl"></param>
/// <returns></returns>
public async Task<(string QrCodeUrl, string ChallengeId)> CreateVerifyCallback(string callbackBaseUrl)
{
	callbackBaseUrl = callbackBaseUrl.Trim();
	if (!callbackBaseUrl.EndsWith('/'))
	{
		callbackBaseUrl = $"{callbackBaseUrl}/";
	}

	var callbackUrlFull = $"{callbackBaseUrl}{MATTR_CALLBACK_VERIFY_PATH}";
	var challenge = GetEncodedRandomString();

	HttpClient client = _clientFactory.CreateClient();
	var accessToken = await _mattrTokenApiService.GetApiToken(client, "mattrAccessToken");

	client.DefaultRequestHeaders.Authorization =
		new AuthenticationHeaderValue("Bearer", accessToken);
	client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

	var template = await _boInsuranceDbService.GetLastDriverLicensePrsentationTemplate();

	// Invoke the Presentation Request
	var invokePresentationResponse = await InvokePresentationRequest(
		client,
		template.DidId,
		template.TemplateId,
		challenge,
		callbackUrlFull);

	// Request DID 
	V1_GetDidResponse did = await RequestDID(template.DidId, client);

	// Sign and Encode the Presentation Request body
	var signAndEncodePresentationRequestBodyResponse = await SignAndEncodePresentationRequestBody(
		client, did, invokePresentationResponse);

	// fix strange DTO
	var jws = signAndEncodePresentationRequestBodyResponse.Replace("\"", "");

	// save to db // TODO add this back once working
	var drivingLicensePresentationVerify = new DrivingLicensePresentationVerify
	{
		DidId = template.DidId,
		TemplateId = template.TemplateId,
		CallbackUrl = callbackUrlFull,
		Challenge = challenge,
		InvokePresentationResponse = JsonConvert.SerializeObject(invokePresentationResponse),
		Did = JsonConvert.SerializeObject(did),
		SignAndEncodePresentationRequestBody = jws
	};
	await _boInsuranceDbService.CreateDrivingLicensePresentationVerify(drivingLicensePresentationVerify);

	var qrCodeUrl = $"didcomm://https://{_mattrConfiguration.TenantSubdomain}/?request={jws}";

	return (qrCodeUrl, challenge);
}

private string GetEncodedRandomString()
{
	var base64 = Convert.ToBase64String(GenerateRandomBytes(30));
	return HtmlEncoder.Default.Encode(base64);
}

private byte[] GenerateRandomBytes(int length)
{
	using var randonNumberGen = new RNGCryptoServiceProvider();
	var byteArray = new byte[length];
	randonNumberGen.GetBytes(byteArray);
	return byteArray;
}

The CreateVerifierDisplayQrCodeModel is the code behind for the Razor page to request a verification and also display the verify QR Code for the digital wallet to scan. The CallbackUrl can be set from the UI so that this is easier for testing. This callback can be any webhook you want or API. To test the application in local development, I used ngrok. The return URL has to match the proxy which tunnels to you PC, once you start. If the API has no public address when debugging, you will not be able to test locally.

public class CreateVerifierDisplayQrCodeModel : PageModel
{
	private readonly MattrCredentialVerifyCallbackService _mattrCredentialVerifyCallbackService;
	public bool CreatingVerifier { get; set; } = true;
	public string QrCodeUrl { get; set; }

	[BindProperty]
	public string ChallengeId { get; set; }

	[BindProperty]
	public CreateVerifierDisplayQrCodeCallbackUrl CallbackUrlDto { get; set; }
	public CreateVerifierDisplayQrCodeModel(MattrCredentialVerifyCallbackService 
		mattrCredentialVerifyCallbackService)
	{
		_mattrCredentialVerifyCallbackService = mattrCredentialVerifyCallbackService;
	}
	public void OnGet()
	{
		CallbackUrlDto = new CreateVerifierDisplayQrCodeCallbackUrl();
		CallbackUrlDto.CallbackUrl = $"https://{HttpContext.Request.Host.Value}";
	}

	public async Task<IActionResult> OnPostAsync()
	{
		if (!ModelState.IsValid)
		{
			return Page();
		}

		var result = await _mattrCredentialVerifyCallbackService
			.CreateVerifyCallback(CallbackUrlDto.CallbackUrl);

		CreatingVerifier = false;

		QrCodeUrl = result.QrCodeUrl;
		ChallengeId = result.ChallengeId;
		return Page();
	}
}

public class CreateVerifierDisplayQrCodeCallbackUrl
{
	[Required]
	public string CallbackUrl { get; set; }
}

The html or template part of the Razor page displays the QR Code from a successful post request. You can set any URL for the callback in the form request. This is really just for testing.

@page
@model BoInsurance.Pages.CreateVerifierDisplayQrCodeModel

<div class="container-fluid">
    <div class="row">
        <div class="col-sm">
            <form method="post">
                <div>
                    <div class="form-group">
                        <label class="control-label">Callback base URL (ngrok in debug...)</label>
                        <input asp-for="CallbackUrlDto.CallbackUrl" class="form-control" />
                        <span asp-validation-for="CallbackUrlDto.CallbackUrl" class="text-danger"></span>
                    </div>
                    <div class="form-group">
                        @if (Model.CreatingVerifier)
                        {
                            <input class="form-control"
                                   type="submit" readonly="@Model.CreatingVerifier"
                                   value="Create Verification" />
                        }

                    </div>
                    <div class="form-group">
                        @if (!Model.CreatingVerifier)
                        {
                            <div class="alert alert-success">
                                <strong>Ready to verify</strong>
                            </div>
                        }
                    </div>
                </div>
            </form>
            <hr />
            <p>When the verification is created, you can scan the QR Code to verify</p>
        </div>
        <div class="col-sm">
            <div>
                <img src="~/ndl_car_01.png" width="200" alt="Driver License">
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-sm">
            <div class="qr" id="qrCode"></div>
            <input asp-for="ChallengeId" hidden/>
        </div>
    </div>
</div>

@section scripts {
<script src="~/js/qrcode.min.js"></script>
<script type="text/javascript">
    new QRCode(document.getElementById("qrCode"),
    {
        text: "@Html.Raw(Model.QrCodeUrl)",
        width: 400,
        height: 400,
        correctLevel: QRCode.CorrectLevel.M
    });

    $(document).ready(() => {
 
    });

    var connection = new signalR.HubConnectionBuilder().withUrl("/mattrVerifiedSuccessHub").build();

    connection.on("MattrCallbackSuccess", function (challengeId) {
        console.log("received verification:" + challengeId);
        window.location.href = "/VerifiedUser?challengeid=" + challengeId;
    });

    connection.start().then(function () {
        //console.log(connection.connectionId);
        const challengeId = $("#ChallengeId").val();
        
        if (challengeId) {
            console.log(challengeId);
            // join message
            connection.invoke("AddChallenge", challengeId, connection.connectionId).catch(function (err) {
                return console.error(err.toString());
            });
        }
    }).catch(function (err) {
        return console.error(err.toString());
    });
</script>
}

Step 4 Implement the Callback and update the UI using SignalR

After a successful verification in the digital wallet, the wallet sends the verified credentials to the API defined in the presentation request. The API handling this needs to update the correct client UI and continue the business process using the verified data. We use SignalR for this with a single client to client connection. The Signal connections for each connection is associated with a challenge ID, the same Id we used to create the presentation request. Using this, only the correct client will be notified and not all clients broadcasted. The DrivingLicenseCallback takes the body with is specific for the credentials you issued. This is always depending on what you request. The data is saved to a database and the client is informed to continue. We send a message directly to the correct client using the connectionId of the SignalR session created for this challenge.

[ApiController]
[Route("api/[controller]")]
public class VerificationController : Controller
{
	private readonly BoInsuranceDbService _boInsuranceDbService;

	private readonly IHubContext<MattrVerifiedSuccessHub> _hubContext;

	public VerificationController(BoInsuranceDbService boInsuranceDbService,
		IHubContext<MattrVerifiedSuccessHub> hubContext)
	{
		_hubContext = hubContext;
		_boInsuranceDbService = boInsuranceDbService;
	}

	/// <summary>
	/// {
	///  "presentationType": "QueryByExample",
	///  "challengeId": "GW8FGpP6jhFrl37yQZIM6w",
	///  "claims": {
	///      "id": "did:key:z6MkfxQU7dy8eKxyHpG267FV23agZQu9zmokd8BprepfHALi",
	///      "name": "Chris",
	///      "firstName": "Shin",
	///      "licenseType": "Certificate Name",
	///      "dateOfBirth": "some data",
	///      "licenseIssuedAt": "dda"
	///  },
	///  "verified": true,
	///  "holder": "did:key:z6MkgmEkNM32vyFeMXcQA7AfQDznu47qHCZpy2AYH2Dtdu1d"
	/// }
	/// </summary>
	/// <param name="body"></param>
	/// <returns></returns>
	[HttpPost]
	[Route("[action]")]
	public async Task<IActionResult> DrivingLicenseCallback([FromBody] VerifiedDriverLicense body)
	{
		string connectionId;
		var found = MattrVerifiedSuccessHub.Challenges
			.TryGetValue(body.ChallengeId, out connectionId);

		// test Signalr
		//await _hubContext.Clients.Client(connectionId).SendAsync("MattrCallbackSuccess", $"{body.ChallengeId}");
		//return Ok();

		var exists = await _boInsuranceDbService.ChallengeExists(body.ChallengeId);

		if (exists)
		{
			await _boInsuranceDbService.PersistVerification(body);

			if (found)
			{
				//$"/VerifiedUser?challengeid={body.ChallengeId}"
				await _hubContext.Clients
					.Client(connectionId)
					.SendAsync("MattrCallbackSuccess", $"{body.ChallengeId}");
			}

			return Ok();
		}

		return BadRequest("unknown verify request");
	}
}

The SignalR server is configured in the Startup class of the ASP.NET Core application. The path for the hub is defined in the MapHub method.

public void ConfigureServices(IServiceCollection services)
{
	// ...
	
	services.AddRazorPages();
	services.AddSignalR();
	services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	// ...

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapRazorPages();
		endpoints.MapHub<MattrVerifiedSuccessHub>("/mattrVerifiedSuccessHub");
		endpoints.MapControllers();
	});
}

The Hub implementation requires only one fixed method. The AddChallenge method takes the challenge Id and adds this the an in-memory cache. The controller implemented for the callbacks uses this ConcurrentDictionary to find the correct connectionId which is mapped to the challenges form the verification.

public class MattrVerifiedSuccessHub : Hub
{
	/// <summary>
	/// This should be replaced with a cache which expires or something
	/// </summary>
	public static readonly ConcurrentDictionary<string, string> Challenges 
		= new ConcurrentDictionary<string, string>();

	public void AddChallenge(string challengeId, string connnectionId)
	{
		Challenges.TryAdd(challengeId, connnectionId);
	}

}

The Javascript SignalR client in the browser connects to the SignalR server and registers the connectionId with the challenge ID used for the verification of the verifiable credentials from the holder of the digital wallet. If a client gets a message from that a verify has completed successfully and the callback has been called, it will redirect to the verified page. The client listens to the MattrCallbackSuccess for messages. These messages are sent from the callback controller directly.

<script type="text/javascript">
    
    var connection = new signalR.HubConnectionBuilder()
		.withUrl("/mattrVerifiedSuccessHub").build();

    connection.on("MattrCallbackSuccess", function (challengeId) {
        console.log("received verification:" + challengeId);
        window.location.href = "/VerifiedUser?challengeid=" + challengeId;
    });

    connection.start().then(function () {
        //console.log(connection.connectionId);
        const challengeId = $("#ChallengeId").val();
        
        if (challengeId) {
            console.log(challengeId);
            // join message
            connection.invoke("AddChallenge", challengeId, 
				connection.connectionId).catch(function (err) {
                return console.error(err.toString());
            });
        }
    }).catch(function (err) {
        return console.error(err.toString());
    });
</script>

The VerifiedUserModel Razor page displays the data and the business process can continue using the verified data.

public class VerifiedUserModel : PageModel
{
	private readonly BoInsuranceDbService _boInsuranceDbService;

	public VerifiedUserModel(BoInsuranceDbService boInsuranceDbService)
	{
		_boInsuranceDbService = boInsuranceDbService;
	}

	public string ChallengeId { get; set; }
	public DriverLicenseClaimsDto VerifiedDriverLicenseClaims { get; private set; }

	public async Task OnGetAsync(string challengeId)
	{
		// user query param to get challenge id and display data
		if (challengeId != null)
		{
			var verifiedDriverLicenseUser = await _boInsuranceDbService.GetVerifiedUser(challengeId);
			VerifiedDriverLicenseClaims = new DriverLicenseClaimsDto
			{
				DateOfBirth = verifiedDriverLicenseUser.DateOfBirth,
				Name = verifiedDriverLicenseUser.Name,
				LicenseType = verifiedDriverLicenseUser.LicenseType,
				FirstName = verifiedDriverLicenseUser.FirstName,
				LicenseIssuedAt = verifiedDriverLicenseUser.LicenseIssuedAt
			};
		}
	}
}

public class DriverLicenseClaimsDto
{
	public string Name { get; set; }
	public string FirstName { get; set; }
	public string LicenseType { get; set; }
	public string DateOfBirth { get; set; }
	public string LicenseIssuedAt { get; set; }
}

Running the verifier

To test the BoInsurance application locally, which is the verifier application, ngrok is used so that we have a public address for the callback. I install ngrok using npm. Without a license, you can only run your application in http.

npm install -g ngrok

Run the ngrok from the command line using the the URL of the application. I start the ASP.NET Core application at localhost port 5000.

ngrok http localhost:5000

You should be able to copied the ngrok URL and use this in the browser to test the verification.

Once running, a verification can be created and you can scan the QR Code with your digital wallet. Once you grant access to your data, the data is sent to the callback API and the UI will be redirected to the success page.

Notes

MATTR APIs work really well and support some of the flows for digital identities. I plan to try out the zero proof flow next. It is only possible to create verifiable credentials from data from your identity provider using the id_token. To issue credentials, you have to implement your own identity provider and cannot use business data from your application. If you have full control like with Openiddict, IdenityServer4 or Auth0, this is no problem, just more complicated to implement. If you do not control the data in your identity provider, you would need to create a second identity provider to issue credentials. This is part of your business logic then and not just an identity provider. This will always be a problem is using Azure AD or IDPs from large, medium size companies. The quality of the verifiable credentials also depend on how good the OIDC credential issuers are implemented as these are still central databases for these credentials and are still open to all the problems we have today. Decentralized identities have to potential to solve many problems but still have many unsolved problems.

Links

https://mattr.global/

https://learn.mattr.global/tutorials/verify/using-callback/callback-e-to-e

https://mattr.global/get-started/

https://learn.mattr.global/

https://keybase.io/

https://learn.mattr.global/tutorials/dids/did-key

https://gunnarpeipman.com/httpclient-remove-charset/

https://auth0.com/

4 comments

  1. […] Present and Verify Verifiable Credentials in ASP.NET Core using Decentralized Identities and MATTR […]

  2. […] Present and Verify Verifiable Credentials in ASP.NET Core using Decentralized Identities and MATTR (Damien Bowden) […]

  3. […] Present and Verify Verifiable Credentials in ASP.NET Core using Decentralized Identities and MATTR# – Damien Bowden […]

  4. […] Present and Verify Verifiable Credentials in ASP.NET Core using Decentralized Identities and MATTR […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.