Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace WalletFramework.Oid4Vc.Oid4Vp.Errors;

/// <summary>
/// Error indicating that the request_uri_method is not supported or invalid.
/// This error is defined in OpenID4VP specification section 5.10.2.
/// </summary>
public record InvalidRequestUriMethodError : VpError
{
private const string Code = "invalid_request_uri_method";

public InvalidRequestUriMethodError(string message) : base(Code, message) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ public record AuthorizationRequestByReference
public Uri AuthorizationRequestUri { get; }

public Uri RequestUri { get; }

public Option<string> RequestUriMethod { get; }

private AuthorizationRequestByReference(Uri authorizationRequestUri, Uri requestUri) => (AuthorizationRequestUri, RequestUri) = (authorizationRequestUri, requestUri);
private AuthorizationRequestByReference(Uri authorizationRequestUri, Uri requestUri, Option<string> requestUriMethod) =>
(AuthorizationRequestUri, RequestUri, RequestUriMethod) = (authorizationRequestUri, requestUri, requestUriMethod);

public static Option<AuthorizationRequestByReference> CreateAuthorizationRequestByReference(Uri uri)
{
var queryString = HttpUtility.ParseQueryString(uri.Query);
var clientId = queryString["client_id"];
var requestUri = queryString["request_uri"];
var requestUriMethod = queryString["request_uri_method"];

if (string.IsNullOrEmpty(clientId)
|| string.IsNullOrEmpty(requestUri))
return Option<AuthorizationRequestByReference>.None;

return new AuthorizationRequestByReference(uri, new Uri(requestUri));
return new AuthorizationRequestByReference(uri, new Uri(requestUri), requestUriMethod);
}
}
15 changes: 14 additions & 1 deletion src/WalletFramework.Oid4Vc/Oid4Vp/Models/RequestObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private RequestObject(JwtSecurityToken token, AuthorizationRequest authorization
/// Creates a new instance of the <see cref="RequestObject" /> class.
/// </summary>
public static Validation<AuthorizationRequestCancellation, RequestObject> FromStr(
string requestObjectJson)
string requestObjectJson, Option<string> walletNonce)
{
var tokenHandler = new JwtSecurityTokenHandler();

Expand All @@ -66,6 +66,19 @@ public static Validation<AuthorizationRequestCancellation, RequestObject> FromSt
return new AuthorizationRequestCancellation(Option<Uri>.None, [error]);
}

walletNonce.IfSome(nonce =>
{
if (jwt.Payload.TryGetValue("wallet_nonce", out var nonceValue))
{
if (nonceValue.ToString() != nonce)
throw new InvalidOperationException("wallet_nonce in request object does not match the provided wallet_nonce");
}
else
{
throw new InvalidOperationException("wallet_nonce is required but not present in the Request Object");
}
});

var json = jwt.Payload.SerializeToJson();

return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net.Http.Headers;
using System.Web;
using Hyperledger.Aries.Utils;
using LanguageExt;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
Expand All @@ -18,6 +19,9 @@ public class AuthorizationRequestService(
IHttpClientFactory httpClientFactory,
IRpAuthService rpAuthService) : IAuthorizationRequestService
{
private const string RequestUriMethodGet = "get";
private const string RequestUriMethodPost = "post";

public async Task<Validation<AuthorizationRequestCancellation, AuthorizationRequest>> GetAuthorizationRequest(
AuthorizationRequestUri authorizationRequestUri) =>
await authorizationRequestUri.Value.Match(
Expand All @@ -40,29 +44,22 @@ await authorizationRequestUri.Value.Match(
seq => seq
);
},
async value =>
{
return await GetAuthRequestByValue(value);
}
);
async value => await GetAuthRequestByValue(value));

