[New post] Create and issuer verifiable credentials in ASP.NET Core using Azure AD
damienbod posted: " This article shows how Azure AD verifiable credentials can be issued and used in an ASP.NET Core application. An ASP.NET Core Razor page application is used to implement the credential issuer. To issue credentials, the application must manage the credent"
This article shows how Azure AD verifiable credentials can be issued and used in an ASP.NET Core application. An ASP.NET Core Razor page application is used to implement the credential issuer. To issue credentials, the application must manage the credential subject data as well require authenticated users who would like to add verifiable credentials to their digital wallet. The Microsoft Authenticator mobile application is used as the digital wallet.
Two ASP.NET Core applications are implemented to issue and verify the verifiable credentials. The credential issuer must administrate and authenticate its identities to issue verifiable credentials. A verifiable credential issuer should never issue credentials to unauthenticated subjects of the credential. As the verifier normally only authorizes the credential, it is important to know that the credentials were at least issued correctly. We do not know as a verifier who or and mostly what sends the verifiable credentials but at least we know that the credentials are valid if we trust the issuer. It is possible to use private holder binding for a holder of a wallet which would increase the trust between the verifier and the issued credentials.
The credential issuer in this demo issues credentials for driving licenses using Azure AD verifiable credentials. The ASP.NET Core application uses Microsoft.Identity.Web to authenticate all identities. In a real application, the application would be authenticated as well requiring 2FA for all users. Azure AD supports this good. The administrators would also require admin rights, which could be implemented using Azure security groups or Azure roles which are added to the application as claims after the OIDC authentication flow.
Any authenticated identity can request credentials (A driving license in this demo) for themselves and no one else. The administrators can create data which is used as the subject, but not issue credentials for others.
Azure AD verifiable credential setup
Azure AD verifiable credentials is setup using the Azure Docs for the Rest API and the Azure verifiable credential ASP.NET Core sample application.
Following the documentation, a display file and a rules file were uploaded for the verifiable credentials created for this issuer. In this demo, two credential subjects are defined to hold the data when issuing or verifying the credentials.
{ "default": { "locale": "en-US", "card": { "title": "National Driving License VC", "issuedBy": "Damienbod", "backgroundColor": "#003333", "textColor": "#ffffff", "logo": { "uri": "https://raw.githubusercontent.com/swiss-ssi-group/TrinsicAspNetCore/main/src/NationalDrivingLicense/wwwroot/ndl_car_01.png", "description": "National Driving License Logo" }, "description": "Use your verified credential to prove to anyone that you can drive." }, "consent": { "title": "Do you want to get your Verified Credential?", "instructions": "Sign in with your account to get your card." }, "claims": { "vc.credentialSubject.name": { "type": "String", "label": "Name" }, "vc.credentialSubject.details": { "type": "String", "label": "Details" } } } }
The rules file defines the attestations for the credentials. Two standard claims are used to hold the data, the given_name and the family_name. These claims are mapped to our name and details subject claims and holds all the data. Adding custom claims to Azure AD or Azure B2C is not so easy and so I decided for the demo, it would be easier to use standard claims which works without custom configurations. The data sent from the issuer to the holder of the claims can be sent in the application. It should be possible to add credential subject properties without requiring standard AD id_token claims, but I was not able to set this up in the current preview version.
The rest of the Azure AD credentials are setup exactly like the documentation.
Administration of the Driving licenses
The verifiable credential issuer application uses a Razor page application which accesses a Microsoft SQL Azure database using Entity Framework Core to access the database. The administrator of the credentials can assign driving licenses to any user. The DrivingLicenseDbContext class is used to define the DBSet for driver licenses.
A DriverLicense entity contains the infomation we use to create verifiable credentials.
public class DriverLicense { [Key] public Guid Id { get; set; } public string UserName { get; set; } = string.Empty; public DateTimeOffset IssuedAt { get; set; } public string Name { get; set; } = string.Empty; public string FirstName { get; set; } = string.Empty; public DateTimeOffset DateOfBirth { get; set; } public string Issuedby { get; set; } = string.Empty; public bool Valid { get; set; } public string DriverLicenseCredentials { get; set; } = string.Empty; public string LicenseType { get; set; } = string.Empty; }
Issuing credentials to authenticated identities
When issuing verifiable credentials using Azure AD Rest API, an IssuanceRequestPayload payload is used to request the credentials which are to be issued to the digital wallet. Verifiable credentials are issued to a digital wallet. The credentials are issued for the holder of the wallet. The payload classes are the same for all API implementations apart from the CredentialsClaims class which contains the subject claims which match the rules file of your definition.
public class IssuanceRequestPayload { [JsonPropertyName("includeQRCode")] public bool IncludeQRCode { get; set; } [JsonPropertyName("callback")] public Callback Callback { get; set; } = new Callback(); [JsonPropertyName("authority")] public string Authority { get; set; } = string.Empty; [JsonPropertyName("registration")] public Registration Registration { get; set; } = new Registration(); [JsonPropertyName("issuance")] public Issuance Issuance { get; set; } = new Issuance(); } public class Callback { [JsonPropertyName("url")] public string Url { get; set; } = string.Empty; [JsonPropertyName("state")] public string State { get; set; } = string.Empty; [JsonPropertyName("headers")] public Headers Headers { get; set; } = new Headers(); } public class Headers { [JsonPropertyName("api-key")] public string ApiKey { get; set; } = string.Empty; } public class Registration { [JsonPropertyName("clientName")] public string ClientName { get; set; } = string.Empty; } public class Issuance { [JsonPropertyName("type")] public string CredentialsType { get; set; } = string.Empty; [JsonPropertyName("manifest")] public string Manifest { get; set; } = string.Empty; [JsonPropertyName("pin")] public Pin Pin { get; set; } = new Pin(); [JsonPropertyName("claims")] public CredentialsClaims Claims { get; set; } = new CredentialsClaims(); } public class Pin { [JsonPropertyName("value")] public string Value { get; set; } = string.Empty; [JsonPropertyName("length")] public int Length { get; set; } = 4; } /// Application specific claims used in the payload of the issue request. /// When using the id_token for the subject claims, the IDP needs to add the values to the id_token! /// The claims can be mapped to anything then. public class CredentialsClaims { /// <summary> /// attribute names need to match a claim from the id_token /// </summary> [JsonPropertyName("given_name")] public string Name { get; set; } = string.Empty; [JsonPropertyName("family_name")] public string Details { get; set; } = string.Empty; }
The GetIssuanceRequestPayloadAsync method sets the data for each identity that requested the credentials. Only a signed in user can request the credentials for themselves. The context.User.Identity is used and the data is selected from the database for the signed in user. It is important that credentials are only issued to authenticated users. Users and the application must be authenticated correctly using 2FA and so on. Per default, the credentials are only authorized on the verifier which is probably not enough for most security flows.
The IssuanceRequestAsync method gets the payload data and request credentials from the Azure AD verifiable credentials REST API and returns this value which can be scanned using a QR code in the Razor page. The request returns fast. Depending on how the flow continues, a web hook in the application will update the status in a cache. This cache is persisted and polled from the UI. This could be improved by using SignalR.
[HttpGet("/api/issuer/issuance-request")] public async Task<ActionResult> IssuanceRequestAsync() { try { var payload = await _issuerService.GetIssuanceRequestPayloadAsync(Request, HttpContext); try { var (Token, Error, ErrorDescription) = await _issuerService.GetAccessToken(); if (string.IsNullOrEmpty(Token)) { _log.LogError($"failed to acquire accesstoken: {Error} : {ErrorDescription}"); return BadRequest(new { error = Error, error_description = ErrorDescription }); } var defaultRequestHeaders = _httpClient.DefaultRequestHeaders; defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token); HttpResponseMessage res = await _httpClient.PostAsJsonAsync( _credentialSettings.ApiEndpoint, payload); var response = await res.Content.ReadFromJsonAsync<IssuanceResponse>(); if(response == null) { return BadRequest(new { error = "400", error_description = "no response from VC API"}); } if (res.StatusCode == HttpStatusCode.Created) { _log.LogTrace("succesfully called Request API"); if (payload.Issuance.Pin.Value != null) { response.Pin = payload.Issuance.Pin.Value; } response.Id = payload.Callback.State; var cacheData = new CacheData { Status = IssuanceConst.NotScanned, Message = "Request ready, please scan with Authenticator", Expiry = response.Expiry.ToString() }; _cache.Set(payload.Callback.State, JsonSerializer.Serialize(cacheData)); return Ok(response); } else { _log.LogError("Unsuccesfully called Request API"); return BadRequest(new { error = "400", error_description = "Something went wrong calling the API: " + response }); } } catch (Exception ex) { return BadRequest(new { error = "400", error_description = "Something went wrong calling the API: " + ex.Message }); } } catch (Exception ex) { return BadRequest(new { error = "400", error_description = ex.Message }); } }
The IssuanceResponse is returned to the UI.
public class IssuanceResponse { [JsonPropertyName("requestId")] public string RequestId { get; set; } = string.Empty; [JsonPropertyName("url")] public string Url { get; set; } = string.Empty; [JsonPropertyName("expiry")] public int Expiry { get; set; } [JsonPropertyName("pin")] public string Pin { get; set; } = string.Empty; [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; }
The IssuanceCallback is used as a web hook for the Azure AD verifiable credentials. When developing or deploying, this web hook needs to have a public IP. I use ngrok to test this. Because the issuer authenticates the identities using an Azure App registration, everytime the ngrok URL changes, the redirect URL needs to be updated. Each callback request updates the cache. This API also needs to allow anonymous requests if the rest of the application is authenticated using OIDC. The AllowAnonymous attribute is required, if you use an authenticated ASP.NET Core application.
[AllowAnonymous] [HttpPost("/api/issuer/issuanceCallback")] public async Task<ActionResult> IssuanceCallback() { string content = await new System.IO.StreamReader(Request.Body).ReadToEndAsync(); var issuanceResponse = JsonSerializer.Deserialize<IssuanceCallbackResponse>(content); try { //there are 2 different callbacks. 1 if the QR code is scanned (or deeplink has been followed) //Scanning the QR code makes Authenticator download the specific request from the server //the request will be deleted from the server immediately. //That's why it is so important to capture this callback and relay this to the UI so the UI can hide //the QR code to prevent the user from scanning it twice (resulting in an error since the request is already deleted) if (issuanceResponse.Code == IssuanceConst.RequestRetrieved) { var cacheData = new CacheData { Status = IssuanceConst.RequestRetrieved, Message = "QR Code is scanned. Waiting for issuance...", }; _cache.Set(issuanceResponse.State, JsonSerializer.Serialize(cacheData)); } if (issuanceResponse.Code == IssuanceConst.IssuanceSuccessful) { var cacheData = new CacheData { Status = IssuanceConst.IssuanceSuccessful, Message = "Credential successfully issued", }; _cache.Set(issuanceResponse.State, JsonSerializer.Serialize(cacheData)); } if (issuanceResponse.Code == IssuanceConst.IssuanceError) { var cacheData = new CacheData { Status = IssuanceConst.IssuanceError, Payload = issuanceResponse.Error?.Code, //at the moment there isn't a specific error for incorrect entry of a pincode. //So assume this error happens when the users entered the incorrect pincode and ask to try again. Message = issuanceResponse.Error?.Message }; _cache.Set(issuanceResponse.State, JsonSerializer.Serialize(cacheData)); } return Ok(); } catch (Exception ex) { return BadRequest(new { error = "400", error_description = ex.Message }); } }
The IssuanceCallbackResponse is returned to the UI.
public class IssuanceCallbackResponse { [JsonPropertyName("code")] public string Code { get; set; } = string.Empty; [JsonPropertyName("requestId")] public string RequestId { get; set; } = string.Empty; [JsonPropertyName("state")] public string State { get; set; } = string.Empty; [JsonPropertyName("error")] public CallbackError? Error { get; set; } }
The IssuanceResponse method is polled from a Javascript client in the Razor page UI. This method updates the status in the UI using the cache and the database.
[HttpGet("/api/issuer/issuance-response")] public ActionResult IssuanceResponse() { try { //the id is the state value initially created when the issuance request was requested from the request API //the in-memory database uses this as key to get and store the state of the process so the UI can be updated string state = this.Request.Query["id"]; if (string.IsNullOrEmpty(state)) { return BadRequest(new { error = "400", error_description = "Missing argument 'id'" }); } CacheData value = null; if (_cache.TryGetValue(state, out string buf)) { value = JsonSerializer.Deserialize<CacheData>(buf); Debug.WriteLine("check if there was a response yet: " + value); return new ContentResult { ContentType = "application/json", Content = JsonSerializer.Serialize(value) }; } return Ok(); } catch (Exception ex) { return BadRequest(new { error = "400", error_description = ex.Message }); } }
The DriverLicenseCredentialsModel class is used for the credential issuing for the sign-in user. The HTML part of the Razor page contains the Javascript client code which was implemented using the code from the Microsoft Azure sample.
public class DriverLicenseCredentialsModel : PageModel { private readonly DriverLicenseService _driverLicenseService; public string DriverLicenseMessage { get; set; } = "Loading credentials"; public bool HasDriverLicense { get; set; } = false; public DriverLicense DriverLicense { get; set; } public DriverLicenseCredentialsModel(DriverLicenseService driverLicenseService) { _driverLicenseService = driverLicenseService; } public async Task OnGetAsync() { DriverLicense = await _driverLicenseService.GetDriverLicense(HttpContext.User.Identity.Name); if (DriverLicense != null) { DriverLicenseMessage = "Add your driver license credentials to your wallet"; HasDriverLicense = true; } else { DriverLicenseMessage = "You have no valid driver license"; } } }
Testing and running the applications
Ngrok is used to provide a public callback for the Azure AD verifiable credentials callback. When the application is started, you need to create a driving license. This is done in the administration Razor page. Once a driving license exists, the View driver license Razor page can be used to issue a verifiable credential to the logged in user. A QR Code is displayed which can be scanned to begin the issue flow.
Using the Microsoft authenticator, you can scan the QR Code and add the verifiable credentials to your digital wallet. The credentials can now be used in any verifier which supports the Microsoft Authenticator wallet. The verify ASP.NET Core application can be used to verify and used the issued verifiable credential from the Wallet.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.