Use a Microsoft Entra Verified ID Employee credential to view paycheck data

This post shows how a Microsoft Entra Verified ID employee credential can be used to access user specific data. This demo shows possible paycheck data from Switzerland. A payment ID can be the printed on the pay slip or the payment document could have a QR Code to scan. The user specific data can then be viewed and possible links to further services can be opened. Access is controlled using the verification presentation. The application is implemented using ASP.NET Core.

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

Get your Verified Employee credential

To use this application, a Microsoft Entra employee credential from the correct tenant must be used. The following post shows how to set this up, get a credential and add this to your Microsoft wallet.

Issue Employee verifiable credentials using Entra Verified ID and ASP.NET Core

Implement the employee credential verifier

The EmployeeClaims class implements the scheme returned from the employee credential. This contains the values from the Microsoft Entra verified employee credential stored on your wallet.

public class EmployeeClaims
{
    [JsonPropertyName("givenName")]
    public string GivenName { get; set; } = string.Empty;
    [JsonPropertyName("surname")]
    public string Surname { get; set; } = string.Empty;
    [JsonPropertyName("mail")]
    public string Mail { get; set; } = string.Empty;
    [JsonPropertyName("jobTitle")]
    public string JobTitle { get; set; } = string.Empty;
    [JsonPropertyName("photo")] // "type": "image/jpg;base64url",
    public string Photo { get; set; } = string.Empty;
    [JsonPropertyName("displayName")]
    public string DisplayName { get; set; } = string.Empty;
    [JsonPropertyName("preferredLanguage")]
    public string PreferredLanguage { get; set; } = string.Empty;

    //[JsonPropertyName("userPrincipalName")]
    [JsonPropertyName("revocationId")]
    public string RevocationId { get; set; } = string.Empty;
}

The GetVerifierRequestPayload is used to initialize the verification presentation request. The configuration must match the Microsoft Entra Verified ID tenant setup.

public VerifierRequestPayload GetVerifierRequestPayload(HttpRequest request)
{
	var payload = new VerifierRequestPayload();

	var host = GetRequestHostName(request);
	payload.Callback.State = Guid.NewGuid().ToString();
	payload.Callback.Url = $"{host}/api/verifier/presentationCallback";
	payload.Callback.Headers.ApiKey = _credentialSettings.VcApiCallbackApiKey;

	payload.Registration.ClientName = "VerifiedEmployee";
	payload.Authority = _credentialSettings.VerifierAuthority;

	var requestedCredentials = new RequestedCredentials
	{
		CrendentialsType = "VerifiedEmployee",
		Purpose = "Verified Employee to authenticate your request"
	};
	requestedCredentials.AcceptedIssuers.Add(_credentialSettings.IssuerAuthority);
	payload.RequestedCredentials.Add(requestedCredentials);

	return payload;
}

The VerifierController implements the API used for the callbacks and the wallet request results. The PresentationRequest method handles the request and uses the Microsoft Entra Verified ID API to send a verification presentation request to the wallet.

[HttpGet("/api/verifier/presentation-request")]
public async Task<ActionResult> PresentationRequest()
{
	try
	{
		var payload = _verifierService.GetVerifierRequestPayload(Request);
		var (Token, Error, ErrorDescription) = await _verifierService.GetAccessToken();

		if (string.IsNullOrEmpty(Token))
		{
			_log.LogError("failed to acquire accesstoken: {Error} : {ErrorDescription}", Error, ErrorDescription);
			return BadRequest(new { error = Error, error_description = ErrorDescription });
		}

		var defaultRequestHeaders = _httpClient.DefaultRequestHeaders;
		defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);

		var res = await _httpClient.PostAsJsonAsync(
			_credentialSettings.Endpoint, payload);

		if (res.IsSuccessStatusCode)
		{
			var response = await res.Content.ReadFromJsonAsync<VerifierResponse>();
			response!.Id = payload.Callback.State;
			_log.LogTrace("succesfully called Request API");

			if (res.StatusCode == HttpStatusCode.Created)
			{
				var cacheData = new CacheData
				{
					Status = VerifierConst.NotScanned,
					Message = "Request ready, please scan with Authenticator",
					Expiry = response.Expiry.ToString(CultureInfo.InvariantCulture),
				};
				CacheData.AddToCache(payload.Callback.State, _distributedCache, cacheData);

				return Ok(response);
			}
		}
		else
		{
			var message = await res.Content.ReadAsStringAsync();

			_log.LogError("Unsuccesfully called Request API {message}", message);
			return BadRequest(new { error = "400", error_description = message });
		}

		var errorResponse = await res.Content.ReadAsStringAsync();
		_log.LogError("Unsuccesfully called Request API");
		return BadRequest(new { error = "400", error_description = "Something went wrong calling the API: " + errorResponse });
	}
	catch (Exception ex)
	{
		return BadRequest(new { error = "400", error_description = ex.Message });
	}
}