private async Task<Validation<AuthorizationRequestCancellation, RequestObject>> GetRequestObject(
AuthorizationRequestByReference authRequestByReference)
{
var httpClient = httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Clear();

var jsonString = await httpClient.GetStringAsync(authRequestByReference.RequestUri);
var requestObjectValidation = FromStr(jsonString);
var requestObjectValidation = await FetchRequestObject(authRequestByReference);

return await requestObjectValidation.MatchAsync(async requestObject =>
return await requestObjectValidation.MatchAsync(
async requestObject =>
{
var authRequest = requestObject.ToAuthorizationRequest();
var clientMetadataOption =
await FetchClientMetadata(authRequest).OnException(_ => Option<ClientMetadata>.None);

var error = new InvalidRequestError($"Client ID Scheme {requestObject.ClientIdScheme} is not supported");

Validation<AuthorizationRequestCancellation, RequestObject> result =
requestObject.ClientIdScheme.Value switch
{
Expand All @@ -79,7 +76,7 @@ private async Task<Validation<AuthorizationRequestCancellation, RequestObject>>
.WithClientMetadata(clientMetadataOption),
_ => new AuthorizationRequestCancellation(authRequest.GetResponseUriMaybe(), [error])
};

return result;
},
seq => seq);
Expand Down Expand Up @@ -137,6 +134,62 @@ private async Task<Validation<AuthorizationRequestCancellation, AuthorizationReq
},
seq => seq);
}

private async Task<Validation<AuthorizationRequestCancellation, RequestObject>> FetchRequestObject(AuthorizationRequestByReference authRequestByReference)
{
return await authRequestByReference.RequestUriMethod.Match<Task<Validation<AuthorizationRequestCancellation, RequestObject>>>(
async method =>
{
return method.ToLowerInvariant() switch
{
RequestUriMethodGet => await FetchRequestObjectViaGet(authRequestByReference),
RequestUriMethodPost => await FetchRequestObjectViaPost(authRequestByReference),
_ => new AuthorizationRequestCancellation(Option<Uri>.None, [new InvalidRequestUriMethodError($"Unsupported request_uri_method: '{method}'.")])
};
},
async () => await FetchRequestObjectViaGet(authRequestByReference));
}

private async Task<Validation<AuthorizationRequestCancellation, RequestObject>> FetchRequestObjectViaPost(AuthorizationRequestByReference authRequestByReference)
{
var httpClient = httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/oauth-authz-req+jwt"));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According the specs the Accept header MUST be set to: application/oauth-authz-req+jwt, see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-5.10

var walletNonce = Base64UrlEncoder.Encode(Guid.NewGuid().ToString());
var keyValuePairs = new List<KeyValuePair<string, string>>();
keyValuePairs.Add(new KeyValuePair<string, string>("wallet_nonce", walletNonce));
keyValuePairs.Add(new KeyValuePair<string, string>("wallet_metadata", new JObject()
{
["vp_formats_supported"] = new JObject()
{
["dc+sd-jwt"] = new JObject()
{
["sd-jwt_alg_values"] = new JArray(){ "ES256", "ES384", "ES512", "RS256" },
["kb-jwt_alg_values"] = new JArray(){ "ES256" }
},
["mso_mdoc"] = new JObject()
{
["issuerauth_alg_values"] = new JArray(){ "-7", "-35", "-36", "-8" },
["deviceauth_alg_values"] = new JArray(){ "-7" }
}
}
}.ToString()));

var response = await httpClient.PostAsync(authRequestByReference.RequestUri, new FormUrlEncodedContent(keyValuePairs));
response.EnsureSuccessStatusCode();
var stringContent = await response.Content.ReadAsStringAsync();

return FromStr(stringContent, walletNonce);
}

private async Task<Validation<AuthorizationRequestCancellation, RequestObject>> FetchRequestObjectViaGet(AuthorizationRequestByReference authRequestByReference)
{
var httpClient = httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Clear();

return FromStr(await httpClient.GetStringAsync(authRequestByReference.RequestUri), Option<string>.None);
}

