diff --git a/.github/workflows/publish-nuget.yaml b/.github/workflows/publish-nuget.yaml index 5971e13..4e53d04 100644 --- a/.github/workflows/publish-nuget.yaml +++ b/.github/workflows/publish-nuget.yaml @@ -48,7 +48,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.* + global-json-file: global.json # - name: Install libindy library # run: | @@ -58,7 +58,7 @@ jobs: # apt-get install -y libindy - name: Build - run: dotnet build $SOLUTION --configuration $BUILD_CONFIG -p:Version=$APP_VERSION --no-restore + run: dotnet build $SOLUTION --configuration $BUILD_CONFIG -p:Version=$APP_VERSION - name: Run tests run: | diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequest.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequest.cs index c6f5964..f7207f0 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequest.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequest.cs @@ -1,6 +1,8 @@ using System.Security.Cryptography.X509Certificates; +using LanguageExt; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using WalletFramework.Core.Functional; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; using static WalletFramework.Oid4Vc.Oid4Vp.Models.ClientIdScheme; @@ -118,7 +120,7 @@ private AuthorizationRequest( /// Thrown when the request does not match the HAIP. public static AuthorizationRequest CreateAuthorizationRequest(string authorizationRequestJson) => CreateAuthorizationRequest(JObject.Parse(authorizationRequestJson)); - + private static AuthorizationRequest CreateAuthorizationRequest(JObject authorizationRequestJson) => IsHaipConform(authorizationRequestJson) ? authorizationRequestJson.ToObject() @@ -175,6 +177,6 @@ internal static AuthorizationRequest WithX509( internal static AuthorizationRequest WithClientMetadata( this AuthorizationRequest authorizationRequest, - ClientMetadata? clientMetadata) - => authorizationRequest with { ClientMetadata = clientMetadata }; + Option clientMetadata) + => authorizationRequest with { ClientMetadata = clientMetadata.ToNullable() }; } diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequestByReference.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequestByReference.cs new file mode 100644 index 0000000..912a83d --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequestByReference.cs @@ -0,0 +1,26 @@ +using System.Web; +using LanguageExt; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Models; + +public record AuthorizationRequestByReference +{ + public Uri AuthorizationRequestUri { get; } + + public Uri RequestUri { get; } + + private AuthorizationRequestByReference(Uri authorizationRequestUri, Uri requestUri) => (AuthorizationRequestUri, RequestUri) = (authorizationRequestUri, requestUri); + + public static Option CreateAuthorizationRequestByReference(Uri uri) + { + var queryString = HttpUtility.ParseQueryString(uri.Query); + var clientId = queryString["client_id"]; + var requestUri = queryString["request_uri"]; + + if (string.IsNullOrEmpty(clientId) + || string.IsNullOrEmpty(requestUri)) + return Option.None; + + return new AuthorizationRequestByReference(uri, new Uri(requestUri)); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequestByValue.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequestByValue.cs new file mode 100644 index 0000000..53319bc --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequestByValue.cs @@ -0,0 +1,30 @@ +using System.Web; +using LanguageExt; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Models; + +public record AuthorizationRequestByValue +{ + public Uri RequestUri { get; } + + public Option PresentationDefinitionUri { get; } + + private AuthorizationRequestByValue(Uri requestUri, Option presentationDefinitionUri) => (RequestUri, PresentationDefinitionUri) = (requestUri, presentationDefinitionUri); + + public static Option CreateAuthorizationRequestByValue(Uri uri) + { + var queryString = HttpUtility.ParseQueryString(uri.Query); + var clientId = queryString["client_id"]; + var nonce = queryString["nonce"]; + var presentationDefinition = queryString["presentation_definition"]; + var presentationDefinitionUri = queryString["presentation_definition_uri"]; + var scope = queryString["scope"]; + + if (string.IsNullOrEmpty(clientId) + || string.IsNullOrEmpty(nonce) + || string.IsNullOrEmpty(presentationDefinition) && string.IsNullOrEmpty(presentationDefinitionUri) && string.IsNullOrEmpty(scope)) + return Option.None; + + return new AuthorizationRequestByValue(uri, string.IsNullOrEmpty(presentationDefinitionUri) ? Option.None : new Uri(presentationDefinitionUri)); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequestUri.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequestUri.cs new file mode 100644 index 0000000..ab3eccb --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/AuthorizationRequestUri.cs @@ -0,0 +1,21 @@ +using OneOf; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Models; + +public struct AuthorizationRequestUri +{ + public OneOf Value { get; } + + private AuthorizationRequestUri(OneOf value) => Value = value; + + public static AuthorizationRequestUri FromUri(Uri uri) + { + return AuthorizationRequestByReference.CreateAuthorizationRequestByReference(uri).Match( + byReference => new AuthorizationRequestUri(byReference), + () => AuthorizationRequestByValue.CreateAuthorizationRequestByValue(uri).Match( + byValue => new AuthorizationRequestUri(byValue), + () => throw new InvalidOperationException("Authorization Request Uri is not a valid") + ) + ); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/HaipAuthorizationRequestUri.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/HaipAuthorizationRequestUri.cs deleted file mode 100644 index 2cc271c..0000000 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/HaipAuthorizationRequestUri.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Hyperledger.Aries.Utils; - -namespace WalletFramework.Oid4Vc.Oid4Vp.Models; - -/// -/// This class represents a haip conform OpenID4VP Authorization Request Uri. -/// -public class HaipAuthorizationRequestUri -{ - /// - /// Gets or sets the uri of the request. - /// - public Uri Uri { get; set; } = null!; - - /// - /// Gets or sets the value of the request_uri parameter. - /// - public string RequestUri { get; set; } = null!; - - /// - /// Validates the hap conformity of an uri and returns a HaipAuthorizationRequestUri. - /// - /// - /// The HaipAuthorizationRequestUri - /// - public static HaipAuthorizationRequestUri FromUri(Uri uri) - { - var request = uri.GetQueryParam("request_uri"); - if (string.IsNullOrEmpty(request)) - throw new InvalidOperationException("HAIP requires request_uri parameter"); - - return new HaipAuthorizationRequestUri() - { - RequestUri = request, - Uri = uri - }; - } -} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/RequestObject.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/RequestObject.cs index 649c799..a1a344d 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Models/RequestObject.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Models/RequestObject.cs @@ -52,6 +52,9 @@ public static RequestObject CreateRequestObject(string requestObjectJson) { var tokenHandler = new JwtSecurityTokenHandler(); + if (requestObjectJson.Split('.').Length == 2) + requestObjectJson += "."; + var jwt = tokenHandler.ReadJwtToken(requestObjectJson); return new RequestObject(jwt); @@ -73,7 +76,7 @@ public static class RequestObjectExtensions /// /// The validated request object. /// Throws when validation fails - public static RequestObject ValidateJwt(this RequestObject requestObject) + public static RequestObject ValidateJwtSignature(this RequestObject requestObject) { var jwt = (JwtSecurityToken)requestObject; var pubKey = requestObject.GetLeafCertificate().GetPublicKey(); diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/AuthorizationRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/AuthorizationRequestService.cs new file mode 100644 index 0000000..8ad3fad --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/AuthorizationRequestService.cs @@ -0,0 +1,107 @@ +using System.Net.Http.Headers; +using System.Web; +using LanguageExt; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WalletFramework.Oid4Vc.Oid4Vp.Models; +using static WalletFramework.Oid4Vc.Oid4Vp.Models.RequestObject; +using static WalletFramework.Oid4Vc.Oid4Vp.Models.ClientIdScheme.ClientIdSchemeValue; +using static Newtonsoft.Json.JsonConvert; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Services; + +public class AuthorizationRequestService( + IHttpClientFactory httpClientFactory) : IAuthorizationRequestService +{ + public async Task CreateAuthorizationRequest(AuthorizationRequestByReference authRequestByReference) + { + var httpClient = httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Clear(); + + var requestObjectJson = await httpClient.GetStringAsync(authRequestByReference.RequestUri); + var requestObject = CreateRequestObject(requestObjectJson); + + var clientMetadata = await FetchClientMetadata(requestObject.ToAuthorizationRequest()); + + return requestObject.ClientIdScheme.Value switch + { + X509SanDns => requestObject + .ValidateJwtSignature() + .ValidateTrustChain() + .ValidateSanName() + .ToAuthorizationRequest() + .WithX509(requestObject) + .WithClientMetadata(clientMetadata), + //TODO: Remove Redirect URi in the future (kept for now for compatibility) + RedirectUri => requestObject + .ToAuthorizationRequest() + .WithClientMetadata(clientMetadata), + VerifierAttestation => + throw new NotImplementedException("Verifier Attestation not yet implemented"), + _ => throw new InvalidOperationException( + $"Client ID Scheme {requestObject.ClientIdScheme} not supported") + }; + } + + public async Task CreateAuthorizationRequest(AuthorizationRequestByValue authRequestByValue) + { + var queryParams = HttpUtility.ParseQueryString(authRequestByValue.RequestUri.Query); + + var jObject = new JObject(); + foreach (var key in queryParams.AllKeys) + { + var value = queryParams[key]; + + if (string.IsNullOrEmpty(value)) + continue; + + try + { + jObject[key] = JToken.Parse(value); + } + catch (JsonReaderException) + { + jObject[key] = value; + } + } + + if (jObject.TryGetValue("presentation_definition_uri", out var presentationDefinitionUri)) + { + var httpClient = httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Clear(); + + var requestObjectJson = await httpClient.GetStringAsync(presentationDefinitionUri.ToString()); + jObject["presentation_definition"] = JToken.Parse(requestObjectJson); + } + + var jsonString = SerializeObject(jObject); + var authRequest = AuthorizationRequest.CreateAuthorizationRequest(jsonString); + + var clientMetadata = await FetchClientMetadata(authRequest); + + return authRequest.ClientIdScheme.Value switch + { + RedirectUri => authRequest + .WithClientMetadata(clientMetadata), + _ => throw new InvalidOperationException( + $"Client ID Scheme {authRequest.ClientIdScheme.Value} not supported when passing the Authorization Request within the Uri") + }; + } + + private async Task> FetchClientMetadata(AuthorizationRequest authorizationRequest) + { + var httpClient = httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + if (authorizationRequest.ClientMetadata != null) + return authorizationRequest.ClientMetadata; + + if (string.IsNullOrWhiteSpace(authorizationRequest.ClientMetadataUri)) + return null; + + var response = await httpClient.GetAsync(authorizationRequest.ClientMetadataUri); + var clientMetadata = await response.Content.ReadAsStringAsync(); + return DeserializeObject(clientMetadata); + } +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IAuthorizationRequestService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IAuthorizationRequestService.cs new file mode 100644 index 0000000..e9f3779 --- /dev/null +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IAuthorizationRequestService.cs @@ -0,0 +1,10 @@ +using WalletFramework.Oid4Vc.Oid4Vp.Models; + +namespace WalletFramework.Oid4Vc.Oid4Vp.Services; + +public interface IAuthorizationRequestService +{ + Task CreateAuthorizationRequest(AuthorizationRequestByReference authorizationRequestUri); + + Task CreateAuthorizationRequest(AuthorizationRequestByValue authorizationRequestUri); +} diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IOid4VpHaipClient.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IOid4VpHaipClient.cs index 93e8c08..8113869 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IOid4VpHaipClient.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/IOid4VpHaipClient.cs @@ -11,11 +11,11 @@ public interface IOid4VpHaipClient /// /// Processes an OpenID4VP Authorization Request Url. /// - /// + /// /// /// A task representing the asynchronous operation. The task result contains the Authorization Response object associated with the OpenID4VP Authorization Request Url. /// - Task ProcessAuthorizationRequestAsync(HaipAuthorizationRequestUri haipAuthorizationRequestUri); + Task ProcessAuthorizationRequestAsync(AuthorizationRequestUri authorizationRequestUri); /// /// Creates the Parameters that are necessary to send an OpenId4VP Authorization Response. diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs index 2170827..29b182e 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpClientService.cs @@ -3,7 +3,6 @@ using System.Text; using Hyperledger.Aries.Agents; using Hyperledger.Aries.Extensions; -using Jose; using LanguageExt; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; @@ -85,7 +84,7 @@ public Oid4VpClientService( public async Task<(AuthorizationRequest, IEnumerable)> ProcessAuthorizationRequestAsync( Uri authorizationRequestUri) { - var haipAuthorizationRequestUri = HaipAuthorizationRequestUri.FromUri(authorizationRequestUri); + var haipAuthorizationRequestUri = AuthorizationRequestUri.FromUri(authorizationRequestUri); var authorizationRequest = await _oid4VpHaipClient.ProcessAuthorizationRequestAsync( haipAuthorizationRequestUri diff --git a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpHaipClient.cs b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpHaipClient.cs index 7d22dc6..f6f9a59 100644 --- a/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpHaipClient.cs +++ b/src/WalletFramework.Oid4Vc/Oid4Vp/Services/Oid4VpHaipClient.cs @@ -1,9 +1,6 @@ -using System.Net.Http.Headers; using WalletFramework.Oid4Vc.Oid4Vp.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Models; using WalletFramework.Oid4Vc.Oid4Vp.PresentationExchange.Services; -using static WalletFramework.Oid4Vc.Oid4Vp.Models.RequestObject; -using static WalletFramework.Oid4Vc.Oid4Vp.Models.ClientIdScheme.ClientIdSchemeValue; using static Newtonsoft.Json.JsonConvert; using Format = WalletFramework.Oid4Vc.Oid4Vci.CredConfiguration.Models.Format; @@ -15,17 +12,17 @@ internal class Oid4VpHaipClient : IOid4VpHaipClient /// /// Initializes a new instance of the class. /// - /// The http client factory to create http clients. + /// The auth srvice used to handle incoming Authorization Request Uris /// The service responsible for presentation exchange protocol operations. public Oid4VpHaipClient( - IHttpClientFactory httpClientFactory, + IAuthorizationRequestService authorizationRequestService, IPexService pexService) { - _httpClientFactory = httpClientFactory; + _authorizationRequestService = authorizationRequestService; _pexService = pexService; } - private readonly IHttpClientFactory _httpClientFactory; + private readonly IAuthorizationRequestService _authorizationRequestService; private readonly IPexService _pexService; /// @@ -64,50 +61,8 @@ public async Task CreateAuthorizationResponseAsync( /// public async Task ProcessAuthorizationRequestAsync( - HaipAuthorizationRequestUri haipAuthorizationRequestUri) - { - var httpClient = _httpClientFactory.CreateClient(); - httpClient.DefaultRequestHeaders.Clear(); - - var requestObjectJson = await httpClient.GetStringAsync(haipAuthorizationRequestUri.RequestUri); - var requestObject = CreateRequestObject(requestObjectJson); - - var authRequest = requestObject.ToAuthorizationRequest(); - var clientMetadata = await FetchClientMetadata(authRequest); - - return requestObject.ClientIdScheme.Value switch - { - X509SanDns => requestObject - .ValidateJwt() - .ValidateTrustChain() - .ValidateSanName() - .ToAuthorizationRequest() - .WithX509(requestObject) - .WithClientMetadata(clientMetadata), - RedirectUri => requestObject - .ToAuthorizationRequest() - .WithClientMetadata(clientMetadata), - VerifierAttestation => - throw new NotImplementedException("Verifier Attestation not yet implemented"), - _ => throw new InvalidOperationException( - $"Client ID Scheme {requestObject.ClientIdScheme} not supported") - }; - } - - private async Task FetchClientMetadata(AuthorizationRequest authorizationRequest) - { - var httpClient = _httpClientFactory.CreateClient(); - httpClient.DefaultRequestHeaders.Clear(); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - if (authorizationRequest.ClientMetadata != null) - return authorizationRequest.ClientMetadata; - - if (string.IsNullOrWhiteSpace(authorizationRequest.ClientMetadataUri)) - return null; - - var response = await httpClient.GetAsync(authorizationRequest.ClientMetadataUri); - var clientMetadata = await response.Content.ReadAsStringAsync(); - return DeserializeObject(clientMetadata); - } + AuthorizationRequestUri authorizationRequestUri) => + await authorizationRequestUri.Value.Match( + async authRequestByReference => await _authorizationRequestService.CreateAuthorizationRequest(authRequestByReference), + async authRequestByValue => await _authorizationRequestService.CreateAuthorizationRequest(authRequestByValue)); } diff --git a/src/WalletFramework.Oid4Vc/SeviceCollectionExtensions.cs b/src/WalletFramework.Oid4Vc/SeviceCollectionExtensions.cs index 963f723..6aac124 100644 --- a/src/WalletFramework.Oid4Vc/SeviceCollectionExtensions.cs +++ b/src/WalletFramework.Oid4Vc/SeviceCollectionExtensions.cs @@ -45,6 +45,7 @@ public static IServiceCollection AddOpenIdServices(this IServiceCollection build builder.AddSingleton(); builder.AddSingleton(); builder.AddSingleton(); + builder.AddSingleton(); builder.AddSdJwtVcServices(); diff --git a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs index 2feda95..cc6bbef 100644 --- a/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs +++ b/test/WalletFramework.Integration.Tests/WalletFramework.Integration.Tests/Oid4Vp/Oid4VpClientServiceTests.cs @@ -40,7 +40,7 @@ public Oid4VpClientServiceTests() var pexService = new PexService(_agentProviderMock.Object, _mdocStorageMock.Object, _sdJwtVcHolderService!); _sdJwtVcHolderService = new SdJwtVcHolderService(holder, _sdJwtSignerService.Object, walletRecordService); - var oid4VpHaipClient = new Oid4VpHaipClient(_httpClientFactoryMock.Object, pexService); + var oid4VpHaipClient = new Oid4VpHaipClient(new AuthorizationRequestService(_httpClientFactoryMock.Object), pexService); _oid4VpRecordService = new Oid4VpRecordService(walletRecordService); _oid4VpClientService = new Oid4VpClientService( diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/AuthRequest/X509SanDnsTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/AuthRequest/X509SanDnsTests.cs index 2106837..eb51e36 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/AuthRequest/X509SanDnsTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/AuthRequest/X509SanDnsTests.cs @@ -11,7 +11,7 @@ public void Valid_Jwt_Signature_Is_Accepted() { var requestObject = RequestObject.CreateRequestObject(SignedRequestObjectWithRs256AndTrustChain); - var sut = requestObject.ValidateJwt(); + var sut = requestObject.ValidateJwtSignature(); sut.Should().NotBeNull(); } @@ -22,7 +22,7 @@ public void Invalid_Jwt_Signature_Results_In_An_Error() var requestObject = RequestObject.CreateRequestObject(SignedRequestObjectWithRs256AndInvalidSignature); try { - requestObject.ValidateJwt(); + requestObject.ValidateJwtSignature(); Assert.Fail("Expected validation to fail"); } catch (Exception) diff --git a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpHaipClientTests.cs b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpHaipClientTests.cs index 247648f..5b6d103 100644 --- a/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpHaipClientTests.cs +++ b/test/WalletFramework.Oid4Vc.Tests/Oid4Vp/Services/Oid4VpHaipClientTests.cs @@ -14,24 +14,71 @@ namespace WalletFramework.Oid4Vc.Tests.Oid4Vp.Services; public class Oid4VpHaipClientTests { - private const string AuthRequestWithRequestUri = - "haip://?client_id=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Fcallback&request_uri=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Frequestobject%2F4ba20ad0cb08545830aa549ab4305c03"; + private const string AuthRequestByReferenceWithRequestUri = + "openid4vp://?client_id=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Fcallback&request_uri=https%3A%2F%2Fnc-sd-jwt.lambda.d3f.me%2Findex.php%2Fapps%2Fssi_login%2Foidc%2Frequestobject%2F4ba20ad0cb08545830aa549ab4305c03"; private const string RequestUriResponse = "eyJhbGciOiJFUzI1NiJ9.eyJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJyZXNwb25zZV9tb2RlIjoiZGlyZWN0X3Bvc3QiLCJjbGllbnRfaWRfc2NoZW1lIjoicmVkaXJlY3RfdXJpIiwiY2xpZW50X2lkIjoiaHR0cHM6Ly92ZXJpZmllci5jb20vcHJlc2VudGF0aW9uL2F1dGhvcml6YXRpb24tcmVzcG9uc2UiLCJjbGllbnRfbWV0YWRhdGFfdXJpIjoiaHR0cHM6Ly92ZXJpZmllci5jb20vbWV0YWRhdGEvMTIzNCIsInJlc3BvbnNlX3VyaSI6Imh0dHBzOi8vdmVyaWZpZXIuY29tL3ByZXNlbnRhdGlvbi9hdXRob3JpemF0aW9uLXJlc3BvbnNlIiwibm9uY2UiOiI4NzU1NDc4NDI2MDI4MDI4MDQ0MjA5MjE4NDE3MTI3NDEzMjQ1OCIsInByZXNlbnRhdGlvbl9kZWZpbml0aW9uIjp7ImlkIjoiNGRkMWMyNmEtMmY0Ni00M2FlLWE3MTEtNzA4ODhjOTNmYjRmIiwiaW5wdXRfZGVzY3JpcHRvcnMiOlt7ImlkIjoiTmV4dGNsb3VkQ3JlZGVudGlhbCIsImZvcm1hdCI6eyJ2YytzZC1qd3QiOnsicHJvb2ZfdHlwZSI6WyJKc29uV2ViU2lnbmF0dXJlMjAyMCJdfX0sImNvbnN0cmFpbnRzIjp7ImxpbWl0X2Rpc2Nsb3N1cmUiOiJyZXF1aXJlZCIsImZpZWxkcyI6W3sicGF0aCI6WyIkLnR5cGUiXSwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJjb25zdCI6IlZlcmlmaWVkRU1haWwifX0seyJwYXRoIjpbIiQuY3JlZGVudGlhbFN1YmplY3QuZW1haWwiXX1dfX1dfX0."; - private static string VerifierMetadataResponse => - new JObject + private const string AuthRequestByValueWithPresentationDefinitionUri = + "openid4vp:///?client_id=https%3A%2F%2Fsome.de%2Fissuer%2Fdirect_post_vci&response_type=vp_token&response_mode=direct_post&response_uri=https%3A%2F%2Fsome.de%2Fissuer%2Fdirect_post_vci&presentation_definition_uri=https%3A%2F%2Fsome.de%2Fissuer%2Fpresentation-definition&client_id_scheme=redirect_uri&client_metadata_uri=https%3A%2F%2Fsome.de%2Fissuer%2Fclient-metadata&nonce=n0S6_WzA2Mj&state=af0ifjsldkj"; + + // private const string UnsignedRequestUriResponse = + // "{\"response_type\":\"vp_token\",\"client_id_scheme\":\"redirect_uri\",\"presentation_definition\":{\"id\":\"4dd1c26a-2f46-43ae-a711-70888c93fb4f\",\"input_descriptors\":[{\"id\":\"NextcloudCredential\",\"format\":{\"vc+sd-jwt\":{\"proof_type\":[\"JsonWebSignature2020\"]}},\"constraints\":{\"limit_disclosure\":\"required\",\"fields\":[{\"path\":[\"$.type\"],\"filter\":{\"type\":\"string\",\"const\":\"VerifiedEMail\"}},{\"path\":[\"$.credentialSubject.email\"]}]}}]},\"client_id\":\"https://verifier.com/presentation/authorization-response\",\"nonce\":\"random_nonce_value\",\"response_uri\":\"https://verifier.com/presentation/authorization-response\",\"response_mode\":\"direct_post\",\"client_metadata_uri\":\"https://example.com/client_metadata\",\"scope\":\"openid\",\"state\":\"random_state_value\"}"; + + private static string PresentationDefinitionUriResponse => + new JObject + { + ["id"] = "4dd1c26a-2f46-43ae-a711-70888c93fb4f", + ["input_descriptors"] = new JArray() { - ["logo_uri"] = "https://some.de/logo", - ["client_name"] = "Some Verifier", - ["client_uri"] = "https://some.de", - ["contacts"] = new JArray("Any contact"), - ["redirect_uris"] = new JArray("https://verifier.com/redirect-uri"), - ["policy_uri"] = "https://some.de/policy", - ["tos_uri"] = "https://some.de/tos", + new JObject() + { + ["id"] = "NextcloudCredential", + ["format"] = new JObject() + { + ["vc+sd-jwt"] = new JObject() + { + ["proof_type"] = new JArray("JsonWebSignature2020") + } + }, + ["constraints"] = new JObject() + { + ["limit_disclosure"] = "required", + ["fields"] = new JArray() + { + new JObject() + { + ["path"] = new JArray("$.type"), + ["filter"] = new JObject() + { + ["type"] = "string", + ["const"] = "VerifiedEMail" + } + }, + new JObject() + { + ["path"] = new JArray("$.credentialSubject.email") + } + } + } + } } - .ToString(); + } + .ToString(); + + private static string VerifierMetadataResponse => + new JObject + { + ["logo_uri"] = "https://some.de/logo", + ["client_name"] = "Some Verifier", + ["client_uri"] = "https://some.de", + ["contacts"] = new JArray("Any contact"), + ["redirect_uris"] = new JArray("https://verifier.com/redirect-uri"), + ["policy_uri"] = "https://some.de/policy", + ["tos_uri"] = "https://some.de/tos", + } + .ToString(); private readonly Mock _agentProviderMock = new(); private readonly Mock _mdocStorageMock = new(); @@ -42,7 +89,7 @@ public class Oid4VpHaipClientTests private Oid4VpHaipClient _oid4VpHaipClient; [Fact] - public async Task CanProcessAuthorizationRequest() + public async Task CanProcessAuthorizationRequestByReference() { // Arrange var httpResponseMessage = new HttpResponseMessage @@ -60,27 +107,27 @@ public async Task CanProcessAuthorizationRequest() SetupHttpClientSequence(httpResponseMessage, verifierMetadataResponseMessage); _oid4VpHaipClient = new Oid4VpHaipClient( - _httpClientFactoryMock.Object, + new AuthorizationRequestService(_httpClientFactoryMock.Object), new PexService(_agentProviderMock.Object, _mdocStorageMock.Object, _sdJwtVcHolderService.Object) ); // Act var authorizationRequest = await _oid4VpHaipClient.ProcessAuthorizationRequestAsync( - HaipAuthorizationRequestUri.FromUri(new Uri(AuthRequestWithRequestUri)) + AuthorizationRequestUri.FromUri(new Uri(AuthRequestByReferenceWithRequestUri)) ); - // Assert - authorizationRequest.ClientId.Should().Be("https://verifier.com/presentation/authorization-response"); - authorizationRequest.ResponseUri.Should().Be("https://verifier.com/presentation/authorization-response"); - authorizationRequest.Nonce.Should().Be("87554784260280280442092184171274132458"); - authorizationRequest.PresentationDefinition.Id.Should().Be("4dd1c26a-2f46-43ae-a711-70888c93fb4f"); - authorizationRequest.ClientMetadata.ClientName.Should().Be("Some Verifier"); - authorizationRequest.ClientMetadata.ClientUri.Should().Be("https://some.de"); - authorizationRequest.ClientMetadata.Contacts.First().Should().Be("Any contact"); - authorizationRequest.ClientMetadata.LogoUri.Should().Be("https://some.de/logo"); - authorizationRequest.ClientMetadata.PolicyUri.Should().Be("https://some.de/policy"); - authorizationRequest.ClientMetadata.TosUri.Should().Be("https://some.de/tos"); - authorizationRequest.ClientMetadata.RedirectUris.First().Should().Be("https://verifier.com/redirect-uri"); + // Assert + authorizationRequest.ClientId.Should().Be("https://verifier.com/presentation/authorization-response"); + authorizationRequest.ResponseUri.Should().Be("https://verifier.com/presentation/authorization-response"); + authorizationRequest.Nonce.Should().Be("87554784260280280442092184171274132458"); + authorizationRequest.PresentationDefinition.Id.Should().Be("4dd1c26a-2f46-43ae-a711-70888c93fb4f"); + authorizationRequest.ClientMetadata.ClientName.Should().Be("Some Verifier"); + authorizationRequest.ClientMetadata.ClientUri.Should().Be("https://some.de"); + authorizationRequest.ClientMetadata.Contacts.First().Should().Be("Any contact"); + authorizationRequest.ClientMetadata.LogoUri.Should().Be("https://some.de/logo"); + authorizationRequest.ClientMetadata.PolicyUri.Should().Be("https://some.de/policy"); + authorizationRequest.ClientMetadata.TosUri.Should().Be("https://some.de/tos"); + authorizationRequest.ClientMetadata.RedirectUris.First().Should().Be("https://verifier.com/redirect-uri"); var inputDescriptor = authorizationRequest.PresentationDefinition.InputDescriptors.First(); @@ -97,7 +144,59 @@ public async Task CanProcessAuthorizationRequest() inputDescriptor.Constraints.Fields![1].Path.First().Should().Be("$.credentialSubject.email"); } + + [Fact] + public async Task CanProcessAuthorizationRequestByValueWithPresentationDefinitionUri() + { + // Arrange + var presentationDefinitionUriResponse = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(PresentationDefinitionUriResponse) + }; + + var verifierMetadataResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(VerifierMetadataResponse) + }; + + SetupHttpClientSequence(presentationDefinitionUriResponse, verifierMetadataResponseMessage); + _oid4VpHaipClient = new Oid4VpHaipClient( + new AuthorizationRequestService(_httpClientFactoryMock.Object), + new PexService(_agentProviderMock.Object, _mdocStorageMock.Object, _sdJwtVcHolderService.Object) + ); + + // Act + var authorizationRequest = await _oid4VpHaipClient.ProcessAuthorizationRequestAsync( + AuthorizationRequestUri.FromUri(new Uri(AuthRequestByValueWithPresentationDefinitionUri)) + ); + + // Assert + authorizationRequest.ClientId.Should().Be("https://some.de/issuer/direct_post_vci"); + authorizationRequest.ResponseUri.Should().Be("https://some.de/issuer/direct_post_vci"); + authorizationRequest.ResponseMode.Should().Be("direct_post"); + authorizationRequest.State.Should().Be("af0ifjsldkj"); + authorizationRequest.Nonce.Should().Be("n0S6_WzA2Mj"); + authorizationRequest.PresentationDefinition.Id.Should().Be("4dd1c26a-2f46-43ae-a711-70888c93fb4f"); + + var inputDescriptor = authorizationRequest.PresentationDefinition.InputDescriptors.First(); + + inputDescriptor.Id.Should().Be("NextcloudCredential"); + + inputDescriptor.Formats.First().Key.Should().Be("vc+sd-jwt"); + inputDescriptor.Formats.First().Value.ProofTypes.First().Should().Be("JsonWebSignature2020"); + + inputDescriptor.Constraints.LimitDisclosure.Should().Be("required"); + + inputDescriptor.Constraints.Fields!.First().Filter!.Type.Should().Be("string"); + inputDescriptor.Constraints.Fields!.First().Filter!.Const.Should().Be("VerifiedEMail"); + inputDescriptor.Constraints.Fields!.First().Path.First().Should().Be("$.type"); + + inputDescriptor.Constraints.Fields![1].Path.First().Should().Be("$.credentialSubject.email"); + } + private void SetupHttpClientSequence(params HttpResponseMessage[] responses) { var responseQueue = new Queue(responses);