The PresentationCallback in the API controller handles the Web hook callback requests. If the presentation is successful, the data is returned and added to the cache using the state value. The UI can poll this later and use the data in the user session.

[HttpPost]
public async Task<ActionResult> PresentationCallback()
{
	var content = await new StreamReader(Request.Body).ReadToEndAsync();
	var verifierCallbackResponse = JsonSerializer.Deserialize<VerifierCallbackResponse>(content);

	try
	{
		if (verifierCallbackResponse != null  && verifierCallbackResponse
			.RequestStatus == VerifierConst.RequestRetrieved)
		{
			var cacheData = new CacheData
			{
				Status = VerifierConst.RequestRetrieved,
				Message = "QR Code is scanned. Waiting for validation...",
			};
			CacheData.AddToCache(verifierCallbackResponse.State, _distributedCache, cacheData);
		}

		if (verifierCallbackResponse != null && verifierCallbackResponse
			.RequestStatus == VerifierConst.PresentationVerified)
		{
			var cacheData = new CacheData
			{
				Status = VerifierConst.PresentationVerified,
				Message = "Presentation verified",
				Payload = JsonSerializer.Serialize(verifierCallbackResponse.VerifiedCredentialsData),
				Subject = verifierCallbackResponse.Subject
			};

			cacheData.Employee.Photo = verifierCallbackResponse
				.VerifiedCredentialsData!.FirstOrDefault()!.Claims.Photo;
			cacheData.Employee.RevocationId = verifierCallbackResponse
				.VerifiedCredentialsData!.FirstOrDefault()!.Claims.RevocationId;
			cacheData.Employee.PreferredLanguage = verifierCallbackResponse
				.VerifiedCredentialsData!.FirstOrDefault()!.Claims.PreferredLanguage;
			cacheData.Employee.Surname = verifierCallbackResponse
				.VerifiedCredentialsData!.FirstOrDefault()!.Claims.Surname;
			cacheData.Employee.GivenName = verifierCallbackResponse
				.VerifiedCredentialsData!.FirstOrDefault()!.Claims.GivenName;
			cacheData.Employee.DisplayName = verifierCallbackResponse
				.VerifiedCredentialsData!.FirstOrDefault()!.Claims.DisplayName;
			cacheData.Employee.Mail = verifierCallbackResponse
				.VerifiedCredentialsData!.FirstOrDefault()!.Claims.Mail;
			cacheData.Employee.JobTitle = verifierCallbackResponse
				.VerifiedCredentialsData!.FirstOrDefault()!.Claims.JobTitle;

			CacheData.AddToCache(verifierCallbackResponse.State, _distributedCache, cacheData);
		}

		return Ok();
	}
	catch (Exception ex)
	{
		return BadRequest(new { error = "400", error_description = ex.Message });
	}
}

Authenticated session setup

The employee verifiable credential is returned and stored in a cache using a random state value. This value needs to be random with a minimum length. This is used to setup the user session. If it is possible to acquire this value in a different way, user data loss is possible. This should only be active for one time usage. This type of flow is also open to phishing attacks as it is cross device with no origin validation.

<form method="post" id="verifyEmployeePaycheck" action="" novalidate>
    <input type="hidden" required id="statePresented" name="statePresented"/>
</form>

When the state is posted to the server, the user is signed in using cookies.