private async Task<Option<ClientMetadata>> FetchClientMetadata(AuthorizationRequest authorizationRequest)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FluentAssertions;
using LanguageExt;
using Newtonsoft.Json.Linq;
using WalletFramework.Core.Functional;
using WalletFramework.Oid4Vc.Oid4Vp.Models;
Expand All @@ -12,7 +13,7 @@ public class X509SanDnsTests
public void Valid_Jwt_Signature_Is_Accepted()
{
var requestObject = RequestObject
.FromStr(SignedRequestObjectWithRs256AndTrustChain)
.FromStr(SignedRequestObjectWithRs256AndTrustChain, Option<string>.None)
.UnwrapOrThrow();

var sut = requestObject.ValidateJwtSignature();
Expand All @@ -24,7 +25,7 @@ public void Valid_Jwt_Signature_Is_Accepted()
public void Invalid_Jwt_Signature_Results_In_An_Error()
{
var requestObject = RequestObject
.FromStr(SignedRequestObjectWithRs256AndInvalidSignature)
.FromStr(SignedRequestObjectWithRs256AndInvalidSignature, Option<string>.None)
.UnwrapOrThrow();
try
{
Expand All @@ -41,7 +42,7 @@ public void Invalid_Jwt_Signature_Results_In_An_Error()
public void Trust_Chain_Is_Being_Validated()
{
var requestObject = RequestObject
.FromStr(SignedRequestObjectWithRs256AndTrustChain)
.FromStr(SignedRequestObjectWithRs256AndTrustChain,Option<string>.None)
.UnwrapOrThrow();

var sut = requestObject.ValidateTrustChain();
Expand All @@ -53,7 +54,7 @@ public void Trust_Chain_Is_Being_Validated()
public void Single_Self_Signed_Certificate_Is_Allowed()
{
var requestObject = RequestObject
.FromStr(SignedRequestObjectWithRs256AndSingleSelfSigned)
.FromStr(SignedRequestObjectWithRs256AndSingleSelfSigned, Option<string>.None)
.UnwrapOrThrow();

var sut = requestObject.ValidateTrustChain();
Expand All @@ -65,7 +66,7 @@ public void Single_Self_Signed_Certificate_Is_Allowed()
public void Single_Non_Self_Signed_Certificate_Is_Not_Allowed()
{
var requestObject = RequestObject
.FromStr(SignedRequestObjectWithRs256AndSingleNonSelfSigned)
.FromStr(SignedRequestObjectWithRs256AndSingleNonSelfSigned, Option<string>.None)
.UnwrapOrThrow();

try
Expand All @@ -83,7 +84,7 @@ public void Single_Non_Self_Signed_Certificate_Is_Not_Allowed()
public void Checks_That_San_Name_Equals_Client_Id()
{
var requestObject = RequestObject
.FromStr(SignedRequestObjectWithRs256AndTrustChain)
.FromStr(SignedRequestObjectWithRs256AndTrustChain, Option<string>.None)
.UnwrapOrThrow();

var sut = requestObject.ValidateSanName();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using LanguageExt;
using Org.BouncyCastle.X509;
using WalletFramework.Core.Functional;
using WalletFramework.Oid4Vc.Oid4Vp.Models;
Expand All @@ -17,10 +18,10 @@ public static class RpAuthSamples
"eyJ0eXAiOiJvYXV0aC1hdXRoei1yZXErand0IiwiYWxnIjoiRVMyNTYiLCJ4NWMiOlsiTUlJQjdEQ0NBWk9nQXdJQkFnSVVIajhxc0JpSFVadm5FcEdlcDk3OE5ZUU5qcE13Q2dZSUtvWkl6ajBFQXdJd0d6RVpNQmNHQTFVRUF3d1FSMlZ5YldGdUlGSmxaMmx6ZEhKaGNqQWVGdzB5TlRBME1URXhOelV5TXpOYUZ3MHlOakEwTVRFeE56VXlNek5hTUJJeEVEQU9CZ05WQkFNTUIwUmxiVzhnVWxBd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFRVUpQR2hhYVQ4SmErdHBuVlVqVXdoWlZxM0xEVjc1RWNCWEdMcGFXL2c0Z2h4ZkRUUVpSTS8zVEQyZ0dWTm5KTjZtNy8vMEZaZzlsemxLVmJtSXMxR280RzlNSUc2TUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRREFnV2dNQk1HQTFVZEpRUU1NQW9HQ0NzR0FRVUZCd01CTUJvR0ExVWRFUVFUTUJHQ0QyWjFibXRsTFhkaGJHeGxkQzVrWlRBdkJnTlZIUjhFS0RBbU1DU2dJcUFnaGg1b2RIUndjem92TDJaMWJtdGxMWGRoYkd4bGRDNWtaUzlqWVM5amNtd3dIUVlEVlIwT0JCWUVGSzRZS0VSa1pYUCt0RzllRFJhVmRFQko2aDIyTUI4R0ExVWRJd1FZTUJhQUZNeG5LTGtHaWZiVEtyeGJHWGNGWEs2UkZRZDNNQW9HQ0NxR1NNNDlCQU1DQTBjQU1FUUNJUUQrWVhmaWkwdHprZEZ4M2lZYTdpc2F2YjFzVmgzaWNDU2IrQkJ4UkxKdTdnSWZUdDIzRGtoZkNhLzVvZ0FUR3c1YXhtWGFPTjFKUFI4OVA2OFVNUFBXTnc9PSJdfQ.eyJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJjbGllbnRfaWQiOiJ4NTA5X3Nhbl9kbnM6ZnVua2Utd2FsbGV0LmRlIiwicmVzcG9uc2VfdXJpIjoiaHR0cHM6Ly9mdW5rZS13YWxsZXQuZGUvb2lkNHZwL3Jlc3BvbnNlIiwicmVzcG9uc2VfbW9kZSI6ImRpcmVjdF9wb3N0Iiwibm9uY2UiOiI4NjAzMDM0MDc1MjgzMjQ1MjY4NzEzMTMiLCJkY3FsX3F1ZXJ5Ijp7ImNyZWRlbnRpYWxzIjpbeyJpZCI6InBpZCIsImZvcm1hdCI6ImRjK3NkLWp3dCIsIm1ldGEiOnsidmN0X3ZhbHVlcyI6WyJodHRwczovL2RlbW8ucGlkLWlzc3Vlci5idW5kZXNkcnVja2VyZWkuZGUvY3JlZGVudGlhbHMvcGlkLzEuMCIsInVybjpldS5ldXJvcGEuZWMuZXVkaTpwaWQ6MSJdfSwiY2xhaW1zIjpbeyJwYXRoIjpbImZhbWlseV9uYW1lIl19LHsicGF0aCI6WyJnaXZlbl9uYW1lIl19LHsicGF0aCI6WyJiaXJ0aGRhdGUiXX1dfV19LCJjbGllbnRfbWV0YWRhdGEiOnsidnBfZm9ybWF0cyI6eyJtc29fbWRvYyI6eyJhbGciOlsiRWREU0EiLCJFUzI1NiIsIkVTMzg0Il19LCJ2YytzZC1qd3QiOnsia2Itand0X2FsZ192YWx1ZXMiOlsiRWREU0EiLCJFUzI1NiIsIkVTMzg0IiwiRVMyNTZLIl0sInNkLWp3dF9hbGdfdmFsdWVzIjpbIkVkRFNBIiwiRVMyNTYiLCJFUzM4NCIsIkVTMjU2SyJdfSwiZGMrc2Qtand0Ijp7ImtiLWp3dF9hbGdfdmFsdWVzIjpbIkVkRFNBIiwiRVMyNTYiLCJFUzM4NCIsIkVTMjU2SyJdLCJzZC1qd3RfYWxnX3ZhbHVlcyI6WyJFZERTQSIsIkVTMjU2IiwiRVMzODQiLCJFUzI1NksiXX19LCJhdXRob3JpemF0aW9uX2VuY3J5cHRlZF9yZXNwb25zZV9hbGciOiJFQ0RILUVTIiwiYXV0aG9yaXphdGlvbl9lbmNyeXB0ZWRfcmVzcG9uc2VfZW5jIjoiQTEyOEdDTSIsImNsaWVudF9uYW1lIjoiR2VybWFuIFJlZ2lzdHJhciIsInJlc3BvbnNlX3R5cGVzX3N1cHBvcnRlZCI6WyJ2cF90b2tlbiJdfSwic3RhdGUiOiI3NjQxNDM1MTY5MzMzMjA1NDA3NDQ1ODQiLCJhdWQiOiJodHRwczovL2Z1bmtlLXdhbGxldC5kZS9vaWQ0dnAvdmFsaWQtcmVxdWVzdCIsImV4cCI6MTc0NTUwMTU5MCwiaWF0IjoxNzQ1NTAxMjkwLCJ2ZXJpZmllcl9hdHRlc3RhdGlvbnMiOlt7ImZvcm1hdCI6Imp3dCIsImRhdGEiOiJleUowZVhBaU9pSnlZeTF5Y0N0cWQzUWlMQ0o0TldNaU9sc2lUVWxKUW1SVVEwTkJVblZuUVhkSlFrRm5TVlZJYzFOdFlrZDFWMEZXV2xaWWFuRnZhV1J4UVZaRGJFZDRORmwzUTJkWlNVdHZXa2w2YWpCRlFYZEpkMGQ2UlZwTlFtTkhRVEZWUlVGM2QxRlNNbFo1WWxkR2RVbEdTbXhhTW14NlpFaEthR05xUVdWR2R6QjVUbFJCZWsxNlFYaFBWRlUwVGxSR1lVWjNNSGxPYWtGNlRYcEJlRTlVVlRST1ZFWmhUVUp6ZUVkVVFWaENaMDVXUWtGTlRVVkZaR3hqYlRGb1ltbENVMXBYWkhCak0xSjVXVmhKZDFkVVFWUkNaMk54YUd0cVQxQlJTVUpDWjJkeGFHdHFUMUJSVFVKQ2QwNURRVUZUVVZkRFJWTkdaREJaZDIwNWMwczROMWg0Y1hoRVVEUjNUMEZoWkVWTFoyTmFSbFpZTjI1d1pUTkJURVpyWW1weldGbGFTbk5VUjJoV2NEQXJRalZhZEZWaGJ6Sk9jM2w2U2tOTGVtNVFkMVI2TW5kS1kyOTZNSGRQZWtGaFFtZE9Wa2hTUlVWRmVrRlNaMmM1YldSWE5YSmFVekV6V1ZkNGMxcFlVWFZhUjFWM1NGRlpSRlpTTUU5Q1FsbEZSazE0Ymt0TWEwZHBabUpVUzNKNFlrZFlZMFpZU3paU1JsRmtNMDFCYjBkRFEzRkhVMDAwT1VKQlRVTkJNR2RCVFVWVlEwbFJSRFJTYVV4S1pYVldSSEpGU0ZOMmExQnBVR1pDZGsxNFFWaFNRelpRZFVWNGIzQlZSME5HWkdaT1RGRkpaMGhIVTJFMWRUVmFjVlYwUTNKdVRXbGhSV0ZuWlU4M01YSnFla0pzYjNZd1dWVklOQ3MyUlV4cGIxazlJbDBzSW1Gc1p5STZJa1ZUTWpVMkluMC5leUp3Y21sMllXTjVYM0J2YkdsamVTSTZJbWgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjB2Y0hKcGRtRmplUzF3YjJ4cFkza2lMQ0p3ZFhKd2IzTmxJanBiZXlKc2IyTmhiR1VpT2lKbGJpMVZVeUlzSW01aGJXVWlPaUpVYnlCeVpXZHBjM1JsY2lCaElHNWxkeUIxYzJWeUluMWRMQ0pqYjI1MFlXTjBJanA3SW5kbFluTnBkR1VpT2lKb2RIUndjem92TDJWNFlXMXdiR1V1WTI5dEwyTnZiblJoWTNRaUxDSmxMVzFoYVd3aU9pSmpiMjUwWVdOMFFHVjRZVzF3YkdVdVkyOXRJaXdpY0dodmJtVWlPaUlyTVRJek5EVTJOemc1TUNKOUxDSmpjbVZrWlc1MGFXRnNjeUk2VzNzaWFXUWlPaUp3YVdRaUxDSm1iM0p0WVhRaU9pSmtZeXR6WkMxcWQzUWlMQ0p0WlhSaElqcDdJblpqZEY5MllXeDFaWE1pT2xzaWFIUjBjSE02THk5a1pXMXZMbkJwWkMxcGMzTjFaWEl1WW5WdVpHVnpaSEoxWTJ0bGNtVnBMbVJsTDJOeVpXUmxiblJwWVd4ekwzQnBaQzh4TGpBaUxDSjFjbTQ2WlhVdVpYVnliM0JoTG1WakxtVjFaR2s2Y0dsa09qRWlYWDBzSW1Oc1lXbHRjeUk2VzNzaWNHRjBhQ0k2V3lKbVlXMXBiSGxmYm1GdFpTSmRmU3g3SW5CaGRHZ2lPbHNpWjJsMlpXNWZibUZ0WlNKZGZWMTlYU3dpWTNKbFpHVnVkR2xoYkY5elpYUnpJanBiZXlKdmNIUnBiMjV6SWpwYld5SndhV1FpWFYwc0luSmxjWFZwY21Wa0lqcDBjblZsTENKd2RYSndiM05sSWpwYmV5SnNiMk5oYkdVaU9pSmxiaTFWVXlJc0ltNWhiV1VpT2lKVWJ5QnlaV2RwYzNSbGNpQmhJRzVsZHlCMWMyVnlJbjFkZlYwc0luTjFZaUk2SWtOT1BVUmxiVzhnVWxBaUxDSnFkR2tpT2lJME1qRmhNamd6T1Mxa05qTXhMVFF5TURNdE9UQmtNeTFsTjJZMlpESmxZV0ZtT0RRaUxDSnpkR0YwZFhNaU9uc2ljM1JoZEhWelgyeHBjM1FpT25zaWFXUjRJam8wT0RBd0xDSjFjbWtpT2lKb2RIUndjem92TDJaMWJtdGxMWGRoYkd4bGRDNWtaUzl6ZEdGMGRYTXRiV0Z1WVdkbGJXVnVkQzl6ZEdGMGRYTXRiR2x6ZENKOWZTd2ljSFZpYkdsalgySnZaSGtpT21aaGJITmxMQ0psYm5ScGRHeGxiV1Z1ZEhNaU9sdGRMQ0p6WlhKMmFXTmxjeUk2VzExOS5yM1REek1HYnJ6VUtfTHJOV3VfMUk0ZjBjRUxkVGhadlJITkhCX1E4ZVBNaGJGUHhOUTdqR0FyS0lYNF9PNTI5RDFYMmhVZXV1U2d5VzYxU1ZOTDBqQSJ9XX0.dqdUVHiMrOuap6DFVCszi1RmgkSVgszmJ_G3ovHpdUKXnYIRaMOyRwGrQRSAKdGRTRenhcZ37Ff7PMpWvp3iYg";

public static RequestObject OverAskingSignedRequestSample =
RequestObject.FromStr(OverAskingAuthRequestJwtStrSample).UnwrapOrThrow();
RequestObject.FromStr(OverAskingAuthRequestJwtStrSample, Option<string>.None).UnwrapOrThrow();

public static RequestObject ValidSignedRequestSample =
RequestObject.FromStr(ValidAuthRequestJwtSampleAsStr).UnwrapOrThrow();
RequestObject.FromStr(ValidAuthRequestJwtSampleAsStr, Option<string>.None).UnwrapOrThrow();

public static RpRegistrarCertificate GetRpRegistrarCertificateSample()
{
Expand Down
Loading