public async Task<IActionResult> OnPostAsync()
{
	if (StatePresented == null)
	{
		ModelState.AddModelError("StatePresented", "no vc");
		return Page();
	}

	var credentialData = CacheData.GetFromCache(StatePresented, _distributedCache);

	var claims = new List<Claim> {
		new Claim("DisplayName", credentialData!.Employee.DisplayName, 
			ClaimValueTypes.String, "damienbodsharepoint"),
		new Claim("JobTitle", credentialData!.Employee.JobTitle, 
			ClaimValueTypes.String, "damienbodsharepoint"),
		new Claim("PreferredLanguage", credentialData!.Employee.PreferredLanguage, 
			ClaimValueTypes.String, "damienbodsharepoint"),
		new Claim("RevocationId", credentialData!.Employee.RevocationId, 
			ClaimValueTypes.String, "damienbodsharepoint"),
		new Claim("GivenName", credentialData!.Employee.GivenName, 
			ClaimValueTypes.String, "damienbodsharepoint"),
		new Claim("Mail", credentialData!.Employee.Mail, 
			ClaimValueTypes.String, "damienbodsharepoint"),
		new Claim("Surname", credentialData!.Employee.Surname, 
			ClaimValueTypes.String, "damienbodsharepoint"),
		new Claim("Photo", credentialData!.Employee.Photo, 
			ClaimValueTypes.String, "damienbodsharepoint"),
	};

	var userIdentity = new ClaimsIdentity(claims, "entraemployee");
	var userPrincipal = new ClaimsPrincipal(userIdentity);

	await HttpContext.SignInAsync(
		CookieAuthenticationDefaults.AuthenticationScheme,
		userPrincipal,
		new AuthenticationProperties
		{
			ExpiresUtc = DateTime.UtcNow.AddMinutes(20),
			IsPersistent = false,
			AllowRefresh = false
		});

	CacheData.RemoveFromCache(StatePresented, _distributedCache);

	return Redirect($"~/Paycheck/PaycheckDetailsS3/{PaycheckId}");
}

Run the application

A demo was deployed using one of my test tenants.

https://issueverifiableemployee.azurewebsites.net/

When the application is started, the process can be started using a paycheck ID. This all depends on how your company supports the pay data. Sometimes, only your ID is supported and the latest one could be displayed. This is always different and depends on the payment service. Some companies use a QR code on the printed pay slip.

The application verifies that you are an employee of the company using the Microsoft Entra Verified ID employee credential.

Once verified, the paycheck data can be displayed.

This demo just displays some typical data from a Swiss paycheck. This would be specific to the software used to manage the data and an API can be built to access the data on behalf of the user.

Notes

The way of verifying employees offers another way to implement access management and offers new possibilities for integrating identity checks across tenant boundaries.

Links

https://learn.microsoft.com/en-us/azure/active-directory/verifiable-credentials/how-to-use-quickstart-multiple

https://github.com/swiss-ssi-group/AzureADVerifiableCredentialsAspNetCore

https://learn.microsoft.com/en-us/azure/active-directory/verifiable-credentials/decentralized-identifier-overview

https://ssi-start.adnovum.com/data

https://github.com/e-id-admin/public-sandbox-trustinfrastructure/discussions/14

https://openid.net/specs/openid-connect-self-issued-v2-1_0.html

https://identity.foundation/jwt-vc-presentation-profile/

https://learn.microsoft.com/en-us/azure/active-directory/verifiable-credentials/verifiable-credentials-standards

https://github.com/Azure-Samples/active-directory-verifiable-credentials-dotnet

https://aka.ms/mysecurityinfo

https://fontawesome.com/

https://developer.microsoft.com/en-us/graph/graph-explorer?tenant=damienbodsharepoint.onmicrosoft.com

https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0

https://github.com/Azure-Samples/VerifiedEmployeeIssuance

https://github.com/AzureAD/microsoft-identity-web/blob/jmprieur/Graph5/src/Microsoft.Identity.Web.GraphServiceClient/Readme.md#replace-the-nuget-packages

https://docs.microsoft.com/azure/app-service/deploy-github-actions#configure-the-github-secret

https://issueverifiableemployee.azurewebsites.net/

Links eIDAS and EUDI standards

Draft: OAuth 2.0 Attestation-Based Client Authentication
https://datatracker.ietf.org/doc/html/draft-looker-oauth-attestation-based-client-auth-00

Draft: OpenID for Verifiable Presentations
https://openid.net/specs/openid-4-verifiable-presentations-1_0.html

Draft: OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop

Draft: OpenID for Verifiable Credential Issuance
https://openid.bitbucket.io/connect/openid-4-verifiable-credential-issuance-1_0.html

Draft: OpenID Connect for Identity Assurance 1.0
https://openid.net/specs/openid-connect-4-identity-assurance-1_0-13.html

Draft: SD-JWT-based Verifiable Credentials (SD-JWT VC)
https://vcstuff.github.io/draft-terbu-sd-jwt-vc/draft-terbu-oauth-sd-jwt-vc.html

One comment

  1. […] Use a Microsoft Entra Verified ID Employee credential to view paycheck data [#.NET Core #ASP.NET Core #Azure #Entra CIAM #Microsoft Entra External ID #Microsoft Entra ID #aad #aspnetcore #AzureAD #Credential #DID #dotnet #e-id #employee-id #entra #holder #iam #Identity #ion #issuer #siop #SSI #VC #verifiedid #verifier #wallet] […]

Leave a comment

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